diff --git a/filterconfig/filterconfig.go b/filterconfig/filterconfig.go index 8956fe1e8..d22c7dfa2 100644 --- a/filterconfig/filterconfig.go +++ b/filterconfig/filterconfig.go @@ -66,6 +66,8 @@ modelNameHeaderKey: x-ai-eg-model // From Envoy configuration perspective, configuring the header matching based on `x-ai-eg-selected-backend` is enough to route the request to the selected backend. // That is because the matching decision is made by the filter and the selected backend is populated in the header `x-ai-eg-selected-backend`. type Config struct { + // UUID is the unique identifier of the filter configuration assigned by the AI Gateway when the configuration is updated. + UUID string `json:"uuid,omitempty"` // MetadataNamespace is the namespace of the dynamic metadata to be used by the filter. MetadataNamespace string `json:"metadataNamespace"` // LLMRequestCost configures the cost of each LLM-related request. Optional. If this is provided, the filter will populate diff --git a/internal/controller/ai_gateway_route.go b/internal/controller/ai_gateway_route.go index ef5c72f56..a208981a4 100644 --- a/internal/controller/ai_gateway_route.go +++ b/internal/controller/ai_gateway_route.go @@ -22,23 +22,10 @@ import ( ) const ( - managedByLabel = "app.kubernetes.io/managed-by" - expProcConfigFileName = "extproc-config.yaml" - k8sClientIndexBackendToReferencingAIGatewayRoute = "BackendToReferencingAIGatewayRoute" + managedByLabel = "app.kubernetes.io/managed-by" + expProcConfigFileName = "extproc-config.yaml" ) -func aiGatewayRouteIndexFunc(o client.Object) []string { - aiGatewayRoute := o.(*aigv1a1.AIGatewayRoute) - var ret []string - for _, rule := range aiGatewayRoute.Spec.Rules { - for _, backend := range rule.BackendRefs { - key := fmt.Sprintf("%s.%s", backend.Name, aiGatewayRoute.Namespace) - ret = append(ret, key) - } - } - return ret -} - // aiGatewayRouteController implements [reconcile.TypedReconciler]. // // This handles the AIGatewayRoute resource and creates the necessary resources for the external process. diff --git a/internal/controller/ai_gateway_route_test.go b/internal/controller/ai_gateway_route_test.go index 80bc2abe6..2d073f5b8 100644 --- a/internal/controller/ai_gateway_route_test.go +++ b/internal/controller/ai_gateway_route_test.go @@ -10,7 +10,6 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" fake2 "k8s.io/client-go/kubernetes/fake" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" @@ -144,50 +143,3 @@ func Test_applyExtProcDeploymentConfigUpdate(t *testing.T) { require.Equal(t, int32(123), *dep.Replicas) }) } - -func Test_aiGatewayRouteIndexFunc(t *testing.T) { - scheme := runtime.NewScheme() - require.NoError(t, aigv1a1.AddToScheme(scheme)) - - c := fake.NewClientBuilder(). - WithScheme(scheme). - WithIndex(&aigv1a1.AIGatewayRoute{}, k8sClientIndexBackendToReferencingAIGatewayRoute, aiGatewayRouteIndexFunc). - Build() - - // Create a AIGatewayRoute. - aiGatewayRoute := &aigv1a1.AIGatewayRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "myroute", - Namespace: "default", - }, - Spec: aigv1a1.AIGatewayRouteSpec{ - TargetRefs: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ - {LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{Name: "mytarget"}}, - {LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{Name: "mytarget2"}}, - }, - Rules: []aigv1a1.AIGatewayRouteRule{ - { - Matches: []aigv1a1.AIGatewayRouteRuleMatch{}, - BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{ - {Name: "backend1", Weight: 1}, - {Name: "backend2", Weight: 1}, - }, - }, - }, - }, - } - require.NoError(t, c.Create(context.Background(), aiGatewayRoute)) - - var aiGatewayRoutes aigv1a1.AIGatewayRouteList - err := c.List(context.Background(), &aiGatewayRoutes, - client.MatchingFields{k8sClientIndexBackendToReferencingAIGatewayRoute: "backend1.default"}) - require.NoError(t, err) - require.Len(t, aiGatewayRoutes.Items, 1) - require.Equal(t, aiGatewayRoute.Name, aiGatewayRoutes.Items[0].Name) - - err = c.List(context.Background(), &aiGatewayRoutes, - client.MatchingFields{k8sClientIndexBackendToReferencingAIGatewayRoute: "backend2.default"}) - require.NoError(t, err) - require.Len(t, aiGatewayRoutes.Items, 1) - require.Equal(t, aiGatewayRoute.Name, aiGatewayRoutes.Items[0].Name) -} diff --git a/internal/controller/ai_service_backend.go b/internal/controller/ai_service_backend.go index c9e0a0cf8..148a67324 100644 --- a/internal/controller/ai_service_backend.go +++ b/internal/controller/ai_service_backend.go @@ -2,7 +2,6 @@ package controller import ( "context" - "fmt" "github.com/go-logr/logr" "k8s.io/client-go/kubernetes" @@ -13,10 +12,6 @@ import ( aigv1a1 "github.com/envoyproxy/ai-gateway/api/v1alpha1" ) -const ( - k8sClientIndexBackendSecurityPolicyToReferencingAIServiceBackend = "BackendSecurityPolicyToReferencingAIServiceBackend" -) - // aiBackendController implements [reconcile.TypedReconciler] for [aigv1a1.AIServiceBackend]. // // This handles the AIServiceBackend resource and sends it to the config sink so that it can modify the configuration together with the state of other resources. @@ -52,12 +47,3 @@ func (l *aiBackendController) Reconcile(ctx context.Context, req reconcile.Reque l.eventChan <- aiBackend.DeepCopy() return ctrl.Result{}, nil } - -func aiServiceBackendIndexFunc(o client.Object) []string { - aiServiceBackend := o.(*aigv1a1.AIServiceBackend) - var ret []string - if ref := aiServiceBackend.Spec.BackendSecurityPolicyRef; ref != nil { - ret = append(ret, fmt.Sprintf("%s.%s", ref.Name, aiServiceBackend.Namespace)) - } - return ret -} diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 93e49253c..c76711441 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -94,6 +94,13 @@ func StartControllers(ctx context.Context, config *rest.Config, logger logr.Logg return fmt.Errorf("failed to create controller for BackendSecurityPolicy: %w", err) } + secretC := NewSecretController(c, kubernetes.NewForConfigOrDie(config), logger, sinkChan) + if err = ctrl.NewControllerManagedBy(mgr). + For(&corev1.Secret{}). + Complete(secretC); err != nil { + return fmt.Errorf("failed to create controller for Secret: %w", err) + } + sink := newConfigSink(c, kubernetes.NewForConfigOrDie(config), logger, sinkChan, options.ExtProcImage) // Before starting the manager, initialize the config sink to sync all AIServiceBackend and AIGatewayRoute objects in the cluster. @@ -109,6 +116,18 @@ func StartControllers(ctx context.Context, config *rest.Config, logger logr.Logg return nil } +const ( + // k8sClientIndexSecretToReferencingBackendSecurityPolicy is the index name that maps + // from a Secret to the BackendSecurityPolicy that references it. + k8sClientIndexSecretToReferencingBackendSecurityPolicy = "SecretToReferencingBackendSecurityPolicy" + // k8sClientIndexBackendToReferencingAIGatewayRoute is the index name that maps from a Backend to the + // AIGatewayRoute that references it. + k8sClientIndexBackendToReferencingAIGatewayRoute = "BackendToReferencingAIGatewayRoute" + // k8sClientIndexBackendSecurityPolicyToReferencingAIServiceBackend is the index name that maps from a BackendSecurityPolicy + // to the AIServiceBackend that references it. + k8sClientIndexBackendSecurityPolicyToReferencingAIServiceBackend = "BackendSecurityPolicyToReferencingAIServiceBackend" +) + func applyIndexing(indexer client.FieldIndexer) error { err := indexer.IndexField(context.Background(), &aigv1a1.AIGatewayRoute{}, k8sClientIndexBackendToReferencingAIGatewayRoute, aiGatewayRouteIndexFunc) @@ -120,6 +139,55 @@ func applyIndexing(indexer client.FieldIndexer) error { if err != nil { return fmt.Errorf("failed to index field for AIServiceBackend: %w", err) } - + err = indexer.IndexField(context.Background(), &aigv1a1.BackendSecurityPolicy{}, + k8sClientIndexSecretToReferencingBackendSecurityPolicy, backendSecurityPolicyIndexFunc) + if err != nil { + return fmt.Errorf("failed to index field for BackendSecurityPolicy: %w", err) + } return nil } + +func aiGatewayRouteIndexFunc(o client.Object) []string { + aiGatewayRoute := o.(*aigv1a1.AIGatewayRoute) + var ret []string + for _, rule := range aiGatewayRoute.Spec.Rules { + for _, backend := range rule.BackendRefs { + key := fmt.Sprintf("%s.%s", backend.Name, aiGatewayRoute.Namespace) + ret = append(ret, key) + } + } + return ret +} + +func aiServiceBackendIndexFunc(o client.Object) []string { + aiServiceBackend := o.(*aigv1a1.AIServiceBackend) + var ret []string + if ref := aiServiceBackend.Spec.BackendSecurityPolicyRef; ref != nil { + ret = append(ret, fmt.Sprintf("%s.%s", ref.Name, aiServiceBackend.Namespace)) + } + return ret +} + +func backendSecurityPolicyIndexFunc(o client.Object) []string { + backendSecurityPolicy := o.(*aigv1a1.BackendSecurityPolicy) + var key string + switch backendSecurityPolicy.Spec.Type { + case aigv1a1.BackendSecurityPolicyTypeAPIKey: + apiKey := backendSecurityPolicy.Spec.APIKey + key = getSecretNameAndNamespace(apiKey.SecretRef, backendSecurityPolicy.Namespace) + case aigv1a1.BackendSecurityPolicyTypeAWSCredentials: + awsCreds := backendSecurityPolicy.Spec.AWSCredentials + if awsCreds.CredentialsFile != nil { + key = getSecretNameAndNamespace(awsCreds.CredentialsFile.SecretRef, backendSecurityPolicy.Namespace) + } + // TODO: OIDC. + } + return []string{key} +} + +func getSecretNameAndNamespace(secretRef *gwapiv1.SecretObjectReference, namespace string) string { + if secretRef.Namespace != nil { + return fmt.Sprintf("%s.%s", secretRef.Name, *secretRef.Namespace) + } + return fmt.Sprintf("%s.%s", secretRef.Name, namespace) +} diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go new file mode 100644 index 000000000..f824db394 --- /dev/null +++ b/internal/controller/controller_test.go @@ -0,0 +1,165 @@ +package controller + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + aigv1a1 "github.com/envoyproxy/ai-gateway/api/v1alpha1" +) + +func Test_aiGatewayRouteIndexFunc(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, aigv1a1.AddToScheme(scheme)) + + c := fake.NewClientBuilder(). + WithScheme(scheme). + WithIndex(&aigv1a1.AIGatewayRoute{}, k8sClientIndexBackendToReferencingAIGatewayRoute, aiGatewayRouteIndexFunc). + Build() + + // Create a AIGatewayRoute. + aiGatewayRoute := &aigv1a1.AIGatewayRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myroute", + Namespace: "default", + }, + Spec: aigv1a1.AIGatewayRouteSpec{ + TargetRefs: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ + {LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{Name: "mytarget"}}, + {LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{Name: "mytarget2"}}, + }, + Rules: []aigv1a1.AIGatewayRouteRule{ + { + Matches: []aigv1a1.AIGatewayRouteRuleMatch{}, + BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{ + {Name: "backend1", Weight: 1}, + {Name: "backend2", Weight: 1}, + }, + }, + }, + }, + } + require.NoError(t, c.Create(context.Background(), aiGatewayRoute)) + + var aiGatewayRoutes aigv1a1.AIGatewayRouteList + err := c.List(context.Background(), &aiGatewayRoutes, + client.MatchingFields{k8sClientIndexBackendToReferencingAIGatewayRoute: "backend1.default"}) + require.NoError(t, err) + require.Len(t, aiGatewayRoutes.Items, 1) + require.Equal(t, aiGatewayRoute.Name, aiGatewayRoutes.Items[0].Name) + + err = c.List(context.Background(), &aiGatewayRoutes, + client.MatchingFields{k8sClientIndexBackendToReferencingAIGatewayRoute: "backend2.default"}) + require.NoError(t, err) + require.Len(t, aiGatewayRoutes.Items, 1) + require.Equal(t, aiGatewayRoute.Name, aiGatewayRoutes.Items[0].Name) +} + +func Test_backendSecurityPolicyIndexFunc(t *testing.T) { + for _, bsp := range []struct { + name string + backendSecurityPolicy *aigv1a1.BackendSecurityPolicy + expKey string + }{ + { + name: "api key with namespace", + backendSecurityPolicy: &aigv1a1.BackendSecurityPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "some-backend-security-policy-1", Namespace: "ns"}, + Spec: aigv1a1.BackendSecurityPolicySpec{ + Type: aigv1a1.BackendSecurityPolicyTypeAPIKey, + APIKey: &aigv1a1.BackendSecurityPolicyAPIKey{ + SecretRef: &gwapiv1.SecretObjectReference{ + Name: "some-secret1", + Namespace: ptr.To[gwapiv1.Namespace]("foo"), + }, + }, + }, + }, + expKey: "some-secret1.foo", + }, + { + name: "api key without namespace", + backendSecurityPolicy: &aigv1a1.BackendSecurityPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "some-backend-security-policy-2", Namespace: "ns"}, + Spec: aigv1a1.BackendSecurityPolicySpec{ + Type: aigv1a1.BackendSecurityPolicyTypeAPIKey, + APIKey: &aigv1a1.BackendSecurityPolicyAPIKey{ + SecretRef: &gwapiv1.SecretObjectReference{Name: "some-secret2"}, + }, + }, + }, + expKey: "some-secret2.ns", + }, + { + name: "aws credentials with namespace", + backendSecurityPolicy: &aigv1a1.BackendSecurityPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "some-backend-security-policy-3", Namespace: "ns"}, + Spec: aigv1a1.BackendSecurityPolicySpec{ + Type: aigv1a1.BackendSecurityPolicyTypeAWSCredentials, + AWSCredentials: &aigv1a1.BackendSecurityPolicyAWSCredentials{ + CredentialsFile: &aigv1a1.AWSCredentialsFile{ + SecretRef: &gwapiv1.SecretObjectReference{ + Name: "some-secret3", Namespace: ptr.To[gwapiv1.Namespace]("foo"), + }, + }, + }, + }, + }, + expKey: "some-secret3.foo", + }, + { + name: "aws credentials without namespace", + backendSecurityPolicy: &aigv1a1.BackendSecurityPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "some-backend-security-policy-4", Namespace: "ns"}, + Spec: aigv1a1.BackendSecurityPolicySpec{ + Type: aigv1a1.BackendSecurityPolicyTypeAWSCredentials, + AWSCredentials: &aigv1a1.BackendSecurityPolicyAWSCredentials{ + CredentialsFile: &aigv1a1.AWSCredentialsFile{ + SecretRef: &gwapiv1.SecretObjectReference{Name: "some-secret4"}, + }, + }, + }, + }, + expKey: "some-secret4.ns", + }, + } { + t.Run(bsp.name, func(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, aigv1a1.AddToScheme(scheme)) + + c := fake.NewClientBuilder(). + WithScheme(scheme). + WithIndex(&aigv1a1.BackendSecurityPolicy{}, k8sClientIndexSecretToReferencingBackendSecurityPolicy, backendSecurityPolicyIndexFunc). + Build() + + require.NoError(t, c.Create(context.Background(), bsp.backendSecurityPolicy)) + + var backendSecurityPolicies aigv1a1.BackendSecurityPolicyList + err := c.List(context.Background(), &backendSecurityPolicies, + client.MatchingFields{k8sClientIndexSecretToReferencingBackendSecurityPolicy: bsp.expKey}) + require.NoError(t, err) + + require.Len(t, backendSecurityPolicies.Items, 1) + require.Equal(t, bsp.backendSecurityPolicy.Name, backendSecurityPolicies.Items[0].Name) + require.Equal(t, bsp.backendSecurityPolicy.Namespace, backendSecurityPolicies.Items[0].Namespace) + }) + } +} + +func Test_getSecretNameAndNamespace(t *testing.T) { + secretRef := &gwapiv1.SecretObjectReference{ + Name: "mysecret", + Namespace: ptr.To[gwapiv1.Namespace]("default"), + } + require.Equal(t, "mysecret.default", getSecretNameAndNamespace(secretRef, "foo")) + secretRef.Namespace = nil + require.Equal(t, "mysecret.foo", getSecretNameAndNamespace(secretRef, "foo")) +} diff --git a/internal/controller/secret.go b/internal/controller/secret.go new file mode 100644 index 000000000..c41d6c029 --- /dev/null +++ b/internal/controller/secret.go @@ -0,0 +1,47 @@ +package controller + +import ( + "context" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/kubernetes" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// secretController implements reconcile.TypedReconciler for corev1.Secret. +type secretController struct { + client client.Client + kubeClient kubernetes.Interface + logger logr.Logger + eventChan chan ConfigSinkEvent +} + +// NewSecretController creates a new reconcile.TypedReconciler[reconcile.Request] for corev1.Secret. +func NewSecretController(client client.Client, kubeClient kubernetes.Interface, + logger logr.Logger, ch chan ConfigSinkEvent, +) reconcile.TypedReconciler[reconcile.Request] { + return &secretController{ + client: client, + kubeClient: kubeClient, + logger: logger, + eventChan: ch, + } +} + +// Reconcile implements the reconcile.Reconciler for corev1.Secret. +func (r *secretController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var secret corev1.Secret + if err := r.client.Get(ctx, req.NamespacedName, &secret); err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + r.logger.Info("Reconciling Secret", "namespace", req.Namespace, "name", req.Name) + r.eventChan <- ConfigSinkEventSecretUpdate{Namespace: secret.Namespace, Name: secret.Name} + return ctrl.Result{}, nil +} diff --git a/internal/controller/secret_test.go b/internal/controller/secret_test.go new file mode 100644 index 000000000..0a606fe32 --- /dev/null +++ b/internal/controller/secret_test.go @@ -0,0 +1,36 @@ +package controller + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + fake2 "k8s.io/client-go/kubernetes/fake" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func TestSecretController_Reconcile(t *testing.T) { + ch := make(chan ConfigSinkEvent, 100) + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + c := NewSecretController(cl, fake2.NewClientset(), ctrl.Log, ch) + + err := cl.Create(context.Background(), &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "mysecret", Namespace: "default"}, + StringData: map[string]string{"key": "value"}, + }) + require.NoError(t, err) + + _, err = c.Reconcile(context.Background(), reconcile.Request{NamespacedName: types.NamespacedName{ + Namespace: "default", Name: "mysecret", + }}) + require.NoError(t, err) + + item, ok := <-ch + require.True(t, ok) + require.Equal(t, ConfigSinkEventSecretUpdate{Namespace: "default", Name: "mysecret"}, item) +} diff --git a/internal/controller/sink.go b/internal/controller/sink.go index 2418df22a..1ef628df9 100644 --- a/internal/controller/sink.go +++ b/internal/controller/sink.go @@ -11,6 +11,7 @@ import ( corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + uuid2 "k8s.io/apimachinery/pkg/util/uuid" "k8s.io/client-go/kubernetes" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" @@ -39,6 +40,12 @@ const mountedExtProcSecretPath = "/etc/backend_security_policy" // #nosec G101 // Exported for internal testing purposes. type ConfigSinkEvent any +// ConfigSinkEventSecretUpdate is an event that indicates that a secret has been updated. +// It only contains the namespace and the name of the secret that has been updated. +type ConfigSinkEventSecretUpdate struct { + Namespace, Name string +} + // configSink centralizes the AIGatewayRoute and AIServiceBackend objects handling // which requires to be done in a single goroutine since we need to // consolidate the information from both objects to generate the ExtProc ConfigMap @@ -112,6 +119,8 @@ func (c *configSink) handleEvent(event ConfigSinkEvent) { c.syncAIGatewayRoute(e) case *aigv1a1.BackendSecurityPolicy: c.syncBackendSecurityPolicy(e) + case ConfigSinkEventSecretUpdate: + c.syncSecret(e.Namespace, e.Name) default: panic(fmt.Sprintf("unexpected event type: %T", e)) } @@ -188,7 +197,7 @@ func (c *configSink) syncAIGatewayRoute(aiGatewayRoute *aigv1a1.AIGatewayRoute) } // Update the extproc configmap. - if err := c.updateExtProcConfigMap(aiGatewayRoute); err != nil { + if err := c.updateExtProcConfigMap(aiGatewayRoute, string(uuid2.NewUUID())); err != nil { c.logger.Error(err, "failed to update extproc configmap", "namespace", aiGatewayRoute.Namespace, "name", aiGatewayRoute.Name) return } @@ -233,14 +242,14 @@ func (c *configSink) syncBackendSecurityPolicy(bsp *aigv1a1.BackendSecurityPolic } // updateExtProcConfigMap updates the external process configmap with the new AIGatewayRoute. -func (c *configSink) updateExtProcConfigMap(aiGatewayRoute *aigv1a1.AIGatewayRoute) error { +func (c *configSink) updateExtProcConfigMap(aiGatewayRoute *aigv1a1.AIGatewayRoute, uuid string) error { configMap, err := c.kube.CoreV1().ConfigMaps(aiGatewayRoute.Namespace).Get(context.Background(), extProcName(aiGatewayRoute), metav1.GetOptions{}) if err != nil { // This is a bug since we should have created the configmap before sending the AIGatewayRoute to the configSink. panic(fmt.Errorf("failed to get configmap %s: %w", extProcName(aiGatewayRoute), err)) } - ec := &filterconfig.Config{} + ec := &filterconfig.Config{UUID: uuid} spec := &aiGatewayRoute.Spec ec.Schema.Name = filterconfig.APISchemaName(spec.APISchema.Name) @@ -569,10 +578,27 @@ func (c *configSink) mountBackendSecurityPolicySecrets(spec *corev1.PodSpec, aiG } } } - return spec, nil } +// syncSecret syncs the state of all resource referencing the given secret. +func (c *configSink) syncSecret(namespace, name string) { + var backendSecurityPolicies aigv1a1.BackendSecurityPolicyList + err := c.client.List(context.Background(), &backendSecurityPolicies, + client.MatchingFields{ + k8sClientIndexSecretToReferencingBackendSecurityPolicy: fmt.Sprintf("%s.%s", name, namespace), + }, + ) + if err != nil { + c.logger.Error(err, "failed to list BackendSecurityPolicy", "secret", name, "namespace", namespace) + return + } + for i := range backendSecurityPolicies.Items { + backendSecurityPolicy := &backendSecurityPolicies.Items[i] + c.syncBackendSecurityPolicy(backendSecurityPolicy) + } +} + func backendSecurityPolicyVolumeName(ruleIndex, backendRefIndex int, name string) string { // Note: do not use "." as it's not allowed in the volume name. return fmt.Sprintf("rule%d-backref%d-%s", ruleIndex, backendRefIndex, name) diff --git a/internal/controller/sink_test.go b/internal/controller/sink_test.go index f9fc1a4ed..f700cc8d3 100644 --- a/internal/controller/sink_test.go +++ b/internal/controller/sink_test.go @@ -14,6 +14,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + uuid2 "k8s.io/apimachinery/pkg/util/uuid" "k8s.io/apimachinery/pkg/util/yaml" fake2 "k8s.io/client-go/kubernetes/fake" "k8s.io/utils/ptr" @@ -368,6 +369,7 @@ func Test_updateExtProcConfigMap(t *testing.T) { }, }, exp: &filterconfig.Config{ + UUID: string(uuid2.NewUUID()), Schema: filterconfig.VersionedAPISchema{Name: filterconfig.APISchemaOpenAI, Version: "v123"}, ModelNameHeaderKey: aigv1a1.AIModelHeaderKey, MetadataNamespace: aigv1a1.AIGatewayFilterMetadataNamespace, @@ -416,7 +418,7 @@ func Test_updateExtProcConfigMap(t *testing.T) { }, metav1.CreateOptions{}) require.NoError(t, err) - err = s.updateExtProcConfigMap(tc.route) + err = s.updateExtProcConfigMap(tc.route, tc.exp.UUID) require.NoError(t, err) cm, err := s.kube.CoreV1().ConfigMaps(tc.route.Namespace).Get(context.Background(), extProcName(tc.route), metav1.GetOptions{}) diff --git a/internal/extproc/backendauth/aws.go b/internal/extproc/backendauth/aws.go index 618964fe3..dc5b88c8c 100644 --- a/internal/extproc/backendauth/aws.go +++ b/internal/extproc/backendauth/aws.go @@ -31,7 +31,6 @@ func newAWSHandler(awsAuth *filterconfig.AWSAuth) (*awsHandler, error) { var credentials aws.Credentials var region string - // TODO: refactor to work with refreshing credentials (similar to API Key) if awsAuth != nil { region = awsAuth.Region if len(awsAuth.CredentialFileName) != 0 { diff --git a/tests/controller/controller_test.go b/tests/controller/controller_test.go index 711bc1c09..32a2f15c5 100644 --- a/tests/controller/controller_test.go +++ b/tests/controller/controller_test.go @@ -464,3 +464,60 @@ func TestAIServiceBackendController(t *testing.T) { require.Equal(t, origin, created) }) } + +func TestSecretController(t *testing.T) { + c, cfg, k := testsinternal.NewEnvTest(t) + + ch := make(chan controller.ConfigSinkEvent) + sc := controller.NewSecretController(c, k, logr.Discard(), ch) + + opt := ctrl.Options{Scheme: c.Scheme(), LeaderElection: false, Controller: config.Controller{SkipNameValidation: ptr.To(true)}} + mgr, err := ctrl.NewManager(cfg, opt) + require.NoError(t, err) + + err = ctrl.NewControllerManagedBy(mgr).For(&corev1.Secret{}).Complete(sc) + require.NoError(t, err) + + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2*time.Minute)) + defer cancel() + go func() { + err := mgr.Start(ctx) + require.NoError(t, err) + }() + + t.Run("create secret", func(t *testing.T) { + origin := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "mysecret", Namespace: "default"}, + StringData: map[string]string{ + "key": "value", + }, + } + err := c.Create(ctx, origin) + require.NoError(t, err) + + item, ok := <-ch + require.True(t, ok) + event, ok := item.(controller.ConfigSinkEventSecretUpdate) + require.True(t, ok) + require.Equal(t, "default", event.Namespace) + require.Equal(t, "mysecret", event.Name) + }) + + t.Run("update secret", func(t *testing.T) { + origin := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "mysecret", Namespace: "default"}, + StringData: map[string]string{ + "key": "value2", + }, + } + err := c.Update(ctx, origin) + require.NoError(t, err) + + item, ok := <-ch + require.True(t, ok) + event, ok := item.(controller.ConfigSinkEventSecretUpdate) + require.True(t, ok) + require.Equal(t, "default", event.Namespace) + require.Equal(t, "mysecret", event.Name) + }) +}