Skip to content
Draft
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
99 changes: 99 additions & 0 deletions PATCHES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Patches Support

Chartify now supports Kustomize-style patches through the `Patches` field in `ChartifyOpts` and the `--patch` CLI flag.

## Overview

The patches feature allows you to apply modifications to Kubernetes resources using either:
- **Strategic Merge Patches**: YAML that gets merged with existing resources
- **JSON Patches**: RFC 6902 JSON patch operations

Patches can be:
- **File-based**: Reference patch files using the `Path` field
- **Inline**: Include patch content directly using the `Patch` field
- **Targeted**: Specify which resources to patch using the `Target` field

## Examples

### Strategic Merge Patch (File-based)

```yaml
patches:
- path: "./my-patch.yaml"
```

Where `my-patch.yaml` contains:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
template:
spec:
containers:
- name: myapp
image: myapp:v2.0.0
```

### Strategic Merge Patch (Inline)

```yaml
patches:
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 5
```

### JSON Patch (Inline with Target)

```yaml
patches:
- target:
kind: Deployment
name: myapp
patch: |-
- op: replace
path: /spec/replicas
value: 7
- op: replace
path: /spec/template/spec/containers/0/image
value: myapp:v3.0.0
```

## CLI Usage

Use the `--patch` flag to specify patch files:

```bash
chartify myapp ./my-chart -o ./output --patch ./my-patch.yaml
```

Multiple patches can be specified:

```bash
chartify myapp ./my-chart -o ./output --patch ./patch1.yaml --patch ./patch2.yaml
```

## Auto-detection

Chartify automatically detects whether a patch is a Strategic Merge Patch or JSON Patch based on the content structure:

- **JSON Patches**: Must contain operations with `op` and `path` fields
- **Strategic Merge Patches**: Standard Kubernetes YAML resources

JSON patches require a target specification to identify which resources to patch.

## Backward Compatibility

The new patches feature is fully backward compatible with existing functionality:
- `JsonPatches` field continues to work as before
- `StrategicMergePatches` field continues to work as before
- `--strategic-merge-patch` CLI flag continues to work as before

The new `Patches` field provides a unified interface that can handle both types.
32 changes: 31 additions & 1 deletion chartify.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,30 @@ var (
ContentDirs = []string{"templates", "charts", "crds"}
)

// PatchTarget specifies the target resource(s) for a patch
type PatchTarget struct {
Group string `yaml:"group,omitempty"`
Version string `yaml:"version,omitempty"`
Kind string `yaml:"kind,omitempty"`
Name string `yaml:"name,omitempty"`
Namespace string `yaml:"namespace,omitempty"`
LabelSelector string `yaml:"labelSelector,omitempty"`
AnnotationSelector string `yaml:"annotationSelector,omitempty"`
}

// Patch represents a patch with optional target specification, similar to Kustomize's patches field
type Patch struct {
// Path is the path to a patch file. Mutually exclusive with Patch.
Path string `yaml:"path,omitempty"`

// Patch is the inline patch content. Mutually exclusive with Path.
Patch string `yaml:"patch,omitempty"`

// Target specifies which resources the patch should be applied to.
// If not specified, the patch is applied based on the patch content's metadata.
Target *PatchTarget `yaml:"target,omitempty"`
}

type ChartifyOpts struct {
// ID is the ID of the temporary chart being generated.
// The ID is used in e.g. the directory name of the temporary local chart
Expand Down Expand Up @@ -64,6 +88,11 @@ type ChartifyOpts struct {
JsonPatches []string
StrategicMergePatches []string

// Patches is a list of patches and their associated targets, similar to Kustomize's patches field.
// Each patch can be applied to multiple objects and auto-detects whether the patch is a Strategic Merge Patch or JSON Patch.
// Supports both inline patches and file-based patches.
Patches []Patch

// Transformers is the list of YAML files each defines a Kustomize transformer
// See https://github.com/kubernetes-sigs/kustomize/blob/master/examples/configureBuiltinPlugin.md#configuring-the-builtin-plugins-instead for more information.
Transformers []string
Expand Down Expand Up @@ -416,7 +445,7 @@ func (r *Runner) Chartify(release, dirOrChart string, opts ...ChartifyOption) (s

var (
needsNamespaceOverride = overrideNamespace != ""
needsKustomizeBuild = len(u.JsonPatches) > 0 || len(u.StrategicMergePatches) > 0 || len(u.Transformers) > 0
needsKustomizeBuild = len(u.JsonPatches) > 0 || len(u.StrategicMergePatches) > 0 || len(u.Patches) > 0 || len(u.Transformers) > 0
needsInjections = len(u.Injectors) > 0 || len(u.Injects) > 0
)

Expand Down Expand Up @@ -449,6 +478,7 @@ func (r *Runner) Chartify(release, dirOrChart string, opts ...ChartifyOption) (s
patchOpts := &PatchOpts{
JsonPatches: u.JsonPatches,
StrategicMergePatches: u.StrategicMergePatches,
Patches: u.Patches,
Transformers: u.Transformers,
EnableAlphaPlugins: u.EnableKustomizeAlphaPlugins,
}
Expand Down
166 changes: 166 additions & 0 deletions chartify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,169 @@ func TestUseHelmChartsInKustomize(t *testing.T) {
})
}
}

func TestPatches(t *testing.T) {
t.Run("strategic merge patch with path", func(t *testing.T) {
patches := []Patch{
{
Path: "./testdata/patches/strategic-merge.yaml",
},
}

// Test that the patch struct is properly constructed
require.Equal(t, "./testdata/patches/strategic-merge.yaml", patches[0].Path)
require.Empty(t, patches[0].Patch)
require.Nil(t, patches[0].Target)
})

t.Run("json patch with inline content and target", func(t *testing.T) {
patches := []Patch{
{
Patch: `- op: replace
path: /spec/replicas
value: 5`,
Target: &PatchTarget{
Kind: "Deployment",
Name: "myapp",
},
},
}

// Test that the patch struct is properly constructed
require.Empty(t, patches[0].Path)
require.Contains(t, patches[0].Patch, "op: replace")
require.Equal(t, "Deployment", patches[0].Target.Kind)
require.Equal(t, "myapp", patches[0].Target.Name)
})

t.Run("strategic merge patch with inline content", func(t *testing.T) {
patches := []Patch{
{
Patch: `apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3`,
},
}

// Test that the patch struct is properly constructed
require.Empty(t, patches[0].Path)
require.Contains(t, patches[0].Patch, "kind: Deployment")
require.Nil(t, patches[0].Target)
})

// Test validation logic that would be in patch processing
t.Run("validation errors", func(t *testing.T) {
testCases := []struct {
name string
patch Patch
wantErr string
}{
{
name: "both path and patch set",
patch: Patch{
Path: "./some/path.yaml",
Patch: "some content",
},
wantErr: "both \"path\" and \"patch\" are set",
},
{
name: "neither path nor patch set",
patch: Patch{
// empty
},
wantErr: "either \"path\" or \"patch\" must be set",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// This simulates the validation that would happen in patch processing
hasPath := tc.patch.Path != ""
hasPatch := tc.patch.Patch != ""

if hasPath && hasPatch {
require.Contains(t, tc.wantErr, "both \"path\" and \"patch\" are set")
} else if !hasPath && !hasPatch {
require.Contains(t, tc.wantErr, "either \"path\" or \"patch\" must be set")
}
})
}
})
}

func TestPatchIntegration(t *testing.T) {
helm := "helm"
if h := os.Getenv("HELM_BIN"); h != "" {
helm = h
}

setupHelmConfig(t)

runner := New(UseHelm3(true), HelmBin(helm))

t.Run("strategic merge patch file", func(t *testing.T) {
// Test that a strategic merge patch file works
opts := ChartifyOpts{
Patches: []Patch{
{
Path: "./testdata/patches/strategic-merge.yaml",
},
},
}

_, err := runner.Chartify("myapp", "./testdata/simple_manifest", WithChartifyOpts(&opts))
require.NoError(t, err)
})

t.Run("inline strategic merge patch", func(t *testing.T) {
// Test that an inline strategic merge patch works
opts := ChartifyOpts{
Patches: []Patch{
{
Patch: `apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 5
template:
spec:
containers:
- name: myapp
image: myapp:v4.0.0`,
},
},
}

resultDir, err := runner.Chartify("myapp", "./testdata/simple_manifest", WithChartifyOpts(&opts))
require.NoError(t, err)
require.DirExists(t, resultDir)
})

t.Run("inline json patch with target", func(t *testing.T) {
// Test that an inline JSON patch with target works
opts := ChartifyOpts{
Patches: []Patch{
{
Patch: `- op: replace
path: /spec/replicas
value: 7
- op: replace
path: /spec/template/spec/containers/0/image
value: myapp:v5.0.0`,
Target: &PatchTarget{
Kind: "Deployment",
Name: "myapp",
},
},
},
}

resultDir, err := runner.Chartify("myapp", "./testdata/simple_manifest", WithChartifyOpts(&opts))
require.NoError(t, err)
require.DirExists(t, resultDir)
})
}
10 changes: 10 additions & 0 deletions cmd/chartify/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,20 @@ func main() {
AdhocChartDependencies: nil,
JsonPatches: nil,
StrategicMergePatches: nil,
Patches: nil,
WorkaroundOutputDirIssue: false,
IncludeCRDs: false,
}

deps := stringSlice{}
patches := stringSlice{}

flag.StringVar(&file, "f", "-", "The path to the input file or stdout(-)")
flag.StringVar(&outDir, "o", "", "The path to the output directory")
flag.Var(&deps, "d", "one or more \"alias=chart:version\" to add adhoc chart dependencies")
flag.BoolVar(&opts.IncludeCRDs, "include-crds", false, "Whether to render CRDs contained in the chart and include the results into the output")
flag.StringVar(&strategicMergePatch, "strategic-merge-patch", "", "Path to a kustomize strategic merge patch file")
flag.Var(&patches, "patch", "Path to a patch file (can be strategic merge patch or JSON patch, auto-detected)")

flag.Parse()

Expand All @@ -60,6 +63,13 @@ func main() {
opts.StrategicMergePatches = append(opts.StrategicMergePatches, strategicMergePatch)
}

// Convert patch file paths to Patch structs
for _, patchFile := range patches {
opts.Patches = append(opts.Patches, chartify.Patch{
Path: patchFile,
})
}

opts.DeprecatedAdhocChartDependencies = deps

c := chartify.New(chartify.UseHelm3(true), chartify.HelmBin("helm"))
Expand Down
Loading