Skip to content

Commit 9bb071e

Browse files
authored
Merge pull request #6632 from chrischdi/pr-topology-mutation-external
✨ RuntimeSDK: Add caBundle injection to Extension controller
2 parents 67ff129 + 2706248 commit 9bb071e

File tree

6 files changed

+262
-18
lines changed

6 files changed

+262
-18
lines changed

exp/runtime/api/v1alpha1/extensionconfig_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,9 @@ const (
202202

203203
// DiscoveryFailedReason documents failure of a Discovery call.
204204
DiscoveryFailedReason string = "DiscoveryFailed"
205+
206+
// InjectCAFromSecretAnnotation is the annotation that specifies that a particular
207+
// object wants injection of CAs. It takes the form of a reference to a Secret
208+
// as namespace/name.
209+
InjectCAFromSecretAnnotation string = "runtime.cluster.x-k8s.io/inject-ca-from-secret"
205210
)

exp/runtime/internal/controllers/extensionconfig_controller.go

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,20 @@ package controllers
1818

1919
import (
2020
"context"
21+
"strings"
2122

2223
"github.com/pkg/errors"
24+
corev1 "k8s.io/api/core/v1"
2325
apierrors "k8s.io/apimachinery/pkg/api/errors"
26+
"k8s.io/apimachinery/pkg/types"
2427
kerrors "k8s.io/apimachinery/pkg/util/errors"
2528
ctrl "sigs.k8s.io/controller-runtime"
29+
"sigs.k8s.io/controller-runtime/pkg/builder"
2630
"sigs.k8s.io/controller-runtime/pkg/client"
2731
"sigs.k8s.io/controller-runtime/pkg/controller"
32+
"sigs.k8s.io/controller-runtime/pkg/handler"
33+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
34+
"sigs.k8s.io/controller-runtime/pkg/source"
2835

2936
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
3037
runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
@@ -35,6 +42,11 @@ import (
3542
"sigs.k8s.io/cluster-api/util/predicates"
3643
)
3744

45+
const (
46+
// tlsCAKey is used as a data key in Secret resources to store a CA certificate.
47+
tlsCAKey = "ca.crt"
48+
)
49+
3850
// +kubebuilder:rbac:groups=runtime.cluster.x-k8s.io,resources=extensionconfigs;extensionconfigs/status,verbs=get;list;watch;patch;update
3951
// +kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch
4052

@@ -50,6 +62,11 @@ type Reconciler struct {
5062
func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
5163
err := ctrl.NewControllerManagedBy(mgr).
5264
For(&runtimev1.ExtensionConfig{}).
65+
Watches(
66+
&source.Kind{Type: &corev1.Secret{}},
67+
handler.EnqueueRequestsFromMapFunc(r.secretToExtensionConfig),
68+
builder.OnlyMetadata,
69+
).
5370
WithOptions(options).
5471
WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(ctrl.LoggerFrom(ctx), r.WatchFilterValue)).
5572
Complete(r)
@@ -103,14 +120,22 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
103120
return r.reconcileDelete(ctx, extensionConfig)
104121
}
105122

123+
// Copy to avoid modifying the original extensionConfig.
124+
original := extensionConfig.DeepCopy()
125+
126+
// Inject CABundle from secret if annotation is set. Otherwise https calls may fail.
127+
if err := reconcileCABundle(ctx, r.Client, extensionConfig); err != nil {
128+
return ctrl.Result{}, err
129+
}
130+
106131
// discoverExtensionConfig will return a discovered ExtensionConfig with the appropriate conditions.
107132
discoveredExtensionConfig, err := discoverExtensionConfig(ctx, r.RuntimeClient, extensionConfig)
108133
if err != nil {
109134
errs = append(errs, err)
110135
}
111136

112-
// Always patch the ExtensionConfig as it may contain updates in conditions.
113-
if err = patchExtensionConfig(ctx, r.Client, extensionConfig, discoveredExtensionConfig); err != nil {
137+
// Always patch the ExtensionConfig as it may contain updates in conditions or clientConfig.caBundle.
138+
if err = patchExtensionConfig(ctx, r.Client, original, discoveredExtensionConfig); err != nil {
114139
errs = append(errs, err)
115140
}
116141

@@ -150,6 +175,29 @@ func (r *Reconciler) reconcileDelete(_ context.Context, extensionConfig *runtime
150175
return ctrl.Result{}, nil
151176
}
152177

178+
// secretToExtensionConfig maps a secret to ExtensionConfigs to reconcile them on updates of the secrets.
179+
func (r *Reconciler) secretToExtensionConfig(secret client.Object) []reconcile.Request {
180+
result := []ctrl.Request{}
181+
182+
extensionConfigs := runtimev1.ExtensionConfigList{}
183+
if err := r.Client.List(context.Background(), &extensionConfigs); err != nil {
184+
return nil
185+
}
186+
187+
for _, ext := range extensionConfigs.Items {
188+
if secretNameRaw, ok := ext.GetAnnotations()[runtimev1.InjectCAFromSecretAnnotation]; ok {
189+
secretName := splitNamespacedName(secretNameRaw)
190+
// append all extensions to the result which refer the object as secret
191+
if secretName.Namespace == secret.GetNamespace() && secretName.Name == secret.GetName() {
192+
name := client.ObjectKey{Namespace: ext.GetNamespace(), Name: ext.GetName()}
193+
result = append(result, ctrl.Request{NamespacedName: name})
194+
}
195+
}
196+
}
197+
198+
return result
199+
}
200+
153201
// discoverExtensionConfig attempts to discover the Handlers for an ExtensionConfig.
154202
// If discovery succeeds it returns the ExtensionConfig with Handlers updated in Status and an updated Condition.
155203
// If discovery fails it returns the ExtensionConfig with no update to Handlers and a Failed Condition.
@@ -164,3 +212,41 @@ func discoverExtensionConfig(ctx context.Context, runtimeClient runtimeclient.Cl
164212
conditions.MarkTrue(discoveredExtension, runtimev1.RuntimeExtensionDiscoveredCondition)
165213
return discoveredExtension, nil
166214
}
215+
216+
// reconcileCABundle reconciles the CA bundle for the ExtensionConfig.
217+
// cert-manager code: pkg/controller/cainjector/sources.go certificateDataSource.
218+
func reconcileCABundle(ctx context.Context, client client.Client, config *runtimev1.ExtensionConfig) error {
219+
secretNameRaw, ok := config.Annotations[runtimev1.InjectCAFromSecretAnnotation]
220+
if !ok {
221+
return nil
222+
}
223+
secretName := splitNamespacedName(secretNameRaw)
224+
225+
if secretName.Namespace == "" || secretName.Name == "" {
226+
return errors.Errorf("secret name %q must be in the form namespace/name", secretNameRaw)
227+
}
228+
229+
var secret corev1.Secret
230+
// Note: this is an expensive API call because secrets are explicitly not cached.
231+
if err := client.Get(ctx, secretName, &secret); err != nil {
232+
return errors.Wrapf(err, "failed to get secret %s", secretNameRaw)
233+
}
234+
235+
caData, hasCAData := secret.Data[tlsCAKey]
236+
if !hasCAData {
237+
return errors.Errorf("secret %s does not contain a %s", secretNameRaw, tlsCAKey)
238+
}
239+
240+
config.Spec.ClientConfig.CABundle = caData
241+
return nil
242+
}
243+
244+
// splitNamespacedName turns the string form of a namespaced name
245+
// (<namespace>/<name>) back into a types.NamespacedName.
246+
func splitNamespacedName(nameStr string) types.NamespacedName {
247+
splitPoint := strings.IndexRune(nameStr, types.Separator)
248+
if splitPoint == -1 {
249+
return types.NamespacedName{Name: nameStr}
250+
}
251+
return types.NamespacedName{Namespace: nameStr[:splitPoint], Name: nameStr[splitPoint+1:]}
252+
}

exp/runtime/internal/controllers/extensionconfig_controller_test.go

Lines changed: 133 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package controllers
1818

1919
import (
20+
"context"
21+
"crypto/tls"
2022
"encoding/json"
2123
"net/http"
2224
"net/http/httptest"
@@ -27,10 +29,13 @@ import (
2729
"github.com/pkg/errors"
2830
corev1 "k8s.io/api/core/v1"
2931
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
"k8s.io/apimachinery/pkg/runtime"
33+
"k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts"
3034
utilfeature "k8s.io/component-base/featuregate/testing"
3135
"k8s.io/utils/pointer"
3236
ctrl "sigs.k8s.io/controller-runtime"
3337
"sigs.k8s.io/controller-runtime/pkg/client"
38+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
3439

3540
runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
3641
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
@@ -67,9 +72,18 @@ func TestExtensionReconciler_Reconcile(t *testing.T) {
6772
RuntimeClient: runtimeClient,
6873
}
6974

70-
server := fakeExtensionServer(discoveryHandler("first", "second", "third"))
71-
extensionConfig := fakeExtensionConfigForURL(ns.Name, "ext1", server.URL)
75+
caCertSecret := fakeCASecret(ns.Name, "ext1-webhook", testcerts.CACert)
76+
server, err := fakeSecureExtensionServer(discoveryHandler("first", "second", "third"))
77+
g.Expect(err).NotTo(HaveOccurred())
7278
defer server.Close()
79+
extensionConfig := fakeExtensionConfigForURL(ns.Name, "ext1", server.URL)
80+
extensionConfig.Annotations[runtimev1.InjectCAFromSecretAnnotation] = caCertSecret.GetNamespace() + "/" + caCertSecret.GetName()
81+
82+
// Create the secret which contains the ca certificate.
83+
g.Expect(env.CreateAndWait(ctx, caCertSecret)).To(Succeed())
84+
defer func() {
85+
g.Expect(env.CleanupAndWait(ctx, caCertSecret)).To(Succeed())
86+
}()
7387
// Create the ExtensionConfig.
7488
g.Expect(env.CreateAndWait(ctx, extensionConfig)).To(Succeed())
7589
defer func() {
@@ -120,7 +134,8 @@ func TestExtensionReconciler_Reconcile(t *testing.T) {
120134

121135
t.Run("Successful reconcile and discovery on Extension update", func(t *testing.T) {
122136
// Start a new ExtensionServer where the second handler is removed.
123-
updatedServer := fakeExtensionServer(discoveryHandler("first", "third"))
137+
updatedServer, err := fakeSecureExtensionServer(discoveryHandler("first", "third"))
138+
g.Expect(err).ToNot(HaveOccurred())
124139
defer updatedServer.Close()
125140
// Close the original server it's no longer serving.
126141
server.Close()
@@ -195,7 +210,8 @@ func TestExtensionReconciler_discoverExtensionConfig(t *testing.T) {
195210
registry := runtimeregistry.New()
196211
g.Expect(runtimehooksv1.AddToCatalog(cat)).To(Succeed())
197212
extensionName := "ext1"
198-
srv1 := fakeExtensionServer(discoveryHandler("first"))
213+
srv1, err := fakeSecureExtensionServer(discoveryHandler("first"))
214+
g.Expect(err).ToNot(HaveOccurred())
199215
defer srv1.Close()
200216

201217
runtimeClient := runtimeclient.New(runtimeclient.Options{
@@ -228,7 +244,7 @@ func TestExtensionReconciler_discoverExtensionConfig(t *testing.T) {
228244
extensionName := "ext1"
229245

230246
// Don't set up a server to run the extensionDiscovery handler.
231-
// srv1 := fakeExtensionServer(discoveryHandler("first"))
247+
// srv1 := fakeSecureExtensionServer(discoveryHandler("first"))
232248
// defer srv1.Close()
233249

234250
runtimeClient := runtimeclient.New(runtimeclient.Options{
@@ -254,6 +270,70 @@ func TestExtensionReconciler_discoverExtensionConfig(t *testing.T) {
254270
})
255271
}
256272

273+
func Test_reconcileCABundle(t *testing.T) {
274+
g := NewWithT(t)
275+
276+
scheme := runtime.NewScheme()
277+
g.Expect(corev1.AddToScheme(scheme)).To(Succeed())
278+
279+
tests := []struct {
280+
name string
281+
client client.Client
282+
config *runtimev1.ExtensionConfig
283+
wantCABundle []byte
284+
wantErr bool
285+
}{
286+
{
287+
name: "No-op because no annotation is set",
288+
client: fake.NewClientBuilder().WithScheme(scheme).Build(),
289+
config: fakeCAInjectionRuntimeExtensionConfig("some-namespace", "some-extension-config", "", ""),
290+
wantErr: false,
291+
},
292+
{
293+
name: "Inject ca-bundle",
294+
client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(
295+
fakeCASecret("some-namespace", "some-ca-secret", []byte("some-ca-data")),
296+
).Build(),
297+
config: fakeCAInjectionRuntimeExtensionConfig("some-namespace", "some-extension-config", "some-namespace/some-ca-secret", ""),
298+
wantCABundle: []byte(`some-ca-data`),
299+
wantErr: false,
300+
},
301+
{
302+
name: "Update ca-bundle",
303+
client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(
304+
fakeCASecret("some-namespace", "some-ca-secret", []byte("some-new-data")),
305+
).Build(),
306+
config: fakeCAInjectionRuntimeExtensionConfig("some-namespace", "some-extension-config", "some-namespace/some-ca-secret", "some-old-ca-data"),
307+
wantCABundle: []byte(`some-new-data`),
308+
wantErr: false,
309+
},
310+
{
311+
name: "Fail because secret does not exist",
312+
client: fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build(),
313+
config: fakeCAInjectionRuntimeExtensionConfig("some-namespace", "some-extension-config", "some-namespace/some-ca-secret", ""),
314+
wantErr: true,
315+
},
316+
{
317+
name: "Fail because secret does not contain a ca.crt",
318+
client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(
319+
fakeCASecret("some-namespace", "some-ca-secret", nil),
320+
).Build(),
321+
config: fakeCAInjectionRuntimeExtensionConfig("some-namespace", "some-extension-config", "some-namespace/some-ca-secret", ""),
322+
wantErr: true,
323+
},
324+
}
325+
for _, tt := range tests {
326+
t.Run(tt.name, func(t *testing.T) {
327+
g := NewWithT(t)
328+
329+
err := reconcileCABundle(context.TODO(), tt.client, tt.config)
330+
g.Expect(err != nil).To(Equal(tt.wantErr))
331+
332+
g.Expect(tt.config.Spec.ClientConfig.CABundle).To(Equal(tt.wantCABundle))
333+
})
334+
}
335+
}
336+
257337
func discoveryHandler(handlerList ...string) func(http.ResponseWriter, *http.Request) {
258338
handlers := []runtimehooksv1.ExtensionHandler{}
259339
for _, name := range handlerList {
@@ -290,8 +370,9 @@ func fakeExtensionConfigForURL(namespace, name, url string) *runtimev1.Extension
290370
APIVersion: runtimehooksv1.GroupVersion.String(),
291371
},
292372
ObjectMeta: metav1.ObjectMeta{
293-
Name: name,
294-
Namespace: namespace,
373+
Name: name,
374+
Namespace: namespace,
375+
Annotations: map[string]string{},
295376
},
296377
Spec: runtimev1.ExtensionConfigSpec{
297378
ClientConfig: runtimev1.ClientConfig{
@@ -302,9 +383,51 @@ func fakeExtensionConfigForURL(namespace, name, url string) *runtimev1.Extension
302383
}
303384
}
304385

305-
func fakeExtensionServer(discoveryHandler func(w http.ResponseWriter, r *http.Request)) *httptest.Server {
386+
func fakeSecureExtensionServer(discoveryHandler func(w http.ResponseWriter, r *http.Request)) (*httptest.Server, error) {
306387
mux := http.NewServeMux()
307388
mux.HandleFunc("/", discoveryHandler)
308-
srv := httptest.NewServer(mux)
309-
return srv
389+
390+
sCert, err := tls.X509KeyPair(testcerts.ServerCert, testcerts.ServerKey)
391+
if err != nil {
392+
return nil, err
393+
}
394+
testServer := httptest.NewUnstartedServer(mux)
395+
testServer.TLS = &tls.Config{
396+
MinVersion: tls.VersionTLS13,
397+
Certificates: []tls.Certificate{sCert},
398+
}
399+
testServer.StartTLS()
400+
401+
return testServer, nil
402+
}
403+
404+
func fakeCASecret(namespace, name string, caData []byte) *corev1.Secret {
405+
secret := &corev1.Secret{
406+
ObjectMeta: metav1.ObjectMeta{
407+
Name: name,
408+
Namespace: namespace,
409+
},
410+
Data: map[string][]byte{},
411+
}
412+
if caData != nil {
413+
secret.Data["ca.crt"] = caData
414+
}
415+
return secret
416+
}
417+
418+
func fakeCAInjectionRuntimeExtensionConfig(namespace, name, annotationString, caBundleData string) *runtimev1.ExtensionConfig {
419+
ext := &runtimev1.ExtensionConfig{
420+
ObjectMeta: metav1.ObjectMeta{
421+
Name: name,
422+
Namespace: namespace,
423+
Annotations: map[string]string{},
424+
},
425+
}
426+
if annotationString != "" {
427+
ext.Annotations[runtimev1.InjectCAFromSecretAnnotation] = annotationString
428+
}
429+
if caBundleData != "" {
430+
ext.Spec.ClientConfig.CABundle = []byte(caBundleData)
431+
}
432+
return ext
310433
}

exp/runtime/internal/controllers/warmup.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ func warmupRegistry(ctx context.Context, client client.Client, reader client.Rea
9898
extensionConfig := &extensionConfigList.Items[i]
9999
original := extensionConfig.DeepCopy()
100100

101+
// Inject CABundle from secret if annotation is set. Otherwise https calls may fail.
102+
if err := reconcileCABundle(ctx, client, extensionConfig); err != nil {
103+
errs = append(errs, err)
104+
}
105+
101106
extensionConfig, err := discoverExtensionConfig(ctx, runtimeClient, extensionConfig)
102107
if err != nil {
103108
errs = append(errs, err)

0 commit comments

Comments
 (0)