Skip to content

Commit e1659db

Browse files
committed
RuntimeSDK: Add caBundle injection to Extension controller
Co-authored-by: Stefan Büringer [email protected]
1 parent 8a6b4c6 commit e1659db

File tree

5 files changed

+260
-17
lines changed

5 files changed

+260
-17
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+
// CABundleInjectFromSecretAnnotation 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+
CABundleInjectFromSecretAnnotation 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

4052
// Reconciler reconciles an ExtensionConfig object.
@@ -49,6 +61,11 @@ type Reconciler struct {
4961
func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
5062
err := ctrl.NewControllerManagedBy(mgr).
5163
For(&runtimev1.ExtensionConfig{}).
64+
Watches(
65+
&source.Kind{Type: &corev1.Secret{}},
66+
handler.EnqueueRequestsFromMapFunc(r.secretToExtensionConfig),
67+
builder.OnlyMetadata,
68+
).
5269
WithOptions(options).
5370
WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(ctrl.LoggerFrom(ctx), r.WatchFilterValue)).
5471
Complete(r)
@@ -102,14 +119,22 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
102119
return r.reconcileDelete(ctx, extensionConfig)
103120
}
104121

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

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

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

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

exp/runtime/internal/controllers/extensionconfig_controller_test.go

Lines changed: 132 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.CABundleInjectFromSecretAnnotation] = 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{
@@ -290,8 +306,9 @@ func fakeExtensionConfigForURL(namespace, name, url string) *runtimev1.Extension
290306
APIVersion: runtimehooksv1.GroupVersion.String(),
291307
},
292308
ObjectMeta: metav1.ObjectMeta{
293-
Name: name,
294-
Namespace: namespace,
309+
Name: name,
310+
Namespace: namespace,
311+
Annotations: map[string]string{},
295312
},
296313
Spec: runtimev1.ExtensionConfigSpec{
297314
ClientConfig: runtimev1.ClientConfig{
@@ -302,9 +319,114 @@ func fakeExtensionConfigForURL(namespace, name, url string) *runtimev1.Extension
302319
}
303320
}
304321

305-
func fakeExtensionServer(discoveryHandler func(w http.ResponseWriter, r *http.Request)) *httptest.Server {
322+
func fakeSecureExtensionServer(discoveryHandler func(w http.ResponseWriter, r *http.Request)) (*httptest.Server, error) {
306323
mux := http.NewServeMux()
307324
mux.HandleFunc("/", discoveryHandler)
308-
srv := httptest.NewServer(mux)
309-
return srv
325+
326+
sCert, err := tls.X509KeyPair(testcerts.ServerCert, testcerts.ServerKey)
327+
if err != nil {
328+
return nil, err
329+
}
330+
testServer := httptest.NewUnstartedServer(mux)
331+
testServer.TLS = &tls.Config{
332+
Certificates: []tls.Certificate{sCert},
333+
}
334+
testServer.StartTLS()
335+
336+
return testServer, nil
337+
}
338+
339+
func fakeCASecret(namespace, name string, caData []byte) *corev1.Secret {
340+
secret := &corev1.Secret{
341+
ObjectMeta: metav1.ObjectMeta{
342+
Name: name,
343+
Namespace: namespace,
344+
},
345+
Data: map[string][]byte{},
346+
}
347+
if caData != nil {
348+
secret.Data["ca.crt"] = caData
349+
}
350+
return secret
351+
}
352+
353+
func Test_reconcileCABundle(t *testing.T) {
354+
g := NewWithT(t)
355+
356+
scheme := runtime.NewScheme()
357+
g.Expect(corev1.AddToScheme(scheme)).To(Succeed())
358+
359+
tests := []struct {
360+
name string
361+
client client.Client
362+
config *runtimev1.ExtensionConfig
363+
wantCABundle []byte
364+
wantErr bool
365+
}{
366+
{
367+
name: "No-op because no annotation is set",
368+
client: fake.NewClientBuilder().WithScheme(scheme).Build(),
369+
config: fakeCAInjectionRuntimeExtensionConfig("some-namespace", "some-extension-config", "", ""),
370+
wantErr: false,
371+
},
372+
{
373+
name: "Inject ca-bundle",
374+
client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(
375+
fakeCASecret("some-namespace", "some-ca-secret", []byte("some-ca-data")),
376+
).Build(),
377+
config: fakeCAInjectionRuntimeExtensionConfig("some-namespace", "some-extension-config", "some-namespace/some-ca-secret", ""),
378+
wantCABundle: []byte(`some-ca-data`),
379+
wantErr: false,
380+
},
381+
{
382+
name: "Update ca-bundle",
383+
client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(
384+
fakeCASecret("some-namespace", "some-ca-secret", []byte("some-new-data")),
385+
).Build(),
386+
config: fakeCAInjectionRuntimeExtensionConfig("some-namespace", "some-extension-config", "some-namespace/some-ca-secret", "some-old-ca-data"),
387+
wantCABundle: []byte(`some-new-data`),
388+
wantErr: false,
389+
},
390+
{
391+
name: "Fail because secret does not exist",
392+
client: fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build(),
393+
config: fakeCAInjectionRuntimeExtensionConfig("some-namespace", "some-extension-config", "some-namespace/some-ca-secret", ""),
394+
wantErr: true,
395+
},
396+
{
397+
name: "Fail because secret does not contain a ca.crt",
398+
client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(
399+
fakeCASecret("some-namespace", "some-ca-secret", nil),
400+
).Build(),
401+
config: fakeCAInjectionRuntimeExtensionConfig("some-namespace", "some-extension-config", "some-namespace/some-ca-secret", ""),
402+
wantErr: true,
403+
},
404+
}
405+
for _, tt := range tests {
406+
t.Run(tt.name, func(t *testing.T) {
407+
g := NewWithT(t)
408+
409+
err := reconcileCABundle(context.TODO(), tt.client, tt.config)
410+
g.Expect(err != nil).To(Equal(tt.wantErr))
411+
412+
g.Expect(tt.config.Spec.ClientConfig.CABundle).To(Equal(tt.wantCABundle))
413+
})
414+
}
415+
}
416+
417+
func fakeCAInjectionRuntimeExtensionConfig(namespace, name, annotationString, caBundleData string) *runtimev1.ExtensionConfig {
418+
ext := &runtimev1.ExtensionConfig{
419+
ObjectMeta: metav1.ObjectMeta{
420+
Name: name,
421+
Namespace: namespace,
422+
Annotations: map[string]string{},
423+
},
424+
}
425+
if annotationString != "" {
426+
ext.Annotations[runtimev1.CABundleInjectFromSecretAnnotation] = annotationString
427+
}
428+
if caBundleData != "" {
429+
ext.Spec.ClientConfig.CABundle = []byte(caBundleData)
430+
}
431+
return ext
310432
}

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)