Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions api/v1beta1/artifactgenerator_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,13 @@ type CopyOperation struct {
// +optional
// +kubebuilder:validation:Enum=Overwrite;Merge;Extract
Strategy string `json:"strategy,omitempty"`

// Optional, when set to true, allows the copy operation to silently
// skip when the source path or glob pattern matches no files.
// When false (default), the reconciliation fails with an error if
// no files are matched.
// +optional
Optional bool `json:"optional,omitempty"`
}

// ArtifactGeneratorStatus defines the observed state of ArtifactGenerator.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ spec:
minLength: 1
pattern: ^@([a-z0-9]([a-z0-9_-]*[a-z0-9])?)/(.*)$
type: string
optional:
description: |-
Optional, when set to true, allows the copy operation to silently
skip when the source path or glob pattern matches no files.
When false (default), the reconciliation fails with an error if
no files are matched.
type: boolean
strategy:
description: |-
Strategy specifies the copy strategy to use.
Expand Down
3 changes: 3 additions & 0 deletions docs/spec/v1beta1/artifactgenerators.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,9 @@ Each copy operation specifies how to copy files from sources into the generated
file name at any depth.
- `strategy` (optional): Defines how to handle files during copy operations:
`Overwrite` (default), `Merge` (for YAML files), or `Extract` (for tarball archives).
- `optional` (optional): When set to `true`, the copy operation silently
skips if the source path or glob pattern matches no files. Defaults to `false`,
which causes the reconciliation to fail with an error on missing sources.

Copy operations use `cp`-like semantics:

Expand Down
9 changes: 9 additions & 0 deletions internal/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ func applyCopyOperation(ctx context.Context,
}

if len(matches) == 0 {
if op.Optional {
return nil
}
return fmt.Errorf("no files match pattern '%s' in source '%s'", srcPattern, srcAlias)
}

Expand All @@ -218,6 +221,9 @@ func applyCopyOperation(ctx context.Context,
}

if len(filteredMatches) == 0 {
if op.Optional {
return nil
}
return fmt.Errorf("all files matching pattern '%s' in source '%s' were excluded", srcPattern, srcAlias)
}

Expand Down Expand Up @@ -266,6 +272,9 @@ func applySingleSourceCopy(ctx context.Context,
srcInfo, err := srcRoot.Stat(srcPath)
if err != nil {
if os.IsNotExist(err) {
if op.Optional {
return nil
}
return fmt.Errorf("source '%s' does not exist", srcPath)
}
return fmt.Errorf("failed to stat source '%s': %w", srcPath, err)
Expand Down
92 changes: 92 additions & 0 deletions internal/builder/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,94 @@ func TestBuildErrors(t *testing.T) {
"source": tmpDir,
}

return spec, sources, workspaceDir
},
},
{
name: "optional skips when source file does not exist",
expectedError: "",
setupFunc: func(t *testing.T) (*swapi.OutputArtifact, map[string]string, string) {
tmpDir := t.TempDir()
srcDir := filepath.Join(tmpDir, "source")
workspaceDir := filepath.Join(tmpDir, "workspace")

setupDirs(t, srcDir, workspaceDir)

spec := &swapi.OutputArtifact{
Name: "optional-source-file",
Copy: []swapi.CopyOperation{
{
From: "@source/test.yaml",
To: "@artifact/",
Optional: true,
},
},
}

sources := map[string]string{
"source": srcDir,
}

return spec, sources, workspaceDir
},
},
{
name: "optional skips when glob pattern matches no files",
expectedError: "",
setupFunc: func(t *testing.T) (*swapi.OutputArtifact, map[string]string, string) {
tmpDir := t.TempDir()
srcDir := filepath.Join(tmpDir, "source")
workspaceDir := filepath.Join(tmpDir, "workspace")

setupDirs(t, srcDir, workspaceDir)

spec := &swapi.OutputArtifact{
Name: "optional-glob-match",
Copy: []swapi.CopyOperation{
{
From: "@source/*.yaml",
To: "@artifact/",
Optional: true,
},
},
}

sources := map[string]string{
"source": srcDir,
}

return spec, sources, workspaceDir
},
},
{
name: "optional skips when all files are excluded",
expectedError: "",
setupFunc: func(t *testing.T) (*swapi.OutputArtifact, map[string]string, string) {
tmpDir := t.TempDir()
srcDir := filepath.Join(tmpDir, "source")
workspaceDir := filepath.Join(tmpDir, "workspace")

setupDirs(t, srcDir, workspaceDir)

createFile(t, srcDir, "test.md", "test file")
createFile(t, srcDir, "other.md", "other file")

spec := &swapi.OutputArtifact{
Name: "optional-all-excluded",
Copy: []swapi.CopyOperation{
{
From: "@source/*.md",
To: "@artifact/",
Exclude: []string{"*.md"},
Optional: true,
},
},
}

sources := map[string]string{
"source": srcDir,
}

return spec, sources, workspaceDir
},
},
Expand All @@ -667,6 +755,10 @@ func TestBuildErrors(t *testing.T) {
spec, sources, workspace := tt.setupFunc(t)

_, err := testBuilder.Build(context.Background(), spec, sources, "test-namespace", workspace)
if tt.expectedError == "" {
g.Expect(err).ToNot(HaveOccurred())
return
}
if err == nil {
t.Logf("Staging directory contents:")
walkErr := filepath.Walk(filepath.Join(workspace, spec.Name), func(path string, info os.FileInfo, err error) error {
Expand Down