Skip to content

Commit b899eb6

Browse files
bomokoBlaize Kayeshreddedbacon
authored
feat: variable replacement support for lagoon.base.image label (#378)
* Adds ability to specify tags in refreshimages * removes println * refactor: just use one tag with variable replacement * test: add test with lagoon and docker-compose files --------- Co-authored-by: Blaize Kaye <blaize.kaye@amazee.io> Co-authored-by: shreddedbacon <b@benjackson.email>
1 parent d039fc9 commit b899eb6

File tree

6 files changed

+234
-9
lines changed

6 files changed

+234
-9
lines changed

cmd/identify_imagebuild_test.go

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"testing"
77

8+
"github.com/andreyvit/diff"
89
"github.com/uselagoon/build-deploy-tool/internal/dbaasclient"
910
"github.com/uselagoon/build-deploy-tool/internal/generator"
1011
"github.com/uselagoon/build-deploy-tool/internal/helpers"
@@ -752,6 +753,61 @@ func TestImageBuildConfigurationIdentification(t *testing.T) {
752753
},
753754
},
754755
},
756+
{
757+
name: "test12 Force Pull Base Images with variable replacement",
758+
args: testdata.GetSeedData(
759+
testdata.TestData{
760+
Namespace: "example-project-main",
761+
ProjectName: "example-project",
762+
EnvironmentName: "main",
763+
Branch: "main",
764+
LagoonYAML: "internal/testdata/basic/lagoon.forcebaseimagepull-2.yml",
765+
ProjectVariables: []lagoon.EnvironmentVariable{
766+
{
767+
Name: "BASE_IMAGE_TAG",
768+
Value: "my-tag",
769+
Scope: "build",
770+
},
771+
{
772+
Name: "BASE_IMAGE_REPO",
773+
Value: "my-repo",
774+
Scope: "build",
775+
},
776+
},
777+
}, true),
778+
want: imageBuild{
779+
BuildKit: helpers.BoolPtr(true),
780+
BuildArguments: map[string]string{
781+
"BASE_IMAGE_TAG": "my-tag",
782+
"BASE_IMAGE_REPO": "my-repo",
783+
"LAGOON_BUILD_NAME": "lagoon-build-abcdefg",
784+
"LAGOON_PROJECT": "example-project",
785+
"LAGOON_ENVIRONMENT": "main",
786+
"LAGOON_ENVIRONMENT_TYPE": "production",
787+
"LAGOON_BUILD_TYPE": "branch",
788+
"LAGOON_GIT_SOURCE_REPOSITORY": "ssh://git@example.com/lagoon-demo.git",
789+
"LAGOON_KUBERNETES": "remote-cluster1",
790+
"LAGOON_GIT_SHA": "abcdefg123456",
791+
"LAGOON_GIT_BRANCH": "main",
792+
"NODE_IMAGE": "example-project-main-node",
793+
"LAGOON_SSH_PRIVATE_KEY": "-----BEGIN OPENSSH PRIVATE KEY-----\nthisisafakekey\n-----END OPENSSH PRIVATE KEY-----",
794+
},
795+
ForcePullImages: []string{
796+
"registry.com/my-repo/imagename:my-tag",
797+
},
798+
Images: []imageBuilds{
799+
{
800+
Name: "node",
801+
ImageBuild: generator.ImageBuild{
802+
BuildImage: "harbor.example/example-project/main/node:latest",
803+
Context: "internal/testdata/basic/docker",
804+
DockerFile: "basic.dockerfile",
805+
TemporaryImage: "example-project-main-node",
806+
},
807+
},
808+
},
809+
},
810+
},
755811
}
756812
for _, tt := range tests {
757813
t.Run(tt.name, func(t *testing.T) {
@@ -788,10 +844,10 @@ func TestImageBuildConfigurationIdentification(t *testing.T) {
788844
t.Errorf("%v", err)
789845
}
790846

791-
oJ, _ := json.Marshal(out)
792-
wJ, _ := json.Marshal(tt.want)
847+
oJ, _ := json.MarshalIndent(out, "", " ")
848+
wJ, _ := json.MarshalIndent(tt.want, "", " ")
793849
if string(oJ) != string(wJ) {
794-
t.Errorf("returned output %v doesn't match want %v", string(oJ), string(wJ))
850+
t.Errorf("ImageBuildConfigurationIdentification() = \n%v", diff.LineDiff(string(oJ), string(wJ)))
795851
}
796852
t.Cleanup(func() {
797853
helpers.UnsetEnvVars(tt.vars)

internal/generator/helpers_generator.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"regexp"
99
"strings"
1010

11+
"github.com/distribution/reference"
12+
1113
"github.com/spf13/cobra"
1214
"github.com/uselagoon/build-deploy-tool/internal/dbaasclient"
1315
"github.com/uselagoon/build-deploy-tool/internal/lagoon"
@@ -271,3 +273,44 @@ func getDBaasEnvironment(
271273
}
272274
return exists, nil
273275
}
276+
277+
var exp = regexp.MustCompile(`(\\*)\$\{(.+?)(?:(\:\-)(.*?))?\}`)
278+
279+
func determineRefreshImage(serviceName, imageName string, envVars []lagoon.EnvironmentVariable) (string, []error) {
280+
errs := []error{}
281+
parsed := exp.ReplaceAllStringFunc(string(imageName), func(match string) string {
282+
tagvalue := ""
283+
re := regexp.MustCompile(`\${?(\w+)?(?::-(\w+))?}?`)
284+
matches := re.FindStringSubmatch(match)
285+
if len(matches) > 0 {
286+
tv := ""
287+
envVarKey := matches[1]
288+
defaultVal := matches[2] //This could be empty
289+
for _, v := range envVars {
290+
if v.Name == envVarKey {
291+
tv = v.Value
292+
}
293+
}
294+
if tv == "" {
295+
if defaultVal != "" {
296+
tagvalue = defaultVal
297+
} else {
298+
errs = append(errs, fmt.Errorf("the 'lagoon.base.image' label defined on service %s in the docker-compose file is invalid ('%s') - no matching variable or fallback found to replace requested variable %s", serviceName, imageName, envVarKey))
299+
}
300+
} else {
301+
tagvalue = tv
302+
}
303+
}
304+
return tagvalue
305+
})
306+
if parsed == imageName {
307+
if !reference.ReferenceRegexp.MatchString(parsed) {
308+
if strings.Contains(parsed, "$") {
309+
errs = append(errs, fmt.Errorf("the 'lagoon.base.image' label defined on service %s in the docker-compose file is invalid ('%s') - variables are defined incorrectly, must contain curly brackets (example: '${VARIABLE}')", serviceName, imageName))
310+
} else {
311+
errs = append(errs, fmt.Errorf("the 'lagoon.base.image' label defined on service %s in the docker-compose file is invalid ('%s') - please ensure it conforms to the structure `[REGISTRY_HOST[:REGISTRY_PORT]/]REPOSITORY[:TAG|@DIGEST]`", serviceName, imageName))
312+
}
313+
}
314+
}
315+
return parsed, errs
316+
}

internal/generator/helpers_generator_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,89 @@ func Test_checkDuplicateCronjobs(t *testing.T) {
8181
})
8282
}
8383
}
84+
85+
func Test_determineRefreshImage(t *testing.T) {
86+
type args struct {
87+
serviceName string
88+
imageName string
89+
envVars []lagoon.EnvironmentVariable
90+
}
91+
tests := []struct {
92+
name string
93+
args args
94+
want string
95+
wantErr bool
96+
}{
97+
{
98+
name: "Identity function",
99+
args: args{
100+
serviceName: "testservice",
101+
imageName: "image/name:latest",
102+
envVars: nil,
103+
},
104+
want: "image/name:latest",
105+
wantErr: false,
106+
},
107+
{
108+
name: "Fails with no matching variable in envvars",
109+
args: args{
110+
serviceName: "testservice",
111+
imageName: "image/name:${NOENVVAR}",
112+
envVars: nil,
113+
},
114+
want: "",
115+
wantErr: true,
116+
},
117+
{
118+
name: "Fails with variable missing curly brackets",
119+
args: args{
120+
serviceName: "testservice",
121+
imageName: "image/name:$NOENVVAR",
122+
envVars: nil,
123+
},
124+
want: "",
125+
wantErr: true,
126+
},
127+
{
128+
name: "Tag with simple arg - fallback to default",
129+
args: args{
130+
serviceName: "testservice",
131+
imageName: "image/name:${ENVVAR:-sometag}",
132+
envVars: nil,
133+
},
134+
want: "image/name:sometag",
135+
wantErr: false,
136+
},
137+
{
138+
name: "Tag with env var that works",
139+
args: args{
140+
serviceName: "testservice",
141+
imageName: "image/name:${ENVVAR:-sometag}",
142+
envVars: []lagoon.EnvironmentVariable{
143+
{
144+
Name: "ENVVAR",
145+
Value: "injectedTag",
146+
},
147+
},
148+
},
149+
want: "image/name:injectedTag",
150+
wantErr: false,
151+
},
152+
}
153+
for _, tt := range tests {
154+
t.Run(tt.name, func(t *testing.T) {
155+
got, errs := determineRefreshImage(tt.args.serviceName, tt.args.imageName, tt.args.envVars)
156+
if len(errs) > 0 && !tt.wantErr {
157+
for idx, err := range errs {
158+
t.Errorf("determineRefreshImage() error = %v, wantErr %v", err, tt.wantErr)
159+
if idx+1 == len(errs) {
160+
return
161+
}
162+
}
163+
}
164+
if got != tt.want && !tt.wantErr {
165+
t.Errorf("determineRefreshImage() got = %v, want %v", got, tt.want)
166+
}
167+
})
168+
}
169+
}

internal/generator/services.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import (
77
"strconv"
88
"strings"
99

10-
"github.com/distribution/reference"
11-
1210
composetypes "github.com/compose-spec/compose-go/types"
1311
"github.com/uselagoon/build-deploy-tool/internal/helpers"
1412
"github.com/uselagoon/build-deploy-tool/internal/lagoon"
@@ -286,13 +284,22 @@ func composeToServiceValues(
286284
}
287285
}
288286

287+
// if any `lagoon.base.image` labels are set, we note them for docker pulling
288+
// this allows us to refresh the docker-host's cache in cases where an image
289+
// may have an update without a change in tag (i.e. "latest" tagged images)
289290
baseimage := lagoon.CheckDockerComposeLagoonLabel(composeServiceValues.Labels, "lagoon.base.image")
290291
if baseimage != "" {
291-
// First, let's ensure that the structure of the base image is valid
292-
if !reference.ReferenceRegexp.MatchString(baseimage) {
293-
return nil, fmt.Errorf("the 'lagoon.base.image' label defined on service %s in the docker-compose file is invalid ('%s') - please ensure it conforms to the structure `[REGISTRY_HOST[:REGISTRY_PORT]/]REPOSITORY[:TAG|@DIGEST]`", composeService, baseimage)
292+
baseImageWithTag, errs := determineRefreshImage(composeService, baseimage, buildValues.EnvironmentVariables)
293+
if len(errs) > 0 {
294+
for idx, err := range errs {
295+
if idx+1 == len(errs) {
296+
return nil, err
297+
} else {
298+
fmt.Println(err)
299+
}
300+
}
294301
}
295-
buildValues.ForcePullImages = append(buildValues.ForcePullImages, baseimage)
302+
buildValues.ForcePullImages = append(buildValues.ForcePullImages, baseImageWithTag)
296303
}
297304

298305
// if there are overrides defined in the lagoon API `LAGOON_SERVICE_TYPES`
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
version: '2'
2+
services:
3+
node:
4+
networks:
5+
- amazeeio-network
6+
- default
7+
build:
8+
context: internal/testdata/basic/docker
9+
dockerfile: basic.dockerfile
10+
labels:
11+
lagoon.type: basic
12+
lagoon.service.usecomposeports: true
13+
lagoon.base.image: registry.com/${BASE_IMAGE_REPO:-namespace}/imagename:${BASE_IMAGE_TAG:-latest}
14+
volumes:
15+
- .:/app:delegated
16+
ports:
17+
- '1234'
18+
- '8191'
19+
- '9001/udp'
20+
21+
networks:
22+
amazeeio-network:
23+
external: true
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
docker-compose-yaml: internal/testdata/basic/docker-compose.forcebaseimagepull-2.yml
2+
3+
environment_variables:
4+
git_sha: "true"
5+
6+
environments:
7+
main:
8+
routes:
9+
- node:
10+
- example.com

0 commit comments

Comments
 (0)