Skip to content

Commit

Permalink
feat: variable replacement support for lagoon.base.image label (#378)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: shreddedbacon <[email protected]>
  • Loading branch information
3 people authored Oct 21, 2024
1 parent d039fc9 commit b899eb6
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 9 deletions.
62 changes: 59 additions & 3 deletions cmd/identify_imagebuild_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"testing"

"github.com/andreyvit/diff"
"github.com/uselagoon/build-deploy-tool/internal/dbaasclient"
"github.com/uselagoon/build-deploy-tool/internal/generator"
"github.com/uselagoon/build-deploy-tool/internal/helpers"
Expand Down Expand Up @@ -752,6 +753,61 @@ func TestImageBuildConfigurationIdentification(t *testing.T) {
},
},
},
{
name: "test12 Force Pull Base Images with variable replacement",
args: testdata.GetSeedData(
testdata.TestData{
Namespace: "example-project-main",
ProjectName: "example-project",
EnvironmentName: "main",
Branch: "main",
LagoonYAML: "internal/testdata/basic/lagoon.forcebaseimagepull-2.yml",
ProjectVariables: []lagoon.EnvironmentVariable{
{
Name: "BASE_IMAGE_TAG",
Value: "my-tag",
Scope: "build",
},
{
Name: "BASE_IMAGE_REPO",
Value: "my-repo",
Scope: "build",
},
},
}, true),
want: imageBuild{
BuildKit: helpers.BoolPtr(true),
BuildArguments: map[string]string{
"BASE_IMAGE_TAG": "my-tag",
"BASE_IMAGE_REPO": "my-repo",
"LAGOON_BUILD_NAME": "lagoon-build-abcdefg",
"LAGOON_PROJECT": "example-project",
"LAGOON_ENVIRONMENT": "main",
"LAGOON_ENVIRONMENT_TYPE": "production",
"LAGOON_BUILD_TYPE": "branch",
"LAGOON_GIT_SOURCE_REPOSITORY": "ssh://[email protected]/lagoon-demo.git",
"LAGOON_KUBERNETES": "remote-cluster1",
"LAGOON_GIT_SHA": "abcdefg123456",
"LAGOON_GIT_BRANCH": "main",
"NODE_IMAGE": "example-project-main-node",
"LAGOON_SSH_PRIVATE_KEY": "-----BEGIN OPENSSH PRIVATE KEY-----\nthisisafakekey\n-----END OPENSSH PRIVATE KEY-----",
},
ForcePullImages: []string{
"registry.com/my-repo/imagename:my-tag",
},
Images: []imageBuilds{
{
Name: "node",
ImageBuild: generator.ImageBuild{
BuildImage: "harbor.example/example-project/main/node:latest",
Context: "internal/testdata/basic/docker",
DockerFile: "basic.dockerfile",
TemporaryImage: "example-project-main-node",
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -788,10 +844,10 @@ func TestImageBuildConfigurationIdentification(t *testing.T) {
t.Errorf("%v", err)
}

oJ, _ := json.Marshal(out)
wJ, _ := json.Marshal(tt.want)
oJ, _ := json.MarshalIndent(out, "", " ")
wJ, _ := json.MarshalIndent(tt.want, "", " ")
if string(oJ) != string(wJ) {
t.Errorf("returned output %v doesn't match want %v", string(oJ), string(wJ))
t.Errorf("ImageBuildConfigurationIdentification() = \n%v", diff.LineDiff(string(oJ), string(wJ)))
}
t.Cleanup(func() {
helpers.UnsetEnvVars(tt.vars)
Expand Down
43 changes: 43 additions & 0 deletions internal/generator/helpers_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"regexp"
"strings"

"github.com/distribution/reference"

"github.com/spf13/cobra"
"github.com/uselagoon/build-deploy-tool/internal/dbaasclient"
"github.com/uselagoon/build-deploy-tool/internal/lagoon"
Expand Down Expand Up @@ -271,3 +273,44 @@ func getDBaasEnvironment(
}
return exists, nil
}

var exp = regexp.MustCompile(`(\\*)\$\{(.+?)(?:(\:\-)(.*?))?\}`)

func determineRefreshImage(serviceName, imageName string, envVars []lagoon.EnvironmentVariable) (string, []error) {
errs := []error{}
parsed := exp.ReplaceAllStringFunc(string(imageName), func(match string) string {
tagvalue := ""
re := regexp.MustCompile(`\${?(\w+)?(?::-(\w+))?}?`)
matches := re.FindStringSubmatch(match)
if len(matches) > 0 {
tv := ""
envVarKey := matches[1]
defaultVal := matches[2] //This could be empty
for _, v := range envVars {
if v.Name == envVarKey {
tv = v.Value
}
}
if tv == "" {
if defaultVal != "" {
tagvalue = defaultVal
} else {
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))
}
} else {
tagvalue = tv
}
}
return tagvalue
})
if parsed == imageName {
if !reference.ReferenceRegexp.MatchString(parsed) {
if strings.Contains(parsed, "$") {
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))
} else {
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))
}
}
}
return parsed, errs
}
86 changes: 86 additions & 0 deletions internal/generator/helpers_generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,89 @@ func Test_checkDuplicateCronjobs(t *testing.T) {
})
}
}

func Test_determineRefreshImage(t *testing.T) {
type args struct {
serviceName string
imageName string
envVars []lagoon.EnvironmentVariable
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "Identity function",
args: args{
serviceName: "testservice",
imageName: "image/name:latest",
envVars: nil,
},
want: "image/name:latest",
wantErr: false,
},
{
name: "Fails with no matching variable in envvars",
args: args{
serviceName: "testservice",
imageName: "image/name:${NOENVVAR}",
envVars: nil,
},
want: "",
wantErr: true,
},
{
name: "Fails with variable missing curly brackets",
args: args{
serviceName: "testservice",
imageName: "image/name:$NOENVVAR",
envVars: nil,
},
want: "",
wantErr: true,
},
{
name: "Tag with simple arg - fallback to default",
args: args{
serviceName: "testservice",
imageName: "image/name:${ENVVAR:-sometag}",
envVars: nil,
},
want: "image/name:sometag",
wantErr: false,
},
{
name: "Tag with env var that works",
args: args{
serviceName: "testservice",
imageName: "image/name:${ENVVAR:-sometag}",
envVars: []lagoon.EnvironmentVariable{
{
Name: "ENVVAR",
Value: "injectedTag",
},
},
},
want: "image/name:injectedTag",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, errs := determineRefreshImage(tt.args.serviceName, tt.args.imageName, tt.args.envVars)
if len(errs) > 0 && !tt.wantErr {
for idx, err := range errs {
t.Errorf("determineRefreshImage() error = %v, wantErr %v", err, tt.wantErr)
if idx+1 == len(errs) {
return
}
}
}
if got != tt.want && !tt.wantErr {
t.Errorf("determineRefreshImage() got = %v, want %v", got, tt.want)
}
})
}
}
19 changes: 13 additions & 6 deletions internal/generator/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"strconv"
"strings"

"github.com/distribution/reference"

composetypes "github.com/compose-spec/compose-go/types"
"github.com/uselagoon/build-deploy-tool/internal/helpers"
"github.com/uselagoon/build-deploy-tool/internal/lagoon"
Expand Down Expand Up @@ -286,13 +284,22 @@ func composeToServiceValues(
}
}

// if any `lagoon.base.image` labels are set, we note them for docker pulling
// this allows us to refresh the docker-host's cache in cases where an image
// may have an update without a change in tag (i.e. "latest" tagged images)
baseimage := lagoon.CheckDockerComposeLagoonLabel(composeServiceValues.Labels, "lagoon.base.image")
if baseimage != "" {
// First, let's ensure that the structure of the base image is valid
if !reference.ReferenceRegexp.MatchString(baseimage) {
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)
baseImageWithTag, errs := determineRefreshImage(composeService, baseimage, buildValues.EnvironmentVariables)
if len(errs) > 0 {
for idx, err := range errs {
if idx+1 == len(errs) {
return nil, err
} else {
fmt.Println(err)
}
}
}
buildValues.ForcePullImages = append(buildValues.ForcePullImages, baseimage)
buildValues.ForcePullImages = append(buildValues.ForcePullImages, baseImageWithTag)
}

// if there are overrides defined in the lagoon API `LAGOON_SERVICE_TYPES`
Expand Down
23 changes: 23 additions & 0 deletions internal/testdata/basic/docker-compose.forcebaseimagepull-2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
version: '2'
services:
node:
networks:
- amazeeio-network
- default
build:
context: internal/testdata/basic/docker
dockerfile: basic.dockerfile
labels:
lagoon.type: basic
lagoon.service.usecomposeports: true
lagoon.base.image: registry.com/${BASE_IMAGE_REPO:-namespace}/imagename:${BASE_IMAGE_TAG:-latest}
volumes:
- .:/app:delegated
ports:
- '1234'
- '8191'
- '9001/udp'

networks:
amazeeio-network:
external: true
10 changes: 10 additions & 0 deletions internal/testdata/basic/lagoon.forcebaseimagepull-2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
docker-compose-yaml: internal/testdata/basic/docker-compose.forcebaseimagepull-2.yml

environment_variables:
git_sha: "true"

environments:
main:
routes:
- node:
- example.com

0 comments on commit b899eb6

Please sign in to comment.