Skip to content

Commit 03725d1

Browse files
Merge pull request #3918 from jrvaldes/OCPBUGS-38401
OCPBUGS-38401: Mirror MAPI userData to openshift-cluster-api namespace for CAPI support
2 parents 374b870 + 6519963 commit 03725d1

3 files changed

Lines changed: 223 additions & 13 deletions

File tree

controllers/secret_controller.go

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434

3535
//+kubebuilder:rbac:groups="",resources=nodes,verbs=get;list;patch
3636
//+kubebuilder:rbac:groups="",resources=secrets,verbs=create;get;list;watch;update
37+
//+kubebuilder:rbac:groups="",resources=namespaces,verbs=get
3738

3839
const (
3940
// SecretController is the name of this controller in logs and other outputs.
@@ -113,9 +114,14 @@ func (r *SecretReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manage
113114
Complete(r)
114115
}
115116

116-
// isUserDataSecret returns true if the provided object is the userData Secret
117+
// isUserDataSecret returns true if the provided object is the userData Secret in either the
118+
// Machine API or Cluster API namespace.
117119
func isUserDataSecret(obj client.Object) bool {
118-
return obj.GetName() == secrets.UserDataSecret && obj.GetNamespace() == cluster.MachineAPINamespace
120+
if obj.GetName() != secrets.UserDataSecret {
121+
return false
122+
}
123+
return obj.GetNamespace() == cluster.MachineAPINamespace ||
124+
obj.GetNamespace() == cluster.ClusterAPINamespace
119125
}
120126

121127
// isPrivateKeySecret returns true if the provided object is the private key secret
@@ -192,25 +198,80 @@ func (r *SecretReconciler) reconcileUserDataSecret(ctx context.Context) error {
192198
// Fetch UserData instance
193199
err = r.client.Get(ctx,
194200
kubeTypes.NamespacedName{Name: secrets.UserDataSecret, Namespace: cluster.MachineAPINamespace}, userData)
195-
if err != nil && k8sapierrors.IsNotFound(err) {
201+
if err != nil {
202+
if !k8sapierrors.IsNotFound(err) {
203+
r.log.Error(err, "error retrieving the secret", "name", secrets.UserDataSecret)
204+
return err
205+
}
196206
// Secret is deleted
197207
r.log.Info("secret not found, creating the secret", "name", secrets.UserDataSecret)
198208
err = r.client.Create(ctx, validUserData)
199209
if err != nil {
200210
return err
201211
}
202-
// Secret created successfully - don't requeue
203-
return nil
204-
} else if err != nil {
205-
r.log.Error(err, "error retrieving the secret", "name", secrets.UserDataSecret)
206-
return err
207-
} else if string(userData.Data["userData"][:]) == string(validUserData.Data["userData"][:]) {
208-
// valid userData secret already exists
209-
return nil
210212
} else {
211-
// userdata secret data does not match what is expected
212-
return r.updateUserData(ctx, keySigner, validUserData)
213+
if string(userData.Data["userData"][:]) != string(validUserData.Data["userData"][:]) {
214+
// userdata secret data does not match what is expected
215+
if err = r.updateUserData(ctx, keySigner, validUserData); err != nil {
216+
return err
217+
}
218+
}
219+
}
220+
221+
// If the ClusterAPIMachineManagement feature gate is enabled, mirror the userData secret
222+
// to the openshift-cluster-api namespace for CAPI-provisioned Windows machines
223+
capiEnabled, err := r.isClusterAPIEnabled(ctx)
224+
if err != nil {
225+
return fmt.Errorf("error checking for Cluster API namespace: %w", err)
226+
}
227+
if capiEnabled {
228+
return r.ensureCAPIUserDataSecret(ctx, validUserData)
229+
}
230+
231+
// reconciliation successful, no need to requeue
232+
return nil
233+
}
234+
235+
// isClusterAPIEnabled returns true when the openshift-cluster-api namespace exist which indicate that the
236+
// ClusterAPIMachineManagement feature gate is active
237+
func (r *SecretReconciler) isClusterAPIEnabled(ctx context.Context) (bool, error) {
238+
ns := &core.Namespace{}
239+
err := r.client.Get(ctx, kubeTypes.NamespacedName{Name: cluster.ClusterAPINamespace}, ns)
240+
if err != nil {
241+
if k8sapierrors.IsNotFound(err) {
242+
return false, nil
243+
}
244+
return false, fmt.Errorf("error getting namespace %s: %w", cluster.ClusterAPINamespace, err)
245+
}
246+
return true, nil
247+
}
248+
249+
// ensureCAPIUserDataSecret creates or updates a copy of the windows userData secret in the
250+
// openshift-cluster-api namespace so that CAPI-based MachineSets can reference it.
251+
func (r *SecretReconciler) ensureCAPIUserDataSecret(ctx context.Context, mapiExpectedSecret *core.Secret) error {
252+
capiSecret := mapiExpectedSecret.DeepCopy()
253+
capiSecret.Namespace = cluster.ClusterAPINamespace
254+
// ensure fields are clear
255+
capiSecret.ResourceVersion = ""
256+
capiSecret.UID = ""
257+
258+
existing := &core.Secret{}
259+
err := r.client.Get(ctx,
260+
kubeTypes.NamespacedName{Name: secrets.UserDataSecret, Namespace: cluster.ClusterAPINamespace}, existing)
261+
if err != nil {
262+
if k8sapierrors.IsNotFound(err) {
263+
r.log.Info("creating user data secret in Cluster API namespace", "name", secrets.UserDataSecret)
264+
return r.client.Create(ctx, capiSecret)
265+
}
266+
return fmt.Errorf("error getting user data secret in Cluster API namespace: %w", err)
267+
}
268+
if string(existing.Data["userData"]) == string(capiSecret.Data["userData"]) {
269+
// secrets match, nothing to do.
270+
return nil
213271
}
272+
r.log.Info("updating user data secret in Cluster API namespace", "name", secrets.UserDataSecret)
273+
capiSecret.ResourceVersion = existing.ResourceVersion
274+
return r.client.Update(ctx, capiSecret)
214275
}
215276

216277
func (r *SecretReconciler) reconcileTLSSecret(ctx context.Context) error {
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package controllers
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
core "k8s.io/api/core/v1"
10+
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
"k8s.io/apimachinery/pkg/runtime"
12+
ctrl "sigs.k8s.io/controller-runtime"
13+
"sigs.k8s.io/controller-runtime/pkg/client"
14+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
15+
16+
"github.com/openshift/windows-machine-config-operator/pkg/cluster"
17+
"github.com/openshift/windows-machine-config-operator/pkg/secrets"
18+
)
19+
20+
// newTestSecretReconciler returns a SecretReconciler backed by a fake client pre-seeded with initObjs.
21+
func newTestSecretReconciler(initObjs ...client.Object) *SecretReconciler {
22+
scheme := runtime.NewScheme()
23+
_ = core.AddToScheme(scheme)
24+
25+
fc := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initObjs...).Build()
26+
return &SecretReconciler{
27+
instanceReconciler: instanceReconciler{
28+
client: fc,
29+
log: ctrl.Log.WithName("test"),
30+
},
31+
}
32+
}
33+
34+
// newUserDataSecret creates a minimal windows-user-data Secret in the given namespace.
35+
func newUserDataSecret(namespace, userData string) *core.Secret {
36+
return &core.Secret{
37+
ObjectMeta: meta.ObjectMeta{
38+
Name: secrets.UserDataSecret,
39+
Namespace: namespace,
40+
},
41+
Data: map[string][]byte{
42+
"userData": []byte(userData),
43+
},
44+
}
45+
}
46+
47+
// TestIsUserDataSecret verifies that isUserDataSecret matches secrets in both the Machine API and
48+
// Cluster API namespaces, but not in unrelated namespaces.
49+
func TestIsUserDataSecret(t *testing.T) {
50+
tests := []struct {
51+
name string
52+
ns string
53+
expected bool
54+
}{
55+
{
56+
name: "Machine API namespace matches",
57+
ns: cluster.MachineAPINamespace,
58+
expected: true,
59+
},
60+
{
61+
name: "Cluster API namespace matches",
62+
ns: cluster.ClusterAPINamespace,
63+
expected: true,
64+
},
65+
{
66+
name: "Unrelated namespace does not match",
67+
ns: "kube-system",
68+
expected: false,
69+
},
70+
}
71+
for _, tt := range tests {
72+
t.Run(tt.name, func(t *testing.T) {
73+
s := newUserDataSecret(tt.ns, "data")
74+
assert.Equal(t, tt.expected, isUserDataSecret(s))
75+
})
76+
}
77+
}
78+
79+
// TestIsClusterAPIEnabled verifies that isClusterAPIEnabled returns true only when the
80+
// openshift-cluster-api namespace exists.
81+
func TestIsClusterAPIEnabled(t *testing.T) {
82+
ctx := context.Background()
83+
84+
t.Run("Namespace absent returns false", func(t *testing.T) {
85+
r := newTestSecretReconciler()
86+
enabled, err := r.isClusterAPIEnabled(ctx)
87+
require.NoError(t, err)
88+
assert.False(t, enabled)
89+
})
90+
91+
t.Run("Namespace present returns true", func(t *testing.T) {
92+
ns := &core.Namespace{ObjectMeta: meta.ObjectMeta{Name: cluster.ClusterAPINamespace}}
93+
r := newTestSecretReconciler(ns)
94+
enabled, err := r.isClusterAPIEnabled(ctx)
95+
require.NoError(t, err)
96+
assert.True(t, enabled)
97+
})
98+
}
99+
100+
// TestEnsureCAPIUserDataSecret verifies the create / no-op / update behaviour of ensureCAPIUserDataSecret.
101+
func TestEnsureCAPIUserDataSecret(t *testing.T) {
102+
ctx := context.Background()
103+
const content = "<powershell>ssh-rsa AAAA...</powershell>"
104+
105+
t.Run("Creates secret when absent in CAPI namespace", func(t *testing.T) {
106+
r := newTestSecretReconciler()
107+
src := newUserDataSecret(cluster.MachineAPINamespace, content)
108+
109+
require.NoError(t, r.ensureCAPIUserDataSecret(ctx, src))
110+
111+
got := &core.Secret{}
112+
require.NoError(t, r.client.Get(ctx,
113+
client.ObjectKey{Name: secrets.UserDataSecret, Namespace: cluster.ClusterAPINamespace}, got))
114+
assert.Equal(t, content, string(got.Data["userData"]))
115+
assert.Equal(t, cluster.ClusterAPINamespace, got.Namespace)
116+
})
117+
118+
t.Run("No-op when CAPI secret already matches", func(t *testing.T) {
119+
existing := newUserDataSecret(cluster.ClusterAPINamespace, content)
120+
existing.ResourceVersion = "42"
121+
r := newTestSecretReconciler(existing)
122+
src := newUserDataSecret(cluster.MachineAPINamespace, content)
123+
124+
require.NoError(t, r.ensureCAPIUserDataSecret(ctx, src))
125+
126+
got := &core.Secret{}
127+
require.NoError(t, r.client.Get(ctx,
128+
client.ObjectKey{Name: secrets.UserDataSecret, Namespace: cluster.ClusterAPINamespace}, got))
129+
// ResourceVersion must be unchanged (no update was issued).
130+
assert.Equal(t, "42", got.ResourceVersion)
131+
})
132+
133+
t.Run("Updates CAPI secret when content differs", func(t *testing.T) {
134+
existing := newUserDataSecret(cluster.ClusterAPINamespace, "old-data")
135+
existing.ResourceVersion = "1"
136+
r := newTestSecretReconciler(existing)
137+
src := newUserDataSecret(cluster.MachineAPINamespace, content)
138+
139+
require.NoError(t, r.ensureCAPIUserDataSecret(ctx, src))
140+
141+
got := &core.Secret{}
142+
require.NoError(t, r.client.Get(ctx,
143+
client.ObjectKey{Name: secrets.UserDataSecret, Namespace: cluster.ClusterAPINamespace}, got))
144+
assert.Equal(t, content, string(got.Data["userData"]))
145+
})
146+
}

pkg/cluster/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ const (
2727
baseK8sVersion = "v1.35"
2828
// MachineAPINamespace is the name of the namespace in which machine objects and userData secret is created.
2929
MachineAPINamespace = "openshift-machine-api"
30+
// ClusterAPINamespace is the namespace used by the Cluster API components when the
31+
// ClusterAPIMachineManagement feature gate is enabled.
32+
ClusterAPINamespace = "openshift-cluster-api"
3033
)
3134

3235
var (

0 commit comments

Comments
 (0)