mirror of https://github.com/helm/helm
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
807 lines
22 KiB
807 lines
22 KiB
/*
|
|
Copyright The Helm Authors.
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package installer // import "helm.sh/helm/v4/internal/plugin/installer"
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/opencontainers/go-digest"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
|
|
"helm.sh/helm/v4/internal/test/ensure"
|
|
"helm.sh/helm/v4/pkg/cli"
|
|
"helm.sh/helm/v4/pkg/getter"
|
|
"helm.sh/helm/v4/pkg/helmpath"
|
|
)
|
|
|
|
var _ Installer = new(OCIInstaller)
|
|
|
|
// createTestPluginTarGz creates a test plugin tar.gz with plugin.yaml
|
|
func createTestPluginTarGz(t *testing.T, pluginName string) []byte {
|
|
t.Helper()
|
|
|
|
var buf bytes.Buffer
|
|
gzWriter := gzip.NewWriter(&buf)
|
|
tarWriter := tar.NewWriter(gzWriter)
|
|
|
|
// Add plugin.yaml
|
|
pluginYAML := fmt.Sprintf(`name: %s
|
|
version: "1.0.0"
|
|
description: "Test plugin for OCI installer"
|
|
command: "$HELM_PLUGIN_DIR/bin/%s"
|
|
`, pluginName, pluginName)
|
|
header := &tar.Header{
|
|
Name: "plugin.yaml",
|
|
Mode: 0644,
|
|
Size: int64(len(pluginYAML)),
|
|
Typeflag: tar.TypeReg,
|
|
}
|
|
if err := tarWriter.WriteHeader(header); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := tarWriter.Write([]byte(pluginYAML)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Add bin directory
|
|
dirHeader := &tar.Header{
|
|
Name: "bin/",
|
|
Mode: 0755,
|
|
Typeflag: tar.TypeDir,
|
|
}
|
|
if err := tarWriter.WriteHeader(dirHeader); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Add executable
|
|
execContent := fmt.Sprintf("#!/bin/sh\necho '%s test plugin'", pluginName)
|
|
execHeader := &tar.Header{
|
|
Name: fmt.Sprintf("bin/%s", pluginName),
|
|
Mode: 0755,
|
|
Size: int64(len(execContent)),
|
|
Typeflag: tar.TypeReg,
|
|
}
|
|
if err := tarWriter.WriteHeader(execHeader); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := tarWriter.Write([]byte(execContent)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tarWriter.Close()
|
|
gzWriter.Close()
|
|
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// mockOCIRegistryWithArtifactType creates a mock OCI registry server using the new artifact type approach
|
|
func mockOCIRegistryWithArtifactType(t *testing.T, pluginName string) (*httptest.Server, string) {
|
|
t.Helper()
|
|
|
|
pluginData := createTestPluginTarGz(t, pluginName)
|
|
layerDigest := fmt.Sprintf("sha256:%x", sha256Sum(pluginData))
|
|
|
|
// Create empty config data (as per OCI v1.1+ spec)
|
|
configData := []byte("{}")
|
|
configDigest := fmt.Sprintf("sha256:%x", sha256Sum(configData))
|
|
|
|
// Create manifest with artifact type
|
|
manifest := ocispec.Manifest{
|
|
MediaType: ocispec.MediaTypeImageManifest,
|
|
ArtifactType: "application/vnd.helm.plugin.v1+json", // Using artifact type
|
|
Config: ocispec.Descriptor{
|
|
MediaType: "application/vnd.oci.empty.v1+json", // Empty config
|
|
Digest: digest.Digest(configDigest),
|
|
Size: int64(len(configData)),
|
|
},
|
|
Layers: []ocispec.Descriptor{
|
|
{
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
|
Digest: digest.Digest(layerDigest),
|
|
Size: int64(len(pluginData)),
|
|
Annotations: map[string]string{
|
|
ocispec.AnnotationTitle: pluginName + "-1.0.0.tgz", // Layer named with version
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
manifestData, err := json.Marshal(manifest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
manifestDigest := fmt.Sprintf("sha256:%x", sha256Sum(manifestData))
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/v2/") && !strings.Contains(r.URL.Path, "/manifests/") && !strings.Contains(r.URL.Path, "/blobs/"):
|
|
// API version check
|
|
w.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("{}"))
|
|
|
|
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/manifests/") && strings.Contains(r.URL.Path, pluginName):
|
|
// Return manifest
|
|
w.Header().Set("Content-Type", ocispec.MediaTypeImageManifest)
|
|
w.Header().Set("Docker-Content-Digest", manifestDigest)
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write(manifestData)
|
|
|
|
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/blobs/"+layerDigest):
|
|
// Return layer data
|
|
w.Header().Set("Content-Type", "application/vnd.oci.image.layer.v1.tar")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write(pluginData)
|
|
|
|
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/blobs/"+configDigest):
|
|
// Return config data
|
|
w.Header().Set("Content-Type", "application/vnd.oci.empty.v1+json")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write(configData)
|
|
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
|
|
// Parse server URL to get host:port format for OCI reference
|
|
serverURL, err := url.Parse(server.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
registryHost := serverURL.Host
|
|
|
|
return server, registryHost
|
|
}
|
|
|
|
// sha256Sum calculates SHA256 sum of data
|
|
func sha256Sum(data []byte) []byte {
|
|
h := sha256.New()
|
|
h.Write(data)
|
|
return h.Sum(nil)
|
|
}
|
|
|
|
func TestNewOCIInstaller(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
source string
|
|
expectName string
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "valid OCI reference with tag",
|
|
source: "oci://ghcr.io/user/plugin-name:v1.0.0",
|
|
expectName: "plugin-name",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "valid OCI reference with digest",
|
|
source: "oci://ghcr.io/user/plugin-name@sha256:1234567890abcdef",
|
|
expectName: "plugin-name",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "valid OCI reference without tag",
|
|
source: "oci://ghcr.io/user/plugin-name",
|
|
expectName: "plugin-name",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "valid OCI reference with multiple path segments",
|
|
source: "oci://registry.example.com/org/team/plugin-name:latest",
|
|
expectName: "plugin-name",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "invalid OCI reference - no path",
|
|
source: "oci://registry.example.com",
|
|
expectName: "",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "valid OCI reference - single path segment",
|
|
source: "oci://registry.example.com/plugin",
|
|
expectName: "plugin",
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
installer, err := NewOCIInstaller(tt.source)
|
|
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Errorf("expected error but got none")
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
return
|
|
}
|
|
|
|
// Check all fields thoroughly
|
|
if installer.PluginName != tt.expectName {
|
|
t.Errorf("expected plugin name %s, got %s", tt.expectName, installer.PluginName)
|
|
}
|
|
|
|
if installer.Source != tt.source {
|
|
t.Errorf("expected source %s, got %s", tt.source, installer.Source)
|
|
}
|
|
|
|
if installer.CacheDir == "" {
|
|
t.Error("expected non-empty cache directory")
|
|
}
|
|
|
|
if !strings.Contains(installer.CacheDir, "plugins") {
|
|
t.Errorf("expected cache directory to contain 'plugins', got %s", installer.CacheDir)
|
|
}
|
|
|
|
if installer.settings == nil {
|
|
t.Error("expected settings to be initialized")
|
|
}
|
|
|
|
// Check that Path() method works
|
|
expectedPath := helmpath.DataPath("plugins", tt.expectName)
|
|
if installer.Path() != expectedPath {
|
|
t.Errorf("expected path %s, got %s", expectedPath, installer.Path())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOCIInstaller_Path(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
source string
|
|
pluginName string
|
|
expectPath string
|
|
}{
|
|
{
|
|
name: "valid plugin name",
|
|
source: "oci://ghcr.io/user/plugin-name:v1.0.0",
|
|
pluginName: "plugin-name",
|
|
expectPath: helmpath.DataPath("plugins", "plugin-name"),
|
|
},
|
|
{
|
|
name: "empty source",
|
|
source: "",
|
|
pluginName: "",
|
|
expectPath: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
installer := &OCIInstaller{
|
|
PluginName: tt.pluginName,
|
|
base: newBase(tt.source),
|
|
settings: cli.New(),
|
|
}
|
|
|
|
path := installer.Path()
|
|
if path != tt.expectPath {
|
|
t.Errorf("expected path %s, got %s", tt.expectPath, path)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOCIInstaller_Install(t *testing.T) {
|
|
// Set up isolated test environment
|
|
ensure.HelmHome(t)
|
|
|
|
pluginName := "test-plugin-basic"
|
|
server, registryHost := mockOCIRegistryWithArtifactType(t, pluginName)
|
|
defer server.Close()
|
|
|
|
// Test OCI reference
|
|
source := fmt.Sprintf("oci://%s/%s:latest", registryHost, pluginName)
|
|
|
|
// Test with plain HTTP (since test server uses HTTP)
|
|
installer, err := NewOCIInstaller(source, getter.WithPlainHTTP(true))
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
|
|
// The OCI installer uses helmpath.DataPath, which is isolated by ensure.HelmHome(t)
|
|
actualPath := installer.Path()
|
|
t.Logf("Installer will use path: %s", actualPath)
|
|
|
|
// Install the plugin
|
|
if err := Install(installer); err != nil {
|
|
t.Fatalf("Expected installation to succeed, got error: %v", err)
|
|
}
|
|
|
|
// Verify plugin was installed to the correct location
|
|
if !isPlugin(actualPath) {
|
|
t.Errorf("Expected plugin directory %s to contain plugin.yaml", actualPath)
|
|
}
|
|
|
|
// Debug: list what was actually created
|
|
if entries, err := os.ReadDir(actualPath); err != nil {
|
|
t.Fatalf("Could not read plugin directory %s: %v", actualPath, err)
|
|
} else {
|
|
t.Logf("Plugin directory %s contains:", actualPath)
|
|
for _, entry := range entries {
|
|
t.Logf(" - %s", entry.Name())
|
|
}
|
|
}
|
|
|
|
// Verify the plugin.yaml file exists and is valid
|
|
pluginFile := filepath.Join(actualPath, "plugin.yaml")
|
|
if _, err := os.Stat(pluginFile); err != nil {
|
|
t.Errorf("Expected plugin.yaml to exist, got error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOCIInstaller_Install_WithGetterOptions(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
pluginName string
|
|
options []getter.Option
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "plain HTTP",
|
|
pluginName: "example-cli-plain-http",
|
|
options: []getter.Option{getter.WithPlainHTTP(true)},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "insecure skip TLS verify",
|
|
pluginName: "example-cli-insecure",
|
|
options: []getter.Option{getter.WithPlainHTTP(true), getter.WithInsecureSkipVerifyTLS(true)},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "with timeout",
|
|
pluginName: "example-cli-timeout",
|
|
options: []getter.Option{getter.WithPlainHTTP(true), getter.WithTimeout(30 * time.Second)},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Set up isolated test environment for each subtest
|
|
ensure.HelmHome(t)
|
|
|
|
server, registryHost := mockOCIRegistryWithArtifactType(t, tc.pluginName)
|
|
defer server.Close()
|
|
|
|
source := fmt.Sprintf("oci://%s/%s:latest", registryHost, tc.pluginName)
|
|
|
|
installer, err := NewOCIInstaller(source, tc.options...)
|
|
if err != nil {
|
|
if !tc.wantErr {
|
|
t.Fatalf("Expected no error creating installer, got %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// The installer now uses our isolated test directory
|
|
actualPath := installer.Path()
|
|
|
|
// Install the plugin
|
|
err = Install(installer)
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Errorf("Expected installation to fail, but it succeeded")
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("Expected installation to succeed, got error: %v", err)
|
|
} else {
|
|
// Verify plugin was installed to the actual path
|
|
if !isPlugin(actualPath) {
|
|
t.Errorf("Expected plugin directory %s to contain plugin.yaml", actualPath)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOCIInstaller_Install_AlreadyExists(t *testing.T) {
|
|
// Set up isolated test environment
|
|
ensure.HelmHome(t)
|
|
|
|
pluginName := "test-plugin-exists"
|
|
server, registryHost := mockOCIRegistryWithArtifactType(t, pluginName)
|
|
defer server.Close()
|
|
|
|
source := fmt.Sprintf("oci://%s/%s:latest", registryHost, pluginName)
|
|
installer, err := NewOCIInstaller(source, getter.WithPlainHTTP(true))
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
|
|
// First install should succeed
|
|
if err := Install(installer); err != nil {
|
|
t.Fatalf("Expected first installation to succeed, got error: %v", err)
|
|
}
|
|
|
|
// Verify plugin was installed
|
|
if !isPlugin(installer.Path()) {
|
|
t.Errorf("Expected plugin directory %s to contain plugin.yaml", installer.Path())
|
|
}
|
|
|
|
// Second install should fail with "plugin already exists"
|
|
err = Install(installer)
|
|
if err == nil {
|
|
t.Error("Expected error when installing plugin that already exists")
|
|
} else if !strings.Contains(err.Error(), "plugin already exists") {
|
|
t.Errorf("Expected 'plugin already exists' error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOCIInstaller_Update(t *testing.T) {
|
|
// Set up isolated test environment
|
|
ensure.HelmHome(t)
|
|
|
|
pluginName := "test-plugin-update"
|
|
server, registryHost := mockOCIRegistryWithArtifactType(t, pluginName)
|
|
defer server.Close()
|
|
|
|
source := fmt.Sprintf("oci://%s/%s:latest", registryHost, pluginName)
|
|
installer, err := NewOCIInstaller(source, getter.WithPlainHTTP(true))
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
|
|
// Test update when plugin does not exist - should fail
|
|
err = Update(installer)
|
|
if err == nil {
|
|
t.Error("Expected error when updating plugin that does not exist")
|
|
} else if !strings.Contains(err.Error(), "plugin does not exist") {
|
|
t.Errorf("Expected 'plugin does not exist' error, got: %v", err)
|
|
}
|
|
|
|
// Install plugin first
|
|
if err := Install(installer); err != nil {
|
|
t.Fatalf("Expected installation to succeed, got error: %v", err)
|
|
}
|
|
|
|
// Verify plugin was installed
|
|
if !isPlugin(installer.Path()) {
|
|
t.Errorf("Expected plugin directory %s to contain plugin.yaml", installer.Path())
|
|
}
|
|
|
|
// Test update when plugin exists - should succeed
|
|
// For OCI, Update() removes old version and reinstalls
|
|
if err := Update(installer); err != nil {
|
|
t.Errorf("Expected update to succeed, got error: %v", err)
|
|
}
|
|
|
|
// Verify plugin is still installed after update
|
|
if !isPlugin(installer.Path()) {
|
|
t.Errorf("Expected plugin directory %s to contain plugin.yaml after update", installer.Path())
|
|
}
|
|
}
|
|
|
|
func TestOCIInstaller_Install_ComponentExtraction(t *testing.T) {
|
|
// Test that we can extract a plugin archive properly
|
|
// This tests the extraction logic that Install() uses
|
|
tempDir := t.TempDir()
|
|
pluginName := "test-plugin-extract"
|
|
|
|
pluginData := createTestPluginTarGz(t, pluginName)
|
|
|
|
// Test extraction
|
|
err := extractTarGz(bytes.NewReader(pluginData), tempDir)
|
|
if err != nil {
|
|
t.Fatalf("Failed to extract plugin: %v", err)
|
|
}
|
|
|
|
// Verify plugin.yaml exists
|
|
pluginYAMLPath := filepath.Join(tempDir, "plugin.yaml")
|
|
if _, err := os.Stat(pluginYAMLPath); os.IsNotExist(err) {
|
|
t.Errorf("plugin.yaml not found after extraction")
|
|
}
|
|
|
|
// Verify bin directory exists
|
|
binPath := filepath.Join(tempDir, "bin")
|
|
if _, err := os.Stat(binPath); os.IsNotExist(err) {
|
|
t.Errorf("bin directory not found after extraction")
|
|
}
|
|
|
|
// Verify executable exists and has correct permissions
|
|
execPath := filepath.Join(tempDir, "bin", pluginName)
|
|
if info, err := os.Stat(execPath); err != nil {
|
|
t.Errorf("executable not found: %v", err)
|
|
} else if info.Mode()&0111 == 0 {
|
|
t.Errorf("file is not executable")
|
|
}
|
|
|
|
// Verify this would be recognized as a plugin
|
|
if !isPlugin(tempDir) {
|
|
t.Errorf("extracted directory is not a valid plugin")
|
|
}
|
|
}
|
|
|
|
func TestExtractTarGz(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
// Create a test tar.gz file
|
|
var buf bytes.Buffer
|
|
gzWriter := gzip.NewWriter(&buf)
|
|
tarWriter := tar.NewWriter(gzWriter)
|
|
|
|
// Add a test file to the archive
|
|
testContent := "test content"
|
|
header := &tar.Header{
|
|
Name: "test-file.txt",
|
|
Mode: 0644,
|
|
Size: int64(len(testContent)),
|
|
Typeflag: tar.TypeReg,
|
|
}
|
|
|
|
if err := tarWriter.WriteHeader(header); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := tarWriter.Write([]byte(testContent)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Add a test directory
|
|
dirHeader := &tar.Header{
|
|
Name: "test-dir/",
|
|
Mode: 0755,
|
|
Typeflag: tar.TypeDir,
|
|
}
|
|
|
|
if err := tarWriter.WriteHeader(dirHeader); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tarWriter.Close()
|
|
gzWriter.Close()
|
|
|
|
// Test extraction
|
|
err := extractTarGz(bytes.NewReader(buf.Bytes()), tempDir)
|
|
if err != nil {
|
|
t.Errorf("extractTarGz failed: %v", err)
|
|
}
|
|
|
|
// Verify extracted file
|
|
extractedFile := filepath.Join(tempDir, "test-file.txt")
|
|
content, err := os.ReadFile(extractedFile)
|
|
if err != nil {
|
|
t.Errorf("failed to read extracted file: %v", err)
|
|
}
|
|
|
|
if string(content) != testContent {
|
|
t.Errorf("expected content %s, got %s", testContent, string(content))
|
|
}
|
|
|
|
// Verify extracted directory
|
|
extractedDir := filepath.Join(tempDir, "test-dir")
|
|
if _, err := os.Stat(extractedDir); os.IsNotExist(err) {
|
|
t.Errorf("extracted directory does not exist: %s", extractedDir)
|
|
}
|
|
}
|
|
|
|
func TestExtractTarGz_InvalidGzip(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
// Test with invalid gzip data
|
|
invalidGzipData := []byte("not gzip data")
|
|
err := extractTarGz(bytes.NewReader(invalidGzipData), tempDir)
|
|
if err == nil {
|
|
t.Error("expected error for invalid gzip data")
|
|
}
|
|
}
|
|
|
|
func TestExtractTar_UnknownFileType(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
// Create a test tar file
|
|
var buf bytes.Buffer
|
|
tarWriter := tar.NewWriter(&buf)
|
|
|
|
// Add a test file
|
|
testContent := "test content"
|
|
header := &tar.Header{
|
|
Name: "test-file.txt",
|
|
Mode: 0644,
|
|
Size: int64(len(testContent)),
|
|
Typeflag: tar.TypeReg,
|
|
}
|
|
|
|
if err := tarWriter.WriteHeader(header); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := tarWriter.Write([]byte(testContent)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Test unknown file type
|
|
unknownHeader := &tar.Header{
|
|
Name: "unknown-type",
|
|
Mode: 0644,
|
|
Typeflag: tar.TypeSymlink, // Use a type that's not handled
|
|
}
|
|
|
|
if err := tarWriter.WriteHeader(unknownHeader); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tarWriter.Close()
|
|
|
|
// Test extraction - should fail due to unknown type
|
|
err := extractTar(bytes.NewReader(buf.Bytes()), tempDir)
|
|
if err == nil {
|
|
t.Error("expected error for unknown tar file type")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "unknown type") {
|
|
t.Errorf("expected 'unknown type' error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestExtractTar_SuccessfulExtraction(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
// Since we can't easily create extended headers with Go's tar package,
|
|
// we'll test the logic that skips them by creating a simple tar with regular files
|
|
// and then testing that the extraction works correctly.
|
|
|
|
// Create a test tar file
|
|
var buf bytes.Buffer
|
|
tarWriter := tar.NewWriter(&buf)
|
|
|
|
// Add a regular file
|
|
testContent := "test content"
|
|
header := &tar.Header{
|
|
Name: "test-file.txt",
|
|
Mode: 0644,
|
|
Size: int64(len(testContent)),
|
|
Typeflag: tar.TypeReg,
|
|
}
|
|
|
|
if err := tarWriter.WriteHeader(header); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := tarWriter.Write([]byte(testContent)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tarWriter.Close()
|
|
|
|
// Test extraction
|
|
err := extractTar(bytes.NewReader(buf.Bytes()), tempDir)
|
|
if err != nil {
|
|
t.Errorf("extractTar failed: %v", err)
|
|
}
|
|
|
|
// Verify the regular file was extracted
|
|
extractedFile := filepath.Join(tempDir, "test-file.txt")
|
|
content, err := os.ReadFile(extractedFile)
|
|
if err != nil {
|
|
t.Errorf("failed to read extracted file: %v", err)
|
|
}
|
|
|
|
if string(content) != testContent {
|
|
t.Errorf("expected content %s, got %s", testContent, string(content))
|
|
}
|
|
}
|
|
|
|
func TestOCIInstaller_Install_PlainHTTPOption(t *testing.T) {
|
|
// Test that PlainHTTP option is properly passed to getter
|
|
source := "oci://example.com/test-plugin:v1.0.0"
|
|
|
|
// Test with PlainHTTP=false (default)
|
|
installer1, err := NewOCIInstaller(source)
|
|
if err != nil {
|
|
t.Fatalf("failed to create installer: %v", err)
|
|
}
|
|
if installer1.getter == nil {
|
|
t.Error("getter should be initialized")
|
|
}
|
|
|
|
// Test with PlainHTTP=true
|
|
installer2, err := NewOCIInstaller(source, getter.WithPlainHTTP(true))
|
|
if err != nil {
|
|
t.Fatalf("failed to create installer with PlainHTTP=true: %v", err)
|
|
}
|
|
if installer2.getter == nil {
|
|
t.Error("getter should be initialized with PlainHTTP=true")
|
|
}
|
|
|
|
// Both installers should have the same basic properties
|
|
if installer1.PluginName != installer2.PluginName {
|
|
t.Error("plugin names should match")
|
|
}
|
|
if installer1.Source != installer2.Source {
|
|
t.Error("sources should match")
|
|
}
|
|
|
|
// Test with multiple options
|
|
installer3, err := NewOCIInstaller(source,
|
|
getter.WithPlainHTTP(true),
|
|
getter.WithBasicAuth("user", "pass"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("failed to create installer with multiple options: %v", err)
|
|
}
|
|
if installer3.getter == nil {
|
|
t.Error("getter should be initialized with multiple options")
|
|
}
|
|
}
|
|
|
|
func TestOCIInstaller_Install_ValidationErrors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
layerData []byte
|
|
expectError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "non-gzip layer",
|
|
layerData: []byte("not gzip data"),
|
|
expectError: true,
|
|
errorMsg: "is not a gzip compressed archive",
|
|
},
|
|
{
|
|
name: "empty layer",
|
|
layerData: []byte{},
|
|
expectError: true,
|
|
errorMsg: "is not a gzip compressed archive",
|
|
},
|
|
{
|
|
name: "single byte layer",
|
|
layerData: []byte{0x1f},
|
|
expectError: true,
|
|
errorMsg: "is not a gzip compressed archive",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Test the gzip validation logic that's used in the Install method
|
|
if len(tt.layerData) < 2 || tt.layerData[0] != 0x1f || tt.layerData[1] != 0x8b {
|
|
// This matches the validation in the Install method
|
|
if !tt.expectError {
|
|
t.Error("expected valid gzip data")
|
|
}
|
|
if !strings.Contains(tt.errorMsg, "is not a gzip compressed archive") {
|
|
t.Errorf("expected error message to contain 'is not a gzip compressed archive'")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|