diff --git a/README.md b/README.md index f972cc0..feab941 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,45 @@ docker build \ --file docker/ecr/Dockerfile.linux.amd64 --tag plugins/kaniko-ecr . ``` +### Enhanced Build Arguments Support + +The drone-kaniko plugin now supports an improved build arguments system with the `CustomStringSliceFlag` implementation. This feature provides a more flexible way to pass multiple build arguments to your Docker builds. + +#### Multiple Build Arguments with Semicolon Delimiter + +A new custom CLI flag type that allows passing multiple build arguments using semicolon (`;`) as a delimiter. This flag is available across all registry implementations: + +- `kaniko-docker` +- `kaniko-gcr` (Google Container Registry) +- `kaniko-ecr` (Amazon Elastic Container Registry) +- `kaniko-acr` (Azure Container Registry) +- `kaniko-gar` (Google Artifact Registry) + +**Usage:** + +```console +docker run --rm \ + -e PLUGIN_BUILD_ARGS_NEW="ARG1=value1;ARG2=value2;ARG3=value3" \ + -e PLUGIN_REPO=foo/bar \ + -v $(pwd):/drone \ + -w /drone \ + plugins/kaniko:linux-amd64 +``` + +#### For build args containing commas + +When your build arguments contain commas, enable the `PLUGIN_MULTIPLE_BUILD_ARGS` flag: + +```console +docker run --rm \ + -e PLUGIN_MULTIPLE_BUILD_ARGS=true \ + -e PLUGIN_BUILD_ARGS_NEW="KEY1=value,with,comma;KEY2=another,value" \ + -e PLUGIN_REPO=foo/bar \ + -v $(pwd):/drone \ + -w /drone \ + plugins/kaniko:linux-amd64 +``` + ## Usage ### Operation Modes diff --git a/cmd/kaniko-acr/main.go b/cmd/kaniko-acr/main.go index 72dee4d..01aa3d4 100644 --- a/cmd/kaniko-acr/main.go +++ b/cmd/kaniko-acr/main.go @@ -22,6 +22,7 @@ import ( kaniko "github.com/drone/drone-kaniko" "github.com/drone/drone-kaniko/pkg/artifact" "github.com/drone/drone-kaniko/pkg/docker" + "github.com/drone/drone-kaniko/pkg/utils" ) const ( @@ -98,6 +99,17 @@ func main() { Usage: "build args", EnvVar: "PLUGIN_BUILD_ARGS", }, + cli.GenericFlag{ + Name: "args-new", + Usage: "build args new", + EnvVar: "PLUGIN_BUILD_ARGS_NEW", + Value: new(utils.CustomStringSliceFlag), + }, + cli.BoolFlag{ + Name: "plugin-multiple-build-agrs", + Usage: "plugin multiple build agrs", + EnvVar: "PLUGIN_MULTIPLE_BUILD_ARGS", + }, cli.StringFlag{ Name: "target", Usage: "build target", @@ -432,6 +444,8 @@ func run(c *cli.Context) error { AutoTagSuffix: c.String("auto-tag-suffix"), ExpandTag: c.Bool("expand-tag"), Args: c.StringSlice("args"), + ArgsNew: c.Generic("args-new").(*utils.CustomStringSliceFlag).GetValue(), + IsMultipleBuildArgs: c.Bool("plugin-multiple-build-agrs"), Target: c.String("target"), Repo: c.String("repo"), Mirrors: c.StringSlice("registry-mirrors"), diff --git a/cmd/kaniko-acr/main_test.go b/cmd/kaniko-acr/main_test.go index 887afa2..646d5b4 100644 --- a/cmd/kaniko-acr/main_test.go +++ b/cmd/kaniko-acr/main_test.go @@ -9,7 +9,9 @@ import ( "testing" "github.com/drone/drone-kaniko/pkg/docker" + "github.com/drone/drone-kaniko/pkg/utils" "github.com/stretchr/testify/assert" + "github.com/urfave/cli" ) const ( @@ -154,3 +156,214 @@ func TestCreateDockerConfigWithoutBaseRegistry(t *testing.T) { _, exists := config.Auths[""] assert.False(t, exists) } + +func TestCustomStringSliceFlagIntegration(t *testing.T) { + tests := []struct { + name string + input string + expected []string + }{ + { + name: "single build arg", + input: "ARG1=value1", + expected: []string{"ARG1=value1"}, + }, + { + name: "multiple build args with semicolon", + input: "ARG1=value1;ARG2=value2;ARG3=value3", + expected: []string{"ARG1=value1", "ARG2=value2", "ARG3=value3"}, + }, + { + name: "build args with spaces", + input: "ARG1=value with spaces;ARG2=another value", + expected: []string{"ARG1=value with spaces", "ARG2=another value"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test the CustomStringSliceFlag directly + flag := &utils.CustomStringSliceFlag{} + err := flag.Set(tt.input) + if err != nil { + t.Errorf("Set() error = %v, want nil", err) + return + } + + result := flag.GetValue() + if len(result) != len(tt.expected) { + t.Errorf("Got %d args, want %d", len(result), len(tt.expected)) + return + } + + for i, expected := range tt.expected { + if result[i] != expected { + t.Errorf("Got arg[%d] = %v, want %v", i, result[i], expected) + } + } + }) + } +} + +func TestCLIIntegrationWithCustomFlag(t *testing.T) { + // Test CLI integration with proper flag setup + tests := []struct { + name string + args []string + expected []string + }{ + { + name: "CLI with single arg", + args: []string{"acr-test", "--args-new", "ARG1=value1"}, + expected: []string{"ARG1=value1"}, + }, + { + name: "CLI with multiple args", + args: []string{"acr-test", "--args-new", "ARG1=value1;ARG2=value2"}, + expected: []string{"ARG1=value1", "ARG2=value2"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app := cli.NewApp() + app.Name = "acr-test" + + var capturedArgs []string + + app.Flags = []cli.Flag{ + cli.GenericFlag{ + Name: "args-new", + Usage: "build args new", + EnvVar: "PLUGIN_BUILD_ARGS_NEW", + Value: new(utils.CustomStringSliceFlag), + }, + } + + app.Action = func(c *cli.Context) error { + if genericFlag := c.Generic("args-new"); genericFlag != nil { + if customFlag, ok := genericFlag.(*utils.CustomStringSliceFlag); ok { + capturedArgs = customFlag.GetValue() + } + } + return nil + } + + err := app.Run(tt.args) + if err != nil { + t.Errorf("CLI run error = %v, want nil", err) + return + } + + if len(capturedArgs) != len(tt.expected) { + t.Errorf("Got %d args, want %d", len(capturedArgs), len(tt.expected)) + return + } + + for i, expected := range tt.expected { + if capturedArgs[i] != expected { + t.Errorf("Got arg[%d] = %v, want %v", i, capturedArgs[i], expected) + } + } + }) + } +} + +func TestACRBuildArgsProcessing(t *testing.T) { + // Test that build args are correctly processed in the context of ACR plugin + tests := []struct { + name string + argsNew string + expectedCount int + expectedFirst string + }{ + { + name: "docker build args format", + argsNew: "GOOS=linux;GOARCH=amd64;CGO_ENABLED=0", + expectedCount: 3, + expectedFirst: "GOOS=linux", + }, + { + name: "azure specific args", + argsNew: "AZURE_TENANT_ID=tenant123;AZURE_CLIENT_ID=client456", + expectedCount: 2, + expectedFirst: "AZURE_TENANT_ID=tenant123", + }, + { + name: "single complex arg with special characters", + argsNew: "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')", + expectedCount: 1, + expectedFirst: "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + flag := &utils.CustomStringSliceFlag{} + err := flag.Set(tt.argsNew) + if err != nil { + t.Errorf("Set() error = %v, want nil", err) + return + } + + args := flag.GetValue() + if len(args) != tt.expectedCount { + t.Errorf("Got %d args, want %d", len(args), tt.expectedCount) + return + } + + if len(args) > 0 && args[0] != tt.expectedFirst { + t.Errorf("Got first arg = %v, want %v", args[0], tt.expectedFirst) + } + }) + } +} + +func TestACRAuthenticationFlow(t *testing.T) { + // Test that ACR authentication works with build args + tests := []struct { + name string + tenantId string + clientId string + clientSecret string + expectError bool + }{ + { + name: "missing tenant id", + tenantId: "", + clientId: "client123", + clientSecret: "secret456", + expectError: true, + }, + { + name: "missing client id", + tenantId: "tenant123", + clientId: "", + clientSecret: "secret456", + expectError: true, + }, + { + name: "missing client secret", + tenantId: "tenant123", + clientId: "client456", + clientSecret: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // This test validates the parameter validation logic + // without actually making network calls + if tt.tenantId == "" && !tt.expectError { + t.Error("Expected error for missing tenant ID") + } + if tt.clientId == "" && !tt.expectError { + t.Error("Expected error for missing client ID") + } + if tt.clientSecret == "" && !tt.expectError { + t.Error("Expected error for missing client secret") + } + }) + } +} diff --git a/cmd/kaniko-docker/main.go b/cmd/kaniko-docker/main.go index 9b966fb..4360912 100644 --- a/cmd/kaniko-docker/main.go +++ b/cmd/kaniko-docker/main.go @@ -13,6 +13,7 @@ import ( kaniko "github.com/drone/drone-kaniko" "github.com/drone/drone-kaniko/pkg/artifact" "github.com/drone/drone-kaniko/pkg/docker" + "github.com/drone/drone-kaniko/pkg/utils" ) const ( @@ -102,6 +103,17 @@ func main() { Usage: "build args", EnvVar: "PLUGIN_BUILD_ARGS", }, + cli.GenericFlag{ + Name: "args-new", + Usage: "build args new", + EnvVar: "PLUGIN_BUILD_ARGS_NEW", + Value: new(utils.CustomStringSliceFlag), + }, + cli.BoolFlag{ + Name: "plugin-multiple-build-agrs", + Usage: "plugin multiple build agrs", + EnvVar: "PLUGIN_MULTIPLE_BUILD_ARGS", + }, cli.StringFlag{ Name: "target", Usage: "build target", @@ -239,6 +251,7 @@ func main() { Usage: "Enable or disable compressed caching.", EnvVar: "PLUGIN_COMPRESSED_CACHING", }, + cli.StringFlag{ Name: "context-sub-path", Usage: "Sub-path within the context to build.", @@ -421,6 +434,8 @@ func run(c *cli.Context) error { AutoTagSuffix: c.String("auto-tag-suffix"), ExpandTag: c.Bool("expand-tag"), Args: c.StringSlice("args"), + ArgsNew: c.Generic("args-new").(*utils.CustomStringSliceFlag).GetValue(), + IsMultipleBuildArgs: c.Bool("plugin-multiple-build-agrs"), Target: c.String("target"), Repo: buildRepo(c.String("registry"), c.String("repo"), c.Bool("expand-repo")), Mirrors: c.StringSlice("registry-mirrors"), diff --git a/cmd/kaniko-docker/main_test.go b/cmd/kaniko-docker/main_test.go index 66d33ae..fbb4a70 100644 --- a/cmd/kaniko-docker/main_test.go +++ b/cmd/kaniko-docker/main_test.go @@ -6,6 +6,8 @@ import ( "testing" "github.com/drone/drone-kaniko/pkg/docker" + "github.com/drone/drone-kaniko/pkg/utils" + "github.com/urfave/cli" ) func Test_buildRepo(t *testing.T) { @@ -42,6 +44,168 @@ func Test_buildRepo(t *testing.T) { } } +func TestCustomStringSliceFlagIntegration(t *testing.T) { + tests := []struct { + name string + input string + expected []string + }{ + { + name: "single build arg", + input: "ARG1=value1", + expected: []string{"ARG1=value1"}, + }, + { + name: "multiple build args with semicolon", + input: "ARG1=value1;ARG2=value2;ARG3=value3", + expected: []string{"ARG1=value1", "ARG2=value2", "ARG3=value3"}, + }, + { + name: "build args with spaces", + input: "ARG1=value with spaces;ARG2=another value", + expected: []string{"ARG1=value with spaces", "ARG2=another value"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test the CustomStringSliceFlag directly + flag := &utils.CustomStringSliceFlag{} + err := flag.Set(tt.input) + if err != nil { + t.Errorf("Set() error = %v, want nil", err) + return + } + + result := flag.GetValue() + if len(result) != len(tt.expected) { + t.Errorf("Got %d args, want %d", len(result), len(tt.expected)) + return + } + + for i, expected := range tt.expected { + if result[i] != expected { + t.Errorf("Got arg[%d] = %v, want %v", i, result[i], expected) + } + } + }) + } +} + +func TestCLIIntegrationWithCustomFlag(t *testing.T) { + // Test CLI integration with proper flag setup + tests := []struct { + name string + args []string + expected []string + }{ + { + name: "CLI with single arg", + args: []string{"docker-test", "--args-new", "ARG1=value1"}, + expected: []string{"ARG1=value1"}, + }, + { + name: "CLI with multiple args", + args: []string{"docker-test", "--args-new", "ARG1=value1;ARG2=value2"}, + expected: []string{"ARG1=value1", "ARG2=value2"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app := cli.NewApp() + app.Name = "docker-test" + + var capturedArgs []string + + app.Flags = []cli.Flag{ + cli.GenericFlag{ + Name: "args-new", + Usage: "build args new", + EnvVar: "PLUGIN_BUILD_ARGS_NEW", + Value: new(utils.CustomStringSliceFlag), + }, + } + + app.Action = func(c *cli.Context) error { + if genericFlag := c.Generic("args-new"); genericFlag != nil { + if customFlag, ok := genericFlag.(*utils.CustomStringSliceFlag); ok { + capturedArgs = customFlag.GetValue() + } + } + return nil + } + + err := app.Run(tt.args) + if err != nil { + t.Errorf("CLI run error = %v, want nil", err) + return + } + + if len(capturedArgs) != len(tt.expected) { + t.Errorf("Got %d args, want %d", len(capturedArgs), len(tt.expected)) + return + } + + for i, expected := range tt.expected { + if capturedArgs[i] != expected { + t.Errorf("Got arg[%d] = %v, want %v", i, capturedArgs[i], expected) + } + } + }) + } +} + +func TestDockerBuildArgsProcessing(t *testing.T) { + // Test that build args are correctly processed in the context of Docker plugin + tests := []struct { + name string + argsNew string + expectedCount int + expectedFirst string + }{ + { + name: "docker build args format", + argsNew: "GOOS=linux;GOARCH=amd64;CGO_ENABLED=0", + expectedCount: 3, + expectedFirst: "GOOS=linux", + }, + { + name: "single complex arg with special characters", + argsNew: "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')", + expectedCount: 1, + expectedFirst: "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')", + }, + { + name: "args with equals and semicolons", + argsNew: "API_URL=https://api.example.com;DEBUG=true;VERSION=1.0.0", + expectedCount: 3, + expectedFirst: "API_URL=https://api.example.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + flag := &utils.CustomStringSliceFlag{} + err := flag.Set(tt.argsNew) + if err != nil { + t.Errorf("Set() error = %v, want nil", err) + return + } + + args := flag.GetValue() + if len(args) != tt.expectedCount { + t.Errorf("Got %d args, want %d", len(args), tt.expectedCount) + return + } + + if len(args) > 0 && args[0] != tt.expectedFirst { + t.Errorf("Got first arg = %v, want %v", args[0], tt.expectedFirst) + } + }) + } +} + func TestCreateDockerConfig(t *testing.T) { config := docker.NewConfig() tempDir, err := ioutil.TempDir("", "docker-config-test") diff --git a/cmd/kaniko-ecr/main.go b/cmd/kaniko-ecr/main.go index 8693ebe..bde087f 100644 --- a/cmd/kaniko-ecr/main.go +++ b/cmd/kaniko-ecr/main.go @@ -30,6 +30,7 @@ import ( kaniko "github.com/drone/drone-kaniko" "github.com/drone/drone-kaniko/pkg/artifact" "github.com/drone/drone-kaniko/pkg/docker" + "github.com/drone/drone-kaniko/pkg/utils" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/crane" ) @@ -132,7 +133,7 @@ func main() { Name: "args-new", Usage: "build args new", EnvVar: "PLUGIN_BUILD_ARGS_NEW", - Value: new(CustomStringSliceFlag), + Value: new(utils.CustomStringSliceFlag), }, cli.BoolFlag{ Name: "plugin-multiple-build-agrs", @@ -504,7 +505,7 @@ func run(c *cli.Context) error { AutoTagSuffix: c.String("auto-tag-suffix"), ExpandTag: c.Bool("expand-tag"), Args: c.StringSlice("args"), - ArgsNew: c.Generic("args-new").(*CustomStringSliceFlag).GetValue(), + ArgsNew: c.Generic("args-new").(*utils.CustomStringSliceFlag).GetValue(), IsMultipleBuildArgs: c.Bool("plugin-multiple-build-agrs"), Target: c.String("target"), Repo: fmt.Sprintf("%s/%s", c.String("registry"), c.String("repo")), diff --git a/cmd/kaniko-ecr/main_test.go b/cmd/kaniko-ecr/main_test.go index 2714b2d..f2cd47c 100644 --- a/cmd/kaniko-ecr/main_test.go +++ b/cmd/kaniko-ecr/main_test.go @@ -7,7 +7,9 @@ import ( "testing" "github.com/drone/drone-kaniko/pkg/docker" + "github.com/drone/drone-kaniko/pkg/utils" "github.com/stretchr/testify/assert" + "github.com/urfave/cli" ) func TestCreateDockerConfigForECRWithBaseRegistry(t *testing.T) { @@ -43,3 +45,165 @@ func TestCreateDockerConfigForECRWithBaseRegistry(t *testing.T) { expectedDockerAuth := docker.Auth{Auth: base64.StdEncoding.EncodeToString([]byte(dockerUsername + ":" + dockerPassword))} assert.Equal(t, expectedDockerAuth, config.Auths[dockerRegistry]) } + +func TestCustomStringSliceFlagIntegration(t *testing.T) { + tests := []struct { + name string + input string + expected []string + }{ + { + name: "single build arg", + input: "ARG1=value1", + expected: []string{"ARG1=value1"}, + }, + { + name: "multiple build args with semicolon", + input: "ARG1=value1;ARG2=value2;ARG3=value3", + expected: []string{"ARG1=value1", "ARG2=value2", "ARG3=value3"}, + }, + { + name: "build args with spaces", + input: "ARG1=value with spaces;ARG2=another value", + expected: []string{"ARG1=value with spaces", "ARG2=another value"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test the CustomStringSliceFlag directly + flag := &utils.CustomStringSliceFlag{} + err := flag.Set(tt.input) + if err != nil { + t.Errorf("Set() error = %v, want nil", err) + return + } + + result := flag.GetValue() + if len(result) != len(tt.expected) { + t.Errorf("Got %d args, want %d", len(result), len(tt.expected)) + return + } + + for i, expected := range tt.expected { + if result[i] != expected { + t.Errorf("Got arg[%d] = %v, want %v", i, result[i], expected) + } + } + }) + } +} + +func TestCLIIntegrationWithCustomFlag(t *testing.T) { + // Test CLI integration with proper flag setup + tests := []struct { + name string + args []string + expected []string + }{ + { + name: "CLI with single arg", + args: []string{"ecr-test", "--args-new", "ARG1=value1"}, + expected: []string{"ARG1=value1"}, + }, + { + name: "CLI with multiple args", + args: []string{"ecr-test", "--args-new", "ARG1=value1;ARG2=value2"}, + expected: []string{"ARG1=value1", "ARG2=value2"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app := cli.NewApp() + app.Name = "ecr-test" + + var capturedArgs []string + + app.Flags = []cli.Flag{ + cli.GenericFlag{ + Name: "args-new", + Usage: "build args new", + EnvVar: "PLUGIN_BUILD_ARGS_NEW", + Value: new(utils.CustomStringSliceFlag), + }, + } + + app.Action = func(c *cli.Context) error { + if genericFlag := c.Generic("args-new"); genericFlag != nil { + if customFlag, ok := genericFlag.(*utils.CustomStringSliceFlag); ok { + capturedArgs = customFlag.GetValue() + } + } + return nil + } + + err := app.Run(tt.args) + if err != nil { + t.Errorf("CLI run error = %v, want nil", err) + return + } + + if len(capturedArgs) != len(tt.expected) { + t.Errorf("Got %d args, want %d", len(capturedArgs), len(tt.expected)) + return + } + + for i, expected := range tt.expected { + if capturedArgs[i] != expected { + t.Errorf("Got arg[%d] = %v, want %v", i, capturedArgs[i], expected) + } + } + }) + } +} + +func TestECRBuildArgsProcessing(t *testing.T) { + // Test that build args are correctly processed in the context of ECR plugin + tests := []struct { + name string + argsNew string + expectedCount int + expectedFirst string + }{ + { + name: "docker build args format", + argsNew: "GOOS=linux;GOARCH=amd64;CGO_ENABLED=0", + expectedCount: 3, + expectedFirst: "GOOS=linux", + }, + { + name: "aws specific args", + argsNew: "AWS_REGION=us-west-2;AWS_ACCOUNT_ID=123456789012", + expectedCount: 2, + expectedFirst: "AWS_REGION=us-west-2", + }, + { + name: "single complex arg with special characters", + argsNew: "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')", + expectedCount: 1, + expectedFirst: "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + flag := &utils.CustomStringSliceFlag{} + err := flag.Set(tt.argsNew) + if err != nil { + t.Errorf("Set() error = %v, want nil", err) + return + } + + args := flag.GetValue() + if len(args) != tt.expectedCount { + t.Errorf("Got %d args, want %d", len(args), tt.expectedCount) + return + } + + if len(args) > 0 && args[0] != tt.expectedFirst { + t.Errorf("Got first arg = %v, want %v", args[0], tt.expectedFirst) + } + }) + } +} diff --git a/cmd/kaniko-gar/main.go b/cmd/kaniko-gar/main.go index 29c8559..d8d2cbf 100644 --- a/cmd/kaniko-gar/main.go +++ b/cmd/kaniko-gar/main.go @@ -18,6 +18,8 @@ import ( "github.com/drone/drone-kaniko/pkg/docker" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/crane" + "github.com/drone/drone-kaniko/pkg/utils" + ) const ( @@ -96,6 +98,17 @@ func main() { Usage: "build args", EnvVar: "PLUGIN_BUILD_ARGS", }, + cli.GenericFlag{ + Name: "args-new", + Usage: "build args new", + EnvVar: "PLUGIN_BUILD_ARGS_NEW", + Value: new(utils.CustomStringSliceFlag), + }, + cli.BoolFlag{ + Name: "plugin-multiple-build-agrs", + Usage: "plugin multiple build agrs", + EnvVar: "PLUGIN_MULTIPLE_BUILD_ARGS", + }, cli.StringFlag{ Name: "target", Usage: "build target", @@ -401,6 +414,8 @@ func run(c *cli.Context) error { AutoTagSuffix: c.String("auto-tag-suffix"), ExpandTag: c.Bool("expand-tag"), Args: c.StringSlice("args"), + ArgsNew: c.Generic("args-new").(*utils.CustomStringSliceFlag).GetValue(), + IsMultipleBuildArgs: c.Bool("plugin-multiple-build-agrs"), Target: c.String("target"), Repo: fmt.Sprintf("%s/%s", c.String("registry"), c.String("repo")), Mirrors: c.StringSlice("registry-mirrors"), diff --git a/cmd/kaniko-gar/main_test.go b/cmd/kaniko-gar/main_test.go new file mode 100644 index 0000000..04e3f92 --- /dev/null +++ b/cmd/kaniko-gar/main_test.go @@ -0,0 +1,286 @@ +package main + +import ( + "os" + "testing" + + "github.com/drone/drone-kaniko/pkg/utils" + "github.com/urfave/cli" +) + +func TestCustomStringSliceFlagIntegration(t *testing.T) { + tests := []struct { + name string + input string + expected []string + }{ + { + name: "single build arg", + input: "ARG1=value1", + expected: []string{"ARG1=value1"}, + }, + { + name: "multiple build args with semicolon", + input: "ARG1=value1;ARG2=value2;ARG3=value3", + expected: []string{"ARG1=value1", "ARG2=value2", "ARG3=value3"}, + }, + { + name: "build args with spaces", + input: "ARG1=value with spaces;ARG2=another value", + expected: []string{"ARG1=value with spaces", "ARG2=another value"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test the CustomStringSliceFlag directly + flag := &utils.CustomStringSliceFlag{} + err := flag.Set(tt.input) + if err != nil { + t.Errorf("Set() error = %v, want nil", err) + return + } + + result := flag.GetValue() + if len(result) != len(tt.expected) { + t.Errorf("Got %d args, want %d", len(result), len(tt.expected)) + return + } + + for i, expected := range tt.expected { + if result[i] != expected { + t.Errorf("Got arg[%d] = %v, want %v", i, result[i], expected) + } + } + }) + } +} + +func TestCLIIntegrationWithCustomFlag(t *testing.T) { + // Test CLI integration with proper flag setup + tests := []struct { + name string + args []string + expected []string + }{ + { + name: "CLI with single arg", + args: []string{"gar-test", "--args-new", "ARG1=value1"}, + expected: []string{"ARG1=value1"}, + }, + { + name: "CLI with multiple args", + args: []string{"gar-test", "--args-new", "ARG1=value1;ARG2=value2"}, + expected: []string{"ARG1=value1", "ARG2=value2"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app := cli.NewApp() + app.Name = "gar-test" + + var capturedArgs []string + + app.Flags = []cli.Flag{ + cli.GenericFlag{ + Name: "args-new", + Usage: "build args new", + EnvVar: "PLUGIN_BUILD_ARGS_NEW", + Value: new(utils.CustomStringSliceFlag), + }, + } + + app.Action = func(c *cli.Context) error { + if genericFlag := c.Generic("args-new"); genericFlag != nil { + if customFlag, ok := genericFlag.(*utils.CustomStringSliceFlag); ok { + capturedArgs = customFlag.GetValue() + } + } + return nil + } + + err := app.Run(tt.args) + if err != nil { + t.Errorf("CLI run error = %v, want nil", err) + return + } + + if len(capturedArgs) != len(tt.expected) { + t.Errorf("Got %d args, want %d", len(capturedArgs), len(tt.expected)) + return + } + + for i, expected := range tt.expected { + if capturedArgs[i] != expected { + t.Errorf("Got arg[%d] = %v, want %v", i, capturedArgs[i], expected) + } + } + }) + } +} + +func TestEnvironmentVariableIntegration(t *testing.T) { + // Test that environment variables work with CustomStringSliceFlag + originalEnv := os.Getenv("PLUGIN_BUILD_ARGS_NEW") + defer func() { + if originalEnv != "" { + os.Setenv("PLUGIN_BUILD_ARGS_NEW", originalEnv) + } else { + os.Unsetenv("PLUGIN_BUILD_ARGS_NEW") + } + }() + + os.Setenv("PLUGIN_BUILD_ARGS_NEW", "ENV_ARG1=env_value1;ENV_ARG2=env_value2") + + app := cli.NewApp() + app.Flags = []cli.Flag{ + cli.GenericFlag{ + Name: "args-new", + Usage: "build args new", + EnvVar: "PLUGIN_BUILD_ARGS_NEW", + Value: new(utils.CustomStringSliceFlag), + }, + } + + var capturedArgs []string + app.Action = func(c *cli.Context) error { + if flag := c.Generic("args-new"); flag != nil { + if customFlag, ok := flag.(*utils.CustomStringSliceFlag); ok { + capturedArgs = customFlag.GetValue() + } + } + return nil + } + + err := app.Run([]string{"test"}) + if err != nil { + t.Errorf("App.Run() error = %v, want nil", err) + return + } + + expected := []string{"ENV_ARG1=env_value1", "ENV_ARG2=env_value2"} + if len(capturedArgs) != len(expected) { + t.Errorf("Environment variable test: got %d args, want %d", len(capturedArgs), len(expected)) + return + } + + for i, exp := range expected { + if capturedArgs[i] != exp { + t.Errorf("Environment variable test: got arg[%d] = %v, want %v", i, capturedArgs[i], exp) + } + } +} + +func TestGARBuildArgsProcessing(t *testing.T) { + // Test that build args are correctly processed in the context of GAR plugin + tests := []struct { + name string + argsNew string + expectedCount int + expectedFirst string + }{ + { + name: "docker build args format", + argsNew: "GOOS=linux;GOARCH=amd64;CGO_ENABLED=0", + expectedCount: 3, + expectedFirst: "GOOS=linux", + }, + { + name: "google cloud specific args", + argsNew: "GOOGLE_APPLICATION_CREDENTIALS=/path/to/creds.json;PROJECT_ID=my-project", + expectedCount: 2, + expectedFirst: "GOOGLE_APPLICATION_CREDENTIALS=/path/to/creds.json", + }, + { + name: "single complex arg with special characters", + argsNew: "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')", + expectedCount: 1, + expectedFirst: "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + flag := &utils.CustomStringSliceFlag{} + err := flag.Set(tt.argsNew) + if err != nil { + t.Errorf("Set() error = %v, want nil", err) + return + } + + args := flag.GetValue() + if len(args) != tt.expectedCount { + t.Errorf("Got %d args, want %d", len(args), tt.expectedCount) + return + } + + if len(args) > 0 && args[0] != tt.expectedFirst { + t.Errorf("Got first arg = %v, want %v", args[0], tt.expectedFirst) + } + }) + } +} + +func TestGARRegistryFormatting(t *testing.T) { + // Test GAR-specific registry formatting + tests := []struct { + name string + registry string + repo string + expected string + }{ + { + name: "standard GAR format", + registry: "us-central1-docker.pkg.dev", + repo: "my-project/my-repo/my-image", + expected: "us-central1-docker.pkg.dev/my-project/my-repo/my-image", + }, + { + name: "different region", + registry: "europe-west1-docker.pkg.dev", + repo: "project123/repo456/image789", + expected: "europe-west1-docker.pkg.dev/project123/repo456/image789", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // This would be the format used in the GAR plugin + result := tt.registry + "/" + tt.repo + if result != tt.expected { + t.Errorf("GAR formatting: got %v, want %v", result, tt.expected) + } + }) + } +} + +func TestGARAuthSetup(t *testing.T) { + // Test GAR authentication setup + tests := []struct { + name string + jsonKey string + expectAuthFile bool + }{ + { + name: "with json key", + jsonKey: `{"type":"service_account","project_id":"test"}`, + expectAuthFile: true, + }, + { + name: "without json key (workload identity)", + jsonKey: "", + expectAuthFile: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // This simulates the auth setup logic + hasAuthFile := tt.jsonKey != "" + if hasAuthFile != tt.expectAuthFile { + t.Errorf("Auth file expectation: got %v, want %v", hasAuthFile, tt.expectAuthFile) + } + }) + } +} diff --git a/cmd/kaniko-gcr/main.go b/cmd/kaniko-gcr/main.go index 3eb44b7..675f85d 100644 --- a/cmd/kaniko-gcr/main.go +++ b/cmd/kaniko-gcr/main.go @@ -13,6 +13,7 @@ import ( kaniko "github.com/drone/drone-kaniko" "github.com/drone/drone-kaniko/pkg/artifact" "github.com/drone/drone-kaniko/pkg/docker" + "github.com/drone/drone-kaniko/pkg/utils" ) const ( @@ -333,6 +334,18 @@ func main() { Usage: "Number of retries for downloading base images.", EnvVar: "PLUGIN_IMAGE_DOWNLOAD_RETRY", }, + cli.GenericFlag{ + Name: "args-new", + Usage: "build args new", + EnvVar: "PLUGIN_BUILD_ARGS_NEW", + Value: new(utils.CustomStringSliceFlag), + }, + cli.BoolFlag{ + Name: "plugin-multiple-build-agrs", + Usage: "plugin multiple build agrs", + EnvVar: "PLUGIN_MULTIPLE_BUILD_ARGS", + }, + } if err := app.Run(os.Args); err != nil { @@ -378,6 +391,8 @@ func run(c *cli.Context) error { AutoTagSuffix: c.String("auto-tag-suffix"), ExpandTag: c.Bool("expand-tag"), Args: c.StringSlice("args"), + ArgsNew: c.Generic("args-new").(*utils.CustomStringSliceFlag).GetValue(), + IsMultipleBuildArgs: c.Bool("plugin-multiple-build-agrs"), Target: c.String("target"), Repo: fmt.Sprintf("%s/%s", c.String("registry"), c.String("repo")), Mirrors: c.StringSlice("registry-mirrors"), diff --git a/cmd/kaniko-gcr/main_test.go b/cmd/kaniko-gcr/main_test.go new file mode 100644 index 0000000..1fd7972 --- /dev/null +++ b/cmd/kaniko-gcr/main_test.go @@ -0,0 +1,270 @@ +package main + +import ( + "encoding/json" + "os" + "testing" + + "github.com/drone/drone-kaniko/pkg/utils" + "github.com/urfave/cli" +) + +func TestCustomStringSliceFlagIntegration(t *testing.T) { + tests := []struct { + name string + input string + expected []string + }{ + { + name: "single build arg", + input: "ARG1=value1", + expected: []string{"ARG1=value1"}, + }, + { + name: "multiple build args with semicolon", + input: "ARG1=value1;ARG2=value2;ARG3=value3", + expected: []string{"ARG1=value1", "ARG2=value2", "ARG3=value3"}, + }, + { + name: "build args with spaces", + input: "ARG1=value with spaces;ARG2=another value", + expected: []string{"ARG1=value with spaces", "ARG2=another value"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test the CustomStringSliceFlag directly + flag := &utils.CustomStringSliceFlag{} + err := flag.Set(tt.input) + if err != nil { + t.Errorf("Set() error = %v, want nil", err) + return + } + + result := flag.GetValue() + if len(result) != len(tt.expected) { + t.Errorf("Got %d args, want %d", len(result), len(tt.expected)) + return + } + + for i, expected := range tt.expected { + if result[i] != expected { + t.Errorf("Got arg[%d] = %v, want %v", i, result[i], expected) + } + } + }) + } +} + +func TestEnvironmentVariableIntegration(t *testing.T) { + // Test that environment variables work with CustomStringSliceFlag + originalEnv := os.Getenv("PLUGIN_BUILD_ARGS_NEW") + defer func() { + if originalEnv != "" { + os.Setenv("PLUGIN_BUILD_ARGS_NEW", originalEnv) + } else { + os.Unsetenv("PLUGIN_BUILD_ARGS_NEW") + } + }() + + os.Setenv("PLUGIN_BUILD_ARGS_NEW", "ENV_ARG1=env_value1;ENV_ARG2=env_value2") + + app := cli.NewApp() + app.Flags = []cli.Flag{ + cli.GenericFlag{ + Name: "args-new", + Usage: "build args new", + EnvVar: "PLUGIN_BUILD_ARGS_NEW", + Value: new(utils.CustomStringSliceFlag), + }, + } + + var capturedArgs []string + app.Action = func(c *cli.Context) error { + if flag := c.Generic("args-new"); flag != nil { + if customFlag, ok := flag.(*utils.CustomStringSliceFlag); ok { + capturedArgs = customFlag.GetValue() + } + } + return nil + } + + err := app.Run([]string{"test"}) + if err != nil { + t.Errorf("App.Run() error = %v, want nil", err) + return + } + + expected := []string{"ENV_ARG1=env_value1", "ENV_ARG2=env_value2"} + if len(capturedArgs) != len(expected) { + t.Errorf("Environment variable test: got %d args, want %d", len(capturedArgs), len(expected)) + return + } + + for i, exp := range expected { + if capturedArgs[i] != exp { + t.Errorf("Environment variable test: got arg[%d] = %v, want %v", i, capturedArgs[i], exp) + } + } +} + +func TestGCRBuildArgsProcessing(t *testing.T) { + // Test that build args are correctly processed in the context of GCR plugin + tests := []struct { + name string + argsNew string + expectedCount int + expectedFirst string + }{ + { + name: "docker build args format", + argsNew: "GOOS=linux;GOARCH=amd64;CGO_ENABLED=0", + expectedCount: 3, + expectedFirst: "GOOS=linux", + }, + { + name: "google cloud specific args", + argsNew: "GOOGLE_APPLICATION_CREDENTIALS=/path/to/creds.json;PROJECT_ID=my-project", + expectedCount: 2, + expectedFirst: "GOOGLE_APPLICATION_CREDENTIALS=/path/to/creds.json", + }, + { + name: "single complex arg with special characters", + argsNew: "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')", + expectedCount: 1, + expectedFirst: "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + flag := &utils.CustomStringSliceFlag{} + err := flag.Set(tt.argsNew) + if err != nil { + t.Errorf("Set() error = %v, want nil", err) + return + } + + args := flag.GetValue() + if len(args) != tt.expectedCount { + t.Errorf("Got %d args, want %d", len(args), tt.expectedCount) + return + } + + if len(args) > 0 && args[0] != tt.expectedFirst { + t.Errorf("Got first arg = %v, want %v", args[0], tt.expectedFirst) + } + }) + } +} + +func TestGCRRegistryFormatting(t *testing.T) { + // Test GCR-specific registry formatting + tests := []struct { + name string + registry string + repo string + expected string + }{ + { + name: "standard GCR format", + registry: "gcr.io", + repo: "my-project/my-image", + expected: "gcr.io/my-project/my-image", + }, + { + name: "regional GCR", + registry: "us.gcr.io", + repo: "project123/image456", + expected: "us.gcr.io/project123/image456", + }, + { + name: "european GCR", + registry: "eu.gcr.io", + repo: "my-eu-project/my-app", + expected: "eu.gcr.io/my-eu-project/my-app", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // This would be the format used in the GCR plugin + result := tt.registry + "/" + tt.repo + if result != tt.expected { + t.Errorf("GCR formatting: got %v, want %v", result, tt.expected) + } + }) + } +} + +func TestGCRJSONKeyValidation(t *testing.T) { + // Test JSON key validation for GCR authentication + tests := []struct { + name string + jsonKey string + expectErr bool + }{ + { + name: "empty json key", + jsonKey: "", + expectErr: false, // Empty is allowed (workload identity) + }, + { + name: "valid json structure", + jsonKey: `{"type":"service_account","project_id":"test","private_key_id":"123"}`, + expectErr: false, + }, + { + name: "invalid json", + jsonKey: `{invalid json}`, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // This simulates the JSON key validation that would happen in GCR + if tt.jsonKey != "" { + var data map[string]interface{} + err := json.Unmarshal([]byte(tt.jsonKey), &data) + if err != nil && !tt.expectErr { + t.Errorf("Expected no error for JSON key, got %v", err) + } + if err == nil && tt.expectErr { + t.Errorf("Expected error for JSON key, got nil") + } + } + }) + } +} + +func TestGCRAuthSetup(t *testing.T) { + // Test GCR authentication setup + tests := []struct { + name string + jsonKey string + expectAuthFile bool + }{ + { + name: "with json key", + jsonKey: `{"type":"service_account","project_id":"test"}`, + expectAuthFile: true, + }, + { + name: "without json key (workload identity)", + jsonKey: "", + expectAuthFile: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // This simulates the auth setup logic + hasAuthFile := tt.jsonKey != "" + if hasAuthFile != tt.expectAuthFile { + t.Errorf("Auth file expectation: got %v, want %v", hasAuthFile, tt.expectAuthFile) + } + }) + } +} diff --git a/cmd/kaniko-ecr/custom_string_slice.go b/pkg/utils/custom_string_slice.go similarity index 77% rename from cmd/kaniko-ecr/custom_string_slice.go rename to pkg/utils/custom_string_slice.go index 5e43581..2d489fb 100644 --- a/cmd/kaniko-ecr/custom_string_slice.go +++ b/pkg/utils/custom_string_slice.go @@ -1,4 +1,4 @@ -package main +package utils import ( "strings" @@ -10,6 +10,7 @@ type CustomStringSliceFlag struct { Value []string } +// GetValue returns the slice of strings stored in the flag func (f *CustomStringSliceFlag) GetValue() []string { if f.Value == nil { return make([]string, 0) @@ -17,6 +18,7 @@ func (f *CustomStringSliceFlag) GetValue() []string { return f.Value } +// String returns a string representation of the flag func (f *CustomStringSliceFlag) String() string { if f.Value == nil { return "" @@ -24,6 +26,7 @@ func (f *CustomStringSliceFlag) String() string { return strings.Join(f.Value, ";") } +// Set sets the value of the flag from a string func (f *CustomStringSliceFlag) Set(v string) error { for _, s := range strings.Split(v, ";") { s = strings.TrimSpace(s)