Skip to content

Commit 37adcd0

Browse files
committed
container image tag patterns
1 parent eadd2b9 commit 37adcd0

File tree

5 files changed

+177
-36
lines changed

5 files changed

+177
-36
lines changed

cmd/bob/bobfile.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,38 @@ type DevShellCommand struct {
144144
Important bool `json:"important"` // important commands are shown as pro-tips on "$ bob dev"
145145
}
146146

147+
type Condition struct {
148+
IsDefaultBranch *bool `json:"is_default_branch"`
149+
}
150+
151+
func (c *Condition) Passes(buildCtx *BuildContext) bool {
152+
if c == nil { // no conditions
153+
return true
154+
}
155+
156+
hasFilter := func(b *bool) bool { return b != nil }
157+
158+
if hasFilter(c.IsDefaultBranch) {
159+
if *c.IsDefaultBranch != buildCtx.IsDefaultBranch {
160+
return false
161+
}
162+
}
163+
164+
return true
165+
}
166+
167+
type TagSpec struct {
168+
Pattern string `json:"pattern"`
169+
UseIf *Condition `json:"use_if"`
170+
}
171+
147172
type DockerImageSpec struct {
148-
Image string `json:"image"`
149-
DockerfilePath string `json:"dockerfile_path"`
150-
AuthType *string `json:"auth_type"` // creds_from_env
151-
Platforms []string `json:"platforms,omitempty"` // if set, uses buildx
152-
TagLatest bool `json:"tag_latest"`
173+
Image string `json:"image"`
174+
DockerfilePath string `json:"dockerfile_path"`
175+
AuthType *string `json:"auth_type"` // creds_from_env
176+
Platforms []string `json:"platforms,omitempty"` // if set, uses buildx
177+
Tags []TagSpec `json:"tags"`
178+
TagLatest bool `json:"tag_latest"` // deprecated
153179
}
154180

155181
// FIXME: Bobfile should actually be read only after correct

cmd/bob/build.go

Lines changed: 16 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,14 @@ func runBuilder(builder BuilderSpec, buildCtx *BuildContext, opDesc string, cmdT
126126
}
127127

128128
func buildAndPushOneDockerImage(dockerImage DockerImageSpec, buildCtx *BuildContext) error {
129-
tagWithoutVersion := dockerImage.Image
130-
tag := tagWithoutVersion + ":" + buildCtx.RevisionId.FriendlyRevisionId
131-
tagLatest := tagWithoutVersion + ":latest"
132-
dockerfilePath := dockerImage.DockerfilePath
133-
134-
// only tag latest from the default branch (= main / master / ...), because it is expected
135-
// that non-default branch builds are dev/experimental builds.
136-
shouldTagLatest := dockerImage.TagLatest && buildCtx.IsDefaultBranch
129+
tagSpecs := func() []TagSpec {
130+
if len(dockerImage.Tags) > 0 {
131+
return dockerImage.Tags
132+
} else {
133+
return createBackwardsCompatTagSpecs(dockerImage.TagLatest)
134+
}
135+
}()
136+
tags := expandTagSpecs(tagSpecs, buildCtx, dockerImage.Image)
137137

138138
labelArgs := []string{
139139
"--label=org.opencontainers.image.created=" + time.Now().UTC().Format(time.RFC3339),
@@ -151,9 +151,9 @@ func buildAndPushOneDockerImage(dockerImage DockerImageSpec, buildCtx *BuildCont
151151
// "" => "."
152152
// "Dockerfile" => "."
153153
// "subdir/Dockerfile" => "subdir"
154-
buildContextDir := filepath.Dir(dockerfilePath)
154+
buildContextDir := filepath.Dir(dockerImage.DockerfilePath)
155155

156-
printHeading(fmt.Sprintf("Building %s", tag))
156+
printHeading(fmt.Sprintf("Building %s", dockerImage.Image))
157157

158158
// use buildx when platforms set. it's almost same as "$ docker build" but it almost transparently
159159
// supports cross-architecture builds via binftm_misc + QEMU userspace emulation
@@ -166,16 +166,11 @@ func buildAndPushOneDockerImage(dockerImage DockerImageSpec, buildCtx *BuildCont
166166
"buildx",
167167
"build",
168168
"--platform", strings.Join(dockerImage.Platforms, ","),
169-
"--file", dockerfilePath,
170-
"--tag=" + tag,
169+
"--file", dockerImage.DockerfilePath,
171170
}
172171

172+
args = append(args, dockerTagArgs(tags)...)
173173
args = append(args, labelArgs...)
174-
175-
if shouldTagLatest {
176-
args = append(args, "--tag="+tagLatest)
177-
}
178-
179174
args = append(args, buildContextDir)
180175

181176
if buildCtx.PublishArtefacts {
@@ -190,8 +185,8 @@ func buildAndPushOneDockerImage(dockerImage DockerImageSpec, buildCtx *BuildCont
190185

191186
dockerBuildArgs := []string{"docker",
192187
"build",
193-
"--file", dockerfilePath,
194-
"--tag", tag}
188+
"--file", dockerImage.DockerfilePath}
189+
dockerBuildArgs = append(dockerBuildArgs, dockerTagArgs(tags)...)
195190
dockerBuildArgs = append(dockerBuildArgs, labelArgs...)
196191
dockerBuildArgs = append(dockerBuildArgs, buildContextDir)
197192

@@ -218,21 +213,11 @@ func buildAndPushOneDockerImage(dockerImage DockerImageSpec, buildCtx *BuildCont
218213
return nil
219214
}
220215

221-
if err := pushTag(tag); err != nil {
222-
return err
223-
}
224-
225-
if shouldTagLatest {
226-
if err := exec.Command("docker", "tag", tag, tagLatest).Run(); err != nil {
227-
return fmt.Errorf("tagging failed %s -> %s failed: %v", tag, tagLatest, err)
228-
}
229-
230-
if err := pushTag(tagLatest); err != nil {
216+
for _, tag := range tags {
217+
if err := pushTag(tag); err != nil {
231218
return err
232219
}
233220
}
234-
235-
return nil
236221
}
237222

238223
return nil

cmd/bob/docker.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ func buildBuilder(bobfile *Bobfile, builder *BuilderSpec) error {
111111
return nil
112112
}
113113

114+
func dockerTagArgs(tags []string) []string {
115+
asArgs := []string{}
116+
for _, tag := range tags {
117+
asArgs = append(asArgs, fmt.Sprintf("--tag=%s", tag))
118+
}
119+
return asArgs
120+
}
121+
114122
func dockerRelayEnvVars(
115123
dockerArgs []string,
116124
revisionId *versioncontrol.RevisionId,

cmd/bob/tags.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
func expandTagSpecs(specs []TagSpec, buildCtx *BuildContext, imagePrefix string) []string {
9+
createTag := func(tag string) string { return fmt.Sprintf("%s:%s", imagePrefix, tag) }
10+
11+
tags := []string{}
12+
13+
for _, spec := range specs {
14+
if !spec.UseIf.Passes(buildCtx) {
15+
continue
16+
}
17+
18+
placeholdersReplaced := strings.ReplaceAll(spec.Pattern, "{rev_short}", buildCtx.RevisionId.RevisionIdShort)
19+
placeholdersReplaced = strings.ReplaceAll(placeholdersReplaced, "{rev_friendly}", buildCtx.RevisionId.FriendlyRevisionId)
20+
21+
tags = append(tags, createTag(placeholdersReplaced))
22+
}
23+
24+
return tags
25+
}
26+
27+
// backwards compat: model old behaviour on top of newer `TagSpec` facility:
28+
//
29+
// 1. `{rev_friendly}` tag gets always pushed
30+
// 2. `latest` gets pushed if `tag_latest=true` and if we're in default branch
31+
func createBackwardsCompatTagSpecs(maybeTagLatest bool) []TagSpec {
32+
tags := []TagSpec{
33+
{
34+
Pattern: "{rev_friendly}",
35+
},
36+
}
37+
38+
if maybeTagLatest {
39+
true_ := true
40+
41+
tags = append(tags, TagSpec{
42+
Pattern: "latest",
43+
UseIf: &Condition{
44+
IsDefaultBranch: &true_,
45+
},
46+
})
47+
}
48+
49+
return tags
50+
}

cmd/bob/tags_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/function61/gokit/testing/assert"
8+
"github.com/function61/turbobob/pkg/versioncontrol"
9+
)
10+
11+
func TestExpandTagSpecs(t *testing.T) {
12+
true_ := true
13+
14+
exampleRevision := &versioncontrol.RevisionId{
15+
RevisionId: "df9e1a0b32d41977b49742d57702fbae0392c49a",
16+
RevisionIdShort: "df9e1a0b",
17+
FriendlyRevisionId: "20240616_0924_df9e1a0b",
18+
}
19+
20+
defaultBuildContext := &BuildContext{RevisionId: exampleRevision}
21+
22+
for _, tc := range []struct {
23+
input TagSpec
24+
buildContext *BuildContext
25+
output string
26+
}{
27+
{
28+
input: TagSpec{
29+
Pattern: "latest",
30+
},
31+
buildContext: &BuildContext{
32+
RevisionId: exampleRevision,
33+
IsDefaultBranch: true,
34+
},
35+
output: "[joonas:latest]",
36+
},
37+
{
38+
input: TagSpec{
39+
Pattern: "latest",
40+
UseIf: &Condition{
41+
IsDefaultBranch: &true_,
42+
},
43+
},
44+
buildContext: &BuildContext{
45+
RevisionId: exampleRevision,
46+
IsDefaultBranch: false,
47+
},
48+
output: "[]",
49+
},
50+
{
51+
input: TagSpec{
52+
Pattern: "sha-{rev_short}",
53+
},
54+
buildContext: defaultBuildContext,
55+
output: "[joonas:sha-df9e1a0b]",
56+
},
57+
{
58+
input: TagSpec{
59+
Pattern: "{rev_friendly}",
60+
},
61+
buildContext: defaultBuildContext,
62+
output: "[joonas:20240616_0924_df9e1a0b]",
63+
},
64+
} {
65+
tc := tc // pin
66+
67+
t.Run(tc.output, func(t *testing.T) {
68+
tags := expandTagSpecs([]TagSpec{tc.input}, tc.buildContext, "joonas")
69+
assert.Equal(t, fmt.Sprintf("%v", tags), tc.output)
70+
})
71+
}
72+
}

0 commit comments

Comments
 (0)