From 0a355384897c943520399c2ad1f9bcb440e2fbf8 Mon Sep 17 00:00:00 2001 From: Kyle Lemons Date: Sun, 17 Oct 2021 11:35:18 -0700 Subject: [PATCH] Add support for automatic tagging (:1, :1.2, :1.2.3, etc) for semantic versions (#22) --- README.md | 32 +++++++++++++++++ cmd/kaniko-docker/main.go | 6 ++++ cmd/kaniko-ecr/main.go | 6 ++++ cmd/kaniko-gcr/main.go | 6 ++++ go.mod | 6 ++-- go.sum | 17 +++++++-- kaniko.go | 48 ++++++++++++++++++++++++- kaniko_test.go | 73 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 189 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3715593..ccbd17e 100644 --- a/README.md +++ b/README.md @@ -51,3 +51,35 @@ docker run --rm \ -w /drone \ plugins/kaniko:linux-amd64 ``` + +### Automatic Tagging + +With auto tagging enabled, semantic versions can be passed to PLUGIN_TAGS directly for expansion: + +```console +docker run --rm \ + -e PLUGIN_TAGS=v1.2.3,latest \ + -e PLUGIN_AUTO_TAG=true \ + -v $(pwd):/drone \ + -w /drone \ + plugins/kaniko:linux-amd64 +``` +would both be equivalent to + +``` +PLUGIN_TAGS=1,1.2,1.2.3,latest +``` + +This allows for passing `$DRONE_TAG` directly as a tag for repos that use [semver](https://semver.org) tags. + +To avoid confusion between repo tags and image tags, `PLUGIN_AUTO_TAG` also recognizes a semantic version +without the `v` prefix. As such, the following is also equivalent to the above: + +```console +docker run --rm \ + -e PLUGIN_TAGS=1.2.3,latest \ + -e PLUGIN_AUTO_TAG=true \ + -v $(pwd):/drone \ + -w /drone \ + plugins/kaniko:linux-amd64 +``` diff --git a/cmd/kaniko-docker/main.go b/cmd/kaniko-docker/main.go index d14f839..a72b2d3 100644 --- a/cmd/kaniko-docker/main.go +++ b/cmd/kaniko-docker/main.go @@ -64,6 +64,11 @@ func main() { EnvVar: "PLUGIN_TAGS", FilePath: ".tags", }, + cli.BoolFlag{ + Name: "auto_tag", + Usage: "enable for semver tagging", + EnvVar: "PLUGIN_AUTO_TAG", + }, cli.StringSliceFlag{ Name: "args", Usage: "build args", @@ -163,6 +168,7 @@ func run(c *cli.Context) error { Dockerfile: c.String("dockerfile"), Context: c.String("context"), Tags: c.StringSlice("tags"), + AutoTag: c.Bool("auto_tag"), Args: c.StringSlice("args"), Target: c.String("target"), Repo: c.String("repo"), diff --git a/cmd/kaniko-ecr/main.go b/cmd/kaniko-ecr/main.go index a838715..72f2971 100644 --- a/cmd/kaniko-ecr/main.go +++ b/cmd/kaniko-ecr/main.go @@ -78,6 +78,11 @@ func main() { EnvVar: "PLUGIN_TAGS", FilePath: ".tags", }, + cli.BoolFlag{ + Name: "auto_tag", + Usage: "enable for semver tagging", + EnvVar: "PLUGIN_AUTO_TAG", + }, cli.StringSliceFlag{ Name: "args", Usage: "build args", @@ -235,6 +240,7 @@ func run(c *cli.Context) error { Dockerfile: c.String("dockerfile"), Context: c.String("context"), Tags: c.StringSlice("tags"), + AutoTag: c.Bool("auto_tag"), Args: c.StringSlice("args"), Target: c.String("target"), Repo: fmt.Sprintf("%s/%s", c.String("registry"), c.String("repo")), diff --git a/cmd/kaniko-gcr/main.go b/cmd/kaniko-gcr/main.go index 9f64f2e..7c5a18d 100644 --- a/cmd/kaniko-gcr/main.go +++ b/cmd/kaniko-gcr/main.go @@ -59,6 +59,11 @@ func main() { EnvVar: "PLUGIN_TAGS", FilePath: ".tags", }, + cli.BoolFlag{ + Name: "auto_tag", + Usage: "enable for semver tagging", + EnvVar: "PLUGIN_AUTO_TAG", + }, cli.StringSliceFlag{ Name: "args", Usage: "build args", @@ -148,6 +153,7 @@ func run(c *cli.Context) error { Dockerfile: c.String("dockerfile"), Context: c.String("context"), Tags: c.StringSlice("tags"), + AutoTag: c.Bool("auto_tag"), Args: c.StringSlice("args"), Target: c.String("target"), Repo: fmt.Sprintf("%s/%s", c.String("registry"), c.String("repo")), diff --git a/go.mod b/go.mod index 8737ad5..7802806 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,12 @@ require ( github.com/aws/aws-sdk-go-v2/service/ecr v1.4.3 github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.4.3 github.com/aws/smithy-go v1.7.0 + github.com/google/go-cmp v0.5.6 github.com/joho/godotenv v1.3.0 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.3.0 github.com/urfave/cli v1.22.2 + golang.org/x/mod v0.4.2 ) require ( @@ -24,8 +26,8 @@ require ( github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect github.com/russross/blackfriday/v2 v2.0.1 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 // indirect - golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 // indirect + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect + golang.org/x/sys v0.0.0-20190412213103-97732733099d // indirect ) go 1.17 diff --git a/go.sum b/go.sum index 9458f9e..c20fb37 100644 --- a/go.sum +++ b/go.sum @@ -53,10 +53,23 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/kaniko.go b/kaniko.go index 3400481..ee2b5f0 100644 --- a/kaniko.go +++ b/kaniko.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/drone/drone-kaniko/cmd/artifact" + "golang.org/x/mod/semver" ) type ( @@ -16,6 +17,7 @@ type ( Dockerfile string // Docker build Dockerfile Context string // Docker build context Tags []string // Docker build tags + AutoTag bool // Set this to create semver-tagged labels Args []string // Docker build args Target string // Docker build target Repo string // Docker build repository @@ -29,6 +31,7 @@ type ( NoPush bool // Set this flag if you only want to build the image, without pushing to a registry Verbosity string // Log level } + // Artifact defines content of artifact file Artifact struct { Tags []string // Docker artifact tags @@ -45,6 +48,47 @@ type ( } ) +// labelsForTag returns the labels to use for the given tag, subject to the value of AutoTag. +// +// Build information (e.g. +linux_amd64) is carried through to all labels. +// Pre-release information (e.g. -rc1) suppresses major and major+minor auto-labels. +func (b Build) labelsForTag(tag string) (labels []string) { + // We strip "v" off of the beginning of semantic versions, as they are not used in docker tags + const VersionPrefix = "v" + + // Semantic Versions don't allow underscores, so replace them with dashes. + // https://semver.org/ + semverTag := strings.ReplaceAll(tag, "_", "-") + + // Allow tags of the form "1.2.3" as well as "v1.2.3" to avoid confusion. + if withV := VersionPrefix + semverTag; !semver.IsValid(semverTag) && semver.IsValid(withV) { + semverTag = withV + } + + // Pass through tags if auto-tag is not set, or if the tag is not a semantic version + if !b.AutoTag || !semver.IsValid(semverTag) { + return []string{tag} + } + tag = semverTag + + // If the version is pre-release, only the full release should be tagged, not the major/minor versions. + if semver.Prerelease(tag) != "" { + return []string{ + strings.TrimPrefix(tag, VersionPrefix), + } + } + + // tagFor carries any build information from the semantic version through to major and minor tags. + labelFor := func(base string) string { + return strings.TrimPrefix(base, VersionPrefix) + semver.Build(tag) + } + return []string{ + labelFor(semver.Major(tag)), + labelFor(semver.MajorMinor(tag)), + labelFor(semver.Canonical(tag)), + } +} + // Exec executes the plugin step func (p Plugin) Exec() error { if !p.Build.NoPush && p.Build.Repo == "" { @@ -63,7 +107,9 @@ func (p Plugin) Exec() error { // Set the destination repository if !p.Build.NoPush { for _, tag := range p.Build.Tags { - cmdArgs = append(cmdArgs, fmt.Sprintf("--destination=%s:%s", p.Build.Repo, tag)) + for _, label := range p.Build.labelsForTag(tag) { + cmdArgs = append(cmdArgs, fmt.Sprintf("--destination=%s:%s", p.Build.Repo, label)) + } } } // Set the build arguments diff --git a/kaniko_test.go b/kaniko_test.go index 297e062..4bd1f47 100644 --- a/kaniko_test.go +++ b/kaniko_test.go @@ -1 +1,74 @@ package kaniko + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestBuild_labelsForTag(t *testing.T) { + tests := []struct { + name string + tag string + autoTags []string + }{ + { + name: "semver", + tag: "v1.2.3", + autoTags: []string{"1", "1.2", "1.2.3"}, + }, + { + name: "no_patch", + tag: "v1.2", + autoTags: []string{"1", "1.2", "1.2.0"}, + }, + { + name: "only_major", + tag: "v1", + autoTags: []string{"1", "1.0", "1.0.0"}, + }, + { + name: "full_with_build", + tag: "v1.2.3+build-info", + autoTags: []string{"1+build-info", "1.2+build-info", "1.2.3+build-info"}, + }, + { + name: "build_with_underscores", + tag: "v1.2.3+linux_amd64", + autoTags: []string{"1+linux-amd64", "1.2+linux-amd64", "1.2.3+linux-amd64"}, + }, + { + name: "prerelease", + tag: "v1.2.3-rc1", + autoTags: []string{"1.2.3-rc1"}, + }, + { + name: "prerelease_with_build", + tag: "v1.2.3-rc1+bld", + autoTags: []string{"1.2.3-rc1+bld"}, + }, + { + name: "invalid_build", + tag: "v1+bld", // can only include build detail with all three elements + autoTags: []string{"v1+bld"}, + }, + { + name: "accidental_non_semver", + tag: "1.2.3", + autoTags: []string{"1", "1.2", "1.2.3"}, + }, + { + name: "non_semver", + tag: "latest", + autoTags: []string{"latest"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tags := Build{AutoTag: true}.labelsForTag(tt.tag) + if got, want := tags, tt.autoTags; !cmp.Equal(got, want) { + t.Errorf("tagsFor(%q) = %q, want %q", tt.tag, got, want) + } + }) + } +}