Compare commits

..

2 Commits

Author SHA1 Message Date
abhay084 22cb6a2d3d correct reference 2024-05-10 11:05:38 +05:30
abhay084 dc4f0a8c21 feat: [CI-10849]: add git-leaks support 2024-05-10 11:05:38 +05:30
18 changed files with 56 additions and 474 deletions
+2 -2
View File
@@ -11,7 +11,7 @@ platform:
steps:
- name: build
image: golang:1.22.4
image: golang:1.22
commands:
- go test ./...
- sh scripts/build.sh
@@ -178,7 +178,7 @@ pool:
steps:
- name: build
image: golang:1.22.4
image: golang:1.22
commands:
- go test ./...
- sh scripts/build.sh
-33
View File
@@ -1,33 +0,0 @@
package main
import (
"strings"
)
// CustomStringSliceFlag is like a regular StringSlice flag but with
// semicolon as a delimiter
type CustomStringSliceFlag struct {
Value []string
}
func (f *CustomStringSliceFlag) GetValue() []string {
if f.Value == nil {
return make([]string, 0)
}
return f.Value
}
func (f *CustomStringSliceFlag) String() string {
if f.Value == nil {
return ""
}
return strings.Join(f.Value, ";")
}
func (f *CustomStringSliceFlag) Set(v string) error {
for _, s := range strings.Split(v, ";") {
s = strings.TrimSpace(s)
f.Value = append(f.Value, s)
}
return nil
}
+2 -76
View File
@@ -7,7 +7,6 @@ import (
"io/ioutil"
"os"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
@@ -18,7 +17,6 @@ import (
"github.com/aws/aws-sdk-go/aws/session"
ecrv1 "github.com/aws/aws-sdk-go/service/ecr"
ecrpublicv1 "github.com/aws/aws-sdk-go/service/ecrpublic"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/aws/smithy-go"
"github.com/hashicorp/go-version"
"github.com/joho/godotenv"
@@ -37,7 +35,6 @@ const (
secretKeyEnv string = "AWS_SECRET_ACCESS_KEY"
ecrPublicDomain string = "public.ecr.aws"
kanikoVersionEnv string = "KANIKO_VERSION"
sessionKeyEnv string = "AWS_SESSION_TOKEN"
oneDotEightVersion string = "1.8.0"
defaultDigestFile string = "/kaniko/digest-file"
@@ -125,17 +122,6 @@ 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(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",
@@ -398,11 +384,6 @@ func main() {
Usage: "Number of retries for downloading base images.",
EnvVar: "PLUGIN_IMAGE_DOWNLOAD_RETRY",
},
cli.StringFlag{
Name: "oidc-token-id",
Usage: "OIDC token for assuming role via web identity",
EnvVar: "PLUGIN_OIDC_TOKEN_ID",
},
}
if err := app.Run(os.Args); err != nil {
@@ -417,7 +398,6 @@ func run(c *cli.Context) error {
noPush := c.Bool("no-push")
assumeRole := c.String("assume-role")
externalId := c.String("external-id")
oidcToken := c.String("oidc-token-id")
// setup docker config for azure registry and base image docker registry
err := setDockerAuth(
@@ -431,7 +411,6 @@ func run(c *cli.Context) error {
externalId,
region,
noPush,
oidcToken,
)
if err != nil {
return errors.Wrap(err, "failed to create docker config")
@@ -475,8 +454,6 @@ 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(),
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"),
@@ -541,7 +518,7 @@ func run(c *cli.Context) error {
}
func setDockerAuth(dockerRegistry, dockerUsername, dockerPassword, accessKey, secretKey,
registry, assumeRole, externalId, region string, noPush bool, oidcToken string) error {
registry, assumeRole, externalId, region string, noPush bool) error {
dockerConfig := docker.NewConfig()
credentials := []docker.RegistryCredentials{}
// set docker credentials for base image registry
@@ -554,24 +531,7 @@ func setDockerAuth(dockerRegistry, dockerUsername, dockerPassword, accessKey, se
credentials = append(credentials, pullFromRegistryCreds)
}
if assumeRole != "" && oidcToken != "" {
oidcAccessKey, oidcSecretKey, oidcSessionKey, err := getOidcCreds(oidcToken, assumeRole)
if err != nil {
return err
}
_ = os.Setenv(accessKeyEnv, oidcAccessKey)
_ = os.Setenv(secretKeyEnv, oidcSecretKey)
_ = os.Setenv(sessionKeyEnv, oidcSessionKey)
// kaniko-executor >=1.8.0 does not require additional cred helper logic for ECR,
// as it discovers ECR repositories automatically and acts accordingly.
if isKanikoVersionBelowOneDotEight(os.Getenv(kanikoVersionEnv)) {
dockerConfig.SetCredHelper(ecrPublicDomain, "ecr-login")
dockerConfig.SetCredHelper(registry, "ecr-login")
}
} else if assumeRole != "" {
if assumeRole != "" {
var err error
username, password, registry, err := getAssumeRoleCreds(region, assumeRole, externalId, "")
if err != nil {
@@ -811,37 +771,3 @@ func isKanikoVersionBelowOneDotEight(v string) bool {
return currVer.LessThan(oneEightVer)
}
func getOidcCreds(oidcToken, assumeRole string) (string, string, string, error) {
// Create a new session
sess, err := session.NewSession()
if err != nil {
return "", "", "", fmt.Errorf("failed to create AWS session: %w", err)
}
// Create a new STS client
svc := sts.New(sess)
// Prepare the input parameters for the STS call
duration := int64(time.Hour / time.Second)
input := &sts.AssumeRoleWithWebIdentityInput{
RoleArn: aws.String(assumeRole),
RoleSessionName: aws.String("kaniko-ecr-oidc"),
WebIdentityToken: aws.String(oidcToken),
DurationSeconds: aws.Int64(duration),
}
// Call the AssumeRoleWithWebIdentity function
result, err := svc.AssumeRoleWithWebIdentity(input)
if err != nil {
return "", "", "", fmt.Errorf("failed to assume role with web identity: %w", err)
}
// Check if credentials exist in the result
if result.Credentials == nil {
return "", "", "", errors.New("no credentials returned by AssumeRoleWithWebIdentity")
}
// Return the credentials
return *result.Credentials.AccessKeyId, *result.Credentials.SecretAccessKey, *result.Credentials.SessionToken, nil
}
+2 -2
View File
@@ -1,5 +1,5 @@
FROM gcr.io/kaniko-project/executor:v1.23.2
FROM gcr.io/kaniko-project/executor:v1.20.1
ENV KANIKO_VERSION=1.23.2
ENV KANIKO_VERSION=1.20.1
ADD release/linux/amd64/kaniko-acr /kaniko/
ENTRYPOINT ["/kaniko/kaniko-acr"]
+2 -2
View File
@@ -1,8 +1,8 @@
FROM gcr.io/kaniko-project/executor:v1.23.0
FROM gcr.io/kaniko-project/executor:v1.20.1
ENV HOME /root
ENV USER root
ENV KANIKO_VERSION=1.23.0
ENV KANIKO_VERSION=1.20.1
ADD release/linux/arm64/kaniko-acr /kaniko/
ENTRYPOINT ["/kaniko/kaniko-acr"]
+2 -2
View File
@@ -1,5 +1,5 @@
FROM gcr.io/kaniko-project/executor:v1.23.2
FROM gcr.io/kaniko-project/executor:v1.20.1
ENV KANIKO_VERSION=1.23.2
ENV KANIKO_VERSION=1.20.1
ADD release/linux/amd64/kaniko-docker /kaniko/
ENTRYPOINT ["/kaniko/kaniko-docker"]
+2 -2
View File
@@ -1,8 +1,8 @@
FROM gcr.io/kaniko-project/executor:v1.23.2
FROM gcr.io/kaniko-project/executor:v1.20.1
ENV HOME /root
ENV USER root
ENV KANIKO_VERSION=1.23.2
ENV KANIKO_VERSION=1.20.1
ADD release/linux/arm64/kaniko-docker /kaniko/
ENTRYPOINT ["/kaniko/kaniko-docker"]
+2 -2
View File
@@ -1,5 +1,5 @@
FROM gcr.io/kaniko-project/executor:v1.23.2
FROM gcr.io/kaniko-project/executor:v1.20.1
ENV KANIKO_VERSION=1.23.2
ENV KANIKO_VERSION=1.20.1
ADD release/linux/amd64/kaniko-ecr /kaniko/
ENTRYPOINT ["/kaniko/kaniko-ecr"]
+2 -2
View File
@@ -1,8 +1,8 @@
FROM gcr.io/kaniko-project/executor:v1.23.2
FROM gcr.io/kaniko-project/executor:v1.20.1
ENV HOME /root
ENV USER root
ENV KANIKO_VERSION=1.23.2
ENV KANIKO_VERSION=1.20.1
ADD release/linux/arm64/kaniko-ecr /kaniko/
ENTRYPOINT ["/kaniko/kaniko-ecr"]
+2 -2
View File
@@ -1,5 +1,5 @@
FROM gcr.io/kaniko-project/executor:v1.23.2
FROM gcr.io/kaniko-project/executor:v1.20.1
ENV KANIKO_VERSION=1.23.2
ENV KANIKO_VERSION=1.20.1
ADD release/linux/amd64/kaniko-gar /kaniko/
ENTRYPOINT ["/kaniko/kaniko-gar"]
+2 -2
View File
@@ -1,8 +1,8 @@
FROM gcr.io/kaniko-project/executor:v1.23.2
FROM gcr.io/kaniko-project/executor:v1.20.1
ENV HOME /root
ENV USER root
ENV KANIKO_VERSION=1.23.2
ENV KANIKO_VERSION=1.20.1
ADD release/linux/arm64/kaniko-gar /kaniko/
ENTRYPOINT ["/kaniko/kaniko-gar"]
+2 -2
View File
@@ -1,5 +1,5 @@
FROM gcr.io/kaniko-project/executor:v1.23.2
FROM gcr.io/kaniko-project/executor:v1.20.1
ENV KANIKO_VERSION=1.23.2
ENV KANIKO_VERSION=1.20.1
ADD release/linux/amd64/kaniko-gcr /kaniko/
ENTRYPOINT ["/kaniko/kaniko-gcr"]
+2 -2
View File
@@ -1,8 +1,8 @@
FROM gcr.io/kaniko-project/executor:v1.23.2
FROM gcr.io/kaniko-project/executor:v1.20.1
ENV HOME /root
ENV USER root
ENV KANIKO_VERSION=1.23.2
ENV KANIKO_VERSION=1.20.1
ADD release/linux/arm64/kaniko-gcr /kaniko/
ENTRYPOINT ["/kaniko/kaniko-gcr"]
+1 -1
View File
@@ -47,4 +47,4 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
)
go 1.22.4
go 1.22.0
+28 -56
View File
@@ -5,7 +5,6 @@ import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/drone/drone-kaniko/pkg/artifact"
@@ -17,32 +16,30 @@ import (
type (
// Build defines Docker build parameters.
Build struct {
DroneCommitRef string // Drone git commit reference
DroneRepoBranch string // Drone repo branch
Dockerfile string // Docker build Dockerfile
Context string // Docker build context
Tags []string // Docker build tags
AutoTag bool // Set this to auto detect tags from git commits and semver-tagged labels
AutoTagSuffix string // Suffix to append to the auto detect tags
ExpandTag bool // Set this to expand the `Tags` into semver-tagged labels
Args []string // Docker build args
ArgsNew []string // docker build args with comma seperated values
IsMultipleBuildArgs bool // env variable for fallback for docker build args
Target string // Docker build target
Repo string // Docker build repository
Mirrors []string // Docker repository mirrors
Labels []string // Label map
SkipTlsVerify bool // Docker skip tls certificate verify for registry
SnapshotMode string // Kaniko snapshot mode
EnableCache bool // Whether to enable kaniko cache
CacheRepo string // Remote repository that will be used to store cached layers
CacheTTL int // Cache timeout in hours
DigestFile string // Digest file location
NoPush bool // Set this flag if you only want to build the image, without pushing to a registry
Verbosity string // Log level
Platform string // Allows to build with another default platform than the host, similarly to docker build --platform
SkipUnusedStages bool // Build only used stages
TarPath string // Set this flag to save the image as a tarball at path
DroneCommitRef string // Drone git commit reference
DroneRepoBranch string // Drone repo branch
Dockerfile string // Docker build Dockerfile
Context string // Docker build context
Tags []string // Docker build tags
AutoTag bool // Set this to auto detect tags from git commits and semver-tagged labels
AutoTagSuffix string // Suffix to append to the auto detect tags
ExpandTag bool // Set this to expand the `Tags` into semver-tagged labels
Args []string // Docker build args
Target string // Docker build target
Repo string // Docker build repository
Mirrors []string // Docker repository mirrors
Labels []string // Label map
SkipTlsVerify bool // Docker skip tls certificate verify for registry
SnapshotMode string // Kaniko snapshot mode
EnableCache bool // Whether to enable kaniko cache
CacheRepo string // Remote repository that will be used to store cached layers
CacheTTL int // Cache timeout in hours
DigestFile string // Digest file location
NoPush bool // Set this flag if you only want to build the image, without pushing to a registry
Verbosity string // Log level
Platform string // Allows to build with another default platform than the host, similarly to docker build --platform
SkipUnusedStages bool // Build only used stages
TarPath string // Set this flag to save the image as a tarball at path
Cache bool // Enable or disable caching during the build process.
CacheDir string // Directory to store cached layers.
@@ -205,14 +202,8 @@ func (p Plugin) Exec() error {
}
// Set the build arguments
if p.Build.IsMultipleBuildArgs {
for _, arg := range p.Build.ArgsNew {
cmdArgs = append(cmdArgs, "--build-arg", arg)
}
} else {
for _, arg := range p.Build.Args {
cmdArgs = append(cmdArgs, "--build-arg", arg)
}
for _, arg := range p.Build.Args {
cmdArgs = append(cmdArgs, fmt.Sprintf("--build-arg=%s", arg))
}
// Set the labels
for _, label := range p.Build.Labels {
@@ -231,7 +222,7 @@ func (p Plugin) Exec() error {
}
if p.Build.SnapshotMode != "" {
cmdArgs = append(cmdArgs, fmt.Sprintf("--snapshot-mode=%s", p.Build.SnapshotMode))
cmdArgs = append(cmdArgs, fmt.Sprintf("--snapshotMode=%s", p.Build.SnapshotMode))
}
if p.Build.EnableCache {
@@ -267,12 +258,6 @@ 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))
}
@@ -422,11 +407,7 @@ func (p Plugin) Exec() error {
}
if p.Output.OutputFile != "" {
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 {
if err = output.WritePluginOutputFile(p.Output.OutputFile, getDigest(p.Build.DigestFile)); err != nil {
fmt.Fprintf(os.Stderr, "failed to write plugin output file at path: %s with error: %s\n", p.Output.OutputFile, err)
}
}
@@ -434,15 +415,6 @@ 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 {
-132
View File
@@ -1,8 +1,6 @@
package kaniko
import (
"os"
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
@@ -150,133 +148,3 @@ 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")
}
})
}
}
+3 -9
View File
@@ -4,15 +4,9 @@ import (
"github.com/joho/godotenv"
)
func WritePluginOutputFile(outputFilePath, digest string, pluginTarPath string) error {
output := make(map[string]string)
if digest != "" {
output["digest"] = digest
func WritePluginOutputFile(outputFilePath, digest string) error {
output := map[string]string{
"digest": digest,
}
if pluginTarPath != "" {
output["IMAGE_TAR_PATH"] = pluginTarPath
}
return godotenv.Write(output, outputFilePath)
}
-145
View File
@@ -1,145 +0,0 @@
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"
}