From de43f3afb698f9e141145b8715cad396ac08f3e6 Mon Sep 17 00:00:00 2001 From: Sinkerine <15cm.github@15cm.net> Date: Wed, 9 Feb 2022 23:36:14 -0800 Subject: [PATCH] Port the auto tag feature from docker plugin (#36) * Port the auto tag feature from https://plugins.drone.io/drone-plugins/drone-docker The logic is forked from https://github.com/drone-plugins/drone-docker code base with necessary modification. I've tested it e2e for DockerHub on my Drone server via this plugin image https://hub.docker.com/repository/docker/15cm/drone-kaniko, for both tag pushes and commit pushes. With this change the .drone.yml in this repo should work as intended. Other changes: - Rename the existing "auto tag" flags/code to "expand tag" for a less misleading naming. - ATTENTION: make a breaking change to set default value of "--tags" to empty. Rationale is to expect most users to use the auto tagging feature. When power users want to specify tags, they should always explicitly set tags instead of being surprised by the default "latest" tag. * Change how --auto-tag flag works with other flags The --auto-tag has to be a breaking change. This commit limit the breaking impact to the users who enable the flag. Behaviors of flag combination after this commit: * --auto-tag=false: No changes. * --auto-tag=false,--expand-tag=true,tags=1.0.0: * Old behavior: Should not happen. --expand-tag didn't exist. * New Behavior: Build with [1,1.0,1.0.0] tags. * --auto-tag=true * Old behavior: Build with the "latest" tag. * New behavior: Build with auto detected tags. Abort if auto detection failed. * --auto-tag=true,tags=latest: same as "--auto-tag=true". * --auto-tag=true,tags=1.0.0: * Old behavior: Build with [1,1.0,1.0.0] tags. * New behavior: Abort the build with an error message. * --auto-tag=true,--expand-tag=true,tags=1.0.0: Abort the build with an error message. Also added a test for the integration of the BUILD struct and the tagger package, which is used by kaniko.go. * Update readme to note that expand-tag and auto-tag don't support artifacts --- README.md | 53 +++++++++- cmd/kaniko-docker/main.go | 62 ++++++++---- cmd/kaniko-ecr/main.go | 58 +++++++---- cmd/kaniko-gcr/main.go | 58 +++++++---- go.mod | 1 + go.sum | 2 + kaniko.go | 83 ++++++++++++---- kaniko_test.go | 137 +++++++++++++++++++------- pkg/tagger/tagger.go | 95 ++++++++++++++++++ pkg/tagger/tagger_test.go | 199 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 635 insertions(+), 113 deletions(-) create mode 100644 pkg/tagger/tagger.go create mode 100644 pkg/tagger/tagger_test.go diff --git a/README.md b/README.md index ccbd17e..1ba2cbd 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ docker build \ ``` ## Usage +### Manual Tagging ```console docker run --rm \ @@ -52,14 +53,14 @@ docker run --rm \ plugins/kaniko:linux-amd64 ``` -### Automatic Tagging +With expanded tagging enabled, semantic versions can be passed to PLUGIN_TAGS directly for expansion. -With auto tagging enabled, semantic versions can be passed to PLUGIN_TAGS directly for expansion: +**Note**: this feature only works for build labels. Artifact labels are not supported. ```console docker run --rm \ -e PLUGIN_TAGS=v1.2.3,latest \ - -e PLUGIN_AUTO_TAG=true \ + -e PLUGIN_EXPAND_TAGS=true \ -v $(pwd):/drone \ -w /drone \ plugins/kaniko:linux-amd64 @@ -72,14 +73,58 @@ 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 +To avoid confusion between repo tags and image tags, `PLUGIN_EXPAND_TAGS` 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_EXPAND_TAGS=true \ + -v $(pwd):/drone \ + -w /drone \ + plugins/kaniko:linux-amd64 +``` + +### Auto Tagging +The [auto tag feature](https://plugins.drone.io/drone-plugins/drone-docker/** of docker plugin is also supported. + +When auto tagging is enabled, if any of the case is matched below, a docker build will be pushed with auto generated tags. Otherwise the docker build will be skipped. + +**Note**: this feature only works for build labels. Artifact labels are not supported. + +#### Git Tag Push: + +```console +docker run --rm \ + -e DRONE_COMMIT_REF=refs/tags/v1.2.3 \ + -e PLUGIN_REPO=foo/bar \ + -e PLUGIN_USERNAME=foo \ + -e PLUGIN_PASSWORD=bar \ -e PLUGIN_AUTO_TAG=true \ -v $(pwd):/drone \ -w /drone \ plugins/kaniko:linux-amd64 ``` + +Tags to push: +- 1.2.3 +- 1.2 +- 1 + +#### Git Commit Push in default branch: + +```console +docker run --rm \ + -e DRONE_COMMIT_REF=refs/heads/master \ + -e DRONE_REPO_BRANCH=main \ + -e PLUGIN_REPO=foo/bar \ + -e PLUGIN_USERNAME=foo \ + -e PLUGIN_PASSWORD=bar \ + -e PLUGIN_AUTO_TAG=true \ + -v $(pwd):/drone \ + -w /drone \ + plugins/kaniko:linux-amd64 +``` + +Tags to push: +- latest diff --git a/cmd/kaniko-docker/main.go b/cmd/kaniko-docker/main.go index 1067d39..7f10884 100644 --- a/cmd/kaniko-docker/main.go +++ b/cmd/kaniko-docker/main.go @@ -58,6 +58,16 @@ func main() { Value: ".", EnvVar: "PLUGIN_CONTEXT", }, + cli.StringFlag{ + Name: "drone-commit-ref", + Usage: "git commit ref passed by Drone", + EnvVar: "DRONE_COMMIT_REF", + }, + cli.StringFlag{ + Name: "drone-repo-branch", + Usage: "git repository default branch passed by Drone", + EnvVar: "DRONE_REPO_BRANCH", + }, cli.StringSliceFlag{ Name: "tags", Usage: "build tags", @@ -66,10 +76,20 @@ func main() { FilePath: ".tags", }, cli.BoolFlag{ - Name: "auto_tag", + Name: "expand-tag", Usage: "enable for semver tagging", + EnvVar: "PLUGIN_EXPAND_TAG", + }, + cli.BoolFlag{ + Name: "auto-tag", + Usage: "enable auto generation of build tags", EnvVar: "PLUGIN_AUTO_TAG", }, + cli.StringFlag{ + Name: "auto-tag-suffix", + Usage: "the suffix of auto build tags", + EnvVar: "PLUGIN_AUTO_TAG_SUFFIX", + }, cli.StringSliceFlag{ Name: "args", Usage: "build args", @@ -171,23 +191,27 @@ func run(c *cli.Context) error { plugin := kaniko.Plugin{ Build: kaniko.Build{ - 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"), - Labels: c.StringSlice("custom-labels"), - SkipTlsVerify: c.Bool("skip-tls-verify"), - SnapshotMode: c.String("snapshot-mode"), - EnableCache: c.Bool("enable-cache"), - CacheRepo: c.String("cache-repo"), - CacheTTL: c.Int("cache-ttl"), - DigestFile: defaultDigestFile, - NoPush: noPush, - Verbosity: c.String("verbosity"), - Platform: c.String("platform"), + DroneCommitRef: c.String("drone-commit-ref"), + DroneRepoBranch: c.String("drone-repo-branch"), + Dockerfile: c.String("dockerfile"), + Context: c.String("context"), + Tags: c.StringSlice("tags"), + AutoTag: c.Bool("auto-tag"), + AutoTagSuffix: c.String("auto-tag-suffix"), + ExpandTag: c.Bool("expand-tag"), + Args: c.StringSlice("args"), + Target: c.String("target"), + Repo: c.String("repo"), + Labels: c.StringSlice("custom-labels"), + SkipTlsVerify: c.Bool("skip-tls-verify"), + SnapshotMode: c.String("snapshot-mode"), + EnableCache: c.Bool("enable-cache"), + CacheRepo: c.String("cache-repo"), + CacheTTL: c.Int("cache-ttl"), + DigestFile: defaultDigestFile, + NoPush: noPush, + Verbosity: c.String("verbosity"), + Platform: c.String("platform"), }, Artifact: kaniko.Artifact{ Tags: c.StringSlice("tags"), @@ -238,7 +262,7 @@ func buildRepo(registry, repo string) string { // No custom registry, just return the repo name return repo } - if strings.HasPrefix(repo, registry + "/") { + if strings.HasPrefix(repo, registry+"/") { // Repo already includes the registry prefix // For backward compatibility, we won't add the prefix again. return repo diff --git a/cmd/kaniko-ecr/main.go b/cmd/kaniko-ecr/main.go index 48fea83..7f35e93 100644 --- a/cmd/kaniko-ecr/main.go +++ b/cmd/kaniko-ecr/main.go @@ -71,6 +71,16 @@ func main() { Value: ".", EnvVar: "PLUGIN_CONTEXT", }, + cli.StringFlag{ + Name: "drone-commit-ref", + Usage: "git commit ref passed by Drone", + EnvVar: "DRONE_COMMIT_REF", + }, + cli.StringFlag{ + Name: "drone-repo-branch", + Usage: "git repository default branch passed by Drone", + EnvVar: "DRONE_REPO_BRANCH", + }, cli.StringSliceFlag{ Name: "tags", Usage: "build tags", @@ -79,10 +89,20 @@ func main() { FilePath: ".tags", }, cli.BoolFlag{ - Name: "auto_tag", + Name: "expand-tag", Usage: "enable for semver tagging", + EnvVar: "PLUGIN_EXPAND_TAG", + }, + cli.BoolFlag{ + Name: "auto-tag", + Usage: "enable auto generation of build tags", EnvVar: "PLUGIN_AUTO_TAG", }, + cli.StringFlag{ + Name: "auto-tag-suffix", + Usage: "the suffix of auto build tags", + EnvVar: "PLUGIN_AUTO_TAG_SUFFIX", + }, cli.StringSliceFlag{ Name: "args", Usage: "build args", @@ -242,22 +262,26 @@ func run(c *cli.Context) error { plugin := kaniko.Plugin{ Build: kaniko.Build{ - 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")), - Labels: c.StringSlice("custom-labels"), - SnapshotMode: c.String("snapshot-mode"), - EnableCache: c.Bool("enable-cache"), - CacheRepo: fmt.Sprintf("%s/%s", c.String("registry"), c.String("cache-repo")), - CacheTTL: c.Int("cache-ttl"), - DigestFile: defaultDigestFile, - NoPush: noPush, - Verbosity: c.String("verbosity"), - Platform: c.String("platform"), + DroneCommitRef: c.String("drone-commit-ref"), + DroneRepoBranch: c.String("drone-repo-branch"), + Dockerfile: c.String("dockerfile"), + Context: c.String("context"), + Tags: c.StringSlice("tags"), + AutoTag: c.Bool("auto-tag"), + AutoTagSuffix: c.String("auto-tag-suffix"), + ExpandTag: c.Bool("expand-tag"), + Args: c.StringSlice("args"), + Target: c.String("target"), + Repo: fmt.Sprintf("%s/%s", c.String("registry"), c.String("repo")), + Labels: c.StringSlice("custom-labels"), + SnapshotMode: c.String("snapshot-mode"), + EnableCache: c.Bool("enable-cache"), + CacheRepo: fmt.Sprintf("%s/%s", c.String("registry"), c.String("cache-repo")), + CacheTTL: c.Int("cache-ttl"), + DigestFile: defaultDigestFile, + NoPush: noPush, + Verbosity: c.String("verbosity"), + Platform: c.String("platform"), }, Artifact: kaniko.Artifact{ Tags: c.StringSlice("tags"), diff --git a/cmd/kaniko-gcr/main.go b/cmd/kaniko-gcr/main.go index 1492a7b..ed6eaa2 100644 --- a/cmd/kaniko-gcr/main.go +++ b/cmd/kaniko-gcr/main.go @@ -52,6 +52,16 @@ func main() { Value: ".", EnvVar: "PLUGIN_CONTEXT", }, + cli.StringFlag{ + Name: "drone-commit-ref", + Usage: "git commit ref passed by Drone", + EnvVar: "DRONE_COMMIT_REF", + }, + cli.StringFlag{ + Name: "drone-repo-branch", + Usage: "git repository default branch passed by Drone", + EnvVar: "DRONE_REPO_BRANCH", + }, cli.StringSliceFlag{ Name: "tags", Usage: "build tags", @@ -60,10 +70,20 @@ func main() { FilePath: ".tags", }, cli.BoolFlag{ - Name: "auto_tag", + Name: "expand-tag", Usage: "enable for semver tagging", + EnvVar: "PLUGIN_EXPAND_TAG", + }, + cli.BoolFlag{ + Name: "auto-tag", + Usage: "enable auto generation of build tags", EnvVar: "PLUGIN_AUTO_TAG", }, + cli.StringFlag{ + Name: "auto-tag-suffix", + Usage: "the suffix of auto build tags", + EnvVar: "PLUGIN_AUTO_TAG_SUFFIX", + }, cli.StringSliceFlag{ Name: "args", Usage: "build args", @@ -157,22 +177,26 @@ func run(c *cli.Context) error { plugin := kaniko.Plugin{ Build: kaniko.Build{ - 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")), - Labels: c.StringSlice("custom-labels"), - SnapshotMode: c.String("snapshot-mode"), - EnableCache: c.Bool("enable-cache"), - CacheRepo: fmt.Sprintf("%s/%s", c.String("registry"), c.String("cache-repo")), - CacheTTL: c.Int("cache-ttl"), - DigestFile: defaultDigestFile, - NoPush: noPush, - Verbosity: c.String("verbosity"), - Platform: c.String("platform"), + DroneCommitRef: c.String("drone-commit-ref"), + DroneRepoBranch: c.String("drone-repo-branch"), + Dockerfile: c.String("dockerfile"), + Context: c.String("context"), + Tags: c.StringSlice("tags"), + AutoTag: c.Bool("auto-tag"), + AutoTagSuffix: c.String("auto-tag-suffix"), + ExpandTag: c.Bool("expand-tag"), + Args: c.StringSlice("args"), + Target: c.String("target"), + Repo: fmt.Sprintf("%s/%s", c.String("registry"), c.String("repo")), + Labels: c.StringSlice("custom-labels"), + SnapshotMode: c.String("snapshot-mode"), + EnableCache: c.Bool("enable-cache"), + CacheRepo: fmt.Sprintf("%s/%s", c.String("registry"), c.String("cache-repo")), + CacheTTL: c.Int("cache-ttl"), + DigestFile: defaultDigestFile, + NoPush: noPush, + Verbosity: c.String("verbosity"), + Platform: c.String("platform"), }, Artifact: kaniko.Artifact{ Tags: c.StringSlice("tags"), diff --git a/go.mod b/go.mod index 7802806..23d75db 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ 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/coreos/go-semver v0.3.0 github.com/google/go-cmp v0.5.6 github.com/joho/godotenv v1.3.0 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index c20fb37..81e7944 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.6.2 h1:l504GWCoQi1Pk68vSUFGLmDIEMzRf github.com/aws/aws-sdk-go-v2/service/sts v1.6.2/go.mod h1:RBhoMJB8yFToaCnbe0jNq5Dcdy0jp6LhHqg55rjClkM= github.com/aws/smithy-go v1.7.0 h1:+cLHMRrDZvQ4wk+KuQ9yH6eEg6KZEJ9RI2IkDqnygCg= github.com/aws/smithy-go v1.7.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/kaniko.go b/kaniko.go index a6eb032..498d51b 100644 --- a/kaniko.go +++ b/kaniko.go @@ -8,29 +8,34 @@ import ( "strings" "github.com/drone/drone-kaniko/pkg/artifact" + "github.com/drone/drone-kaniko/pkg/tagger" "golang.org/x/mod/semver" ) type ( // Build defines Docker build parameters. Build struct { - 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 - Labels []string // Label map - SkipTlsVerify bool // Docker skip tls certificate verify for registry - SnapshotMode string // Kaniko snapshot mode - EnableCache bool // Whether to enable kaniko cache - CacheRepo string // Remote repository that will be used to store cached layers - CacheTTL int // Cache timeout in hours - DigestFile string // Digest file location - NoPush bool // Set this flag if you only want to build the image, without pushing to a registry - Verbosity string // Log level - Platform string // Allows to build with another default platform than the host, similarly to docker build --platform + DroneCommitRef string // Drone git commit reference + DroneRepoBranch string // Drone repo branch + Dockerfile string // Docker build Dockerfile + Context string // Docker build context + Tags []string // Docker build tags + AutoTag bool // Set this to auto detect tags from git commits and semver-tagged labels + AutoTagSuffix string // Suffix to append to the auto detect tags + ExpandTag bool // Set this to expand the `Tags` into semver-tagged labels + Args []string // Docker build args + Target string // Docker build target + Repo string // Docker build repository + Labels []string // Label map + SkipTlsVerify bool // Docker skip tls certificate verify for registry + SnapshotMode string // Kaniko snapshot mode + EnableCache bool // Whether to enable kaniko cache + CacheRepo string // Remote repository that will be used to store cached layers + CacheTTL int // Cache timeout in hours + DigestFile string // Digest file location + NoPush bool // Set this flag if you only want to build the image, without pushing to a registry + Verbosity string // Log level + Platform string // Allows to build with another default platform than the host, similarly to docker build --platform } // Artifact defines content of artifact file @@ -49,7 +54,7 @@ type ( } ) -// labelsForTag returns the labels to use for the given tag, subject to the value of AutoTag. +// labelsForTag returns the labels to use for the given tag, subject to the value of ExpandTag. // // 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. @@ -66,8 +71,8 @@ func (b Build) labelsForTag(tag string) (labels []string) { 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) { + // Pass through tags if expand-tag is not set, or if the tag is not a semantic version + if !b.ExpandTag || !semver.IsValid(semverTag) { return []string{tag} } tag = semverTag @@ -90,6 +95,30 @@ func (b Build) labelsForTag(tag string) (labels []string) { } } +// Returns the auto detected tags. See the AutoTag section of +// https://plugins.drone.io/drone-plugins/drone-docker/ for more info. +func (b Build) AutoTags() (tags []string, err error) { + if len(b.Tags) > 1 || len(b.Tags) == 1 && b.Tags[0] != "latest" { + err = fmt.Errorf("The auto-tag flag does not work with user provided tags %s", b.Tags) + return + } + // We have tried the best to prevent enabling auto-tag and passing in + // user specified at the same time. Starts to auto detect tags. + // Note: passing in a "latest" tag with auto-tag enabled won't trigger the + // early returns above, because we cannot tell if the tag is provided by + // the default value of by the users. + commitRef := b.DroneCommitRef + if !tagger.UseAutoTag(commitRef, b.DroneRepoBranch) { + err = fmt.Errorf("Could not auto detect the tag. Skipping automated docker build for commit %s", commitRef) + return + } + tags, err = tagger.AutoTagsSuffix(commitRef, b.AutoTagSuffix) + if err != nil { + err = fmt.Errorf("Invalid semantic version when auto detecting the tag. Skipping automated docker build for %s.", commitRef) + } + return +} + // Exec executes the plugin step func (p Plugin) Exec() error { if !p.Build.NoPush && p.Build.Repo == "" { @@ -100,6 +129,18 @@ func (p Plugin) Exec() error { return fmt.Errorf("dockerfile does not exist at path: %s", p.Build.Dockerfile) } + var tags = p.Build.Tags + if p.Build.AutoTag && p.Build.ExpandTag { + return fmt.Errorf("The auto-tag flag conflicts with the expand-tag flag") + } + if p.Build.AutoTag { + var err error + tags, err = p.Build.AutoTags() + if err != nil { + return err + } + } + cmdArgs := []string{ fmt.Sprintf("--dockerfile=%s", p.Build.Dockerfile), fmt.Sprintf("--context=dir://%s", p.Build.Context), @@ -107,7 +148,7 @@ func (p Plugin) Exec() error { // Set the destination repository if !p.Build.NoPush { - for _, tag := range p.Build.Tags { + for _, tag := range tags { for _, label := range p.Build.labelsForTag(tag) { cmdArgs = append(cmdArgs, fmt.Sprintf("--destination=%s:%s", p.Build.Repo, label)) } diff --git a/kaniko_test.go b/kaniko_test.go index 4bd1f47..8b87252 100644 --- a/kaniko_test.go +++ b/kaniko_test.go @@ -8,67 +8,134 @@ import ( func TestBuild_labelsForTag(t *testing.T) { tests := []struct { - name string - tag string - autoTags []string + name string + tag string + expandTags []string }{ { - name: "semver", - tag: "v1.2.3", - autoTags: []string{"1", "1.2", "1.2.3"}, + name: "semver", + tag: "v1.2.3", + expandTags: []string{"1", "1.2", "1.2.3"}, }, { - name: "no_patch", - tag: "v1.2", - autoTags: []string{"1", "1.2", "1.2.0"}, + name: "no_patch", + tag: "v1.2", + expandTags: []string{"1", "1.2", "1.2.0"}, }, { - name: "only_major", - tag: "v1", - autoTags: []string{"1", "1.0", "1.0.0"}, + name: "only_major", + tag: "v1", + expandTags: []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: "full_with_build", + tag: "v1.2.3+build-info", + expandTags: []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: "build_with_underscores", + tag: "v1.2.3+linux_amd64", + expandTags: []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", + tag: "v1.2.3-rc1", + expandTags: []string{"1.2.3-rc1"}, }, { - name: "prerelease_with_build", - tag: "v1.2.3-rc1+bld", - autoTags: []string{"1.2.3-rc1+bld"}, + name: "prerelease_with_build", + tag: "v1.2.3-rc1+bld", + expandTags: []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: "invalid_build", + tag: "v1+bld", // can only include build detail with all three elements + expandTags: []string{"v1+bld"}, }, { - name: "accidental_non_semver", - tag: "1.2.3", - autoTags: []string{"1", "1.2", "1.2.3"}, + name: "accidental_non_semver", + tag: "1.2.3", + expandTags: []string{"1", "1.2", "1.2.3"}, }, { - name: "non_semver", - tag: "latest", - autoTags: []string{"latest"}, + name: "non_semver", + tag: "latest", + expandTags: []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) { + tags := Build{ExpandTag: true}.labelsForTag(tt.tag) + if got, want := tags, tt.expandTags; !cmp.Equal(got, want) { t.Errorf("tagsFor(%q) = %q, want %q", tt.tag, got, want) } }) } } + +func TestBuild_AutoTags(t *testing.T) { + tests := []struct { + name string + repoBranch string + commitRef string + autoTagSuffix string + expectedTags []string + }{ + { + name: "commit push", + repoBranch: "master", + commitRef: "refs/heads/master", + autoTagSuffix: "", + expectedTags: []string{"latest"}, + }, + { + name: "tag push", + repoBranch: "master", + commitRef: "refs/tags/v1.0.0", + autoTagSuffix: "", + expectedTags: []string{ + "1", + "1.0", + "1.0.0", + }, + }, + { + name: "tag push", + repoBranch: "master", + commitRef: "refs/tags/v1.0.0", + autoTagSuffix: "linux-amd64", + expectedTags: []string{ + "1-linux-amd64", + "1.0-linux-amd64", + "1.0.0-linux-amd64", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := Build{DroneCommitRef: tt.commitRef, DroneRepoBranch: tt.repoBranch, AutoTag: true} + if tt.autoTagSuffix != "" { + b.AutoTagSuffix = tt.autoTagSuffix + } + tags, err := b.AutoTags() + if err != nil { + t.Errorf("Unexpected err %q", err) + } + if got, want := tags, tt.expectedTags; !cmp.Equal(got, want) { + t.Errorf("auto detected tags = %q, wanted = %q", got, want) + } + }) + } + t.Run("flag conflict", func(t *testing.T) { + b := Build{ + DroneCommitRef: "refs/tags/v1.0.0", + DroneRepoBranch: "master", + AutoTag: true, + Tags: []string{"v1"}, + } + _, err := b.AutoTags() + if err == nil { + t.Errorf("Expect flag conflict error") + } + }) +} diff --git a/pkg/tagger/tagger.go b/pkg/tagger/tagger.go new file mode 100644 index 0000000..bb0720c --- /dev/null +++ b/pkg/tagger/tagger.go @@ -0,0 +1,95 @@ +// A fork of https://github.com/drone-plugins/drone-docker/blob/master/tags.go + +package tagger + +import ( + "fmt" + "strings" + + "github.com/coreos/go-semver/semver" +) + +// AutoTagsSuffix returns a set of default suggested tags +// based on the commit ref with an attached suffix. +func AutoTagsSuffix(ref, suffix string) ([]string, error) { + tags, err := AutoTags(ref) + if err != nil { + return nil, err + } + if len(suffix) == 0 { + return tags, nil + } + for i, tag := range tags { + if tag == "latest" { + tags[i] = suffix + } else { + tags[i] = fmt.Sprintf("%s-%s", tag, suffix) + } + } + return tags, nil +} + +func splitOff(input string, delim string) string { + parts := strings.SplitN(input, delim, 2) + + if len(parts) == 2 { + return parts[0] + } + + return input +} + +// AutoTags returns a set of default suggested tags based on +// the commit ref. +func AutoTags(ref string) ([]string, error) { + if !strings.HasPrefix(ref, "refs/tags/") { + return []string{"latest"}, nil + } + v := stripTagPrefix(ref) + version, err := semver.NewVersion(v) + if err != nil { + return []string{"latest"}, err + } + if version.PreRelease != "" || version.Metadata != "" { + return []string{ + version.String(), + }, nil + } + + v = stripTagPrefix(ref) + v = splitOff(splitOff(v, "+"), "-") + dotParts := strings.SplitN(v, ".", 3) + + if version.Major == 0 { + return []string{ + fmt.Sprintf("%0*d.%0*d", len(dotParts[0]), version.Major, len(dotParts[1]), version.Minor), + fmt.Sprintf("%0*d.%0*d.%0*d", len(dotParts[0]), version.Major, len(dotParts[1]), version.Minor, len(dotParts[2]), version.Patch), + }, nil + } + return []string{ + fmt.Sprintf("%0*d", len(dotParts[0]), version.Major), + fmt.Sprintf("%0*d.%0*d", len(dotParts[0]), version.Major, len(dotParts[1]), version.Minor), + fmt.Sprintf("%0*d.%0*d.%0*d", len(dotParts[0]), version.Major, len(dotParts[1]), version.Minor, len(dotParts[2]), version.Patch), + }, nil +} + +// UseAutoTag for keep only default branch for latest tag. +func UseAutoTag(ref, defaultBranch string) bool { + if strings.HasPrefix(ref, "refs/tags/") { + return true + } + if stripHeadPrefix(ref) == defaultBranch { + return true + } + return false +} + +func stripHeadPrefix(ref string) string { + return strings.TrimPrefix(ref, "refs/heads/") +} + +func stripTagPrefix(ref string) string { + ref = strings.TrimPrefix(ref, "refs/tags/") + ref = strings.TrimPrefix(ref, "v") + return ref +} diff --git a/pkg/tagger/tagger_test.go b/pkg/tagger/tagger_test.go new file mode 100644 index 0000000..fddc8aa --- /dev/null +++ b/pkg/tagger/tagger_test.go @@ -0,0 +1,199 @@ +// A fork of https://github.com/drone-plugins/drone-docker/blob/master/tags_test.go + +package tagger + +import ( + "reflect" + "testing" +) + +func Test_stripTagPrefix(t *testing.T) { + var tests = []struct { + Before string + After string + }{ + {"refs/tags/1.0.0", "1.0.0"}, + {"refs/tags/v1.0.0", "1.0.0"}, + {"v1.0.0", "1.0.0"}, + } + + for _, test := range tests { + got, want := stripTagPrefix(test.Before), test.After + if got != want { + t.Errorf("Got tag %s, want %s", got, want) + } + } +} + +func TestAutoTags(t *testing.T) { + var tests = []struct { + Before string + After []string + }{ + {"", []string{"latest"}}, + {"refs/heads/master", []string{"latest"}}, + {"refs/tags/0.9.0", []string{"0.9", "0.9.0"}}, + {"refs/tags/1.0.0", []string{"1", "1.0", "1.0.0"}}, + {"refs/tags/v1.0.0", []string{"1", "1.0", "1.0.0"}}, + {"refs/tags/v1.0.0-alpha.1", []string{"1.0.0-alpha.1"}}, + } + + for _, test := range tests { + tags, err := AutoTags(test.Before) + if err != nil { + t.Error(err) + continue + } + got, want := tags, test.After + if !reflect.DeepEqual(got, want) { + t.Errorf("Got tag %v, want %v", got, want) + } + } +} + +func TestAutoTagsError(t *testing.T) { + var tests = []string{ + "refs/tags/x1.0.0", + "refs/tags/20190203", + } + + for _, test := range tests { + _, err := AutoTags(test) + if err == nil { + t.Errorf("Expect tag error for %s", test) + } + } +} + +func TestAutoTagsSuffix(t *testing.T) { + var tests = []struct { + Before string + Suffix string + After []string + }{ + // without suffix + { + After: []string{"latest"}, + }, + { + Before: "refs/tags/v1.0.0", + After: []string{ + "1", + "1.0", + "1.0.0", + }, + }, + // with suffix + { + Suffix: "linux-amd64", + After: []string{"linux-amd64"}, + }, + { + Before: "refs/tags/v1.0.0", + Suffix: "linux-amd64", + After: []string{ + "1-linux-amd64", + "1.0-linux-amd64", + "1.0.0-linux-amd64", + }, + }, + { + Suffix: "nanoserver", + After: []string{"nanoserver"}, + }, + { + Before: "refs/tags/v1.9.2", + Suffix: "nanoserver", + After: []string{ + "1-nanoserver", + "1.9-nanoserver", + "1.9.2-nanoserver", + }, + }, + { + Before: "refs/tags/v18.06.0", + Suffix: "nanoserver", + After: []string{ + "18-nanoserver", + "18.06-nanoserver", + "18.06.0-nanoserver", + }, + }, + } + + for _, test := range tests { + tag, err := AutoTagsSuffix(test.Before, test.Suffix) + if err != nil { + t.Error(err) + continue + } + got, want := tag, test.After + if !reflect.DeepEqual(got, want) { + t.Errorf("Got tag %v, want %v", got, want) + } + } +} + +func Test_stripHeadPrefix(t *testing.T) { + type args struct { + ref string + } + tests := []struct { + args args + want string + }{ + { + args: args{ + ref: "refs/heads/master", + }, + want: "master", + }, + } + for _, tt := range tests { + if got := stripHeadPrefix(tt.args.ref); got != tt.want { + t.Errorf("stripHeadPrefix() = %v, want %v", got, tt.want) + } + } +} + +func TestUseAutoTag(t *testing.T) { + type args struct { + ref string + defaultBranch string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "latest tag for default branch", + args: args{ + ref: "refs/heads/master", + defaultBranch: "master", + }, + want: true, + }, + { + name: "build from tags", + args: args{ + ref: "refs/tags/v1.0.0", + defaultBranch: "master", + }, + want: true, + }, + { + name: "skip build for not default branch", + args: args{ + ref: "refs/heads/develop", + defaultBranch: "master", + }, + want: false, + }, + } + for _, tt := range tests { + if got := UseAutoTag(tt.args.ref, tt.args.defaultBranch); got != tt.want { + t.Errorf("%q. UseAutoTag() = %v, want %v", tt.name, got, tt.want) + } + } +}