diff --git a/config/samples/konnect_apiauth_configuration.yaml b/config/samples/konnect_apiauth_configuration.yaml new file mode 100644 index 000000000..a2bdbff9e --- /dev/null +++ b/config/samples/konnect_apiauth_configuration.yaml @@ -0,0 +1,30 @@ +kind: KonnectAPIAuthConfiguration +apiVersion: konnect.konghq.com/v1alpha1 +metadata: + name: konnect-api-auth-1 + namespace: default +spec: + type: token + token: kpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + serverURL: eu.api.konghq.com +--- +kind: KonnectAPIAuthConfiguration +apiVersion: konnect.konghq.com/v1alpha1 +metadata: + name: konnect-api-auth-2 + namespace: default +spec: + type: secretRef + secretRef: + name: konnect-api-auth-secret + serverURL: eu.api.konghq.com +--- +kind: Secret +apiVersion: v1 +metadata: + name: konnect-api-auth-secret + namespace: default + labels: + konghq.com/credential: konnect +stringData: + token: kpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx diff --git a/controller/konnect/reconciler_konnectapiauth.go b/controller/konnect/reconciler_konnectapiauth.go index 2c3d32c97..2d9b011a6 100644 --- a/controller/konnect/reconciler_konnectapiauth.go +++ b/controller/konnect/reconciler_konnectapiauth.go @@ -7,10 +7,15 @@ import ( sdkkonnectgoops "github.com/Kong/sdk-konnect-go/models/operations" konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" + corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" "github.com/kong/gateway-operator/controller/pkg/log" k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" @@ -23,6 +28,17 @@ type KonnectAPIAuthConfigurationReconciler struct { Client client.Client } +const ( + // SecretTokenKey is the key used to store the token in the Secret. + SecretTokenKey = "token" + // SecretCredentialLabel is the label used to identify Secrets holding + // KonnectAPIAuthConfiguration tokens. + SecretCredentialLabel = "konghq.com/credential" + // SecretCredentialLabelValueKonnect is the value of the label used to + // identify Secrets holding KonnectAPIAuthConfiguration tokens. + SecretCredentialLabelValueKonnect = "konnect" +) + // NewKonnectAPIAuthConfigurationReconciler creates a new KonnectAPIAuthConfigurationReconciler. func NewKonnectAPIAuthConfigurationReconciler( sdkFactory SDKFactory, @@ -38,8 +54,26 @@ func NewKonnectAPIAuthConfigurationReconciler( // SetupWithManager sets up the controller with the Manager. func (r *KonnectAPIAuthConfigurationReconciler) SetupWithManager(mgr ctrl.Manager) error { + secretLabelPredicate, err := predicate.LabelSelectorPredicate( + metav1.LabelSelector{ + MatchLabels: map[string]string{ + SecretCredentialLabel: SecretCredentialLabelValueKonnect, + }, + }, + ) + if err != nil { + return fmt.Errorf("failed to create Secret label selector predicate: %w", err) + } + b := ctrl.NewControllerManagedBy(mgr). For(&konnectv1alpha1.KonnectAPIAuthConfiguration{}). + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc( + listKonnectAPIAuthConfigurationsReferencingSecret(mgr.GetClient()), + ), + builder.WithPredicates(secretLabelPredicate), + ). Named("KonnectAPIAuthConfiguration") return b.Complete(r) @@ -80,9 +114,30 @@ func (r *KonnectAPIAuthConfigurationReconciler) Reconcile( return ctrl.Result{}, nil } + token, err := getTokenFromKonnectAPIAuthConfiguration(ctx, r.Client, &apiAuth) + if err != nil { + k8sutils.SetCondition( + k8sutils.NewConditionWithGeneration( + KonnectAPIAuthConfigurationValidConditionType, + metav1.ConditionFalse, + KonnectAPIAuthConfigurationReasonInvalid, + err.Error(), + apiAuth.GetGeneration(), + ), + &apiAuth.Status, + ) + if err := r.Client.Status().Update(ctx, &apiAuth); err != nil { + if k8serrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to update status of %s: %w", entityTypeName, err) + } + return ctrl.Result{}, err + } + sdk := r.SDKFactory.NewKonnectSDK( "https://"+apiAuth.Spec.ServerURL, - SDKToken(apiAuth.Spec.Token), + SDKToken(token), ) // TODO(pmalek): check if api auth config has a valid status condition @@ -157,3 +212,38 @@ func (r *KonnectAPIAuthConfigurationReconciler) Reconcile( return ctrl.Result{}, nil } + +// getTokenFromKonnectAPIAuthConfiguration returns the token from the secret reference or the token field. +func getTokenFromKonnectAPIAuthConfiguration( + ctx context.Context, cl client.Client, apiAuth *konnectv1alpha1.KonnectAPIAuthConfiguration, +) (string, error) { + switch apiAuth.Spec.Type { + case konnectv1alpha1.KonnectAPIAuthTypeToken: + return apiAuth.Spec.Token, nil + case konnectv1alpha1.KonnectAPIAuthTypeSecretRef: + var secret corev1.Secret + nn := types.NamespacedName{ + Namespace: apiAuth.Spec.SecretRef.Namespace, + Name: apiAuth.Spec.SecretRef.Name, + } + if nn.Namespace == "" { + nn.Namespace = apiAuth.Namespace + } + + if err := cl.Get(ctx, nn, &secret); err != nil { + return "", fmt.Errorf("failed to get Secret %s: %w", nn, err) + } + if secret.Labels == nil || secret.Labels[SecretCredentialLabel] != SecretCredentialLabelValueKonnect { + return "", fmt.Errorf("Secret %s does not have label %s: %s", nn, SecretCredentialLabel, SecretCredentialLabelValueKonnect) + } + if secret.Data == nil { + return "", fmt.Errorf("Secret %s has no data", nn) + } + if _, ok := secret.Data[SecretTokenKey]; !ok { + return "", fmt.Errorf("Secret %s does not have key %s", nn, SecretTokenKey) + } + return string(secret.Data[SecretTokenKey]), nil + } + + return "", fmt.Errorf("unknown KonnectAPIAuthType: %s", apiAuth.Spec.Type) +} diff --git a/controller/konnect/reconciler_konnectapiauth_test.go b/controller/konnect/reconciler_konnectapiauth_test.go new file mode 100644 index 000000000..193d6d9dd --- /dev/null +++ b/controller/konnect/reconciler_konnectapiauth_test.go @@ -0,0 +1,161 @@ +package konnect + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" +) + +func TestGetTokenFromKonnectAPIAuthConfiguration(t *testing.T) { + tests := []struct { + name string + apiAuth *konnectv1alpha1.KonnectAPIAuthConfiguration + secret *corev1.Secret + expectedToken string + expectedError bool + }{ + { + name: "valid Token", + apiAuth: &konnectv1alpha1.KonnectAPIAuthConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-api-auth", + Namespace: "default", + }, + Spec: konnectv1alpha1.KonnectAPIAuthConfigurationSpec{ + Type: konnectv1alpha1.KonnectAPIAuthTypeToken, + Token: "kpat_xxxxxxxxxxxx", + }, + }, + expectedToken: "kpat_xxxxxxxxxxxx", + }, + { + name: "valid Secret Reference", + apiAuth: &konnectv1alpha1.KonnectAPIAuthConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-api-auth", + Namespace: "default", + }, + Spec: konnectv1alpha1.KonnectAPIAuthConfigurationSpec{ + Type: konnectv1alpha1.KonnectAPIAuthTypeSecretRef, + SecretRef: &corev1.SecretReference{ + Name: "test-secret", + Namespace: "default", + }, + }, + }, + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "default", + Labels: map[string]string{ + "konghq.com/credential": "konnect", + }, + }, + Data: map[string][]byte{ + "token": []byte("test-token"), + }, + }, + expectedToken: "test-token", + }, + { + name: "Secret is missing konghq.com/credential=konnect label", + apiAuth: &konnectv1alpha1.KonnectAPIAuthConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-api-auth", + Namespace: "default", + }, + Spec: konnectv1alpha1.KonnectAPIAuthConfigurationSpec{ + Type: konnectv1alpha1.KonnectAPIAuthTypeSecretRef, + SecretRef: &corev1.SecretReference{ + Name: "test-secret", + Namespace: "default", + }, + }, + }, + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "default", + }, + Data: map[string][]byte{ + "token": []byte("test-token"), + }, + }, + expectedError: true, + }, + { + name: "missing token from referred Secret", + apiAuth: &konnectv1alpha1.KonnectAPIAuthConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-api-auth", + Namespace: "default", + }, + Spec: konnectv1alpha1.KonnectAPIAuthConfigurationSpec{ + Type: konnectv1alpha1.KonnectAPIAuthTypeSecretRef, + SecretRef: &corev1.SecretReference{ + Name: "test-secret", + Namespace: "default", + }, + }, + }, + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "default", + Labels: map[string]string{ + "konghq.com/credential": "konnect", + }, + }, + Data: map[string][]byte{ + "random_key": []byte("dummy"), + }, + }, + expectedToken: "test-token", + expectedError: true, + }, + { + name: "Invalid Secret Reference", + apiAuth: &konnectv1alpha1.KonnectAPIAuthConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-api-auth", + Namespace: "default", + }, + Spec: konnectv1alpha1.KonnectAPIAuthConfigurationSpec{ + Type: konnectv1alpha1.KonnectAPIAuthTypeSecretRef, + SecretRef: &corev1.SecretReference{ + Name: "non-existent-secret", + Namespace: "default", + }, + }, + }, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + clientBuilder := fake.NewClientBuilder() + + // Create the secret in the fake client + if tt.secret != nil { + clientBuilder.WithObjects(tt.secret) + } + cl := clientBuilder.Build() + + // Call the function under test + token, err := getTokenFromKonnectAPIAuthConfiguration(context.Background(), cl, tt.apiAuth) + if tt.expectedError { + assert.NotNil(t, err) + return + } + + assert.Equal(t, tt.expectedToken, token) + }) + } +} diff --git a/controller/konnect/reconciler_konnectapiauth_watch.go b/controller/konnect/reconciler_konnectapiauth_watch.go new file mode 100644 index 000000000..7a1c87ccd --- /dev/null +++ b/controller/konnect/reconciler_konnectapiauth_watch.go @@ -0,0 +1,68 @@ +package konnect + +import ( + "context" + "fmt" + "reflect" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" + + operatorerrors "github.com/kong/gateway-operator/internal/errors" +) + +func listKonnectAPIAuthConfigurationsReferencingSecret(cl client.Client) func(ctx context.Context, obj client.Object) []reconcile.Request { + return func(ctx context.Context, obj client.Object) []reconcile.Request { + logger := log.FromContext(ctx) + + secret, ok := obj.(*corev1.Secret) + if !ok { + logger.Error( + operatorerrors.ErrUnexpectedObject, + "failed to run map funcs", + "expected", "Secret", "found", reflect.TypeOf(obj), + ) + return nil + } + + var konnectAPIAuthConfigList konnectv1alpha1.KonnectAPIAuthConfigurationList + if err := cl.List(ctx, &konnectAPIAuthConfigList); err != nil { + log.FromContext(ctx).Error( + fmt.Errorf("unexpected error occurred while listing KonnectAPIAuthConfiguration resources"), + "failed to run map funcs", + "error", err.Error(), + ) + return nil + } + + var recs []reconcile.Request + for _, apiAuth := range konnectAPIAuthConfigList.Items { + if apiAuth.Spec.Type != konnectv1alpha1.KonnectAPIAuthTypeSecretRef { + continue + } + + if apiAuth.Spec.SecretRef == nil || + apiAuth.Spec.SecretRef.Name != secret.Name { + continue + } + + if (apiAuth.Spec.SecretRef.Namespace != "" && apiAuth.Spec.SecretRef.Namespace != secret.Namespace) || + (apiAuth.Spec.SecretRef.Namespace == "" && secret.Namespace != apiAuth.Namespace) { + continue + } + + recs = append(recs, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: apiAuth.Namespace, + Name: apiAuth.Name, + }, + }) + } + return recs + } +} diff --git a/go.mod b/go.mod index 4edca9d1f..5fbc55c42 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,8 @@ toolchain go1.22.5 // This retraction is to prevent it from being used and from breaking builds of dependent projects. retract v1.2.2 +replace github.com/kong/kubernetes-configuration => ../kubernetes-configuration + require ( github.com/Kong/sdk-konnect-go v0.0.0-20240723160412-999d9a987e1a github.com/Masterminds/semver v1.5.0 diff --git a/modules/manager/scheme/scheme.go b/modules/manager/scheme/scheme.go index dc6d0a2ce..81d5e5c64 100644 --- a/modules/manager/scheme/scheme.go +++ b/modules/manager/scheme/scheme.go @@ -1,7 +1,6 @@ package scheme import ( - configurationv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -10,16 +9,30 @@ import ( operatorv1alpha1 "github.com/kong/gateway-operator/api/v1alpha1" operatorv1beta1 "github.com/kong/gateway-operator/api/v1beta1" + + configurationv1 "github.com/kong/kubernetes-configuration/api/configuration/v1" + configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" + configurationv1beta1 "github.com/kong/kubernetes-configuration/api/configuration/v1beta1" + + konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" ) // Get returns a scheme aware of all types the manager can interact with. func Get() *runtime.Scheme { scheme := runtime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(operatorv1alpha1.AddToScheme(scheme)) utilruntime.Must(operatorv1beta1.AddToScheme(scheme)) + utilruntime.Must(gatewayv1.Install(scheme)) utilruntime.Must(gatewayv1beta1.Install(scheme)) + utilruntime.Must(configurationv1.AddToScheme(scheme)) + utilruntime.Must(configurationv1alpha1.AddToScheme(scheme)) + utilruntime.Must(configurationv1beta1.AddToScheme(scheme)) + + utilruntime.Must(konnectv1alpha1.AddToScheme(scheme)) + return scheme }