diff --git a/e2e/install_test.go b/e2e/install_test.go index 8f18c699a..cd7fd4a13 100644 --- a/e2e/install_test.go +++ b/e2e/install_test.go @@ -2,7 +2,6 @@ package e2e import ( "encoding/base64" - "encoding/json" "fmt" "os" "strings" @@ -11,12 +10,10 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" "github.com/replicatedhq/embedded-cluster/e2e/cluster/cmx" "github.com/replicatedhq/embedded-cluster/e2e/cluster/docker" "github.com/replicatedhq/embedded-cluster/e2e/cluster/lxd" - "github.com/replicatedhq/embedded-cluster/pkg/certs" ) func TestSingleNodeInstallation(t *testing.T) { @@ -1728,62 +1725,6 @@ func TestFiveNodesAirgapUpgrade(t *testing.T) { t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) } -func TestInstallWithPrivateCAs(t *testing.T) { - RequireEnvVars(t, []string{"SHORT_SHA"}) - - input := &lxd.ClusterInput{ - T: t, - Nodes: 1, - Image: "ubuntu/jammy", - LicensePath: "licenses/license.yaml", - EmbeddedClusterPath: "../output/bin/embedded-cluster", - } - tc := lxd.NewCluster(input) - defer tc.Cleanup() - - certBuilder, err := certs.NewBuilder() - require.NoError(t, err, "unable to create new cert builder") - crtContent, _, err := certBuilder.Generate() - require.NoError(t, err, "unable to build test certificate") - - tmpfile, err := os.CreateTemp("", "test-temp-cert-*.crt") - require.NoError(t, err, "unable to create temp file") - defer os.Remove(tmpfile.Name()) - - _, err = tmpfile.WriteString(crtContent) - require.NoError(t, err, "unable to write to temp file") - tmpfile.Close() - - lxd.CopyFileToNode(input, tc.Nodes[0], lxd.File{ - SourcePath: tmpfile.Name(), - DestPath: "/tmp/ca.crt", - Mode: 0666, - }) - - installSingleNodeWithOptions(t, tc, installOptions{ - privateCA: "/tmp/ca.crt", - }) - - if _, _, err := tc.SetupPlaywrightAndRunTest("deploy-app"); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v", err) - } - - checkInstallationState(t, tc) - - t.Logf("checking if the configmap was created with the right values") - line := []string{"kubectl", "get", "cm", "kotsadm-private-cas", "-n", "kotsadm", "-o", "json"} - stdout, _, err := tc.RunCommandOnNode(0, line, lxd.WithECShellEnv("/var/lib/embedded-cluster")) - require.NoError(t, err, "unable get kotsadm-private-cas configmap") - - var cm corev1.ConfigMap - err = json.Unmarshal([]byte(stdout), &cm) - require.NoErrorf(t, err, "unable to unmarshal output to configmap: %q", stdout) - require.Contains(t, cm.Data, "ca_0.crt", "index ca_0.crt not found in ca secret") - require.Equal(t, crtContent, cm.Data["ca_0.crt"], "content mismatch") - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} - func TestInstallWithConfigValues(t *testing.T) { t.Parallel() diff --git a/pkg/addons/adminconsole/adminconsole.go b/pkg/addons/adminconsole/adminconsole.go index 2f0b7d32a..514773861 100644 --- a/pkg/addons/adminconsole/adminconsole.go +++ b/pkg/addons/adminconsole/adminconsole.go @@ -26,6 +26,14 @@ type AdminConsole struct { ReplicatedAppDomain string ProxyRegistryDomain string ReplicatedRegistryDomain string + HostCABundlePath string + + // DryRun is a flag to enable dry-run mode for Admin Console. + // If true, Admin Console will only render the helm template and additional manifests, but not install + // the release. + DryRun bool + + dryRunManifests [][]byte } type KotsInstaller func(msg *spinner.MessageWriter) error @@ -110,3 +118,8 @@ func (a *AdminConsole) ChartLocation() string { } return chartName } + +// DryRunManifests returns the manifests generated during a dry run. +func (a *AdminConsole) DryRunManifests() [][]byte { + return a.dryRunManifests +} diff --git a/pkg/addons/adminconsole/install.go b/pkg/addons/adminconsole/install.go index aee73a096..a107422e6 100644 --- a/pkg/addons/adminconsole/install.go +++ b/pkg/addons/adminconsole/install.go @@ -1,6 +1,7 @@ package adminconsole import ( + "bytes" "context" "encoding/base64" "fmt" @@ -9,13 +10,27 @@ import ( "github.com/pkg/errors" "github.com/replicatedhq/embedded-cluster/pkg/addons/registry" "github.com/replicatedhq/embedded-cluster/pkg/helm" + "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" "github.com/replicatedhq/embedded-cluster/pkg/spinner" "golang.org/x/crypto/bcrypt" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + jsonserializer "k8s.io/apimachinery/pkg/runtime/serializer/json" "sigs.k8s.io/controller-runtime/pkg/client" ) +var ( + serializer runtime.Serializer +) + +func init() { + scheme := kubeutils.Scheme + serializer = jsonserializer.NewSerializerWithOptions(jsonserializer.DefaultMetaFactory, scheme, scheme, jsonserializer.SerializerOptions{ + Yaml: true, + }) +} + func (a *AdminConsole) Install(ctx context.Context, kcli client.Client, hcli helm.Client, overrides []string, writer *spinner.MessageWriter) error { // some resources are not part of the helm chart and need to be created before the chart is installed // TODO: move this to the helm chart @@ -28,24 +43,33 @@ func (a *AdminConsole) Install(ctx context.Context, kcli client.Client, hcli hel return errors.Wrap(err, "generate helm values") } - _, err = hcli.Install(ctx, helm.InstallOptions{ + opts := helm.InstallOptions{ ReleaseName: releaseName, ChartPath: a.ChartLocation(), ChartVersion: Metadata.Version, Values: values, Namespace: namespace, Labels: getBackupLabels(), - }) - if err != nil { - return errors.Wrap(err, "helm install") } - // install the application - - if a.KotsInstaller != nil { - err := a.KotsInstaller(writer) + if a.DryRun { + manifests, err := hcli.Render(ctx, opts) if err != nil { - return err + return errors.Wrap(err, "dry run render") + } + a.dryRunManifests = append(a.dryRunManifests, manifests...) + } else { + _, err = hcli.Install(ctx, opts) + if err != nil { + return errors.Wrap(err, "helm install") + } + + // install the application + if a.KotsInstaller != nil { + err := a.KotsInstaller(writer) + if err != nil { + return err + } } } @@ -53,15 +77,15 @@ func (a *AdminConsole) Install(ctx context.Context, kcli client.Client, hcli hel } func (a *AdminConsole) createPreRequisites(ctx context.Context, kcli client.Client) error { - if err := createNamespace(ctx, kcli, namespace); err != nil { + if err := a.createNamespace(ctx, kcli, namespace); err != nil { return errors.Wrap(err, "create namespace") } - if err := createPasswordSecret(ctx, kcli, namespace, a.Password); err != nil { + if err := a.createPasswordSecret(ctx, kcli, namespace, a.Password); err != nil { return errors.Wrap(err, "create kots password secret") } - if err := createCAConfigmap(ctx, kcli, namespace, a.PrivateCAs); err != nil { + if err := a.createCAConfigmap(ctx, kcli, namespace, a.PrivateCAs); err != nil { return errors.Wrap(err, "create kots CA configmap") } @@ -70,7 +94,7 @@ func (a *AdminConsole) createPreRequisites(ctx context.Context, kcli client.Clie if err != nil { return errors.Wrap(err, "get registry cluster IP") } - if err := createRegistrySecret(ctx, kcli, namespace, registryIP); err != nil { + if err := a.createRegistrySecret(ctx, kcli, namespace, registryIP); err != nil { return errors.Wrap(err, "create registry secret") } } @@ -78,31 +102,19 @@ func (a *AdminConsole) createPreRequisites(ctx context.Context, kcli client.Clie return nil } -func createNamespace(ctx context.Context, kcli client.Client, namespace string) error { - ns := corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: namespace, - }, - } - if err := kcli.Create(ctx, &ns); client.IgnoreAlreadyExists(err) != nil { - return err - } - return nil -} - -func createPasswordSecret(ctx context.Context, kcli client.Client, namespace string, password string) error { - passwordBcrypt, err := bcrypt.GenerateFromPassword([]byte(password), 10) +func (a *AdminConsole) createCAConfigmap(ctx context.Context, cli client.Client, namespace string, privateCAs []string) error { + cas, err := privateCAsToMap(privateCAs) if err != nil { - return errors.Wrap(err, "generate bcrypt from password") + return errors.Wrap(err, "create private cas map") } - kotsPasswordSecret := corev1.Secret{ + kotsCAConfigmap := corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ - Kind: "Secret", + Kind: "ConfigMap", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: "kotsadm-password", + Name: "kotsadm-private-cas", Namespace: namespace, Labels: map[string]string{ "kots.io/kotsadm": "true", @@ -110,32 +122,58 @@ func createPasswordSecret(ctx context.Context, kcli client.Client, namespace str "replicated.com/disaster-recovery-chart": "admin-console", }, }, - Data: map[string][]byte{ - "passwordBcrypt": []byte(passwordBcrypt), - }, + Data: cas, } - err = kcli.Create(ctx, &kotsPasswordSecret) - if err != nil { - return errors.Wrap(err, "create kotsadm-password secret") + if a.DryRun { + b := bytes.NewBuffer(nil) + if err := serializer.Encode(&kotsCAConfigmap, b); err != nil { + return errors.Wrap(err, "serialize CA configmap") + } + a.dryRunManifests = append(a.dryRunManifests, b.Bytes()) + } else { + if err := cli.Create(ctx, &kotsCAConfigmap); client.IgnoreAlreadyExists(err) != nil { + return errors.Wrap(err, "create kotsadm-private-cas configmap") + } } return nil } -func createCAConfigmap(ctx context.Context, cli client.Client, namespace string, privateCAs []string) error { - cas, err := privateCAsToMap(privateCAs) +func (a *AdminConsole) createNamespace(ctx context.Context, kcli client.Client, namespace string) error { + ns := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + } + + if a.DryRun { + b := bytes.NewBuffer(nil) + if err := serializer.Encode(&ns, b); err != nil { + return errors.Wrap(err, "serialize namespace") + } + a.dryRunManifests = append(a.dryRunManifests, b.Bytes()) + } else { + if err := kcli.Create(ctx, &ns); client.IgnoreAlreadyExists(err) != nil { + return err + } + } + return nil +} + +func (a *AdminConsole) createPasswordSecret(ctx context.Context, kcli client.Client, namespace string, password string) error { + passwordBcrypt, err := bcrypt.GenerateFromPassword([]byte(password), 10) if err != nil { - return errors.Wrap(err, "create private cas map") + return errors.Wrap(err, "generate bcrypt from password") } - kotsCAConfigmap := corev1.ConfigMap{ + kotsPasswordSecret := corev1.Secret{ TypeMeta: metav1.TypeMeta{ - Kind: "ConfigMap", + Kind: "Secret", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: "kotsadm-private-cas", + Name: "kotsadm-password", Namespace: namespace, Labels: map[string]string{ "kots.io/kotsadm": "true", @@ -143,17 +181,28 @@ func createCAConfigmap(ctx context.Context, cli client.Client, namespace string, "replicated.com/disaster-recovery-chart": "admin-console", }, }, - Data: cas, + Data: map[string][]byte{ + "passwordBcrypt": []byte(passwordBcrypt), + }, } - if err := cli.Create(ctx, &kotsCAConfigmap); client.IgnoreAlreadyExists(err) != nil { - return errors.Wrap(err, "create kotsadm-private-cas configmap") + if a.DryRun { + b := bytes.NewBuffer(nil) + if err := serializer.Encode(&kotsPasswordSecret, b); err != nil { + return errors.Wrap(err, "serialize password secret") + } + a.dryRunManifests = append(a.dryRunManifests, b.Bytes()) + } else { + err = kcli.Create(ctx, &kotsPasswordSecret) + if err != nil { + return errors.Wrap(err, "create kotsadm-password secret") + } } return nil } -func createRegistrySecret(ctx context.Context, kcli client.Client, namespace string, registryIP string) error { +func (a *AdminConsole) createRegistrySecret(ctx context.Context, kcli client.Client, namespace string, registryIP string) error { authString := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("embedded-cluster:%s", registry.GetRegistryPassword()))) authConfig := fmt.Sprintf(`{"auths":{"%s:5000":{"username": "embedded-cluster", "password": "%s", "auth": "%s"}}}`, registryIP, registry.GetRegistryPassword(), authString) @@ -177,9 +226,17 @@ func createRegistrySecret(ctx context.Context, kcli client.Client, namespace str Type: "kubernetes.io/dockerconfigjson", } - err := kcli.Create(ctx, ®istryCreds) - if err != nil { - return errors.Wrap(err, "create registry-auth secret") + if a.DryRun { + b := bytes.NewBuffer(nil) + if err := serializer.Encode(®istryCreds, b); err != nil { + return errors.Wrap(err, "serialize registry secret") + } + a.dryRunManifests = append(a.dryRunManifests, b.Bytes()) + } else { + err := kcli.Create(ctx, ®istryCreds) + if err != nil { + return errors.Wrap(err, "create registry-auth secret") + } } return nil @@ -187,6 +244,12 @@ func createRegistrySecret(ctx context.Context, kcli client.Client, namespace str func privateCAsToMap(privateCAs []string) (map[string]string, error) { cas := map[string]string{} + + // Handle nil privateCAs + if privateCAs == nil { + return cas, nil + } + for i, path := range privateCAs { data, err := os.ReadFile(path) if err != nil { diff --git a/pkg/addons/adminconsole/integration/hostcabundle_test.go b/pkg/addons/adminconsole/integration/hostcabundle_test.go new file mode 100644 index 000000000..46d486cc7 --- /dev/null +++ b/pkg/addons/adminconsole/integration/hostcabundle_test.go @@ -0,0 +1,79 @@ +package integration + +import ( + "context" + "strings" + "testing" + + "github.com/replicatedhq/embedded-cluster/pkg/addons/adminconsole" + "github.com/replicatedhq/embedded-cluster/pkg/helm" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/yaml" +) + +func TestHostCABundle(t *testing.T) { + addon := &adminconsole.AdminConsole{ + DryRun: true, + HostCABundlePath: "/etc/ssl/certs/ca-certificates.crt", + } + + hcli, err := helm.NewClient(helm.HelmOptions{}) + require.NoError(t, err, "NewClient should not return an error") + + err = addon.Install(context.Background(), nil, hcli, nil, nil) + require.NoError(t, err, "adminconsole.Install should not return an error") + + manifests := addon.DryRunManifests() + require.NotEmpty(t, manifests, "DryRunManifests should not be empty") + + var adminDeployment *appsv1.Deployment + for _, manifest := range manifests { + manifestStr := string(manifest) + // Look for the kotsadm deployment by its template source + if strings.Contains(manifestStr, "# Source: admin-console/templates/kotsadm-deployment.yaml") { + err := yaml.Unmarshal(manifest, &adminDeployment) + require.NoError(t, err, "Failed to unmarshal Admin Console deployment") + break + } + } + + require.NotNil(t, adminDeployment, "Admin Console deployment should not be nil") + + // Check for host-ca-bundle volume + var volume *corev1.Volume + for _, v := range adminDeployment.Spec.Template.Spec.Volumes { + if v.Name == "host-ca-bundle" { + volume = &v + } + } + if assert.NotNil(t, volume, "Admin Console host-ca-bundle volume should not be nil") { + assert.Equal(t, volume.VolumeSource.HostPath.Path, "/etc/ssl/certs/ca-certificates.crt") + assert.Equal(t, volume.VolumeSource.HostPath.Type, ptr.To(corev1.HostPathFileOrCreate)) + } + + // Check for host-ca-bundle volume mount + var volumeMount *corev1.VolumeMount + for _, v := range adminDeployment.Spec.Template.Spec.Containers[0].VolumeMounts { + if v.Name == "host-ca-bundle" { + volumeMount = &v + } + } + if assert.NotNil(t, volumeMount, "Admin Console host-ca-bundle volume mount should not be nil") { + assert.Equal(t, volumeMount.MountPath, "/certs/ca-certificates.crt") + } + + // Check for SSL_CERT_DIR environment variable + var sslCertDirEnv *corev1.EnvVar + for _, env := range adminDeployment.Spec.Template.Spec.Containers[0].Env { + if env.Name == "SSL_CERT_DIR" { + sslCertDirEnv = &env + } + } + if assert.NotNil(t, sslCertDirEnv, "Admin Console SSL_CERT_DIR environment variable should not be nil") { + assert.Equal(t, sslCertDirEnv.Value, "/certs") + } +} diff --git a/pkg/addons/adminconsole/static/metadata.yaml b/pkg/addons/adminconsole/static/metadata.yaml index 0612fdb27..a7fcf6b25 100644 --- a/pkg/addons/adminconsole/static/metadata.yaml +++ b/pkg/addons/adminconsole/static/metadata.yaml @@ -5,24 +5,24 @@ # $ make buildtools # $ output/bin/buildtools update addon # -version: 1.124.16-ec.3 +version: 1.124.16-ec.4 location: oci://proxy.replicated.com/anonymous/registry.replicated.com/library/admin-console images: kotsadm: repo: proxy.replicated.com/anonymous/kotsadm/kotsadm tag: - amd64: v1.124.16-ec.3-amd64@sha256:229fbedd2ff319b3b3ee39f0186d0db4c995c66b3f6938f44af0636529573327 - arm64: v1.124.16-ec.3-arm64@sha256:73bbfe280dfec038840e60c8e07a46202af3a07f9e74e54901910d8203bbc7e5 + amd64: v1.124.16-ec.4-amd64@sha256:25867454b00cae4c36dcb1cab30fe833227337fa698afe1d9450e85d0dda0461 + arm64: v1.124.16-ec.4-arm64@sha256:76d95bd8c53a9e7f3e170b927f847c6b308457dd83a7e37229c0f0770e40d0d6 kotsadm-migrations: repo: proxy.replicated.com/anonymous/kotsadm/kotsadm-migrations tag: - amd64: v1.124.16-ec.3-amd64@sha256:db38f5452a1858b81ca87b4f7007ab7a08b8b4f3655b87883c2c1695e9692c35 - arm64: v1.124.16-ec.3-arm64@sha256:8ecd83febceef657477eabdc1772fae34a86adb37c578dcca0eabd8041bbbe72 + amd64: v1.124.16-ec.4-amd64@sha256:b0ac39c9cd9a727ab8e11738aa28ba8e3f5415fdf444b3d7e10dbdc47078b281 + arm64: v1.124.16-ec.4-arm64@sha256:7b6754b4e444436c429161a0b292f5c6afe2d9aa7a04083cab588a2ecdb5e87f kurl-proxy: repo: proxy.replicated.com/anonymous/kotsadm/kurl-proxy tag: - amd64: v1.124.16-ec.3-amd64@sha256:c8a7ac1d70216a5c48dff3e425c50e64e9316688acff769309a5158307d4dc60 - arm64: v1.124.16-ec.3-arm64@sha256:5ed75c824821b6fcee88b1217ac73f551b9bb29c31feb23b35d92c9d292d753c + amd64: v1.124.16-ec.4-amd64@sha256:5296b3ce56fbf55eee420efc7e939514fc11e0e6c83ff1240288ecca36c47473 + arm64: v1.124.16-ec.4-arm64@sha256:35af6a5e0566416b5a9d06da20725fdbfaa10727f255cb31520715e422c220e3 rqlite: repo: proxy.replicated.com/anonymous/kotsadm/rqlite tag: diff --git a/pkg/addons/adminconsole/static/values.tpl.yaml b/pkg/addons/adminconsole/static/values.tpl.yaml index dc2ecfc06..678709def 100644 --- a/pkg/addons/adminconsole/static/values.tpl.yaml +++ b/pkg/addons/adminconsole/static/values.tpl.yaml @@ -19,6 +19,3 @@ passwordSecretRef: name: kotsadm-password service: enabled: false -privateCAs: - enabled: true - configmapName: "kotsadm-private-cas" diff --git a/pkg/addons/adminconsole/values.go b/pkg/addons/adminconsole/values.go index 9084e9079..fc14deb0b 100644 --- a/pkg/addons/adminconsole/values.go +++ b/pkg/addons/adminconsole/values.go @@ -76,7 +76,32 @@ func (a *AdminConsole) GenerateHelmValues(ctx context.Context, kcli client.Clien ) } + extraVolumes := []map[string]interface{}{} + extraVolumeMounts := []map[string]interface{}{} + + if a.HostCABundlePath != "" { + extraVolumes = append(extraVolumes, map[string]interface{}{ + "name": "host-ca-bundle", + "hostPath": map[string]interface{}{ + "path": a.HostCABundlePath, + "type": "FileOrCreate", + }, + }) + + extraVolumeMounts = append(extraVolumeMounts, map[string]interface{}{ + "name": "host-ca-bundle", + "mountPath": "/certs/ca-certificates.crt", + }) + + extraEnv = append(extraEnv, map[string]interface{}{ + "name": "SSL_CERT_DIR", + "value": "/certs", + }) + } + copiedValues["extraEnv"] = extraEnv + copiedValues["extraVolumes"] = extraVolumes + copiedValues["extraVolumeMounts"] = extraVolumeMounts err = helm.SetValue(copiedValues, "kurlProxy.nodePort", runtimeconfig.AdminConsolePort()) if err != nil { diff --git a/pkg/addons/adminconsole/values_test.go b/pkg/addons/adminconsole/values_test.go new file mode 100644 index 000000000..99a8ed921 --- /dev/null +++ b/pkg/addons/adminconsole/values_test.go @@ -0,0 +1,82 @@ +package adminconsole + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateHelmValues_HostCABundlePath(t *testing.T) { + t.Run("with host CA bundle path", func(t *testing.T) { + adminConsole := &AdminConsole{ + HostCABundlePath: "/etc/ssl/certs/ca-certificates.crt", + } + + values, err := adminConsole.GenerateHelmValues(context.Background(), nil, nil) + require.NoError(t, err, "GenerateHelmValues should not return an error") + + // Verify structure types + require.NotEmpty(t, values["extraVolumes"]) + require.IsType(t, []map[string]interface{}{}, values["extraVolumes"]) + require.Len(t, values["extraVolumes"].([]map[string]interface{}), 1) + + require.NotEmpty(t, values["extraVolumeMounts"]) + require.IsType(t, []map[string]interface{}{}, values["extraVolumeMounts"]) + require.Len(t, values["extraVolumeMounts"].([]map[string]interface{}), 1) + + require.NotEmpty(t, values["extraEnv"]) + require.IsType(t, []map[string]interface{}{}, values["extraEnv"]) + + // Verify volume configuration + extraVolume := values["extraVolumes"].([]map[string]interface{})[0] + assert.Equal(t, "host-ca-bundle", extraVolume["name"]) + if assert.NotNil(t, extraVolume["hostPath"]) { + hostPath := extraVolume["hostPath"].(map[string]interface{}) + assert.Equal(t, "/etc/ssl/certs/ca-certificates.crt", hostPath["path"]) + assert.Equal(t, "FileOrCreate", hostPath["type"]) + } + + // Verify volume mount configuration + extraVolumeMount := values["extraVolumeMounts"].([]map[string]interface{})[0] + assert.Equal(t, "host-ca-bundle", extraVolumeMount["name"]) + assert.Equal(t, "/certs/ca-certificates.crt", extraVolumeMount["mountPath"]) + + // Verify SSL_CERT_DIR environment variable + extraEnv := values["extraEnv"].([]map[string]interface{}) + var foundSSLCertDir bool + for _, env := range extraEnv { + if env["name"] == "SSL_CERT_DIR" { + foundSSLCertDir = true + assert.Equal(t, "/certs", env["value"]) + break + } + } + assert.True(t, foundSSLCertDir, "SSL_CERT_DIR environment variable should be set") + }) + + t.Run("without host CA bundle path", func(t *testing.T) { + adminConsole := &AdminConsole{ + // HostCABundlePath intentionally not set + } + + values, err := adminConsole.GenerateHelmValues(context.Background(), nil, nil) + require.NoError(t, err, "GenerateHelmValues should not return an error") + + // Verify structure types + require.IsType(t, []map[string]interface{}{}, values["extraVolumes"]) + require.Len(t, values["extraVolumes"].([]map[string]interface{}), 0) + + require.IsType(t, []map[string]interface{}{}, values["extraVolumeMounts"]) + require.Len(t, values["extraVolumeMounts"].([]map[string]interface{}), 0) + + require.IsType(t, []map[string]interface{}{}, values["extraEnv"]) + + // Verify SSL_CERT_DIR is not present in any environment variable + extraEnv := values["extraEnv"].([]map[string]interface{}) + for _, env := range extraEnv { + assert.NotEqual(t, "SSL_CERT_DIR", env["name"], "SSL_CERT_DIR environment variable should not be set") + } + }) +} diff --git a/pkg/addons/install.go b/pkg/addons/install.go index 5a9d32b14..6b08366c2 100644 --- a/pkg/addons/install.go +++ b/pkg/addons/install.go @@ -104,6 +104,7 @@ func getAddOnsForInstall(opts InstallOptions) []types.AddOn { ReplicatedAppDomain: domains.ReplicatedAppDomain, ProxyRegistryDomain: domains.ProxyRegistryDomain, ReplicatedRegistryDomain: domains.ReplicatedRegistryDomain, + HostCABundlePath: opts.HostCABundlePath, } addOns = append(addOns, adminConsoleAddOn) diff --git a/pkg/addons/install_test.go b/pkg/addons/install_test.go index ac2f089e2..1df61b0d4 100644 --- a/pkg/addons/install_test.go +++ b/pkg/addons/install_test.go @@ -350,6 +350,38 @@ defaultDomains: assert.Equal(t, "registry.example.com", adminConsole.ReplicatedRegistryDomain) }, }, + { + name: "with host CA bundle path", + opts: InstallOptions{ + IsAirgap: false, + DisasterRecoveryEnabled: true, // Enable disaster recovery to also check Velero + AdminConsolePwd: "password123", + HostCABundlePath: "/etc/ssl/certs/ca-certificates.crt", + }, + verify: func(t *testing.T, addons []types.AddOn) { + // Find Velero and AdminConsole add-ons to verify HostCABundlePath + var vel *velero.Velero + var adminConsole *adminconsole.AdminConsole + + for _, addon := range addons { + switch a := addon.(type) { + case *velero.Velero: + vel = a + case *adminconsole.AdminConsole: + adminConsole = a + } + } + + require.NotNil(t, vel, "Velero add-on should be present") + require.NotNil(t, adminConsole, "AdminConsole add-on should be present") + + // Verify HostCABundlePath is properly passed + assert.Equal(t, "/etc/ssl/certs/ca-certificates.crt", vel.HostCABundlePath, + "Velero should have the correct HostCABundlePath") + assert.Equal(t, "/etc/ssl/certs/ca-certificates.crt", adminConsole.HostCABundlePath, + "AdminConsole should have the correct HostCABundlePath") + }, + }, } for _, tt := range tests { diff --git a/tests/dryrun/install_test.go b/tests/dryrun/install_test.go index 9420ed89a..b453ab241 100644 --- a/tests/dryrun/install_test.go +++ b/tests/dryrun/install_test.go @@ -587,12 +587,8 @@ func TestNoDomains(t *testing.T) { t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) } -// this test is to ensure that http proxy settings are passed through correctly -func TestInstall_HTTPProxy(t *testing.T) { - t.Setenv("HTTP_PROXY", "http://localhost:3128") - t.Setenv("HTTPS_PROXY", "http://localhost:3128") - t.Setenv("NO_PROXY", "localhost,127.0.0.1,10.0.0.0/8") - +// this test is to verify HTTP proxy + CA bundle configuration together in Helm values for addons +func TestHTTPProxyWithCABundleConfiguration(t *testing.T) { hostCABundle := findHostCABundle(t) hcli := &helm.MockClient{} @@ -603,6 +599,11 @@ func TestInstall_HTTPProxy(t *testing.T) { hcli.On("Close").Once().Return(nil), ) + // Set HTTP proxy environment variables + t.Setenv("HTTP_PROXY", "http://localhost:3128") + t.Setenv("HTTPS_PROXY", "http://localhost:3128") + t.Setenv("NO_PROXY", "localhost,127.0.0.1,10.0.0.0/8") + dr := dryrunInstall(t, &dryrun.Client{HelmClient: hcli}) // --- validate addons --- // @@ -660,6 +661,7 @@ func TestInstall_HTTPProxy(t *testing.T) { assert.Equal(t, "Install", hcli.Calls[2].Method) veleroOpts := hcli.Calls[2].Arguments[1].(helm.InstallOptions) assert.Equal(t, "velero", veleroOpts.ReleaseName) + assertHelmValues(t, veleroOpts.Values, map[string]any{ "configuration.extraEnvVars": map[string]any{ "HTTPS_PROXY": "http://localhost:3128", @@ -684,6 +686,7 @@ func TestInstall_HTTPProxy(t *testing.T) { assert.Equal(t, "Install", hcli.Calls[3].Method) adminConsoleOpts := hcli.Calls[3].Arguments[1].(helm.InstallOptions) assert.Equal(t, "admin-console", adminConsoleOpts.ReleaseName) + assertHelmValues(t, adminConsoleOpts.Values, map[string]any{ "extraEnv": []map[string]any{ { @@ -702,9 +705,23 @@ func TestInstall_HTTPProxy(t *testing.T) { "name": "NO_PROXY", "value": noProxy, }, + { + "name": "SSL_CERT_DIR", + "value": "/certs", + }, }, + "extraVolumes": []map[string]any{{ + "name": "host-ca-bundle", + "hostPath": map[string]any{ + "path": hostCABundle, + "type": "FileOrCreate", + }, + }}, + "extraVolumeMounts": []map[string]any{{ + "mountPath": "/certs/ca-certificates.crt", + "name": "host-ca-bundle", + }}, }) - // TODO: CA // --- validate host preflight spec --- // assertCollectors(t, dr.HostPreflightSpec.Collectors, map[string]struct { @@ -735,9 +752,6 @@ func TestInstall_HTTPProxy(t *testing.T) { t.Fatalf("failed to create kube client: %v", err) } - // TODO: CA - // assertConfigMapExists(t, kcli, "private-cas", "kotsadm") - // --- validate installation object --- // in, err := kubeutils.GetLatestInstallation(context.TODO(), kcli) if err != nil { @@ -745,6 +759,9 @@ func TestInstall_HTTPProxy(t *testing.T) { } assert.Equal(t, hostCABundle, in.Spec.RuntimeConfig.HostCABundlePath) + + // Verify some metrics were captured + assert.NotEmpty(t, dr.Metrics) } func findHostCABundle(t *testing.T) string {