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
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ go 1.25.0
require (
github.com/ghodss/yaml v1.0.0
github.com/spf13/cobra v1.10.2
go.yaml.in/yaml/v3 v3.0.4
helm.sh/helm/v3 v3.18.5
helm.sh/helm/v4 v4.1.0
sigs.k8s.io/yaml v1.6.0
)

require (
Expand Down Expand Up @@ -85,7 +87,6 @@ require (
github.com/xlab/treeprint v1.2.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
Expand Down Expand Up @@ -117,5 +118,4 @@ require (
sigs.k8s.io/kustomize/kyaml v0.21.0 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
150 changes: 150 additions & 0 deletions pkg/helm/plugin_manifest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package helm

import (
"bytes"
"os"
"testing"

v3plugin "helm.sh/helm/v3/pkg/plugin"
yamlv3 "go.yaml.in/yaml/v3"
sigsyaml "sigs.k8s.io/yaml"
)

// helmV4MetadataV1 mirrors Helm v4's internal MetadataV1 struct.
// Helm v4 uses strict YAML parsing (KnownFields) for v1 manifests,
// so any field not in this struct will cause an unmarshal error.
type helmV4MetadataV1 struct {
APIVersion string `yaml:"apiVersion"`
Name string `yaml:"name"`
Type string `yaml:"type"`
Runtime string `yaml:"runtime"`
Version string `yaml:"version"`
SourceURL string `yaml:"sourceURL,omitempty"`
Config map[string]any `yaml:"config"`
RuntimeConfig map[string]any `yaml:"runtimeConfig"`
}

// helmV4MetadataLegacy mirrors Helm v4's internal MetadataLegacy struct.
// Helm v4 uses non-strict YAML parsing for legacy manifests (no apiVersion).
type helmV4MetadataLegacy struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
Usage string `yaml:"usage"`
Description string `yaml:"description"`
PlatformCommand []helmV4PlatformCommand `yaml:"platformCommand"`
Command string `yaml:"command"`
IgnoreFlags bool `yaml:"ignoreFlags"`
PlatformHooks map[string][]helmV4PlatformCommand `yaml:"platformHooks"`
Hooks map[string]string `yaml:"hooks"`
Downloaders []helmV4Downloaders `yaml:"downloaders"`
}

type helmV4PlatformCommand struct {
OperatingSystem string `yaml:"os"`
Architecture string `yaml:"arch"`
Command string `yaml:"command"`
Args []string `yaml:"args"`
}

type helmV4Downloaders struct {
Protocols []string `yaml:"protocols"`
Command string `yaml:"command"`
}

func TestPluginManifestHelm3Compatible(t *testing.T) {
// Helm 3 uses sigs.k8s.io/yaml.UnmarshalStrict which converts YAML to JSON
// and rejects unknown fields. This is the exact parsing path used by
// helm.sh/helm/v3/pkg/plugin.LoadDir().
files := map[string]string{
"plugin.yaml": "../../plugin.yaml",
"testdata/plugin-helm3.yaml": "../../testdata/plugin-helm3.yaml",
}

for name, path := range files {
t.Run(name, func(t *testing.T) {
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("failed to read %s: %v", path, err)
}

var m v3plugin.Metadata
if err := sigsyaml.UnmarshalStrict(data, &m); err != nil {
t.Errorf("%s is not valid for Helm 3: %v", name, err)
}

if m.Name != "cm-push" {
t.Errorf("expected plugin name 'cm-push', got %q", m.Name)
}
})
}
}

func TestPluginManifestHelm4V1Compatible(t *testing.T) {
// Helm 4 uses go.yaml.in/yaml/v3 with KnownFields(true) for v1 manifests
// (those with apiVersion set). This rejects any YAML field not defined
// in MetadataV1. This is the exact parsing path used by
// helm.sh/helm/v4/internal/plugin.loadMetadataV1().
path := "../../testdata/plugin-helm4.yaml"
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("failed to read %s: %v", path, err)
}

var mv1 helmV4MetadataV1
d := yamlv3.NewDecoder(bytes.NewReader(data))
d.KnownFields(true)
if err := d.Decode(&mv1); err != nil {
t.Errorf("testdata/plugin-helm4.yaml is not valid for Helm 4 (v1 format): %v", err)
}

if mv1.Name != "cm-push" {
t.Errorf("expected plugin name 'cm-push', got %q", mv1.Name)
}
if mv1.APIVersion != "v1" {
t.Errorf("expected apiVersion 'v1', got %q", mv1.APIVersion)
}
}

func TestPluginManifestHelm4LegacyCompatible(t *testing.T) {
// Helm 4 supports legacy plugin manifests (no apiVersion field).
// It uses non-strict YAML parsing for these. The shipped plugin.yaml
// must be parseable by Helm 4's legacy loader since it's the first
// file Helm sees before the install hook can swap manifests.
// This is the exact parsing path used by
// helm.sh/helm/v4/internal/plugin.loadMetadataLegacy().
files := map[string]string{
"plugin.yaml": "../../plugin.yaml",
"testdata/plugin-helm3.yaml": "../../testdata/plugin-helm3.yaml",
}

for name, path := range files {
t.Run(name, func(t *testing.T) {
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("failed to read %s: %v", path, err)
}

// Verify no apiVersion field (which would route to strict V1 parsing)
var peek struct {
APIVersion string `yaml:"apiVersion"`
}
if err := yamlv3.Unmarshal(data, &peek); err != nil {
t.Fatalf("failed to peek apiVersion: %v", err)
}
if peek.APIVersion != "" {
t.Errorf("%s has apiVersion=%q; shipped plugin.yaml must use legacy format (no apiVersion) for Helm 3/4 compatibility", name, peek.APIVersion)
}

// Parse as legacy (non-strict, matching Helm 4's loadMetadataLegacy)
var ml helmV4MetadataLegacy
d := yamlv3.NewDecoder(bytes.NewReader(data))
if err := d.Decode(&ml); err != nil {
t.Errorf("%s is not valid for Helm 4 legacy format: %v", name, err)
}

if ml.Name != "cm-push" {
t.Errorf("expected plugin name 'cm-push', got %q", ml.Name)
}
})
}
}
14 changes: 3 additions & 11 deletions plugin.yaml
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
name: "cm-push"
version: "0.11.0"
apiVersion: v1
type: cli/v1
runtime: subprocess
config:
usage: cm-push
shortHelp: Push chart package to ChartMuseum
longHelp: Helm plugin to push chart package to ChartMuseum
runtimeConfig:
platformCommand:
- command: ${HELM_PLUGIN_DIR}/bin/helm-cm-push
usage: "Please see https://github.com/chartmuseum/helm-push for usage"
description: "Push chart package to ChartMuseum"
command: "$HELM_PLUGIN_DIR/bin/helm-cm-push"
downloaders:
- command: "bin/helm-cm-push"
protocols:
- "cm"
useTunnel: false
hooks:
install: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh"
update: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh"
1 change: 0 additions & 1 deletion testdata/plugin-helm3.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ downloaders:
- command: "bin/helm-cm-push"
protocols:
- "cm"
useTunnel: false
hooks:
install: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh"
update: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh"
8 changes: 0 additions & 8 deletions testdata/plugin-helm4.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,3 @@ config:
runtimeConfig:
platformCommand:
- command: ${HELM_PLUGIN_DIR}/bin/helm-cm-push
downloaders:
- command: "bin/helm-cm-push"
protocols:
- "cm"
useTunnel: false
hooks:
install: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh"
update: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh"
Loading