Skip to content

Commit e1e37ae

Browse files
committed
ensure image pull secrets
1 parent 38b4972 commit e1e37ae

File tree

7 files changed

+228
-79
lines changed

7 files changed

+228
-79
lines changed

api/controllers/app/install.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func (c *AppController) InstallApp(ctx context.Context, opts InstallAppOptions)
117117
}
118118

119119
// Install the app with installable charts and KOTS CLI
120-
err = c.appInstallManager.Install(ctx, installableCharts, kotsConfigValues)
120+
err = c.appInstallManager.Install(ctx, installableCharts, kotsConfigValues, opts.RegistrySettings)
121121
if err != nil {
122122
return fmt.Errorf("install app: %w", err)
123123
}

api/internal/managers/app/install/install.go

Lines changed: 9 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,25 @@ import (
66
"os"
77
"runtime/debug"
88

9-
"github.com/replicatedhq/embedded-cluster/api/internal/utils"
109
"github.com/replicatedhq/embedded-cluster/api/types"
11-
kotscli "github.com/replicatedhq/embedded-cluster/cmd/installer/kotscli"
1210
"github.com/replicatedhq/embedded-cluster/pkg/helm"
13-
"github.com/replicatedhq/embedded-cluster/pkg/netutils"
1411
"github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig"
1512
kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1"
16-
kyaml "sigs.k8s.io/yaml"
1713
)
1814

1915
// Install installs the app with the provided config values
20-
func (m *appInstallManager) Install(ctx context.Context, installableCharts []types.InstallableHelmChart, configValues kotsv1beta1.ConfigValues) error {
21-
license := &kotsv1beta1.License{}
22-
if err := kyaml.Unmarshal(m.license, license); err != nil {
23-
return fmt.Errorf("parse license: %w", err)
24-
}
25-
16+
func (m *appInstallManager) Install(ctx context.Context, installableCharts []types.InstallableHelmChart, configValues kotsv1beta1.ConfigValues, registrySettings *types.RegistrySettings) error {
2617
if err := m.initKubeClient(); err != nil {
2718
return fmt.Errorf("init kube client: %w", err)
2819
}
2920

21+
// Start the namespace reconciler to ensure image pull secrets and other required resources in app namespaces
22+
nsReconciler, err := runNamespaceReconciler(ctx, m.kcli, registrySettings, m.logger)
23+
if err != nil {
24+
return fmt.Errorf("start namespace reconciler: %w", err)
25+
}
26+
defer nsReconciler.Stop()
27+
3028
kotsadmNamespace, err := runtimeconfig.KotsadmNamespace(ctx, m.kcli)
3129
if err != nil {
3230
return fmt.Errorf("get kotsadm namespace: %w", err)
@@ -37,49 +35,14 @@ func (m *appInstallManager) Install(ctx context.Context, installableCharts []typ
3735
return fmt.Errorf("initialize components: %w", err)
3836
}
3937

40-
// Install Helm charts first
38+
// Install Helm charts
4139
if err := m.installHelmCharts(ctx, installableCharts, kotsadmNamespace); err != nil {
4240
return fmt.Errorf("install helm charts: %w", err)
4341
}
4442

45-
// Then install the app using KOTS CLI
46-
if err := m.installWithKotsCLI(license, kotsadmNamespace, configValues); err != nil {
47-
return fmt.Errorf("install with kots cli: %w", err)
48-
}
49-
5043
return nil
5144
}
5245

53-
func (m *appInstallManager) installWithKotsCLI(license *kotsv1beta1.License, kotsadmNamespace string, configValues kotsv1beta1.ConfigValues) error {
54-
ecDomains := utils.GetDomains(m.releaseData)
55-
56-
installOpts := kotscli.InstallOptions{
57-
AppSlug: license.Spec.AppSlug,
58-
License: m.license,
59-
Namespace: kotsadmNamespace,
60-
ClusterID: m.clusterID,
61-
AirgapBundle: m.airgapBundle,
62-
// Skip running the KOTS app preflights in the Admin Console; they run in the manager experience installer when ENABLE_V3 is enabled
63-
SkipPreflights: true,
64-
// Skip pushing images to the registry since we do it separately earlier in the install process
65-
DisableImagePush: true,
66-
ReplicatedAppEndpoint: netutils.MaybeAddHTTPS(ecDomains.ReplicatedAppDomain),
67-
Stdout: m.newLogWriter(),
68-
}
69-
70-
configValuesFile, err := m.createConfigValuesFile(configValues)
71-
if err != nil {
72-
return fmt.Errorf("creating config values file: %w", err)
73-
}
74-
installOpts.ConfigValuesFile = configValuesFile
75-
76-
if m.kotsCLI != nil {
77-
return m.kotsCLI.Install(installOpts)
78-
}
79-
80-
return kotscli.Install(installOpts)
81-
}
82-
8346
func (m *appInstallManager) installHelmCharts(ctx context.Context, installableCharts []types.InstallableHelmChart, kotsadmNamespace string) error {
8447
logFn := m.logFn("app")
8548

api/internal/managers/app/install/install_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ func TestAppInstallManager_Install(t *testing.T) {
216216
require.NoError(t, err)
217217

218218
// Run installation with InstallableHelmCharts
219-
err = manager.Install(context.Background(), installableCharts, configValues)
219+
err = manager.Install(context.Background(), installableCharts, configValues, nil)
220220
require.NoError(t, err)
221221

222222
mockHelmClient.AssertExpectations(t)
@@ -260,7 +260,7 @@ func TestAppInstallManager_Install(t *testing.T) {
260260
assert.Equal(t, types.StatePending, appInstall.Status.State)
261261

262262
// Run installation
263-
err = manager.Install(t.Context(), installableCharts, kotsv1beta1.ConfigValues{})
263+
err = manager.Install(t.Context(), installableCharts, kotsv1beta1.ConfigValues{}, nil)
264264
require.NoError(t, err)
265265

266266
// Verify components status
@@ -299,7 +299,7 @@ func TestAppInstallManager_Install(t *testing.T) {
299299
require.NoError(t, err)
300300

301301
// Run installation (should fail)
302-
err = manager.Install(t.Context(), installableCharts, kotsv1beta1.ConfigValues{})
302+
err = manager.Install(t.Context(), installableCharts, kotsv1beta1.ConfigValues{}, nil)
303303
assert.Error(t, err)
304304

305305
mockHelmClient.AssertExpectations(t)
@@ -465,7 +465,7 @@ func TestComponentStatusTracking(t *testing.T) {
465465
require.NoError(t, err)
466466

467467
// Install the charts
468-
err = manager.Install(t.Context(), installableCharts, kotsv1beta1.ConfigValues{})
468+
err = manager.Install(t.Context(), installableCharts, kotsv1beta1.ConfigValues{}, nil)
469469
require.NoError(t, err)
470470

471471
// Verify that components were registered and have correct status
@@ -513,7 +513,7 @@ func TestComponentStatusTracking(t *testing.T) {
513513
require.NoError(t, err)
514514

515515
// Install the charts (should fail)
516-
err = manager.Install(t.Context(), installableCharts, kotsv1beta1.ConfigValues{})
516+
err = manager.Install(t.Context(), installableCharts, kotsv1beta1.ConfigValues{}, nil)
517517
require.Error(t, err)
518518

519519
// Verify that component failure is tracked

api/internal/managers/app/install/manager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ var _ AppInstallManager = &appInstallManager{}
2020
// AppInstallManager provides methods for managing app installation
2121
type AppInstallManager interface {
2222
// Install installs the app with the provided config values
23-
Install(ctx context.Context, installableCharts []types.InstallableHelmChart, configValues kotsv1beta1.ConfigValues) error
23+
Install(ctx context.Context, installableCharts []types.InstallableHelmChart, configValues kotsv1beta1.ConfigValues, registrySettings *types.RegistrySettings) error
2424
}
2525

2626
// appInstallManager is an implementation of the AppInstallManager interface

api/internal/managers/app/install/mock.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ type MockAppInstallManager struct {
1414
}
1515

1616
// Install mocks the Install method
17-
func (m *MockAppInstallManager) Install(ctx context.Context, installableCharts []types.InstallableHelmChart, configValues kotsv1beta1.ConfigValues) error {
18-
args := m.Called(ctx, installableCharts, configValues)
17+
func (m *MockAppInstallManager) Install(ctx context.Context, installableCharts []types.InstallableHelmChart, configValues kotsv1beta1.ConfigValues, registrySettings *types.RegistrySettings) error {
18+
args := m.Called(ctx, installableCharts, configValues, registrySettings)
1919
return args.Error(0)
2020
}
2121

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package install
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"fmt"
7+
"time"
8+
9+
"github.com/replicatedhq/embedded-cluster/api/types"
10+
"github.com/replicatedhq/embedded-cluster/pkg/release"
11+
"github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig"
12+
"github.com/sirupsen/logrus"
13+
corev1 "k8s.io/api/core/v1"
14+
k8serrors "k8s.io/apimachinery/pkg/api/errors"
15+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
"sigs.k8s.io/controller-runtime/pkg/client"
17+
)
18+
19+
const (
20+
reconcileInterval = 5 * time.Second
21+
)
22+
23+
// NamespaceReconciler handles ensuring image pull secrets in app namespaces.
24+
// It reads additionalNamespaces from the Application CR, ensures secrets exist
25+
// in those namespaces plus the kotsadm namespace, and polls for new namespace
26+
// creation to deploy secrets to them.
27+
type NamespaceReconciler struct {
28+
kcli client.Client
29+
registrySettings *types.RegistrySettings
30+
logger logrus.FieldLogger
31+
32+
watchedNamespaces []string
33+
cancel context.CancelFunc
34+
}
35+
36+
// runNamespaceReconciler creates and starts a reconciler that:
37+
// 1. Reads additionalNamespaces from release.GetApplication()
38+
// 2. Immediately ensures image pull secrets and other resources in all watched namespaces
39+
// 3. Starts background polling to reconcile namespaces periodically
40+
// Returns nil if registry settings are not provided (nothing to reconcile).
41+
func runNamespaceReconciler(
42+
ctx context.Context,
43+
kcli client.Client,
44+
registrySettings *types.RegistrySettings,
45+
logger logrus.FieldLogger,
46+
) (*NamespaceReconciler, error) {
47+
// If no registry settings, nothing to do
48+
if registrySettings == nil || registrySettings.ImagePullSecretName == "" || registrySettings.ImagePullSecretValue == "" {
49+
return nil, fmt.Errorf("registry settings are nil or empty")
50+
}
51+
52+
// Get kotsadm namespace
53+
kotsadmNamespace, err := runtimeconfig.KotsadmNamespace(ctx, kcli)
54+
if err != nil {
55+
return nil, fmt.Errorf("get kotsadm namespace: %w", err)
56+
}
57+
58+
// Get watched namespaces from Application CR
59+
watchedNamespaces := []string{kotsadmNamespace}
60+
if app := release.GetApplication(); app != nil {
61+
watchedNamespaces = append(watchedNamespaces, app.Spec.AdditionalNamespaces...)
62+
}
63+
64+
ctx, cancel := context.WithCancel(ctx)
65+
66+
r := &NamespaceReconciler{
67+
kcli: kcli,
68+
registrySettings: registrySettings,
69+
logger: logger,
70+
watchedNamespaces: watchedNamespaces,
71+
cancel: cancel,
72+
}
73+
74+
// Immediately reconcile all namespaces
75+
r.reconcile(ctx)
76+
77+
// Start background polling
78+
go r.run(ctx)
79+
80+
return r, nil
81+
}
82+
83+
// Stop stops the background reconciler
84+
func (r *NamespaceReconciler) Stop() {
85+
if r.cancel != nil {
86+
r.cancel()
87+
}
88+
}
89+
90+
// run polls periodically to reconcile namespaces
91+
func (r *NamespaceReconciler) run(ctx context.Context) {
92+
ticker := time.NewTicker(reconcileInterval)
93+
defer ticker.Stop()
94+
95+
for {
96+
select {
97+
case <-ctx.Done():
98+
return
99+
case <-ticker.C:
100+
r.reconcile(ctx)
101+
}
102+
}
103+
}
104+
105+
// reconcile ensures all watched namespaces have the required resources
106+
func (r *NamespaceReconciler) reconcile(ctx context.Context) {
107+
namespaces := r.watchedNamespaces
108+
109+
// If watching all namespaces, list them
110+
if r.watchesAllNamespaces() {
111+
nsList := &corev1.NamespaceList{}
112+
if err := r.kcli.List(ctx, nsList); err != nil {
113+
r.logger.WithError(err).Warn("failed to list namespaces")
114+
return
115+
}
116+
namespaces = make([]string, 0, len(nsList.Items))
117+
for _, ns := range nsList.Items {
118+
namespaces = append(namespaces, ns.Name)
119+
}
120+
}
121+
122+
for _, ns := range namespaces {
123+
if err := r.reconcileNamespace(ctx, ns); err != nil {
124+
r.logger.WithError(err).Warnf("failed to reconcile namespace %s", ns)
125+
}
126+
}
127+
}
128+
129+
// watchesAllNamespaces returns true if "*" is in the watched namespaces list
130+
func (r *NamespaceReconciler) watchesAllNamespaces() bool {
131+
for _, ns := range r.watchedNamespaces {
132+
if ns == "*" {
133+
return true
134+
}
135+
}
136+
return false
137+
}
138+
139+
// reconcileNamespace creates namespace if needed and ensures required resources exist
140+
func (r *NamespaceReconciler) reconcileNamespace(ctx context.Context, namespace string) error {
141+
// Skip wildcard entry
142+
if namespace == "*" {
143+
return nil
144+
}
145+
146+
// Create namespace if it doesn't exist
147+
ns := &corev1.Namespace{}
148+
err := r.kcli.Get(ctx, client.ObjectKey{Name: namespace}, ns)
149+
if k8serrors.IsNotFound(err) {
150+
ns = &corev1.Namespace{
151+
ObjectMeta: metav1.ObjectMeta{Name: namespace},
152+
}
153+
if err := r.kcli.Create(ctx, ns); err != nil && !k8serrors.IsAlreadyExists(err) {
154+
return fmt.Errorf("create namespace: %w", err)
155+
}
156+
r.logger.Infof("created namespace %s", namespace)
157+
} else if err != nil {
158+
return fmt.Errorf("get namespace: %w", err)
159+
}
160+
161+
if err := r.ensureImagePullSecret(ctx, namespace); err != nil {
162+
return fmt.Errorf("ensure image pull secret: %w", err)
163+
}
164+
165+
return nil
166+
}
167+
168+
// ensureImagePullSecret creates or updates the image pull secret in a namespace
169+
func (r *NamespaceReconciler) ensureImagePullSecret(ctx context.Context, namespace string) error {
170+
secretData, err := base64.StdEncoding.DecodeString(r.registrySettings.ImagePullSecretValue)
171+
if err != nil {
172+
return fmt.Errorf("decode secret value: %w", err)
173+
}
174+
175+
secret := &corev1.Secret{}
176+
key := client.ObjectKey{Namespace: namespace, Name: r.registrySettings.ImagePullSecretName}
177+
err = r.kcli.Get(ctx, key, secret)
178+
179+
if k8serrors.IsNotFound(err) {
180+
secret = &corev1.Secret{
181+
ObjectMeta: metav1.ObjectMeta{
182+
Name: r.registrySettings.ImagePullSecretName,
183+
Namespace: namespace,
184+
},
185+
Type: corev1.SecretTypeDockerConfigJson,
186+
Data: map[string][]byte{
187+
".dockerconfigjson": secretData,
188+
},
189+
}
190+
if err := r.kcli.Create(ctx, secret); err != nil {
191+
return fmt.Errorf("create secret: %w", err)
192+
}
193+
r.logger.Infof("created image pull secret %s in namespace %s", r.registrySettings.ImagePullSecretName, namespace)
194+
return nil
195+
}
196+
if err != nil {
197+
return fmt.Errorf("get secret: %w", err)
198+
}
199+
200+
// Update existing secret if data differs
201+
if string(secret.Data[".dockerconfigjson"]) != string(secretData) {
202+
secret.Data[".dockerconfigjson"] = secretData
203+
if err := r.kcli.Update(ctx, secret); err != nil {
204+
return fmt.Errorf("update secret: %w", err)
205+
}
206+
r.logger.Infof("updated image pull secret %s in namespace %s", r.registrySettings.ImagePullSecretName, namespace)
207+
}
208+
209+
return nil
210+
}

0 commit comments

Comments
 (0)