diff --git a/pkg/addons/install.go b/pkg/addons/install.go index 4f363f76ac..a9776b07d5 100644 --- a/pkg/addons/install.go +++ b/pkg/addons/install.go @@ -170,9 +170,10 @@ func GetAddOnsForInstall(opts InstallOptions) []types.AddOn { if opts.DisasterRecoveryEnabled { addOns = append(addOns, &velero.Velero{ - Proxy: opts.ProxySpec, - HostCABundlePath: opts.HostCABundlePath, - K0sDataDir: opts.K0sDataDir, + Proxy: opts.ProxySpec, + HostCABundlePath: opts.HostCABundlePath, + K0sDataDir: opts.K0sDataDir, + EmbeddedConfigSpec: opts.EmbeddedConfigSpec, }) } @@ -206,9 +207,10 @@ func GetAddOnsForRestore(opts RestoreOptions) []types.AddOn { OpenEBSDataDir: opts.OpenEBSDataDir, }, &velero.Velero{ - Proxy: opts.ProxySpec, - HostCABundlePath: opts.HostCABundlePath, - K0sDataDir: opts.K0sDataDir, + Proxy: opts.ProxySpec, + HostCABundlePath: opts.HostCABundlePath, + K0sDataDir: opts.K0sDataDir, + EmbeddedConfigSpec: opts.EmbeddedConfigSpec, }, } return addOns diff --git a/pkg/addons/metadata.go b/pkg/addons/metadata.go index d54177143c..a48ff94d34 100644 --- a/pkg/addons/metadata.go +++ b/pkg/addons/metadata.go @@ -13,6 +13,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/addons/registry" "github.com/replicatedhq/embedded-cluster/pkg/addons/seaweedfs" "github.com/replicatedhq/embedded-cluster/pkg/addons/velero" + "github.com/replicatedhq/embedded-cluster/pkg/release" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -78,7 +79,11 @@ func GenerateChartConfigs(ctx context.Context, kcli client.Client) ([]ecv1beta1. repositories = append(repositories, repos...) // velero - chart, repos, err = velero.GenerateChartConfig() + var ecConfig *ecv1beta1.ConfigSpec + if ecCfg := release.GetEmbeddedClusterConfig(); ecCfg != nil { + ecConfig = &ecCfg.Spec + } + chart, repos, err = velero.GenerateChartConfig(ecConfig) if err != nil { return nil, nil, errors.Wrap(err, "generate chart config for velero") } diff --git a/pkg/addons/upgrade.go b/pkg/addons/upgrade.go index 256d37da96..0c2c70969d 100644 --- a/pkg/addons/upgrade.go +++ b/pkg/addons/upgrade.go @@ -136,9 +136,10 @@ func (a *AddOns) getAddOnsForUpgrade(meta *ectypes.ReleaseMetadata, opts Upgrade if opts.DisasterRecoveryEnabled { addOns = append(addOns, &velero.Velero{ - Proxy: opts.ProxySpec, - HostCABundlePath: opts.HostCABundlePath, - K0sDataDir: opts.K0sDataDir, + Proxy: opts.ProxySpec, + HostCABundlePath: opts.HostCABundlePath, + K0sDataDir: opts.K0sDataDir, + EmbeddedConfigSpec: opts.EmbeddedConfigSpec, }) } diff --git a/pkg/addons/velero/metadata.go b/pkg/addons/velero/metadata.go index 29d09a3215..32a2ceaf90 100644 --- a/pkg/addons/velero/metadata.go +++ b/pkg/addons/velero/metadata.go @@ -48,8 +48,8 @@ func GetAdditionalImages() []string { return images } -func GenerateChartConfig() ([]ecv1beta1.Chart, []k0sv1beta1.Repository, error) { - hv, err := helmValues() +func GenerateChartConfig(ecConfig *ecv1beta1.ConfigSpec) ([]ecv1beta1.Chart, []k0sv1beta1.Repository, error) { + hv, err := helmValues(ecConfig) if err != nil { return nil, nil, errors.Wrap(err, "get helm values") } diff --git a/pkg/addons/velero/values.go b/pkg/addons/velero/values.go index a38b99c818..0c7a384a07 100644 --- a/pkg/addons/velero/values.go +++ b/pkg/addons/velero/values.go @@ -19,7 +19,7 @@ var ( ) func (v *Velero) GenerateHelmValues(ctx context.Context, kcli client.Client, domains ecv1beta1.Domains, overrides []string) (map[string]interface{}, error) { - hv, err := helmValues() + hv, err := helmValues(v.EmbeddedConfigSpec) if err != nil { return nil, errors.Wrap(err, "get helm values") } @@ -112,11 +112,63 @@ func (v *Velero) GenerateHelmValues(ctx context.Context, kcli client.Client, dom return copiedValues, nil } -func helmValues() (map[string]interface{}, error) { +func helmValues(ecConfig *ecv1beta1.ConfigSpec) (map[string]interface{}, error) { hv, err := release.RenderHelmValues(rawvalues, Metadata) if err != nil { return nil, errors.Wrap(err, "render helm values") } + // Inject custom Velero plugins from ConfigSpec if available + if err := injectPluginInitContainers(hv, ecConfig); err != nil { + return nil, errors.Wrap(err, "inject plugin init containers") + } + return hv, nil } + +// injectPluginInitContainers injects custom Velero plugin initContainers from ConfigSpec +func injectPluginInitContainers(values map[string]interface{}, ecConfig *ecv1beta1.ConfigSpec) error { + if ecConfig == nil || len(ecConfig.Extensions.Velero.Plugins) == 0 { + return nil + } + + allPlugins := ecConfig.Extensions.Velero.Plugins + + // Get existing initContainers or create empty slice + var existingInitContainers []any + if existing, ok := values["initContainers"]; ok { + if containers, ok := existing.([]any); ok { + existingInitContainers = containers + } + } + + // Process each plugin and create initContainer + for _, plugin := range allPlugins { + imagePullPolicy := plugin.ImagePullPolicy + if imagePullPolicy == "" { + imagePullPolicy = "IfNotPresent" // Default to match AWS plugin + } + + initContainer := generatePluginContainer(plugin.Name, plugin.Image, imagePullPolicy) + existingInitContainers = append(existingInitContainers, initContainer) + } + + // Update values with merged initContainers + values["initContainers"] = existingInitContainers + return nil +} + +// generatePluginContainer creates an initContainer spec for a Velero plugin +func generatePluginContainer(name, image, imagePullPolicy string) map[string]interface{} { + return map[string]interface{}{ + "name": name, + "image": image, + "imagePullPolicy": imagePullPolicy, + "volumeMounts": []map[string]interface{}{ + { + "mountPath": "/target", + "name": "plugins", + }, + }, + } +} diff --git a/pkg/addons/velero/values_test.go b/pkg/addons/velero/values_test.go index 9b1702c3da..89c2e9aca6 100644 --- a/pkg/addons/velero/values_test.go +++ b/pkg/addons/velero/values_test.go @@ -5,6 +5,7 @@ import ( "testing" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -62,3 +63,259 @@ func TestGenerateHelmValues_HostCABundlePath(t *testing.T) { assert.Equal(t, "host-ca-bundle", extraVolumeMounts[0]["name"]) assert.Equal(t, "/certs/ca-certificates.crt", extraVolumeMounts[0]["mountPath"]) } + +func TestGenerateHelmValues_NoPlugins(t *testing.T) { + v := &Velero{ + EmbeddedConfigSpec: nil, + } + + values, err := v.GenerateHelmValues(context.Background(), nil, ecv1beta1.Domains{}, nil) + require.NoError(t, err) + + // Should have at most the default AWS plugin + initContainers, ok := values["initContainers"].([]any) + require.True(t, ok, "initContainers should be a slice") + assert.LessOrEqual(t, len(initContainers), 1, "Should have at most the default AWS plugin") +} + +func TestGenerateHelmValues_SinglePlugin(t *testing.T) { + v := &Velero{ + EmbeddedConfigSpec: &ecv1beta1.ConfigSpec{ + Extensions: ecv1beta1.Extensions{ + Velero: ecv1beta1.VeleroExtensions{ + Plugins: []ecv1beta1.VeleroPlugin{ + { + Name: "velero-plugin-postgresql", + Image: "myvendor/velero-postgresql:v1.0.0", + }, + }, + }, + }, + }, + } + + values, err := v.GenerateHelmValues(context.Background(), nil, ecv1beta1.Domains{}, nil) + require.NoError(t, err) + + require.NotEmpty(t, values["initContainers"]) + initContainers, ok := values["initContainers"].([]any) + require.True(t, ok, "initContainers should be a slice") + assert.Equal(t, len(initContainers), 2, "Should have exactly 2 init containers: velero-plugin-postgresql and AWS plugin") + + // Check that we have the custom plugin with volumeMounts + var foundPostgresPlugin bool + var foundAWSPlugin bool + for _, container := range initContainers { + containerMap, ok := container.(map[string]any) + require.True(t, ok, "container should be a map") + + if name, _ := containerMap["name"].(string); name == "velero-plugin-postgresql" { + foundPostgresPlugin = true + assert.Equal(t, "myvendor/velero-postgresql:v1.0.0", containerMap["image"]) + assert.Equal(t, "IfNotPresent", containerMap["imagePullPolicy"]) + assert.Contains(t, containerMap, "volumeMounts") + } else if name == "velero-plugin-for-aws" { + foundAWSPlugin = true + assert.Equal(t, "IfNotPresent", containerMap["imagePullPolicy"]) + assert.Contains(t, containerMap, "volumeMounts") + } + } + + assert.True(t, foundPostgresPlugin, "Should have velero-plugin-postgresql") + assert.True(t, foundAWSPlugin, "Should have velero-plugin-for-aws") +} + +func TestGenerateHelmValues_MultiplePlugins(t *testing.T) { + v := &Velero{ + EmbeddedConfigSpec: &ecv1beta1.ConfigSpec{ + Extensions: ecv1beta1.Extensions{ + Velero: ecv1beta1.VeleroExtensions{ + Plugins: []ecv1beta1.VeleroPlugin{ + { + Name: "velero-plugin-postgresql", + Image: "myvendor/velero-postgresql:v1.0.0", + }, + { + Name: "velero-plugin-mongodb", + Image: "myvendor/velero-mongodb:v2.1.0", + }, + }, + }, + }, + }, + } + + values, err := v.GenerateHelmValues(context.Background(), nil, ecv1beta1.Domains{}, nil) + require.NoError(t, err) + + require.NotEmpty(t, values["initContainers"]) + initContainers, ok := values["initContainers"].([]any) + require.True(t, ok, "initContainers should be a slice") + assert.Equal(t, len(initContainers), 3, "Should have exactly 3 init containers: AWS, velero-plugin-postgresql, and velero-plugin-mongodb") + + // Check that we have the custom plugins with volumeMounts + var foundPostgresPlugin bool + var foundMongoDBPlugin bool + var foundAWSPlugin bool + for _, container := range initContainers { + containerMap, ok := container.(map[string]any) + require.True(t, ok, "container should be a map") + + name, _ := containerMap["name"].(string) + switch name { + case "velero-plugin-postgresql": + foundPostgresPlugin = true + assert.Equal(t, "myvendor/velero-postgresql:v1.0.0", containerMap["image"]) + assert.Equal(t, "IfNotPresent", containerMap["imagePullPolicy"]) + assert.Contains(t, containerMap, "volumeMounts") + case "velero-plugin-mongodb": + foundMongoDBPlugin = true + assert.Equal(t, "myvendor/velero-mongodb:v2.1.0", containerMap["image"]) + assert.Equal(t, "IfNotPresent", containerMap["imagePullPolicy"]) + assert.Contains(t, containerMap, "volumeMounts") + case "velero-plugin-for-aws": + foundAWSPlugin = true + assert.Equal(t, "IfNotPresent", containerMap["imagePullPolicy"]) + assert.Contains(t, containerMap, "volumeMounts") + } + } + + assert.True(t, foundPostgresPlugin, "Should have velero-plugin-postgresql") + assert.True(t, foundMongoDBPlugin, "Should have velero-plugin-mongodb") + assert.True(t, foundAWSPlugin, "Should have velero-plugin-for-aws") +} + +func TestGenerateHelmValues_PluginWithImagePullPolicy(t *testing.T) { + v := &Velero{ + EmbeddedConfigSpec: &ecv1beta1.ConfigSpec{ + Extensions: ecv1beta1.Extensions{ + Velero: ecv1beta1.VeleroExtensions{ + Plugins: []ecv1beta1.VeleroPlugin{ + { + Name: "velero-plugin-postgresql", + Image: "myvendor/velero-postgresql:v1.0.0", + ImagePullPolicy: "Always", + }, + }, + }, + }, + }, + } + + values, err := v.GenerateHelmValues(context.Background(), nil, ecv1beta1.Domains{}, nil) + require.NoError(t, err) + + require.NotEmpty(t, values["initContainers"]) + initContainers, ok := values["initContainers"].([]any) + require.True(t, ok, "initContainers should be a slice") + assert.Equal(t, len(initContainers), 2, "Should have exactly 2 init containers: velero-plugin-postgresql and AWS plugin") + + // Check that we have the custom plugin with the custom imagePullPolicy + var foundPostgresPlugin bool + for _, container := range initContainers { + containerMap, ok := container.(map[string]any) + require.True(t, ok, "container should be a map") + + if name, _ := containerMap["name"].(string); name == "velero-plugin-postgresql" { + foundPostgresPlugin = true + assert.Equal(t, "myvendor/velero-postgresql:v1.0.0", containerMap["image"]) + assert.Equal(t, "Always", containerMap["imagePullPolicy"]) + assert.Contains(t, containerMap, "volumeMounts") + } + } + + assert.True(t, foundPostgresPlugin, "Should have velero-plugin-postgresql with ImagePullPolicy=Always") +} + +func TestGenerateHelmValues_PluginVolumeMounts(t *testing.T) { + v := &Velero{ + EmbeddedConfigSpec: &ecv1beta1.ConfigSpec{ + Extensions: ecv1beta1.Extensions{ + Velero: ecv1beta1.VeleroExtensions{ + Plugins: []ecv1beta1.VeleroPlugin{ + { + Name: "velero-plugin-postgresql", + Image: "myvendor/velero-postgresql:v1.0.0", + }, + }, + }, + }, + }, + } + + values, err := v.GenerateHelmValues(context.Background(), nil, ecv1beta1.Domains{}, nil) + require.NoError(t, err) + + require.NotEmpty(t, values["initContainers"]) + initContainers, ok := values["initContainers"].([]any) + require.True(t, ok, "initContainers should be a slice") + assert.Equal(t, len(initContainers), 2, "Should have exactly 2 init containers, velero-plugin-postgresql and AWS") + + // Find our plugin container + var pluginContainer map[string]any + for _, container := range initContainers { + if containerMap, ok := container.(map[string]any); ok { + if name, _ := containerMap["name"].(string); name == "velero-plugin-postgresql" { + pluginContainer = containerMap + break + } + } + } + + require.NotNil(t, pluginContainer) + require.Contains(t, pluginContainer, "volumeMounts") + + volumeMounts, ok := pluginContainer["volumeMounts"].([]any) + require.True(t, ok, "volumeMounts should be []any") + + require.Len(t, volumeMounts, 1) + volumeMount, ok := volumeMounts[0].(map[string]any) + require.True(t, ok, "volumeMount should be map[string]any") + assert.Equal(t, "/target", volumeMount["mountPath"]) + assert.Equal(t, "plugins", volumeMount["name"]) +} + +func TestGenerateHelmValues_CustomDomainReplacement(t *testing.T) { + tests := []struct { + name string + proxyRegistryDomain string + expectReplacement bool + expectedDomain string + }{ + { + name: "empty domain should not replace", + proxyRegistryDomain: "", + expectReplacement: false, + }, + { + name: "custom domain should replace", + proxyRegistryDomain: "custom-registry.example.com", + expectReplacement: true, + expectedDomain: "custom-registry.example.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &Velero{} + domains := ecv1beta1.Domains{ + ProxyRegistryDomain: tt.proxyRegistryDomain, + } + + values, err := v.GenerateHelmValues(context.Background(), nil, domains, nil) + require.NoError(t, err) + + // Marshal values back to YAML to check for domain replacement + marshalled, err := helm.MarshalValues(values) + require.NoError(t, err) + marshalledStr := string(marshalled) + + if tt.expectReplacement { + assert.NotContains(t, marshalledStr, "proxy.replicated.com", "should not contain proxy.replicated.com") + assert.Contains(t, marshalledStr, tt.expectedDomain, "should contain custom domain") + } else { + assert.Contains(t, marshalledStr, "proxy.replicated.com", "should contain proxy.replicated.com when domain is empty") + } + }) + } +} diff --git a/pkg/addons/velero/velero.go b/pkg/addons/velero/velero.go index cf3f19b944..c95b3adfc8 100644 --- a/pkg/addons/velero/velero.go +++ b/pkg/addons/velero/velero.go @@ -37,6 +37,10 @@ type Velero struct { HostCABundlePath string K0sDataDir string + // EmbeddedConfigSpec contains the embedded cluster config spec + // Note: EndUserConfigSpec is not stored here as it's only used for overrides via addOnOverrides + EmbeddedConfigSpec *ecv1beta1.ConfigSpec + // DryRun is a flag to enable dry-run mode for Velero. // If true, Velero will only render the helm template and additional manifests, but not install // the release. diff --git a/pkg/lint/testdata/specs/02-warning-multiple-ports.yaml b/pkg/lint/testdata/specs/02-warning-multiple-ports.yaml index 60b57e90ef..406332d4aa 100644 --- a/pkg/lint/testdata/specs/02-warning-multiple-ports.yaml +++ b/pkg/lint/testdata/specs/02-warning-multiple-ports.yaml @@ -13,7 +13,7 @@ spec: - name: adminconsole values: | service: - nodePort: 8080 + nodePort: 8080 - name: openebs values: | apiServer: @@ -25,4 +25,4 @@ spec: - name: registry values: | service: - nodePort: 9090 \ No newline at end of file + nodePort: 9090 \ No newline at end of file diff --git a/tests/dryrun/assets/cluster-config-with-velero-plugins.yaml b/tests/dryrun/assets/cluster-config-with-velero-plugins.yaml new file mode 100644 index 0000000000..086da4a286 --- /dev/null +++ b/tests/dryrun/assets/cluster-config-with-velero-plugins.yaml @@ -0,0 +1,12 @@ +apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +metadata: + name: "testconfig-velero-plugins" +spec: + version: "testversion" + extensions: + velero: + plugins: + - name: velero-plugin-example + image: docker.io/library/velero-plugin-example:latest + imagePullPolicy: Always diff --git a/tests/dryrun/install_test.go b/tests/dryrun/install_test.go index 2bfb2f3cb5..f950e1d7a9 100644 --- a/tests/dryrun/install_test.go +++ b/tests/dryrun/install_test.go @@ -802,3 +802,29 @@ func TestIgnoreAppPreflightsInstallation(t *testing.T) { t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) } + +func TestVeleroPluginsInstallation(t *testing.T) { + hcli := &helm.MockClient{} + + mock.InOrder( + // 4 addons (no extensions in this cluster config) + hcli.On("Install", mock.Anything, mock.Anything).Times(4).Return(nil, nil), + hcli.On("Close").Once().Return(nil), + ) + + dryrunInstallWithClusterConfig(t, &dryrun.Client{HelmClient: hcli}, clusterConfigWithVeleroPluginsData) + + // --- validate velero addon --- // + veleroOpts, found := isHelmReleaseInstalled(hcli, "velero") + require.True(t, found, "velero helm release should be installed") + + // Validate basic Velero values + assertHelmValues(t, veleroOpts.Values, map[string]interface{}{ + "nodeAgent.podVolumePath": "/var/lib/embedded-cluster/k0s/kubelet/pods", + }) + + // Validate plugin configuration + validateVeleroPlugin(t, hcli) + + t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) +} diff --git a/tests/dryrun/util.go b/tests/dryrun/util.go index d2f1b1f1dd..5373358161 100644 --- a/tests/dryrun/util.go +++ b/tests/dryrun/util.go @@ -51,6 +51,9 @@ var ( //go:embed assets/cluster-config-nodomains.yaml clusterConfigNoDomainsData string + //go:embed assets/cluster-config-with-velero-plugins.yaml + clusterConfigWithVeleroPluginsData string + //go:embed assets/kotskinds-application.yaml applicationData string diff --git a/tests/dryrun/v3_install_test.go b/tests/dryrun/v3_install_test.go index 8f6101d1a2..6cebe8d86f 100644 --- a/tests/dryrun/v3_install_test.go +++ b/tests/dryrun/v3_install_test.go @@ -1674,6 +1674,141 @@ func validateCustomTLSConfiguration(t *testing.T, certData string, keyData strin assert.Equal(t, "0", tlsSecret.Annotations["acceptAnonymousUploads"], "should have acceptAnonymousUploads annotation") } +func TestV3InstallHeadless_VeleroPlugin(t *testing.T) { + hcli := setupV3TestHelmClient() + setupV3TestOpts := setupV3TestOpts{ + helmClient: hcli, + clusterConfigData: clusterConfigWithVeleroPluginsData, + } + licenseFile, configFile := setupV3Test(t, setupV3TestOpts) + + // Run installer command with headless flag and required arguments + err := runInstallerCmd( + "install", + "--headless", + "--target", "linux", + "--license", licenseFile, + "--config-values", configFile, + "--admin-console-password", "password123", + "--yes", + ) + + require.NoError(t, err, "headless installation should succeed") + + validateVeleroPlugin(t, hcli) + + if !t.Failed() { + t.Logf("V3 headless velero plugin test passed") + } +} + +func TestV3Install_VeleroPlugin(t *testing.T) { + hcli := setupV3TestHelmClient() + setupV3TestOpts := setupV3TestOpts{ + helmClient: hcli, + clusterConfigData: clusterConfigWithVeleroPluginsData, + } + licenseFile, configFile := setupV3Test(t, setupV3TestOpts) + + // Start installer in non-headless mode so API stays up; bypass prompts with --yes + go func() { + err := runInstallerCmd( + "install", + "--target", "linux", + "--license", licenseFile, + "--admin-console-password", "password123", + "--yes", + ) + if err != nil { + t.Logf("installer exited with error: %v", err) + } + }() + + runV3Install(t, v3InstallArgs{ + managerPort: 30080, + password: "password123", + isAirgap: false, + configValuesFile: configFile, + installationConfig: apitypes.LinuxInstallationConfig{}, + ignoreHostPreflights: false, + ignoreAppPreflights: false, + }) + + validateVeleroPlugin(t, hcli) + + if !t.Failed() { + t.Logf("V3 velero plugin test passed") + } +} + +func validateVeleroPlugin(t *testing.T, hcli *helm.MockClient) { + t.Helper() + + // Validate velero addon is installed + veleroOpts, found := isHelmReleaseInstalled(hcli, "velero") + require.True(t, found, "velero helm release should be installed") + + // Validate initContainers exist and contain the plugin + require.Contains(t, veleroOpts.Values, "initContainers", "initContainers should exist in Velero helm values") + initContainersAny := veleroOpts.Values["initContainers"] + require.NotNil(t, initContainersAny, "initContainers should not be nil") + + initContainers, ok := initContainersAny.([]any) + require.True(t, ok, "initContainers should be a slice") + require.NotEmpty(t, initContainers, "initContainers should not be empty") + + // Find the plugin container by name + var pluginContainer map[string]any + for _, container := range initContainers { + if containerMap, ok := container.(map[string]any); ok { + if name, _ := containerMap["name"].(string); name == "velero-plugin-example" { + pluginContainer = containerMap + break + } + } + } + + require.NotNil(t, pluginContainer, "velero-plugin-example container should exist in initContainers") + assert.Equal(t, "velero-plugin-example", pluginContainer["name"], "plugin container should have correct name") + assert.Equal(t, "docker.io/library/velero-plugin-example:latest", pluginContainer["image"], "plugin container should have correct image") + assert.Equal(t, "Always", pluginContainer["imagePullPolicy"], "plugin container should have correct imagePullPolicy") + + // Validate volumeMounts + require.Contains(t, pluginContainer, "volumeMounts", "plugin container should have volumeMounts") + volumeMountsAny := pluginContainer["volumeMounts"] + require.NotNil(t, volumeMountsAny, "volumeMounts should not be nil") + + var volumeMounts []any + switch v := volumeMountsAny.(type) { + case []any: + volumeMounts = v + case []map[string]any: + volumeMounts = make([]any, len(v)) + for i, vm := range v { + volumeMounts[i] = vm + } + default: + t.Fatalf("volumeMounts should be a slice, got %T", v) + } + + require.NotEmpty(t, volumeMounts, "volumeMounts should not be empty") + + // Find the plugins volume mount + var pluginsVolumeMount map[string]any + for _, vm := range volumeMounts { + if vmMap, ok := vm.(map[string]any); ok { + if name, _ := vmMap["name"].(string); name == "plugins" { + pluginsVolumeMount = vmMap + break + } + } + } + + require.NotNil(t, pluginsVolumeMount, "plugins volumeMount should exist") + assert.Equal(t, "plugins", pluginsVolumeMount["name"], "volumeMount should have correct name") + assert.Equal(t, "/target", pluginsVolumeMount["mountPath"], "volumeMount should have correct mountPath") +} + var ( //go:embed assets/rendered-chart-preflight.yaml renderedChartPreflightData string