Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(konnect): support Secrets in KonnectAPIAuthConfiguration #459

Merged
merged 5 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -513,9 +513,18 @@ install: manifests kustomize install-gateway-api-crds
$(KUSTOMIZE) build $(KIC_CRDS_URL) | kubectl apply -f -
$(KUSTOMIZE) build config/crd | kubectl apply --server-side -f -

KUBERNETES_CONFIGURATION_CRDS_PACKAGE ?= github.com/kong/kubernetes-configuration
KUBERNETES_CONFIGURATION_CRDS_VERSION ?= $(shell go list -m -f '{{ .Version }}' $(KUBERNETES_CONFIGURATION_CRDS_PACKAGE))
KUBERNETES_CONFIGURATION_CRDS_CRDS_LOCAL_PATH = $(shell go env GOPATH)/pkg/mod/$(KUBERNETES_CONFIGURATION_CRDS_PACKAGE)@$(KUBERNETES_CONFIGURATION_CRDS_VERSION)/config/crd

# Install kubernetes-configuration CRDs into the K8s cluster specified in ~/.kube/config.
.PHONY: install.kubernetes-configuration-crds
install.kubernetes-configuration-crds: kustomize
$(KUSTOMIZE) build $(KUBERNETES_CONFIGURATION_CRDS_CRDS_LOCAL_PATH) | kubectl apply -f -

# Install standard and experimental CRDs into the K8s cluster specified in ~/.kube/config.
.PHONY: install.all
install.all: manifests kustomize install-gateway-api-crds
install.all: manifests kustomize install-gateway-api-crds install.kubernetes-configuration-crds
$(KUSTOMIZE) build $(KIC_CRDS_URL) | kubectl apply -f -
kubectl apply --server-side -f $(PROJECT_DIR)/config/crd/bases/
kubectl get crd -ojsonpath='{.items[*].metadata.name}' | xargs -n1 kubectl wait --for condition=established crd
Expand All @@ -527,10 +536,14 @@ uninstall: manifests kustomize uninstall-gateway-api-crds
$(KUSTOMIZE) build $(KIC_CRDS_URL) | kubectl delete --ignore-not-found=$(ignore-not-found) -f -
$(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f -

.PHONY: uninstall.kubernetes-configuration-crds
uninstall.kubernetes-configuration-crds: kustomize
$(KUSTOMIZE) build $(KUBERNETES_CONFIGURATION_CRDS_CRDS_LOCAL_PATH) | kubectl delete -f -

# Uninstall standard and experimental CRDs from the K8s cluster specified in ~/.kube/config.
# Call with ignore-not-found=true to ignore resource not found errors during deletion.
.PHONY: uninstall.all
uninstall.all: manifests kustomize uninstall-gateway-api-crds
uninstall.all: manifests kustomize uninstall-gateway-api-crds uninstall.kubernetes-configuration-crds
$(KUSTOMIZE) build $(KIC_CRDS_URL) | kubectl apply -f -
kubectl delete --ignore-not-found=$(ignore-not-found) -f $(PROJECT_DIR)/config/crd/bases/

Expand Down
34 changes: 34 additions & 0 deletions config/samples/konnect_apiauth_configuration.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
kind: KonnectAPIAuthConfiguration
apiVersion: konnect.konghq.com/v1alpha1
metadata:
name: konnect-api-auth-1
namespace: default
spec:
type: token
token: kpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# For complete list of available API URLs see: https://docs.konghq.com/konnect/network/
serverURL: eu.api.konghq.com
randmonkey marked this conversation as resolved.
Show resolved Hide resolved
pmalek marked this conversation as resolved.
Show resolved Hide resolved
---
kind: KonnectAPIAuthConfiguration
apiVersion: konnect.konghq.com/v1alpha1
metadata:
name: konnect-api-auth-2
namespace: default
spec:
type: secretRef
secretRef:
name: konnect-api-auth-secret
# For complete list of available API URLs see: https://docs.konghq.com/konnect/network/
serverURL: eu.api.konghq.com
pmalek marked this conversation as resolved.
Show resolved Hide resolved
---
kind: Secret
apiVersion: v1
metadata:
name: konnect-api-auth-secret
namespace: default
labels:
# NOTE: this label is required on Konnect credential secrets to make
# Secret watch efficient in the operator.
konghq.com/credential: konnect
randmonkey marked this conversation as resolved.
Show resolved Hide resolved
pmalek marked this conversation as resolved.
Show resolved Hide resolved
stringData:
token: kpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
108 changes: 105 additions & 3 deletions controller/konnect/reconciler_konnectapiauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ import (
"time"

sdkkonnectgoops "github.com/Kong/sdk-konnect-go/models/operations"
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"
Expand All @@ -24,6 +29,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" //nolint:gosec
randmonkey marked this conversation as resolved.
Show resolved Hide resolved
// 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,
Expand All @@ -39,8 +55,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,
},
pmalek marked this conversation as resolved.
Show resolved Hide resolved
},
)
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)
Expand Down Expand Up @@ -81,9 +115,30 @@ func (r *KonnectAPIAuthConfigurationReconciler) Reconcile(
return ctrl.Result{}, nil
}

token, err := getTokenFromKonnectAPIAuthConfiguration(ctx, r.Client, &apiAuth)
if err != nil {
k8sutils.SetCondition(
k8sutils.NewConditionWithGeneration(
KonnectEntityAPIAuthConfigurationValidConditionType,
metav1.ConditionFalse,
KonnectEntityAPIAuthConfigurationReasonInvalid,
err.Error(),
apiAuth.GetGeneration(),
),
&apiAuth,
)
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
Expand All @@ -101,6 +156,7 @@ func (r *KonnectAPIAuthConfigurationReconciler) Reconcile(
cond.Status != metav1.ConditionFalse ||
cond.Reason != KonnectEntityAPIAuthConfigurationReasonInvalid ||
cond.ObservedGeneration != apiAuth.GetGeneration() ||
cond.Message != err.Error() ||
apiAuth.Status.OrganizationID != "" ||
apiAuth.Status.ServerURL != apiAuth.Spec.ServerURL {

Expand All @@ -123,9 +179,20 @@ func (r *KonnectAPIAuthConfigurationReconciler) Reconcile(
}

// Update the status only if it would change to prevent unnecessary updates.
condMessage := "Token is valid"
if apiAuth.Spec.Type == konnectv1alpha1.KonnectAPIAuthTypeSecretRef {
nn := types.NamespacedName{
Namespace: apiAuth.Spec.SecretRef.Namespace,
Name: apiAuth.Spec.SecretRef.Name,
}
if nn.Namespace == "" {
nn.Namespace = apiAuth.Namespace
}
condMessage = fmt.Sprintf("Token from Secret %s is valid", nn)
}
if cond, ok := k8sutils.GetCondition(KonnectEntityAPIAuthConfigurationValidConditionType, &apiAuth); !ok ||
cond.Status != metav1.ConditionTrue ||
cond.Message != "" ||
cond.Message != condMessage ||
cond.Reason != KonnectEntityAPIAuthConfigurationReasonValid ||
cond.ObservedGeneration != apiAuth.GetGeneration() ||
apiAuth.Status.OrganizationID != *respOrg.MeOrganization.ID ||
Expand All @@ -139,7 +206,7 @@ func (r *KonnectAPIAuthConfigurationReconciler) Reconcile(
KonnectEntityAPIAuthConfigurationValidConditionType,
metav1.ConditionTrue,
KonnectEntityAPIAuthConfigurationReasonValid,
fmt.Sprintf("Referenced KonnectAPIAuthConfiguration %s is valid", client.ObjectKeyFromObject(&apiAuth)),
condMessage,
)
if err != nil || res.Requeue {
return res, err
Expand All @@ -149,3 +216,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:
nn := types.NamespacedName{
Namespace: apiAuth.Spec.SecretRef.Namespace,
Name: apiAuth.Spec.SecretRef.Name,
}
if nn.Namespace == "" {
nn.Namespace = apiAuth.Namespace
}

var secret corev1.Secret
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)
}
2 changes: 2 additions & 0 deletions controller/konnect/reconciler_konnectapiauth_rbac.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ package konnect

//+kubebuilder:rbac:groups=konnect.konghq.com,resources=konnectapiauthconfigurations,verbs=get;list;watch;update;patch
//+kubebuilder:rbac:groups=konnect.konghq.com,resources=konnectapiauthconfigurations/status,verbs=get;update;patch

//+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch
Loading
Loading