From 4b0b4ee38b97a57eb5fc26acf769f7ff754533f9 Mon Sep 17 00:00:00 2001 From: "Caroline (Frank) Scherf" <105299358+fcaroline2020@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:09:42 +0000 Subject: [PATCH] Adds ability for use to define additional tags for images --- cmd/build.go | 9 +++++++++ pkg/devcontainer/build.go | 4 ++++ pkg/devcontainer/config/build.go | 1 + pkg/devcontainer/prebuild.go | 27 +++++++++++++++++++++++--- pkg/driver/docker/build.go | 2 ++ pkg/driver/docker/docker.go | 4 ++-- pkg/image/image.go | 33 ++++++++++++++++++++++++++++++-- pkg/provider/workspace.go | 1 + 8 files changed, 74 insertions(+), 7 deletions(-) diff --git a/cmd/build.go b/cmd/build.go index ea58fe7bc..c0794fa30 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -52,6 +52,14 @@ func NewBuildCmd(flags *flags.GlobalFlags) *cobra.Command { if err != nil { return fmt.Errorf("cannot push to %s, please make sure you have push permissions to repository %s", cmd.Repository, cmd.Repository) } + + if cmd.Tag != nil { + err = image.ValidateTags(cmd.Tag) + + if err != nil { + return fmt.Errorf("cannot build image, invalid tag defined %s", cmd.Tag) + } + } } // create a temporary workspace @@ -112,6 +120,7 @@ func NewBuildCmd(flags *flags.GlobalFlags) *cobra.Command { buildCmd.Flags().BoolVar(&cmd.SkipDelete, "skip-delete", false, "If true will not delete the workspace after building it") buildCmd.Flags().StringVar(&cmd.Machine, "machine", "", "The machine to use for this workspace. The machine needs to exist beforehand or the command will fail. If the workspace already exists, this option has no effect") buildCmd.Flags().StringVar(&cmd.Repository, "repository", "", "The repository to push to") + buildCmd.Flags().StringSliceVar(&cmd.Tag, "tag", []string{}, "Image Tag(s) in the form of a comma separated list --tag latest,arm64 or multiple flags --tag latest --tag arm64") buildCmd.Flags().StringSliceVar(&cmd.Platform, "platform", []string{}, "Set target platform for build") buildCmd.Flags().BoolVar(&cmd.SkipPush, "skip-push", false, "If true will not push the image to the repository, useful for testing") buildCmd.Flags().Var(&cmd.GitCloneStrategy, "git-clone-strategy", "The git clone strategy DevPod uses to checkout git based workspaces. Can be full (default), blobless, treeless or shallow") diff --git a/pkg/devcontainer/build.go b/pkg/devcontainer/build.go index ff2525a58..5abd97850 100644 --- a/pkg/devcontainer/build.go +++ b/pkg/devcontainer/build.go @@ -104,6 +104,7 @@ func (r *runner) build( ImageName: overrideBuildImageName, PrebuildHash: imageTag, RegistryCache: options.RegistryCache, + Tags: options.Tag, }, nil } @@ -135,6 +136,7 @@ func (r *runner) extendImage( ImageMetadata: extendedBuildInfo.MetadataConfig, ImageName: imageBase, RegistryCache: options.RegistryCache, + Tags: options.Tag, }, nil } @@ -322,6 +324,7 @@ func (r *runner) buildImage( ImageName: prebuildImage, PrebuildHash: prebuildHash, RegistryCache: options.RegistryCache, + Tags: options.Tag, }, nil } else if err != nil { r.Log.Debugf("Error trying to find prebuild image %s: %v", prebuildImage, err) @@ -385,6 +388,7 @@ func dockerlessFallback( User: buildInfo.User, }, RegistryCache: options.RegistryCache, + Tags: options.Tag, }, nil } diff --git a/pkg/devcontainer/config/build.go b/pkg/devcontainer/config/build.go index 89a7521ca..fc1f954e9 100644 --- a/pkg/devcontainer/config/build.go +++ b/pkg/devcontainer/config/build.go @@ -22,6 +22,7 @@ type BuildInfo struct { ImageName string PrebuildHash string RegistryCache string + Tags []string Dockerless *BuildInfoDockerless } diff --git a/pkg/devcontainer/prebuild.go b/pkg/devcontainer/prebuild.go index e6c5eb352..2bbf0ee6f 100644 --- a/pkg/devcontainer/prebuild.go +++ b/pkg/devcontainer/prebuild.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/loft-sh/devpod/pkg/devcontainer/config" "github.com/loft-sh/devpod/pkg/driver" @@ -87,10 +88,30 @@ func (r *runner) Build(ctx context.Context, options provider.BuildOptions) (stri ) } + // Setup all image tags (prebuild and any user defined tags) + ImageRefs := []string{prebuildImage} + r.Log.Debug("Prebuilt Image=%s buildInfo.ImageName=%s", prebuildImage, buildInfo.ImageName) + + imageRepoName := strings.Split(prebuildImage, ":") + if buildInfo.Tags != nil { + for _, tag := range buildInfo.Tags { + ImageRefs = append(ImageRefs, imageRepoName[0]+":"+tag) + } + } + + // tag the image + for _, imageRef := range ImageRefs { + r.Log.Debug("Tagging Image=%s Tag=%s", prebuildImage, imageRef) + err = dockerDriver.TagDevContainer(ctx, prebuildImage, imageRef) + } + // push the image to the registry - err = dockerDriver.PushDevContainer(ctx, prebuildImage) - if err != nil { - return "", errors.Wrap(err, "push image") + for _, imageRef := range ImageRefs { + r.Log.Debug("Pushing Image=%s", imageRef) + err = dockerDriver.PushDevContainer(ctx, imageRef) + if err != nil { + return "", errors.Wrap(err, "push image") + } } return prebuildImage, nil diff --git a/pkg/driver/docker/build.go b/pkg/driver/docker/build.go index c17388aef..f224dc8cc 100644 --- a/pkg/driver/docker/build.go +++ b/pkg/driver/docker/build.go @@ -46,6 +46,7 @@ func (d *dockerDriver) BuildDevContainer( ImageName: imageName, PrebuildHash: prebuildHash, RegistryCache: options.RegistryCache, + Tags: options.Tag, }, nil } else if err != nil { d.Log.Debugf("Error trying to find local image %s: %v", imageName, err) @@ -114,6 +115,7 @@ func (d *dockerDriver) BuildDevContainer( ImageName: imageName, PrebuildHash: prebuildHash, RegistryCache: options.RegistryCache, + Tags: options.Tag, }, nil } diff --git a/pkg/driver/docker/docker.go b/pkg/driver/docker/docker.go index 416e130ce..c9b00db27 100644 --- a/pkg/driver/docker/docker.go +++ b/pkg/driver/docker/docker.go @@ -112,7 +112,7 @@ func (d *dockerDriver) PushDevContainer(ctx context.Context, image string) error } func (d *dockerDriver) TagDevContainer(ctx context.Context, image, tag string) error { - // push image + // Tag image writer := d.Log.Writer(logrus.InfoLevel, false) defer writer.Close() @@ -127,7 +127,7 @@ func (d *dockerDriver) TagDevContainer(ctx context.Context, image, tag string) e d.Log.Debugf("Running docker command: %s %s", d.Docker.DockerCommand, strings.Join(args, " ")) err := d.Docker.Run(ctx, args, nil, writer, writer) if err != nil { - return errors.Wrap(err, "push image") + return errors.Wrap(err, "tag image") } return nil diff --git a/pkg/image/image.go b/pkg/image/image.go index f5ef097cc..751aa3920 100644 --- a/pkg/image/image.go +++ b/pkg/image/image.go @@ -3,13 +3,18 @@ package image import ( "context" "fmt" - "net/http" - "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/pkg/errors" + "net/http" + "regexp" +) + +var ( + dockerTagRegexp = regexp.MustCompile(`^[\w][\w.-]*$`) + DockerTagMaxSize = 128 ) func GetImage(ctx context.Context, image string) (v1.Image, error) { @@ -58,3 +63,27 @@ func GetImageConfig(ctx context.Context, image string) (*v1.ConfigFile, v1.Image return configFile, img, nil } + +func ValidateTags(tags []string) error { + for _, tag := range tags { + if !IsValidDockerTag(tag) { + return fmt.Errorf(`%q is not a valid docker tag + + - a tag name must be valid ASCII and may contain lowercase and uppercase letters, digits, underscores, periods and dashes; + - a tag name may not start with a period or a dash and may contain a maximum of 128 characters.`, tag) + } + } + return nil +} + +func IsValidDockerTag(tag string) bool { + if shouldNotBeSlugged(tag, dockerTagRegexp, DockerTagMaxSize) { + return true + } + + return false +} + +func shouldNotBeSlugged(data string, regexp *regexp.Regexp, maxSize int) bool { + return len(data) == 0 || regexp.Match([]byte(data)) && len(data) <= maxSize +} diff --git a/pkg/provider/workspace.go b/pkg/provider/workspace.go index facb53b3f..af505a8bc 100644 --- a/pkg/provider/workspace.go +++ b/pkg/provider/workspace.go @@ -222,6 +222,7 @@ type CLIOptions struct { Repository string `json:"repository,omitempty"` SkipPush bool `json:"skipPush,omitempty"` Platform []string `json:"platform,omitempty"` + Tag []string `json:"tag,omitempty"` ForceBuild bool `json:"forceBuild,omitempty"` ForceDockerless bool `json:"forceDockerless,omitempty"`