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 78d1f97..b28b588 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) + } + } +}