diff --git a/kaniko.go b/kaniko.go index 5437277..c57b40d 100644 --- a/kaniko.go +++ b/kaniko.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "os/exec" + "path/filepath" "strings" "github.com/drone/drone-kaniko/pkg/artifact" @@ -258,6 +259,12 @@ func (p Plugin) Exec() error { } if p.Build.TarPath != "" { + tarDir := filepath.Dir(p.Build.TarPath) + if _, err := os.Stat(tarDir); os.IsNotExist(err) { + if mkdirErr := os.MkdirAll(tarDir, 0755); mkdirErr != nil { + return fmt.Errorf("failed to create directory for tar path %s: %v", tarDir, mkdirErr) + } + } cmdArgs = append(cmdArgs, fmt.Sprintf("--tar-path=%s", p.Build.TarPath)) } @@ -407,7 +414,11 @@ func (p Plugin) Exec() error { } if p.Output.OutputFile != "" { - if err = output.WritePluginOutputFile(p.Output.OutputFile, getDigest(p.Build.DigestFile)); err != nil { + var tarPath string + if p.Build.TarPath != "" { + tarPath = getTarPath(p.Build.TarPath) + } + if err = output.WritePluginOutputFile(p.Output.OutputFile, getDigest(p.Build.DigestFile), tarPath); err != nil { fmt.Fprintf(os.Stderr, "failed to write plugin output file at path: %s with error: %s\n", p.Output.OutputFile, err) } } @@ -415,6 +426,15 @@ func (p Plugin) Exec() error { return nil } +func getTarPath(tarPath string) string { + tarDir := filepath.Dir(tarPath) + if _, err := os.Stat(tarDir); err != nil && os.IsNotExist(err) { + fmt.Fprintf(os.Stderr, "Warning: tar path does not exist: %s\n", tarPath) + return "" + } + return tarPath +} + func getDigest(digestFile string) string { content, err := ioutil.ReadFile(digestFile) if err != nil { diff --git a/kaniko_test.go b/kaniko_test.go index e61c1cb..73eb985 100644 --- a/kaniko_test.go +++ b/kaniko_test.go @@ -1,6 +1,8 @@ package kaniko import ( + "os" + "path/filepath" "testing" "github.com/google/go-cmp/cmp" @@ -148,3 +150,133 @@ func TestBuild_AutoTags(t *testing.T) { } }) } + +func TestTarPathValidation(t *testing.T) { + tests := []struct { + name string + tarPath string + setup func(string) error + cleanup func(string) error + expectSuccess bool + privileged bool + }{ + { + name: "valid_path_privileged", + tarPath: "", + setup: func(path string) error { + tmpDir, err := os.MkdirTemp("", "test-image-tar") + if err != nil { + return err + } + os.Setenv("DRONE_WORKSPACE", tmpDir) + return nil + }, + cleanup: func(path string) error { + tmpDir := os.Getenv("DRONE_WORKSPACE") + os.Unsetenv("DRONE_WORKSPACE") + return os.RemoveAll(tmpDir) + }, + expectSuccess: true, + privileged: true, + }, + { + name: "valid_path_unprivileged", + tarPath: "", + setup: func(path string) error { + tmpDir, err := os.MkdirTemp("", "test-image-tar") + if err != nil { + return err + } + os.Setenv("DRONE_WORKSPACE", tmpDir) + return nil + }, + cleanup: func(path string) error { + tmpDir := os.Getenv("DRONE_WORKSPACE") + os.Unsetenv("DRONE_WORKSPACE") + return os.RemoveAll(tmpDir) + }, + expectSuccess: true, + privileged: false, + }, + { + name: "empty_path", + tarPath: "", + setup: func(path string) error { return nil }, + cleanup: func(path string) error { return nil }, + expectSuccess: false, + privileged: false, + }, + { + name: "relative_path_dots", + tarPath: "", + setup: func(path string) error { + tmpDir, err := os.MkdirTemp("", "test-image-tar") + if err != nil { + return err + } + os.Setenv("DRONE_WORKSPACE", tmpDir) + return nil + }, + cleanup: func(path string) error { + tmpDir := os.Getenv("DRONE_WORKSPACE") + os.Unsetenv("DRONE_WORKSPACE") + return os.RemoveAll(tmpDir) + }, + expectSuccess: true, + privileged: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Skip privileged tests if not running as root + if tt.privileged && os.Getuid() != 0 { + t.Skip("Skipping privileged test as not running as root") + } + + if err := tt.setup(tt.tarPath); err != nil { + t.Fatalf("Setup failed: %v", err) + } + defer tt.cleanup(tt.tarPath) + + // Determine tar path based on test case + var tarPath string + tmpDir := os.Getenv("DRONE_WORKSPACE") + switch tt.name { + case "valid_path_privileged", "valid_path_unprivileged": + tarPath = filepath.Join(tmpDir, "test", "image.tar") + case "invalid_path_no_permissions": + tarPath = "/test/image.tar" + case "relative_path_dots": + tarPath = filepath.Join("..", "test", "image.tar") + default: + tarPath = tt.tarPath + } + + p := Plugin{ + Build: Build{ + TarPath: tarPath, + }, + } + + tarDir := filepath.Dir(p.Build.TarPath) + err := os.MkdirAll(tarDir, 0755) + if tt.expectSuccess { + if err != nil { + t.Errorf("Expected directory creation to succeed, got error: %v", err) + } + if _, err := os.Stat(tarDir); err != nil { + t.Errorf("Expected directory to exist after creation, got error: %v", err) + } + } + + result := getTarPath(p.Build.TarPath) + if tt.expectSuccess && result == "" { + t.Error("Expected non-empty tar path, got empty string") + } + if !tt.expectSuccess && result != "" { + t.Error("Expected empty tar path, got non-empty string") + } + }) + } +} diff --git a/pkg/output/output.go b/pkg/output/output.go index de6349d..cde3cd0 100644 --- a/pkg/output/output.go +++ b/pkg/output/output.go @@ -4,9 +4,15 @@ import ( "github.com/joho/godotenv" ) -func WritePluginOutputFile(outputFilePath, digest string) error { - output := map[string]string{ - "digest": digest, +func WritePluginOutputFile(outputFilePath, digest string, pluginTarPath string) error { + output := make(map[string]string) + if digest != "" { + output["digest"] = digest } + + if pluginTarPath != "" { + output["IMAGE_TAR_PATH"] = pluginTarPath + } + return godotenv.Write(output, outputFilePath) } diff --git a/pkg/output/output_test.go b/pkg/output/output_test.go new file mode 100644 index 0000000..2909d8e --- /dev/null +++ b/pkg/output/output_test.go @@ -0,0 +1,145 @@ +package output + +import ( + "os" + "path/filepath" + "testing" +) + +func TestWritePluginOutputFile(t *testing.T) { + tests := []struct { + name string + outputPath string + digest string + tarPath string + setup func(string) error + cleanup func(string) error + expectError bool + privileged bool + }{ + { + name: "valid_output_privileged", + outputPath: "", + digest: "sha256:test", + tarPath: "", + setup: func(path string) error { + tmpDir, err := os.MkdirTemp("", "test-output") + if err != nil { + return err + } + os.Setenv("DRONE_WORKSPACE", tmpDir) + return nil + }, + cleanup: func(path string) error { + tmpDir := os.Getenv("DRONE_WORKSPACE") + os.Unsetenv("DRONE_WORKSPACE") + return os.RemoveAll(tmpDir) + }, + expectError: false, + privileged: true, + }, + { + name: "valid_output_unprivileged", + outputPath: "", + digest: "sha256:test", + tarPath: "", + setup: func(path string) error { + tmpDir, err := os.MkdirTemp("", "test-output") + if err != nil { + return err + } + os.Setenv("DRONE_WORKSPACE", tmpDir) + return nil + }, + cleanup: func(path string) error { + tmpDir := os.Getenv("DRONE_WORKSPACE") + os.Unsetenv("DRONE_WORKSPACE") + return os.RemoveAll(tmpDir) + }, + expectError: false, + privileged: false, + }, + { + name: "digest_only", + outputPath: "", + digest: "sha256:test", + tarPath: "", + setup: func(path string) error { + tmpDir, err := os.MkdirTemp("", "test-output") + if err != nil { + return err + } + os.Setenv("DRONE_WORKSPACE", tmpDir) + return nil + }, + cleanup: func(path string) error { + tmpDir := os.Getenv("DRONE_WORKSPACE") + os.Unsetenv("DRONE_WORKSPACE") + return os.RemoveAll(tmpDir) + }, + expectError: false, + privileged: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Skip privileged tests if not running as root + if tt.privileged && os.Getuid() != 0 { + t.Skip("Skipping privileged test as not running as root") + } + + if err := tt.setup(tt.outputPath); err != nil { + t.Fatalf("Setup failed: %v", err) + } + defer tt.cleanup(tt.outputPath) + + tmpDir := os.Getenv("DRONE_WORKSPACE") + var outputPath, tarPath string + switch tt.name { + case "valid_output_privileged", "valid_output_unprivileged": + outputPath = filepath.Join(tmpDir, "test", "output.env") + tarPath = filepath.Join(tmpDir, "test", "image.tar") + case "invalid_output_path": + outputPath = filepath.Join("/root", "test", "output.env") + tarPath = filepath.Join("/root", "test", "image.tar") + case "digest_only": + outputPath = filepath.Join(tmpDir, "test", "output.env") + tarPath = "" + } + + err := os.MkdirAll(filepath.Dir(outputPath), 0755) + if err != nil { + t.Fatalf("Failed to create output directory: %v", err) + } + + err = WritePluginOutputFile(outputPath, tt.digest, tarPath) + + if tt.expectError && err == nil { + t.Error("Expected error, got none") + } + if !tt.expectError && err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if !tt.expectError && err == nil { + content, err := os.ReadFile(outputPath) + if err != nil { + t.Fatalf("Failed to read output file: %v", err) + } + + if tt.digest != "" && !contains(string(content), tt.digest) { + t.Error("Expected digest in output file") + } + + if tarPath != "" && !contains(string(content), tarPath) { + t.Error("Expected tar path in output file") + } + } + }) + } +} + +func contains(content, substring string) bool { + return len(substring) > 0 && content != "" && content != "\n" && content != "\r\n" +}