package util import ( "context" "fmt" "io" "os" "path/filepath" "time" "github.com/distribution/reference" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/image" "github.com/docker/cli/cli/flags" "github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/trust" "github.com/docker/docker/api/types" typesimage "github.com/docker/docker/api/types/image" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/client" "github.com/docker/docker/pkg/jsonmessage" "github.com/hashicorp/go-version" "github.com/moby/term" "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" v12 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/kubectl/pkg/cmd/util" client2 "sigs.k8s.io/controller-runtime/pkg/client" ) func GetClient() (*client.Client, *command.DockerCli, error) { client, err := client.NewClientWithOpts( client.FromEnv, client.WithAPIVersionNegotiation(), ) if err != nil { return nil, nil, fmt.Errorf("can not create docker client from env, err: %v", err) } var cli *command.DockerCli cli, err = command.NewDockerCli(command.WithAPIClient(client)) if err != nil { return nil, nil, fmt.Errorf("can not create docker client from env, err: %v", err) } err = cli.Initialize(flags.NewClientOptions()) if err != nil { return nil, nil, fmt.Errorf("can not init docker client, err: %v", err) } return client, cli, nil } // TransferImage // 1) if not special ssh config, just pull image and tag and push // 2) if special ssh config, pull image, tag image, save image and scp image to remote, load image and push func TransferImage(ctx context.Context, conf *SshConfig, imageSource, imageTarget string, out io.Writer) error { client, cli, err := GetClient() if err != nil { log.Errorf("failed to get docker client: %v", err) return err } // todo add flags? or detect k8s node runtime ? platform := &v1.Platform{Architecture: "amd64", OS: "linux"} err = PullImage(ctx, platform, client, cli, imageSource, out) if err != nil { log.Errorf("failed to pull image: %v", err) return err } err = client.ImageTag(ctx, imageSource, imageTarget) if err != nil { log.Errorf("failed to tag image %s to %s: %v", imageSource, imageTarget, err) return err } // use it if sshConfig is not empty if conf.ConfigAlias == "" && conf.Addr == "" { var distributionRef reference.Named distributionRef, err = reference.ParseNormalizedNamed(imageTarget) if err != nil { log.Errorf("can not parse image name %s: %v", imageTarget, err) return err } var imgRefAndAuth trust.ImageRefAndAuth imgRefAndAuth, err = trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(cli), distributionRef.String()) if err != nil { log.Errorf("can not get image auth: %v", err) return err } var encodedAuth string encodedAuth, err = registrytypes.EncodeAuthConfig(*imgRefAndAuth.AuthConfig()) if err != nil { log.Errorf("can not encode auth config to base64: %v", err) return err } requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(cli, imgRefAndAuth.RepoInfo().Index, "push") var readCloser io.ReadCloser readCloser, err = client.ImagePush(ctx, imageTarget, types.ImagePushOptions{ RegistryAuth: encodedAuth, PrivilegeFunc: requestPrivilege, }) if err != nil { log.Errorf("can not push image %s, err: %v", imageTarget, err) return err } defer readCloser.Close() if out == nil { _, out, _ = term.StdStreams() } outWarp := streams.NewOut(out) err = jsonmessage.DisplayJSONMessagesToStream(readCloser, outWarp, nil) if err != nil { log.Errorf("can not display message, err: %v", err) return err } return nil } // transfer image to remote var sshClient *ssh.Client sshClient, err = DialSshRemote(ctx, conf) if err != nil { return err } defer sshClient.Close() var responseReader io.ReadCloser responseReader, err = client.ImageSave(ctx, []string{imageTarget}) if err != nil { log.Errorf("can not save image %s: %v", imageTarget, err) return err } defer responseReader.Close() file, err := os.CreateTemp("", "*.tar") if err != nil { return err } logrus.Infof("saving image %s to temp file %s", imageTarget, file.Name()) if _, err = io.Copy(file, responseReader); err != nil { return err } if err = file.Close(); err != nil { return err } defer os.Remove(file.Name()) logrus.Infof("Transferring image %s", imageTarget) filename := filepath.Base(file.Name()) cmd := fmt.Sprintf( "(docker load -i ~/.kubevpn/%s && docker push %s) || (nerdctl image load -i ~/.kubevpn/%s && nerdctl image push %s)", filename, imageTarget, filename, imageTarget, ) stdout := log.StandardLogger().Out err = SCPAndExec(stdout, stdout, sshClient, file.Name(), filename, []string{cmd}...) if err != nil { return err } logrus.Infof("Loaded image: %s", imageTarget) return nil } // PullImage image.RunPull(ctx, c, image.PullOptions{}) func PullImage(ctx context.Context, platform *v1.Platform, cli *client.Client, c *command.DockerCli, img string, out io.Writer) error { var readCloser io.ReadCloser var plat string if platform != nil && platform.Architecture != "" && platform.OS != "" { plat = fmt.Sprintf("%s/%s", platform.OS, platform.Architecture) } distributionRef, err := reference.ParseNormalizedNamed(img) if err != nil { log.Errorf("can not parse image name %s: %v", img, err) return err } var imgRefAndAuth trust.ImageRefAndAuth imgRefAndAuth, err = trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(c), distributionRef.String()) if err != nil { log.Errorf("can not get image auth: %v", err) return err } var encodedAuth string encodedAuth, err = registrytypes.EncodeAuthConfig(*imgRefAndAuth.AuthConfig()) if err != nil { log.Errorf("can not encode auth config to base64: %v", err) return err } requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(c, imgRefAndAuth.RepoInfo().Index, "pull") readCloser, err = cli.ImagePull(ctx, img, typesimage.PullOptions{ All: false, RegistryAuth: encodedAuth, PrivilegeFunc: requestPrivilege, Platform: plat, }) if err != nil { log.Errorf("can not pull image %s, err: %s, please make sure image is exist and can be pulled from local", img, err) return err } defer readCloser.Close() if out == nil { _, out, _ = term.StdStreams() } outWarp := streams.NewOut(out) err = jsonmessage.DisplayJSONMessagesToStream(readCloser, outWarp, nil) if err != nil { log.Errorf("can not display message, err: %v", err) return err } return nil } // UpdateImage update to newer image func UpdateImage(ctx context.Context, factory util.Factory, ns string, deployName string, image string) error { clientSet, err2 := factory.KubernetesClientSet() if err2 != nil { return err2 } deployment, err := clientSet.AppsV1().Deployments(ns).Get(ctx, deployName, v12.GetOptions{}) if err != nil { return err } origin := deployment.DeepCopy() newImg, err := reference.ParseNormalizedNamed(image) if err != nil { return err } newTag, ok := newImg.(reference.NamedTagged) if !ok { return nil } oldImg, err := reference.ParseNormalizedNamed(deployment.Spec.Template.Spec.Containers[0].Image) if err != nil { return err } var oldTag reference.NamedTagged oldTag, ok = oldImg.(reference.NamedTagged) if !ok { return nil } if reference.Domain(newImg) != reference.Domain(oldImg) { return nil } var oldVersion, newVersion *version.Version oldVersion, err = version.NewVersion(oldTag.Tag()) if err != nil { return nil } newVersion, err = version.NewVersion(newTag.Tag()) if err != nil { return nil } if oldVersion.GreaterThanOrEqual(newVersion) { return nil } log.Infof("found newer image %s, set image from %s to it...", image, deployment.Spec.Template.Spec.Containers[0].Image) for i := range deployment.Spec.Template.Spec.Containers { deployment.Spec.Template.Spec.Containers[i].Image = image } p := client2.MergeFrom(deployment) data, err := client2.MergeFrom(origin).Data(deployment) if err != nil { return err } _, err = clientSet.AppsV1().Deployments(ns).Patch(ctx, deployName, p.Type(), data, v12.PatchOptions{}) if err != nil { return err } err = RolloutStatus(ctx, factory, ns, fmt.Sprintf("deployments/%s", deployName), time.Minute*60) return err }