Skip to content

feat(adminconsole): Mount Host CA Bundle into Admin Console Containers #2175

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 56 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
6deffba
feat(velero): add support for mitm proxy
emosbaugh May 15, 2025
d9c5aa0
f
emosbaugh May 15, 2025
2f92e11
f
emosbaugh May 15, 2025
b6cee6b
f
emosbaugh May 15, 2025
2bd93d3
f
emosbaugh May 15, 2025
f50cfd0
f
emosbaugh May 16, 2025
c4e2020
f
emosbaugh May 16, 2025
52c5fe6
f
emosbaugh May 16, 2025
4fd04c8
velero use ca from host
emosbaugh May 16, 2025
7956d8c
http proxy dryrun test
emosbaugh May 16, 2025
7c91a94
f
emosbaugh May 16, 2025
0bb2644
f
emosbaugh May 16, 2025
f5993e8
f
emosbaugh May 16, 2025
bdde8b1
f
emosbaugh May 16, 2025
abc2e7f
f
emosbaugh May 16, 2025
e7e9735
f
emosbaugh May 16, 2025
7830566
f
emosbaugh May 16, 2025
cd0ce8b
f
emosbaugh May 16, 2025
827e415
f
emosbaugh May 16, 2025
9246ad7
f
emosbaugh May 16, 2025
2ca6801
f
emosbaugh May 16, 2025
ff13106
f
emosbaugh May 16, 2025
3aa9924
f
emosbaugh May 16, 2025
457866e
f
emosbaugh May 16, 2025
bc440db
f
emosbaugh May 16, 2025
5617ee8
f
emosbaugh May 16, 2025
c258970
f
emosbaugh May 17, 2025
4fcde92
f
emosbaugh May 17, 2025
04a23b8
f
emosbaugh May 17, 2025
ffc5cdc
refactor host ca retrieval
diamonwiggins May 19, 2025
a95da78
add unit tests
diamonwiggins May 20, 2025
8e4c5ab
add dry run capability for adminconsole addon
diamonwiggins May 20, 2025
4440b9a
make adminconsole unit test consistent with other addons
diamonwiggins May 20, 2025
59bd4b8
remove privateCa from admin console
diamonwiggins May 20, 2025
f0f5456
update adminconsole version and add integration test
diamonwiggins May 20, 2025
7eb7603
Merge branch 'main' into diamonwiggins/sc-123404/mount-ca-into-kotsad…
diamonwiggins May 20, 2025
238dae0
add tests for cabundle to dryrun
diamonwiggins May 20, 2025
bab578d
update TestInstallWithPrivateCAs e2e test
diamonwiggins May 20, 2025
9ad0ad4
fix dry run tests to make sure they use dry run client
diamonwiggins May 20, 2025
de80ae2
more debug
diamonwiggins May 20, 2025
27f6965
more debug
diamonwiggins May 20, 2025
cfa276e
fix tests
diamonwiggins May 20, 2025
67ae766
fix tests
diamonwiggins May 21, 2025
4f1dc55
remove debug values
diamonwiggins May 21, 2025
42140f8
debug integration test
diamonwiggins May 21, 2025
9d6b698
fix integration test
diamonwiggins May 21, 2025
9f2f039
move cert generation to bash
diamonwiggins May 21, 2025
66e6ea0
debug privateca e2e test
diamonwiggins May 21, 2025
e387e16
debug privateca e2e test
diamonwiggins May 21, 2025
ce61ae5
re-add privateca in order to get operator working correctly for mitm …
diamonwiggins May 21, 2025
e6a150f
fix create configmap
diamonwiggins May 21, 2025
8d48233
remove uneeded e2e test
diamonwiggins May 21, 2025
3831ddd
consolidate unit test
diamonwiggins May 21, 2025
08ee403
remove privateca from static values
diamonwiggins May 21, 2025
7ba7165
remove redundant dry run test
diamonwiggins May 21, 2025
bf12e64
Merge branch 'main' into diamonwiggins/sc-123404/mount-ca-into-kotsad…
diamonwiggins May 23, 2025
e9c68f4
Merge branch 'main' into diamonwiggins/sc-123404/mount-ca-into-kotsad…
diamonwiggins May 23, 2025
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
59 changes: 0 additions & 59 deletions e2e/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package e2e

import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"strings"
Expand All @@ -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) {
Expand Down Expand Up @@ -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()

Expand Down
13 changes: 13 additions & 0 deletions pkg/addons/adminconsole/adminconsole.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
163 changes: 113 additions & 50 deletions pkg/addons/adminconsole/install.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package adminconsole

import (
"bytes"
"context"
"encoding/base64"
"fmt"
Expand All @@ -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
Expand All @@ -28,40 +43,49 @@ 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
}
}
}

return nil
}

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")
}

Expand All @@ -70,90 +94,115 @@ 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")
}
}

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",
"replicated.com/disaster-recovery": "infra",
"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",
"replicated.com/disaster-recovery": "infra",
"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)

Expand All @@ -177,16 +226,30 @@ func createRegistrySecret(ctx context.Context, kcli client.Client, namespace str
Type: "kubernetes.io/dockerconfigjson",
}

err := kcli.Create(ctx, &registryCreds)
if err != nil {
return errors.Wrap(err, "create registry-auth secret")
if a.DryRun {
b := bytes.NewBuffer(nil)
if err := serializer.Encode(&registryCreds, b); err != nil {
return errors.Wrap(err, "serialize registry secret")
}
a.dryRunManifests = append(a.dryRunManifests, b.Bytes())
} else {
err := kcli.Create(ctx, &registryCreds)
if err != nil {
return errors.Wrap(err, "create registry-auth secret")
}
}

return nil
}

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 {
Expand Down
Loading