|
|
|
@ -1,3 +1,5 @@
|
|
|
|
|
//go:build !windows
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
Copyright The Helm Authors.
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
@ -16,7 +18,10 @@ limitations under the License.
|
|
|
|
|
package pusher
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"io"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"helm.sh/helm/v4/pkg/registry"
|
|
|
|
@ -94,3 +99,330 @@ func TestNewOCIPusher(t *testing.T) {
|
|
|
|
|
t.Errorf("Expected NewOCIPusher to contain %p as RegistryClient, got %p", registryClient, op.opts.registryClient)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestOCIPusher_Push_ErrorHandling(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
chartRef string
|
|
|
|
|
expectedError string
|
|
|
|
|
setupFunc func() string
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "non-existent file",
|
|
|
|
|
chartRef: "/non/existent/file.tgz",
|
|
|
|
|
expectedError: "no such file",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "directory instead of file",
|
|
|
|
|
expectedError: "cannot push directory, must provide chart archive (.tgz)",
|
|
|
|
|
setupFunc: func() string {
|
|
|
|
|
tempDir := t.TempDir()
|
|
|
|
|
return tempDir
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
pusher, err := NewOCIPusher()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
chartRef := tt.chartRef
|
|
|
|
|
if tt.setupFunc != nil {
|
|
|
|
|
chartRef = tt.setupFunc()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = pusher.Push(chartRef, "oci://localhost:5000/test")
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("Expected error but got none")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !strings.Contains(err.Error(), tt.expectedError) {
|
|
|
|
|
t.Errorf("Expected error containing %q, got %q", tt.expectedError, err.Error())
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestOCIPusher_newRegistryClient(t *testing.T) {
|
|
|
|
|
cd := "../../testdata"
|
|
|
|
|
join := filepath.Join
|
|
|
|
|
ca, pub, priv := join(cd, "rootca.crt"), join(cd, "crt.pem"), join(cd, "key.pem")
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
opts []Option
|
|
|
|
|
expectError bool
|
|
|
|
|
errorContains string
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "plain HTTP",
|
|
|
|
|
opts: []Option{WithPlainHTTP(true)},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "with TLS client config",
|
|
|
|
|
opts: []Option{
|
|
|
|
|
WithTLSClientConfig(pub, priv, ca),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "with insecure skip TLS verify",
|
|
|
|
|
opts: []Option{
|
|
|
|
|
WithInsecureSkipTLSVerify(true),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "with cert and key only",
|
|
|
|
|
opts: []Option{
|
|
|
|
|
WithTLSClientConfig(pub, priv, ""),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "with CA file only",
|
|
|
|
|
opts: []Option{
|
|
|
|
|
WithTLSClientConfig("", "", ca),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "default client without options",
|
|
|
|
|
opts: []Option{},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "invalid cert file",
|
|
|
|
|
opts: []Option{
|
|
|
|
|
WithTLSClientConfig("/non/existent/cert.pem", priv, ca),
|
|
|
|
|
},
|
|
|
|
|
expectError: true,
|
|
|
|
|
errorContains: "can't create TLS config",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "invalid key file",
|
|
|
|
|
opts: []Option{
|
|
|
|
|
WithTLSClientConfig(pub, "/non/existent/key.pem", ca),
|
|
|
|
|
},
|
|
|
|
|
expectError: true,
|
|
|
|
|
errorContains: "can't create TLS config",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "invalid CA file",
|
|
|
|
|
opts: []Option{
|
|
|
|
|
WithTLSClientConfig("", "", "/non/existent/ca.crt"),
|
|
|
|
|
},
|
|
|
|
|
expectError: true,
|
|
|
|
|
errorContains: "can't create TLS config",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "combined TLS options",
|
|
|
|
|
opts: []Option{
|
|
|
|
|
WithTLSClientConfig(pub, priv, ca),
|
|
|
|
|
WithInsecureSkipTLSVerify(true),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
pusher, err := NewOCIPusher(tt.opts...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
op, ok := pusher.(*OCIPusher)
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatal("Expected *OCIPusher")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client, err := op.newRegistryClient()
|
|
|
|
|
if tt.expectError {
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("Expected error but got none")
|
|
|
|
|
}
|
|
|
|
|
if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) {
|
|
|
|
|
t.Errorf("Expected error containing %q, got %q", tt.errorContains, err.Error())
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if client == nil {
|
|
|
|
|
t.Fatal("Expected non-nil registry client")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestOCIPusher_Push_ChartOperations(t *testing.T) {
|
|
|
|
|
// Path to test charts
|
|
|
|
|
chartPath := "../../pkg/cmd/testdata/testcharts/compressedchart-0.1.0.tgz"
|
|
|
|
|
chartWithProvPath := "../../pkg/cmd/testdata/testcharts/signtest-0.1.0.tgz"
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
chartRef string
|
|
|
|
|
href string
|
|
|
|
|
options []Option
|
|
|
|
|
setupFunc func(t *testing.T) (string, func())
|
|
|
|
|
expectError bool
|
|
|
|
|
errorContains string
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "invalid chart file",
|
|
|
|
|
chartRef: "../../pkg/action/testdata/charts/corrupted-compressed-chart.tgz",
|
|
|
|
|
href: "oci://localhost:5000/test",
|
|
|
|
|
expectError: true,
|
|
|
|
|
errorContains: "does not appear to be a gzipped archive",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "chart read error",
|
|
|
|
|
setupFunc: func(t *testing.T) (string, func()) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
// Create a valid chart file that we'll make unreadable
|
|
|
|
|
tempDir := t.TempDir()
|
|
|
|
|
tempChart := filepath.Join(tempDir, "temp-chart.tgz")
|
|
|
|
|
|
|
|
|
|
// Copy a valid chart
|
|
|
|
|
src, err := os.Open(chartPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
defer src.Close()
|
|
|
|
|
|
|
|
|
|
dst, err := os.Create(tempChart)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, err := io.Copy(dst, src); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
dst.Close()
|
|
|
|
|
|
|
|
|
|
// Make the file unreadable
|
|
|
|
|
if err := os.Chmod(tempChart, 0000); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return tempChart, func() {
|
|
|
|
|
os.Chmod(tempChart, 0644) // Restore permissions for cleanup
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
href: "oci://localhost:5000/test",
|
|
|
|
|
expectError: true,
|
|
|
|
|
errorContains: "permission denied",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "push with provenance file - loading phase",
|
|
|
|
|
chartRef: chartWithProvPath,
|
|
|
|
|
href: "oci://registry.example.com/charts",
|
|
|
|
|
setupFunc: func(t *testing.T) (string, func()) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
// Copy chart and create a .prov file for it
|
|
|
|
|
tempDir := t.TempDir()
|
|
|
|
|
tempChart := filepath.Join(tempDir, "signtest-0.1.0.tgz")
|
|
|
|
|
tempProv := filepath.Join(tempDir, "signtest-0.1.0.tgz.prov")
|
|
|
|
|
|
|
|
|
|
// Copy chart file
|
|
|
|
|
src, err := os.Open(chartWithProvPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
defer src.Close()
|
|
|
|
|
|
|
|
|
|
dst, err := os.Create(tempChart)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, err := io.Copy(dst, src); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
dst.Close()
|
|
|
|
|
|
|
|
|
|
// Create provenance file
|
|
|
|
|
if err := os.WriteFile(tempProv, []byte("test provenance data"), 0644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return tempChart, func() {}
|
|
|
|
|
},
|
|
|
|
|
expectError: true, // Will fail at the registry push step
|
|
|
|
|
errorContains: "", // Error depends on registry client behavior
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
chartRef := tt.chartRef
|
|
|
|
|
var cleanup func()
|
|
|
|
|
|
|
|
|
|
if tt.setupFunc != nil {
|
|
|
|
|
chartRef, cleanup = tt.setupFunc(t)
|
|
|
|
|
if cleanup != nil {
|
|
|
|
|
defer cleanup()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skip test if chart file doesn't exist and we're not expecting an error
|
|
|
|
|
if _, err := os.Stat(chartRef); err != nil && !tt.expectError {
|
|
|
|
|
t.Skipf("Test chart %s not found, skipping test", chartRef)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pusher, err := NewOCIPusher(tt.options...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = pusher.Push(chartRef, tt.href)
|
|
|
|
|
|
|
|
|
|
if tt.expectError {
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("Expected error but got none")
|
|
|
|
|
}
|
|
|
|
|
if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) {
|
|
|
|
|
t.Errorf("Expected error containing %q, got %q", tt.errorContains, err.Error())
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestOCIPusher_Push_MultipleOptions(t *testing.T) {
|
|
|
|
|
chartPath := "../../pkg/cmd/testdata/testcharts/compressedchart-0.1.0.tgz"
|
|
|
|
|
|
|
|
|
|
// Skip test if chart file doesn't exist
|
|
|
|
|
if _, err := os.Stat(chartPath); err != nil {
|
|
|
|
|
t.Skipf("Test chart %s not found, skipping test", chartPath)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pusher, err := NewOCIPusher()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test that multiple options are applied correctly
|
|
|
|
|
err = pusher.Push(chartPath, "oci://localhost:5000/test",
|
|
|
|
|
WithPlainHTTP(true),
|
|
|
|
|
WithInsecureSkipTLSVerify(true),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// We expect an error since we're not actually pushing to a registry
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("Expected error when pushing without a valid registry")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify options were applied
|
|
|
|
|
op := pusher.(*OCIPusher)
|
|
|
|
|
if !op.opts.plainHTTP {
|
|
|
|
|
t.Error("Expected plainHTTP option to be applied")
|
|
|
|
|
}
|
|
|
|
|
if !op.opts.insecureSkipTLSverify {
|
|
|
|
|
t.Error("Expected insecureSkipTLSverify option to be applied")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|