diff --git a/api/adc/plugin_types.go b/api/adc/plugin_types.go new file mode 100644 index 000000000..57e4aef7c --- /dev/null +++ b/api/adc/plugin_types.go @@ -0,0 +1,125 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package adc + +// IPRestrictConfig is the rule config for ip-restriction plugin. +// +k8s:deepcopy-gen=true +type IPRestrictConfig struct { + Allowlist []string `json:"whitelist,omitempty"` + Blocklist []string `json:"blacklist,omitempty"` +} + +// CorsConfig is the rule config for cors plugin. +// +k8s:deepcopy-gen=true +type CorsConfig struct { + AllowOrigins string `json:"allow_origins,omitempty"` + AllowMethods string `json:"allow_methods,omitempty"` + AllowHeaders string `json:"allow_headers,omitempty"` +} + +// CSRfConfig is the rule config for csrf plugin. +// +k8s:deepcopy-gen=true +type CSRFConfig struct { + Key string `json:"key"` +} + +// KeyAuthConsumerConfig is the rule config for key-auth plugin +// used in Consumer object. +// +k8s:deepcopy-gen=true +type KeyAuthConsumerConfig struct { + Key string `json:"key"` +} + +// KeyAuthRouteConfig is the rule config for key-auth plugin +// used in Route object. +type KeyAuthRouteConfig struct { + Header string `json:"header,omitempty"` +} + +// BasicAuthConsumerConfig is the rule config for basic-auth plugin +// used in Consumer object. +// +k8s:deepcopy-gen=true +type BasicAuthConsumerConfig struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// JwtAuthConsumerConfig is the rule config for jwt-auth plugin +// used in Consumer object. +// +k8s:deepcopy-gen=true +type JwtAuthConsumerConfig struct { + Key string `json:"key" yaml:"key"` + Secret string `json:"secret,omitempty" yaml:"secret,omitempty"` + PublicKey string `json:"public_key,omitempty" yaml:"public_key,omitempty"` + PrivateKey string `json:"private_key" yaml:"private_key,omitempty"` + Algorithm string `json:"algorithm,omitempty" yaml:"algorithm,omitempty"` + Exp int64 `json:"exp,omitempty" yaml:"exp,omitempty"` + Base64Secret bool `json:"base64_secret,omitempty" yaml:"base64_secret,omitempty"` + LifetimeGracePeriod int64 `json:"lifetime_grace_period,omitempty" yaml:"lifetime_grace_period,omitempty"` +} + +// HMACAuthConsumerConfig is the rule config for hmac-auth plugin +// used in Consumer object. +// +k8s:deepcopy-gen=true +type HMACAuthConsumerConfig struct { + AccessKey string `json:"access_key" yaml:"access_key"` + SecretKey string `json:"secret_key" yaml:"secret_key"` + Algorithm string `json:"algorithm,omitempty" yaml:"algorithm,omitempty"` + ClockSkew int64 `json:"clock_skew,omitempty" yaml:"clock_skew,omitempty"` + SignedHeaders []string `json:"signed_headers,omitempty" yaml:"signed_headers,omitempty"` + KeepHeaders bool `json:"keep_headers,omitempty" yaml:"keep_headers,omitempty"` + EncodeURIParams bool `json:"encode_uri_params,omitempty" yaml:"encode_uri_params,omitempty"` + ValidateRequestBody bool `json:"validate_request_body,omitempty" yaml:"validate_request_body,omitempty"` + MaxReqBody int64 `json:"max_req_body,omitempty" yaml:"max_req_body,omitempty"` +} + +// LDAPAuthConsumerConfig is the rule config for ldap-auth plugin +// used in Consumer object. +// +k8s:deepcopy-gen=true +type LDAPAuthConsumerConfig struct { + UserDN string `json:"user_dn"` +} + +// BasicAuthRouteConfig is the rule config for basic-auth plugin +// used in Route object. +// +k8s:deepcopy-gen=true +type BasicAuthRouteConfig struct{} + +// WolfRBACConsumerConfig is the rule config for wolf-rbac plugin +// used in Consumer object. +// +k8s:deepcopy-gen=true +type WolfRBACConsumerConfig struct { + Server string `json:"server,omitempty"` + Appid string `json:"appid,omitempty"` + HeaderPrefix string `json:"header_prefix,omitempty"` +} + +// ForwardAuthConfig is the rule config for forward-auth plugin. +// +k8s:deepcopy-gen=true +type ForwardAuthConfig struct { + URI string `json:"uri"` + SSLVerify bool `json:"ssl_verify"` + RequestHeaders []string `json:"request_headers,omitempty"` + UpstreamHeaders []string `json:"upstream_headers,omitempty"` + ClientHeaders []string `json:"client_headers,omitempty"` +} + +// BasicAuthConfig is the rule config for basic-auth plugin. +// +k8s:deepcopy-gen=true +type BasicAuthConfig struct { +} + +// KeyAuthConfig is the rule config for key-auth plugin. +// +k8s:deepcopy-gen=true +type KeyAuthConfig struct { +} diff --git a/api/adc/zz_generated.deepcopy.go b/api/adc/zz_generated.deepcopy.go index 9330480da..b5ce7c00b 100644 --- a/api/adc/zz_generated.deepcopy.go +++ b/api/adc/zz_generated.deepcopy.go @@ -22,6 +22,66 @@ package adc import () +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BasicAuthConfig) DeepCopyInto(out *BasicAuthConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BasicAuthConfig. +func (in *BasicAuthConfig) DeepCopy() *BasicAuthConfig { + if in == nil { + return nil + } + out := new(BasicAuthConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BasicAuthConsumerConfig) DeepCopyInto(out *BasicAuthConsumerConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BasicAuthConsumerConfig. +func (in *BasicAuthConsumerConfig) DeepCopy() *BasicAuthConsumerConfig { + if in == nil { + return nil + } + out := new(BasicAuthConsumerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BasicAuthRouteConfig) DeepCopyInto(out *BasicAuthRouteConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BasicAuthRouteConfig. +func (in *BasicAuthRouteConfig) DeepCopy() *BasicAuthRouteConfig { + if in == nil { + return nil + } + out := new(BasicAuthRouteConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CSRFConfig) DeepCopyInto(out *CSRFConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CSRFConfig. +func (in *CSRFConfig) DeepCopy() *CSRFConfig { + if in == nil { + return nil + } + out := new(CSRFConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Certificate) DeepCopyInto(out *Certificate) { *out = *in @@ -125,6 +185,21 @@ func (in *ConsumerGroup) DeepCopy() *ConsumerGroup { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CorsConfig) DeepCopyInto(out *CorsConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CorsConfig. +func (in *CorsConfig) DeepCopy() *CorsConfig { + if in == nil { + return nil + } + out := new(CorsConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Credential) DeepCopyInto(out *Credential) { *out = *in @@ -142,6 +217,36 @@ func (in *Credential) DeepCopy() *Credential { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ForwardAuthConfig) DeepCopyInto(out *ForwardAuthConfig) { + *out = *in + if in.RequestHeaders != nil { + in, out := &in.RequestHeaders, &out.RequestHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.UpstreamHeaders != nil { + in, out := &in.UpstreamHeaders, &out.UpstreamHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ClientHeaders != nil { + in, out := &in.ClientHeaders, &out.ClientHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ForwardAuthConfig. +func (in *ForwardAuthConfig) DeepCopy() *ForwardAuthConfig { + if in == nil { + return nil + } + out := new(ForwardAuthConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GlobalRuleItem) DeepCopyInto(out *GlobalRuleItem) { *out = *in @@ -159,6 +264,111 @@ func (in *GlobalRuleItem) DeepCopy() *GlobalRuleItem { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HMACAuthConsumerConfig) DeepCopyInto(out *HMACAuthConsumerConfig) { + *out = *in + if in.SignedHeaders != nil { + in, out := &in.SignedHeaders, &out.SignedHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HMACAuthConsumerConfig. +func (in *HMACAuthConsumerConfig) DeepCopy() *HMACAuthConsumerConfig { + if in == nil { + return nil + } + out := new(HMACAuthConsumerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPRestrictConfig) DeepCopyInto(out *IPRestrictConfig) { + *out = *in + if in.Allowlist != nil { + in, out := &in.Allowlist, &out.Allowlist + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Blocklist != nil { + in, out := &in.Blocklist, &out.Blocklist + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPRestrictConfig. +func (in *IPRestrictConfig) DeepCopy() *IPRestrictConfig { + if in == nil { + return nil + } + out := new(IPRestrictConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JwtAuthConsumerConfig) DeepCopyInto(out *JwtAuthConsumerConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JwtAuthConsumerConfig. +func (in *JwtAuthConsumerConfig) DeepCopy() *JwtAuthConsumerConfig { + if in == nil { + return nil + } + out := new(JwtAuthConsumerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeyAuthConfig) DeepCopyInto(out *KeyAuthConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyAuthConfig. +func (in *KeyAuthConfig) DeepCopy() *KeyAuthConfig { + if in == nil { + return nil + } + out := new(KeyAuthConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeyAuthConsumerConfig) DeepCopyInto(out *KeyAuthConsumerConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyAuthConsumerConfig. +func (in *KeyAuthConsumerConfig) DeepCopy() *KeyAuthConsumerConfig { + if in == nil { + return nil + } + out := new(KeyAuthConsumerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPAuthConsumerConfig) DeepCopyInto(out *LDAPAuthConsumerConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPAuthConsumerConfig. +func (in *LDAPAuthConsumerConfig) DeepCopy() *LDAPAuthConsumerConfig { + if in == nil { + return nil + } + out := new(LDAPAuthConsumerConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Metadata) DeepCopyInto(out *Metadata) { *out = *in @@ -663,3 +873,18 @@ func (in *UpstreamPassiveHealthCheckUnhealthy) DeepCopy() *UpstreamPassiveHealth in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WolfRBACConsumerConfig) DeepCopyInto(out *WolfRBACConsumerConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WolfRBACConsumerConfig. +func (in *WolfRBACConsumerConfig) DeepCopy() *WolfRBACConsumerConfig { + if in == nil { + return nil + } + out := new(WolfRBACConsumerConfig) + in.DeepCopyInto(out) + return out +} diff --git a/api/v2/apisixroute_types.go b/api/v2/apisixroute_types.go index 989667425..6208b35ef 100644 --- a/api/v2/apisixroute_types.go +++ b/api/v2/apisixroute_types.go @@ -140,7 +140,7 @@ type ApisixRoutePlugin struct { Enable bool `json:"enable" yaml:"enable"` // Plugin configuration. // +kubebuilder:validation:Optional - Config ApisixRoutePluginConfig `json:"config" yaml:"config"` + Config apiextensionsv1.JSON `json:"config" yaml:"config"` // Plugin configuration secretRef. // +kubebuilder:validation:Optional SecretRef string `json:"secretRef" yaml:"secretRef"` diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index c97bfe340..9a8c078cb 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -936,13 +936,7 @@ func (in *ApisixRouteList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ApisixRoutePlugin) DeepCopyInto(out *ApisixRoutePlugin) { *out = *in - if in.Config != nil { - in, out := &in.Config, &out.Config - *out = make(ApisixRoutePluginConfig, len(*in)) - for key, val := range *in { - (*out)[key] = *val.DeepCopy() - } - } + in.Config.DeepCopyInto(&out.Config) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRoutePlugin. diff --git a/config/crd/bases/apisix.apache.org_apisixglobalrules.yaml b/config/crd/bases/apisix.apache.org_apisixglobalrules.yaml index 797528d2b..670c495b4 100644 --- a/config/crd/bases/apisix.apache.org_apisixglobalrules.yaml +++ b/config/crd/bases/apisix.apache.org_apisixglobalrules.yaml @@ -50,10 +50,8 @@ spec: description: ApisixRoutePlugin represents an APISIX plugin. properties: config: - additionalProperties: - x-kubernetes-preserve-unknown-fields: true description: Plugin configuration. - type: object + x-kubernetes-preserve-unknown-fields: true enable: default: true description: Whether this plugin is in use, default is true. diff --git a/config/crd/bases/apisix.apache.org_apisixpluginconfigs.yaml b/config/crd/bases/apisix.apache.org_apisixpluginconfigs.yaml index b967d2395..4af1bcbc1 100644 --- a/config/crd/bases/apisix.apache.org_apisixpluginconfigs.yaml +++ b/config/crd/bases/apisix.apache.org_apisixpluginconfigs.yaml @@ -51,10 +51,8 @@ spec: description: ApisixRoutePlugin represents an APISIX plugin. properties: config: - additionalProperties: - x-kubernetes-preserve-unknown-fields: true description: Plugin configuration. - type: object + x-kubernetes-preserve-unknown-fields: true enable: default: true description: Whether this plugin is in use, default is true. diff --git a/config/crd/bases/apisix.apache.org_apisixroutes.yaml b/config/crd/bases/apisix.apache.org_apisixroutes.yaml index be66dd25f..13ce72edb 100644 --- a/config/crd/bases/apisix.apache.org_apisixroutes.yaml +++ b/config/crd/bases/apisix.apache.org_apisixroutes.yaml @@ -247,10 +247,8 @@ spec: description: ApisixRoutePlugin represents an APISIX plugin. properties: config: - additionalProperties: - x-kubernetes-preserve-unknown-fields: true description: Plugin configuration. - type: object + x-kubernetes-preserve-unknown-fields: true enable: default: true description: Whether this plugin is in use, default is @@ -364,10 +362,8 @@ spec: description: ApisixRoutePlugin represents an APISIX plugin. properties: config: - additionalProperties: - x-kubernetes-preserve-unknown-fields: true description: Plugin configuration. - type: object + x-kubernetes-preserve-unknown-fields: true enable: default: true description: Whether this plugin is in use, default is diff --git a/docs/crd/api.md b/docs/crd/api.md index a337c2db5..4ef3e2a1a 100644 --- a/docs/crd/api.md +++ b/docs/crd/api.md @@ -1159,7 +1159,7 @@ ApisixRoutePlugin represents an APISIX plugin. | --- | --- | | `name` _string_ | The plugin name. | | `enable` _boolean_ | Whether this plugin is in use, default is true. | -| `config` _[ApisixRoutePluginConfig](#apisixroutepluginconfig)_ | Plugin configuration. | +| `config` _[JSON](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#json-v1-apiextensions-k8s-io)_ | Plugin configuration. | | `secretRef` _string_ | Plugin configuration secretRef. | @@ -1169,18 +1169,7 @@ _Appears in:_ - [ApisixRouteHTTP](#apisixroutehttp) - [ApisixRouteStream](#apisixroutestream) -#### ApisixRoutePluginConfig -_Base type:_ `[JSON](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#json-v1-apiextensions-k8s-io)` -ApisixRoutePluginConfig is the configuration for -any plugins. - - - - - -_Appears in:_ -- [ApisixRoutePlugin](#apisixrouteplugin) #### ApisixRouteSpec diff --git a/internal/controller/apisixconsumer_controller.go b/internal/controller/apisixconsumer_controller.go index 97a247593..3ec1757f4 100644 --- a/internal/controller/apisixconsumer_controller.go +++ b/internal/controller/apisixconsumer_controller.go @@ -14,15 +14,28 @@ package controller import ( "context" + "fmt" "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "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" - gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/apache/apisix-ingress-controller/api/v1alpha1" apiv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/internal/controller/indexer" + "github.com/apache/apisix-ingress-controller/internal/controller/status" + "github.com/apache/apisix-ingress-controller/internal/provider" + "github.com/apache/apisix-ingress-controller/internal/utils" ) // ApisixConsumerReconciler reconciles a ApisixConsumer object @@ -30,40 +43,194 @@ type ApisixConsumerReconciler struct { client.Client Scheme *runtime.Scheme Log logr.Logger + + Provider provider.Provider + Updater status.Updater } // Reconcile FIXME: implement the reconcile logic (For now, it dose nothing other than directly accepting) func (r *ApisixConsumerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { r.Log.Info("reconcile", "request", req.NamespacedName) - var obj apiv2.ApisixConsumer - if err := r.Get(ctx, req.NamespacedName, &obj); err != nil { + ac := &apiv2.ApisixConsumer{} + if err := r.Get(ctx, req.NamespacedName, ac); err != nil { + if k8serrors.IsNotFound(err) { + ac.Namespace = req.Namespace + ac.Name = req.Name + ac.TypeMeta = metav1.TypeMeta{ + Kind: KindApisixConsumer, + APIVersion: apiv2.GroupVersion.String(), + } + if err := r.Provider.Delete(ctx, ac); err != nil { + r.Log.Error(err, "failed to delete provider", "ApisixConsumer", ac) + return ctrl.Result{}, err + } + } r.Log.Error(err, "failed to get ApisixConsumer", "request", req.NamespacedName) return ctrl.Result{}, err } - obj.Status.Conditions = []metav1.Condition{ - { - Type: string(gatewayv1.RouteConditionAccepted), - Status: metav1.ConditionTrue, - ObservedGeneration: obj.GetGeneration(), - LastTransitionTime: metav1.Now(), - Reason: string(gatewayv1.RouteReasonAccepted), - }, + var ( + tctx = provider.NewDefaultTranslateContext(ctx) + ingressClass *networkingv1.IngressClass + err error + ) + defer func() { + r.updateStatus(ac, err) + }() + + ingressClass, err = GetIngressClass(tctx, r.Client, r.Log, ac.Spec.IngressClassName) + if err != nil { + r.Log.Error(err, "failed to get IngressClass") + return ctrl.Result{}, err } - if err := r.Status().Update(ctx, &obj); err != nil { - r.Log.Error(err, "failed to update status", "request", req.NamespacedName) + if err = ProcessIngressClassParameters(tctx, r.Client, r.Log, ac, ingressClass); err != nil { + r.Log.Error(err, "failed to process IngressClass parameters", "ingressClass", ingressClass.Name) return ctrl.Result{}, err } + if err = r.processSpec(ctx, tctx, ac); err != nil { + return ctrl.Result{}, err + } + + if err = r.Provider.Update(ctx, tctx, ac); err != nil { + r.Log.Error(err, "failed to update provider", "ApisixConsumer", ac) + return ctrl.Result{}, err + } return ctrl.Result{}, nil } // SetupWithManager sets up the controller with the Manager. func (r *ApisixConsumerReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&apiv2.ApisixConsumer{}). + For(&apiv2.ApisixConsumer{}, + builder.WithPredicates( + predicate.NewPredicateFuncs(r.checkIngressClass), + )). + WithEventFilter( + predicate.Or( + predicate.GenerationChangedPredicate{}, + predicate.AnnotationChangedPredicate{}, + ), + ). + Watches( + &networkingv1.IngressClass{}, + handler.EnqueueRequestsFromMapFunc(r.listApisixConsumerForIngressClass), + builder.WithPredicates( + predicate.NewPredicateFuncs(matchesIngressController), + ), + ). + Watches(&v1alpha1.GatewayProxy{}, + handler.EnqueueRequestsFromMapFunc(r.listApisixConsumerForGatewayProxy), + ). + Watches(&corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(r.listApisixConsumerForSecret), + ). Named("apisixconsumer"). Complete(r) } + +func (r *ApisixConsumerReconciler) checkIngressClass(obj client.Object) bool { + ac, ok := obj.(*apiv2.ApisixConsumer) + if !ok { + return false + } + + return matchesIngressClass(r.Client, r.Log, ac.Spec.IngressClassName) +} + +func (r *ApisixConsumerReconciler) listApisixConsumerForGatewayProxy(ctx context.Context, obj client.Object) []reconcile.Request { + return listIngressClassRequestsForGatewayProxy(ctx, r.Client, obj, r.Log, r.listApisixConsumerForIngressClass) +} + +func (r *ApisixConsumerReconciler) listApisixConsumerForIngressClass(ctx context.Context, obj client.Object) []reconcile.Request { + ingressClass, ok := obj.(*networkingv1.IngressClass) + if !ok { + return nil + } + + return ListMatchingRequests( + ctx, + r.Client, + r.Log, + &apiv2.ApisixConsumerList{}, + func(obj client.Object) bool { + ac, ok := obj.(*apiv2.ApisixConsumer) + if !ok { + r.Log.Error(fmt.Errorf("expected ApisixConsumer, got %T", obj), "failed to match object type") + return false + } + return (IsDefaultIngressClass(ingressClass) && ac.Spec.IngressClassName == "") || ac.Spec.IngressClassName == ingressClass.Name + }, + ) +} + +func (r *ApisixConsumerReconciler) listApisixConsumerForSecret(ctx context.Context, obj client.Object) []reconcile.Request { + secret, ok := obj.(*corev1.Secret) + if !ok { + r.Log.Error(nil, "failed to convert to Secret", "object", obj) + return nil + } + return ListRequests( + ctx, + r.Client, + r.Log, + &apiv2.ApisixConsumerList{}, + client.MatchingFields{ + indexer.SecretIndexRef: indexer.GenIndexKey(secret.GetNamespace(), secret.GetName()), + }, + ) +} + +func (r *ApisixConsumerReconciler) processSpec(ctx context.Context, tctx *provider.TranslateContext, ac *apiv2.ApisixConsumer) error { + var secretRef *corev1.LocalObjectReference + if ac.Spec.AuthParameter.KeyAuth != nil { + secretRef = ac.Spec.AuthParameter.KeyAuth.SecretRef + } else if ac.Spec.AuthParameter.BasicAuth != nil { + secretRef = ac.Spec.AuthParameter.BasicAuth.SecretRef + } else if ac.Spec.AuthParameter.JwtAuth != nil { + secretRef = ac.Spec.AuthParameter.JwtAuth.SecretRef + } else if ac.Spec.AuthParameter.WolfRBAC != nil { + secretRef = ac.Spec.AuthParameter.WolfRBAC.SecretRef + } else if ac.Spec.AuthParameter.HMACAuth != nil { + secretRef = ac.Spec.AuthParameter.HMACAuth.SecretRef + } else if ac.Spec.AuthParameter.LDAPAuth != nil { + secretRef = ac.Spec.AuthParameter.LDAPAuth.SecretRef + } + if secretRef == nil { + return nil + } + + namespacedName := types.NamespacedName{ + Name: secretRef.Name, + Namespace: ac.Namespace, + } + + secret := &corev1.Secret{} + if err := r.Get(ctx, namespacedName, secret); err != nil { + if k8serrors.IsNotFound(err) { + r.Log.Info("secret not found", "secret", namespacedName.String()) + return nil + } else { + r.Log.Error(err, "failed to get secret", "secret", namespacedName.String()) + return err + } + } + tctx.Secrets[namespacedName] = secret + return nil +} + +func (r *ApisixConsumerReconciler) updateStatus(consumer *apiv2.ApisixConsumer, err error) { + SetApisixCRDConditionAccepted(&consumer.Status, consumer.GetGeneration(), err) + r.Updater.Update(status.Update{ + NamespacedName: utils.NamespacedName(consumer), + Resource: &apiv2.ApisixConsumer{}, + Mutator: status.MutatorFunc(func(obj client.Object) client.Object { + ac := obj.(*apiv2.ApisixConsumer) + acCopy := ac.DeepCopy() + acCopy.Status = consumer.Status + return acCopy + }), + }) +} diff --git a/internal/controller/apisixglobalrule_controller.go b/internal/controller/apisixglobalrule_controller.go index e327ca468..9dc89fbe9 100644 --- a/internal/controller/apisixglobalrule_controller.go +++ b/internal/controller/apisixglobalrule_controller.go @@ -14,16 +14,13 @@ package controller import ( "context" - "errors" "fmt" "github.com/api7/gopkg/pkg/log" "github.com/go-logr/logr" - corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "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" @@ -79,14 +76,14 @@ func (r *ApisixGlobalRuleReconciler) Reconcile(ctx context.Context, req ctrl.Req tctx := provider.NewDefaultTranslateContext(ctx) // get the ingress class - ingressClass, err := r.getIngressClass(&globalRule) + ingressClass, err := GetIngressClass(tctx, r.Client, r.Log, globalRule.Spec.IngressClassName) if err != nil { log.Error(err, "failed to get IngressClass") return ctrl.Result{}, err } // process IngressClass parameters if they reference GatewayProxy - if err := r.processIngressClassParameters(ctx, tctx, &globalRule, ingressClass); err != nil { + if err := ProcessIngressClassParameters(tctx, r.Client, r.Log, &globalRule, ingressClass); err != nil { log.Error(err, "failed to process IngressClass parameters", "ingressClass", ingressClass.Name) return ctrl.Result{}, err } @@ -137,12 +134,13 @@ func (r *ApisixGlobalRuleReconciler) SetupWithManager(mgr ctrl.Manager) error { &networkingv1.IngressClass{}, handler.EnqueueRequestsFromMapFunc(r.listGlobalRulesForIngressClass), builder.WithPredicates( - predicate.NewPredicateFuncs(r.matchesIngressController), + predicate.NewPredicateFuncs(matchesIngressController), ), ). Watches(&v1alpha1.GatewayProxy{}, handler.EnqueueRequestsFromMapFunc(r.listGlobalRulesForGatewayProxy), ). + Named("apisixglobalrule"). Complete(r) } @@ -187,15 +185,6 @@ func (r *ApisixGlobalRuleReconciler) matchesIngressClass(ingressClassName string return matchesController(ingressClass.Spec.Controller) } -// matchesIngressController check if the ingress class is controlled by us -func (r *ApisixGlobalRuleReconciler) matchesIngressController(obj client.Object) bool { - ingressClass, ok := obj.(*networkingv1.IngressClass) - if !ok { - return false - } - return matchesController(ingressClass.Spec.Controller) -} - // listGlobalRulesForIngressClass list all global rules that use a specific ingress class func (r *ApisixGlobalRuleReconciler) listGlobalRulesForIngressClass(ctx context.Context, obj client.Object) []reconcile.Request { ingressClass, ok := obj.(*networkingv1.IngressClass) @@ -203,165 +192,24 @@ func (r *ApisixGlobalRuleReconciler) listGlobalRulesForIngressClass(ctx context. return nil } - var requests []reconcile.Request - - // List all global rules and filter based on ingress class - globalRuleList := &apiv2.ApisixGlobalRuleList{} - if err := r.List(ctx, globalRuleList); err != nil { - r.Log.Error(err, "failed to list global rules") - return nil - } - - isDefaultClass := IsDefaultIngressClass(ingressClass) - for _, globalRule := range globalRuleList.Items { - if (isDefaultClass && globalRule.Spec.IngressClassName == "") || - globalRule.Spec.IngressClassName == ingressClass.Name { - requests = append(requests, reconcile.Request{ - NamespacedName: client.ObjectKey{ - Namespace: globalRule.Namespace, - Name: globalRule.Name, - }, - }) - } - } - - return requests -} - -// listGlobalRulesForGatewayProxy list all global rules that use a specific gateway proxy -func (r *ApisixGlobalRuleReconciler) listGlobalRulesForGatewayProxy(ctx context.Context, obj client.Object) []reconcile.Request { - gatewayProxy, ok := obj.(*v1alpha1.GatewayProxy) - if !ok { - return nil - } - - // Find all ingress classes that reference this gateway proxy - ingressClassList := &networkingv1.IngressClassList{} - if err := r.List(ctx, ingressClassList, client.MatchingFields{ - indexer.IngressClassParametersRef: indexer.GenIndexKey(gatewayProxy.GetNamespace(), gatewayProxy.GetName()), - }); err != nil { - r.Log.Error(err, "failed to list ingress classes for gateway proxy", "gatewayproxy", gatewayProxy.GetName()) - return nil - } - - var requests []reconcile.Request - for _, ingressClass := range ingressClassList.Items { - requests = append(requests, r.listGlobalRulesForIngressClass(ctx, &ingressClass)...) - } - - // Remove duplicates - uniqueRequests := make(map[string]reconcile.Request) - for _, request := range requests { - uniqueRequests[request.String()] = request - } - - distinctRequests := make([]reconcile.Request, 0, len(uniqueRequests)) - for _, request := range uniqueRequests { - distinctRequests = append(distinctRequests, request) - } - - return distinctRequests -} - -// getIngressClass get the ingress class for the global rule -func (r *ApisixGlobalRuleReconciler) getIngressClass(globalRule *apiv2.ApisixGlobalRule) (*networkingv1.IngressClass, error) { - if globalRule.Spec.IngressClassName == "" { - // Check for default ingress class - ingressClassList := &networkingv1.IngressClassList{} - if err := r.List(context.Background(), ingressClassList, client.MatchingFields{ - indexer.IngressClass: config.GetControllerName(), - }); err != nil { - r.Log.Error(err, "failed to list ingress classes") - return nil, err - } - - // Find the ingress class that is marked as default - for _, ic := range ingressClassList.Items { - if IsDefaultIngressClass(&ic) && matchesController(ic.Spec.Controller) { - return &ic, nil + return ListMatchingRequests( + ctx, + r.Client, + r.Log, + &apiv2.ApisixGlobalRuleList{}, + func(obj client.Object) bool { + agr, ok := obj.(*apiv2.ApisixGlobalRule) + if !ok { + r.Log.Error(fmt.Errorf("expected ApisixGlobalRule, got %T", obj), "failed to match object type") + return false } - } - log.Debugw("no default ingress class found") - return nil, errors.New("no default ingress class found") - } - - // Check if the specified ingress class is controlled by us - var ingressClass networkingv1.IngressClass - if err := r.Get(context.Background(), client.ObjectKey{Name: globalRule.Spec.IngressClassName}, &ingressClass); err != nil { - return nil, err - } - - if matchesController(ingressClass.Spec.Controller) { - return &ingressClass, nil - } - - return nil, errors.New("ingress class is not controlled by us") + return (IsDefaultIngressClass(ingressClass) && agr.Spec.IngressClassName == "") || agr.Spec.IngressClassName == ingressClass.Name + }, + ) } -// processIngressClassParameters processes the IngressClass parameters that reference GatewayProxy -func (r *ApisixGlobalRuleReconciler) processIngressClassParameters(ctx context.Context, tctx *provider.TranslateContext, globalRule *apiv2.ApisixGlobalRule, ingressClass *networkingv1.IngressClass) error { - if ingressClass == nil || ingressClass.Spec.Parameters == nil { - return nil - } - - ingressClassKind := utils.NamespacedNameKind(ingressClass) - globalRuleKind := utils.NamespacedNameKind(globalRule) - - parameters := ingressClass.Spec.Parameters - // check if the parameters reference GatewayProxy - if parameters.APIGroup != nil && *parameters.APIGroup == v1alpha1.GroupVersion.Group && parameters.Kind == KindGatewayProxy { - ns := globalRule.GetNamespace() - if parameters.Namespace != nil { - ns = *parameters.Namespace - } - - gatewayProxy := &v1alpha1.GatewayProxy{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: ns, - Name: parameters.Name, - }, gatewayProxy); err != nil { - r.Log.Error(err, "failed to get GatewayProxy", "namespace", ns, "name", parameters.Name) - return err - } - - r.Log.Info("found GatewayProxy for IngressClass", "ingressClass", ingressClass.Name, "gatewayproxy", gatewayProxy.Name) - tctx.GatewayProxies[ingressClassKind] = *gatewayProxy - tctx.ResourceParentRefs[globalRuleKind] = append(tctx.ResourceParentRefs[globalRuleKind], ingressClassKind) - - // check if the provider field references a secret - if gatewayProxy.Spec.Provider != nil && gatewayProxy.Spec.Provider.Type == v1alpha1.ProviderTypeControlPlane { - if gatewayProxy.Spec.Provider.ControlPlane != nil && - gatewayProxy.Spec.Provider.ControlPlane.Auth.Type == v1alpha1.AuthTypeAdminKey && - gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey != nil && - gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom != nil && - gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom.SecretKeyRef != nil { - - secretRef := gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom.SecretKeyRef - secret := &corev1.Secret{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: ns, - Name: secretRef.Name, - }, secret); err != nil { - r.Log.Error(err, "failed to get secret for GatewayProxy provider", - "namespace", ns, - "name", secretRef.Name) - return err - } - - r.Log.Info("found secret for GatewayProxy provider", - "ingressClass", ingressClass.Name, - "gatewayproxy", gatewayProxy.Name, - "secret", secretRef.Name) - - tctx.Secrets[types.NamespacedName{ - Namespace: ns, - Name: secretRef.Name, - }] = secret - } - } - } - - return nil +func (r *ApisixGlobalRuleReconciler) listGlobalRulesForGatewayProxy(ctx context.Context, obj client.Object) []reconcile.Request { + return listIngressClassRequestsForGatewayProxy(ctx, r.Client, obj, r.Log, r.listGlobalRulesForIngressClass) } // updateStatus updates the ApisixGlobalRule status with the given condition diff --git a/internal/controller/apisixroute_controller.go b/internal/controller/apisixroute_controller.go index 4b52e530d..105cfe669 100644 --- a/internal/controller/apisixroute_controller.go +++ b/internal/controller/apisixroute_controller.go @@ -35,7 +35,6 @@ import ( "github.com/apache/apisix-ingress-controller/api/v1alpha1" apiv2 "github.com/apache/apisix-ingress-controller/api/v2" - "github.com/apache/apisix-ingress-controller/internal/controller/config" "github.com/apache/apisix-ingress-controller/internal/controller/indexer" "github.com/apache/apisix-ingress-controller/internal/controller/status" "github.com/apache/apisix-ingress-controller/internal/provider" @@ -66,9 +65,9 @@ func (r *ApisixRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { ), ). Watches(&networkingv1.IngressClass{}, - handler.EnqueueRequestsFromMapFunc(r.listApiRouteForIngressClass), + handler.EnqueueRequestsFromMapFunc(r.listApisixRouteForIngressClass), builder.WithPredicates( - predicate.NewPredicateFuncs(r.matchesIngressController), + predicate.NewPredicateFuncs(matchesIngressController), ), ). Watches(&v1alpha1.GatewayProxy{}, @@ -119,10 +118,10 @@ func (r *ApisixRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) r.updateStatus(&ar, err) }() - if ic, err = r.getIngressClass(&ar); err != nil { + if ic, err = GetIngressClass(tctx, r.Client, r.Log, ar.Spec.IngressClassName); err != nil { return ctrl.Result{}, err } - if err = r.processIngressClassParameters(ctx, tctx, &ar, ic); err != nil { + if err = ProcessIngressClassParameters(tctx, r.Client, r.Log, &ar, ic); err != nil { return ctrl.Result{}, err } if err = r.processApisixRoute(ctx, tctx, &ar); err != nil { @@ -154,6 +153,16 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov } rules[http.Name] = struct{}{} + // check secret + for _, plugin := range http.Plugins { + if !plugin.Enable { + continue + } + // check secret + if err := r.validateSecrets(ctx, tc, in, plugin.SecretRef); err != nil { + return err + } + } // check plugin config reference if http.PluginConfigName != "" { if err := r.validatePluginConfig(ctx, tc, in, http); err != nil { @@ -161,11 +170,6 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov } } - // check secret - if err := r.validateSecrets(ctx, tc, in, http); err != nil { - return err - } - // check vars if _, err := http.Match.NginxVars.ToVars(); err != nil { return ReasonError{ @@ -237,52 +241,37 @@ func (r *ApisixRouteReconciler) validatePluginConfig(ctx context.Context, tc *pr // Also check secrets referenced by plugin config for _, plugin := range pc.Spec.Plugins { - if !plugin.Enable || plugin.Config == nil || plugin.SecretRef == "" { + if !plugin.Enable { continue } - var ( - secret = corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: plugin.SecretRef, - Namespace: pc.Namespace, - }, - } - secretNN = utils.NamespacedName(&secret) - ) - if err := r.Get(ctx, secretNN, &secret); err != nil { - return ReasonError{ - Reason: string(apiv2.ConditionReasonInvalidSpec), - Message: fmt.Sprintf("failed to get Secret: %s", secretNN), - } + if err := r.validateSecrets(ctx, tc, in, plugin.SecretRef); err != nil { + return err } - tc.Secrets[secretNN] = &secret } return nil } -func (r *ApisixRouteReconciler) validateSecrets(ctx context.Context, tc *provider.TranslateContext, in *apiv2.ApisixRoute, http apiv2.ApisixRouteHTTP) error { - for _, plugin := range http.Plugins { - if !plugin.Enable || plugin.Config == nil || plugin.SecretRef == "" { - continue +func (r *ApisixRouteReconciler) validateSecrets(ctx context.Context, tc *provider.TranslateContext, in *apiv2.ApisixRoute, secretRef string) error { + if secretRef == "" { + return nil + } + var ( + secret = corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretRef, + Namespace: in.Namespace, + }, } - var ( - secret = corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: plugin.SecretRef, - Namespace: in.Namespace, - }, - } - secretNN = utils.NamespacedName(&secret) - ) - if err := r.Get(ctx, secretNN, &secret); err != nil { - return ReasonError{ - Reason: string(apiv2.ConditionReasonInvalidSpec), - Message: fmt.Sprintf("failed to get Secret: %s", secretNN), - } + secretNN = utils.NamespacedName(&secret) + ) + if err := r.Get(ctx, secretNN, &secret); err != nil { + return ReasonError{ + Reason: string(apiv2.ConditionReasonInvalidSpec), + Message: fmt.Sprintf("failed to get Secret: %s", secretNN), } - - tc.Secrets[utils.NamespacedName(&secret)] = &secret } + + tc.Secrets[utils.NamespacedName(&secret)] = &secret return nil } @@ -477,44 +466,30 @@ func (r *ApisixRouteReconciler) listApisixRoutesForSecret(ctx context.Context, o return pkgutils.DedupComparable(allRequests) } -func (r *ApisixRouteReconciler) listApiRouteForIngressClass(ctx context.Context, object client.Object) (requests []reconcile.Request) { - ic, ok := object.(*networkingv1.IngressClass) +func (r *ApisixRouteReconciler) listApisixRouteForIngressClass(ctx context.Context, object client.Object) (requests []reconcile.Request) { + ingressClass, ok := object.(*networkingv1.IngressClass) if !ok { return nil } - isDefaultIngressClass := IsDefaultIngressClass(ic) - var arList apiv2.ApisixRouteList - if err := r.List(ctx, &arList); err != nil { - return nil - } - for _, ar := range arList.Items { - if ar.Spec.IngressClassName == ic.Name || (isDefaultIngressClass && ar.Spec.IngressClassName == "") { - requests = append(requests, reconcile.Request{NamespacedName: utils.NamespacedName(&ar)}) - } - } - return pkgutils.DedupComparable(requests) + return ListMatchingRequests( + ctx, + r.Client, + r.Log, + &apiv2.ApisixRouteList{}, + func(obj client.Object) bool { + ar, ok := obj.(*apiv2.ApisixRoute) + if !ok { + r.Log.Error(fmt.Errorf("expected ApisixRoute, got %T", obj), "failed to match object type") + return false + } + return (IsDefaultIngressClass(ingressClass) && ar.Spec.IngressClassName == "") || ar.Spec.IngressClassName == ingressClass.Name + }, + ) } func (r *ApisixRouteReconciler) listApisixRouteForGatewayProxy(ctx context.Context, object client.Object) (requests []reconcile.Request) { - gp, ok := object.(*v1alpha1.GatewayProxy) - if !ok { - return nil - } - - var icList networkingv1.IngressClassList - if err := r.List(ctx, &icList, client.MatchingFields{ - indexer.IngressClassParametersRef: indexer.GenIndexKey(gp.GetNamespace(), gp.GetName()), - }); err != nil { - r.Log.Error(err, "failed to list ingress classes for gateway proxy", "gatewayproxy", gp.GetName()) - return nil - } - - for _, ic := range icList.Items { - requests = append(requests, r.listApiRouteForIngressClass(ctx, &ic)...) - } - - return pkgutils.DedupComparable(requests) + return listIngressClassRequestsForGatewayProxy(ctx, r.Client, object, r.Log, r.listApisixRouteForIngressClass) } func (r *ApisixRouteReconciler) listApisixRouteForApisixUpstream(ctx context.Context, object client.Object) (requests []reconcile.Request) { @@ -535,109 +510,6 @@ func (r *ApisixRouteReconciler) listApisixRouteForApisixUpstream(ctx context.Con return pkgutils.DedupComparable(requests) } -func (r *ApisixRouteReconciler) matchesIngressController(obj client.Object) bool { - ingressClass, ok := obj.(*networkingv1.IngressClass) - if !ok { - return false - } - return matchesController(ingressClass.Spec.Controller) -} - -func (r *ApisixRouteReconciler) getIngressClass(ar *apiv2.ApisixRoute) (*networkingv1.IngressClass, error) { - if ar.Spec.IngressClassName == "" { - return r.getDefaultIngressClass() - } - - var ic networkingv1.IngressClass - if err := r.Get(context.Background(), client.ObjectKey{Name: ar.Spec.IngressClassName}, &ic); err != nil { - return nil, err - } - return &ic, nil -} - -func (r *ApisixRouteReconciler) getDefaultIngressClass() (*networkingv1.IngressClass, error) { - var icList networkingv1.IngressClassList - if err := r.List(context.Background(), &icList, client.MatchingFields{ - indexer.IngressClass: config.GetControllerName(), - }); err != nil { - r.Log.Error(err, "failed to list ingress classes") - return nil, err - } - for _, ic := range icList.Items { - if IsDefaultIngressClass(&ic) && matchesController(ic.Spec.Controller) { - return &ic, nil - } - } - return nil, ReasonError{ - Reason: string(metav1.StatusReasonNotFound), - Message: "default ingress class not found or dose not match the controller", - } -} - -// processIngressClassParameters processes the IngressClass parameters that reference GatewayProxy -func (r *ApisixRouteReconciler) processIngressClassParameters(ctx context.Context, tc *provider.TranslateContext, ar *apiv2.ApisixRoute, ingressClass *networkingv1.IngressClass) error { - if ingressClass == nil || ingressClass.Spec.Parameters == nil { - return nil - } - - var ( - ingressClassKind = utils.NamespacedNameKind(ingressClass) - globalRuleKind = utils.NamespacedNameKind(ar) - parameters = ingressClass.Spec.Parameters - ) - if parameters.APIGroup == nil || *parameters.APIGroup != v1alpha1.GroupVersion.Group || parameters.Kind != KindGatewayProxy { - return nil - } - - // check if the parameters reference GatewayProxy - var ( - gatewayProxy v1alpha1.GatewayProxy - ns = *cmp.Or(parameters.Namespace, &ar.Namespace) - ) - - if err := r.Get(ctx, client.ObjectKey{Namespace: ns, Name: parameters.Name}, &gatewayProxy); err != nil { - r.Log.Error(err, "failed to get GatewayProxy", "namespace", ns, "name", parameters.Name) - return err - } - - tc.GatewayProxies[ingressClassKind] = gatewayProxy - tc.ResourceParentRefs[globalRuleKind] = append(tc.ResourceParentRefs[globalRuleKind], ingressClassKind) - - // check if the provider field references a secret - if gatewayProxy.Spec.Provider != nil && gatewayProxy.Spec.Provider.Type == v1alpha1.ProviderTypeControlPlane { - if gatewayProxy.Spec.Provider.ControlPlane != nil && - gatewayProxy.Spec.Provider.ControlPlane.Auth.Type == v1alpha1.AuthTypeAdminKey && - gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey != nil && - gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom != nil && - gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom.SecretKeyRef != nil { - - secretRef := gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom.SecretKeyRef - secret := &corev1.Secret{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: ns, - Name: secretRef.Name, - }, secret); err != nil { - r.Log.Error(err, "failed to get secret for GatewayProxy provider", - "namespace", ns, - "name", secretRef.Name) - return err - } - - r.Log.Info("found secret for GatewayProxy provider", - "ingressClass", ingressClass.Name, - "gatewayproxy", gatewayProxy.Name, - "secret", secretRef.Name) - - tc.Secrets[types.NamespacedName{ - Namespace: ns, - Name: secretRef.Name, - }] = secret - } - } - - return nil -} - func (r *ApisixRouteReconciler) updateStatus(ar *apiv2.ApisixRoute, err error) { SetApisixCRDConditionAccepted(&ar.Status, ar.GetGeneration(), err) r.Updater.Update(status.Update{ diff --git a/internal/controller/apisixtls_controller.go b/internal/controller/apisixtls_controller.go index 08d631602..dc4b614a3 100644 --- a/internal/controller/apisixtls_controller.go +++ b/internal/controller/apisixtls_controller.go @@ -14,7 +14,6 @@ package controller import ( "context" - "errors" "fmt" "github.com/api7/gopkg/pkg/log" @@ -67,7 +66,7 @@ func (r *ApisixTlsReconciler) SetupWithManager(mgr ctrl.Manager) error { &networkingv1.IngressClass{}, handler.EnqueueRequestsFromMapFunc(r.listApisixTlsForIngressClass), builder.WithPredicates( - predicate.NewPredicateFuncs(r.matchesIngressController), + predicate.NewPredicateFuncs(matchesIngressController), ), ). Watches(&v1alpha1.GatewayProxy{}, @@ -108,7 +107,7 @@ func (r *ApisixTlsReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( tctx := provider.NewDefaultTranslateContext(ctx) // get the ingress class - ingressClass, err := r.getIngressClass(&tls) + ingressClass, err := GetIngressClass(tctx, r.Client, r.Log, tls.Spec.IngressClassName) if err != nil { log.Error(err, "failed to get IngressClass") r.updateStatus(&tls, metav1.Condition{ @@ -123,7 +122,7 @@ func (r *ApisixTlsReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( } // process IngressClass parameters if they reference GatewayProxy - if err := r.processIngressClassParameters(ctx, tctx, &tls, ingressClass); err != nil { + if err := ProcessIngressClassParameters(tctx, r.Client, r.Log, &tls, ingressClass); err != nil { log.Error(err, "failed to process IngressClass parameters", "ingressClass", ingressClass.Name) r.updateStatus(&tls, metav1.Condition{ Type: string(apiv2.ConditionTypeAccepted), @@ -208,107 +207,6 @@ func (r *ApisixTlsReconciler) validateSecret(ctx context.Context, tc *provider.T return nil } -// getIngressClass get the ingress class for the TLS -func (r *ApisixTlsReconciler) getIngressClass(tls *apiv2.ApisixTls) (*networkingv1.IngressClass, error) { - if tls.Spec.IngressClassName == "" { - // Check for default ingress class - ingressClassList := &networkingv1.IngressClassList{} - if err := r.List(context.Background(), ingressClassList, client.MatchingFields{ - indexer.IngressClass: config.GetControllerName(), - }); err != nil { - r.Log.Error(err, "failed to list ingress classes") - return nil, err - } - - // Find the ingress class that is marked as default - for _, ic := range ingressClassList.Items { - if IsDefaultIngressClass(&ic) && matchesController(ic.Spec.Controller) { - return &ic, nil - } - } - log.Debugw("no default ingress class found") - return nil, errors.New("no default ingress class found") - } - - // Check if the specified ingress class is controlled by us - var ingressClass networkingv1.IngressClass - if err := r.Get(context.Background(), client.ObjectKey{Name: tls.Spec.IngressClassName}, &ingressClass); err != nil { - return nil, err - } - - if matchesController(ingressClass.Spec.Controller) { - return &ingressClass, nil - } - - return nil, errors.New("ingress class is not controlled by us") -} - -// processIngressClassParameters processes the IngressClass parameters that reference GatewayProxy -func (r *ApisixTlsReconciler) processIngressClassParameters(ctx context.Context, tctx *provider.TranslateContext, tls *apiv2.ApisixTls, ingressClass *networkingv1.IngressClass) error { - if ingressClass == nil || ingressClass.Spec.Parameters == nil { - return nil - } - - ingressClassKind := utils.NamespacedNameKind(ingressClass) - tlsKind := utils.NamespacedNameKind(tls) - - parameters := ingressClass.Spec.Parameters - // check if the parameters reference GatewayProxy - if parameters.APIGroup != nil && *parameters.APIGroup == v1alpha1.GroupVersion.Group && parameters.Kind == KindGatewayProxy { - ns := tls.GetNamespace() - if parameters.Namespace != nil { - ns = *parameters.Namespace - } - - gatewayProxy := &v1alpha1.GatewayProxy{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: ns, - Name: parameters.Name, - }, gatewayProxy); err != nil { - r.Log.Error(err, "failed to get GatewayProxy", "namespace", ns, "name", parameters.Name) - return err - } - - r.Log.Info("found GatewayProxy for IngressClass", "ingressClass", ingressClass.Name, "gatewayproxy", gatewayProxy.Name) - tctx.GatewayProxies[ingressClassKind] = *gatewayProxy - tctx.ResourceParentRefs[tlsKind] = append(tctx.ResourceParentRefs[tlsKind], ingressClassKind) - - // check if the provider field references a secret - if gatewayProxy.Spec.Provider != nil && gatewayProxy.Spec.Provider.Type == v1alpha1.ProviderTypeControlPlane { - if gatewayProxy.Spec.Provider.ControlPlane != nil && - gatewayProxy.Spec.Provider.ControlPlane.Auth.Type == v1alpha1.AuthTypeAdminKey && - gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey != nil && - gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom != nil && - gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom.SecretKeyRef != nil { - - secretRef := gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom.SecretKeyRef - secret := &corev1.Secret{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: ns, - Name: secretRef.Name, - }, secret); err != nil { - r.Log.Error(err, "failed to get secret for GatewayProxy provider", - "namespace", ns, - "name", secretRef.Name) - return err - } - - r.Log.Info("found secret for GatewayProxy provider", - "ingressClass", ingressClass.Name, - "gatewayproxy", gatewayProxy.Name, - "secret", secretRef.Name) - - tctx.Secrets[types.NamespacedName{ - Namespace: ns, - Name: secretRef.Name, - }] = secret - } - } - } - - return nil -} - // updateStatus updates the ApisixTls status with the given condition func (r *ApisixTlsReconciler) updateStatus(tls *apiv2.ApisixTls, condition metav1.Condition) { r.Updater.Update(status.Update{ @@ -374,32 +272,15 @@ func (r *ApisixTlsReconciler) listApisixTlsForSecret(ctx context.Context, obj cl return nil } - // Use index to find all ApisixTls that reference this secret - var tlsList apiv2.ApisixTlsList - if err := r.List(ctx, &tlsList, client.MatchingFields{ - indexer.SecretIndexRef: indexer.GenIndexKey(secret.Namespace, secret.Name), - }); err != nil { - r.Log.Error(err, "failed to list ApisixTls by secret index") - return nil - } - - requests := make([]reconcile.Request, 0, len(tlsList.Items)) - for _, tls := range tlsList.Items { - requests = append(requests, reconcile.Request{ - NamespacedName: utils.NamespacedName(&tls), - }) - } - - return requests -} - -func (r *ApisixTlsReconciler) matchesIngressController(obj client.Object) bool { - ingressClass, ok := obj.(*networkingv1.IngressClass) - if !ok { - r.Log.Error(fmt.Errorf("unexpected object type"), "failed to convert object to IngressClass") - return false - } - return matchesController(ingressClass.Spec.Controller) + return ListRequests( + ctx, + r.Client, + r.Log, + &apiv2.ApisixConsumerList{}, + client.MatchingFields{ + indexer.SecretIndexRef: indexer.GenIndexKey(secret.GetNamespace(), secret.GetName()), + }, + ) } // listApisixTlsForIngressClass list all TLS that use a specific ingress class @@ -409,73 +290,23 @@ func (r *ApisixTlsReconciler) listApisixTlsForIngressClass(ctx context.Context, return nil } - // Use index to find all ApisixTls that reference this ingress class - tlsList := &apiv2.ApisixTlsList{} - requests := make([]reconcile.Request, 0, len(tlsList.Items)) - if err := r.List(ctx, tlsList, client.MatchingFields{ - indexer.IngressClassRef: ingressClass.Name, - }); err != nil { - r.Log.Error(err, "failed to list ApisixTls by ingress class index") - return nil - } - - for _, tls := range tlsList.Items { - requests = append(requests, reconcile.Request{ - NamespacedName: utils.NamespacedName(&tls), - }) - } - - // If this is the default ingress class, also find TLS with empty ingress class - if IsDefaultIngressClass(ingressClass) { - var tlsListWithoutClass apiv2.ApisixTlsList - if err := r.List(ctx, &tlsListWithoutClass); err != nil { - r.Log.Error(err, "failed to list all ApisixTls") - return requests - } - - for _, tls := range tlsListWithoutClass.Items { - if tls.Spec.IngressClassName == "" { - requests = append(requests, reconcile.Request{ - NamespacedName: utils.NamespacedName(&tls), - }) + return ListMatchingRequests( + ctx, + r.Client, + r.Log, + &apiv2.ApisixTlsList{}, + func(obj client.Object) bool { + atls, ok := obj.(*apiv2.ApisixTls) + if !ok { + r.Log.Error(fmt.Errorf("expected ApisixTls, got %T", obj), "failed to match object type") + return false } - } - } - - return requests + return (IsDefaultIngressClass(ingressClass) && atls.Spec.IngressClassName == "") || atls.Spec.IngressClassName == ingressClass.Name + }, + ) } // listApisixTlsForGatewayProxy list all TLS that use a specific gateway proxy func (r *ApisixTlsReconciler) listApisixTlsForGatewayProxy(ctx context.Context, obj client.Object) []reconcile.Request { - gatewayProxy, ok := obj.(*v1alpha1.GatewayProxy) - if !ok { - return nil - } - - // Find all ingress classes that reference this gateway proxy - ingressClassList := &networkingv1.IngressClassList{} - if err := r.List(ctx, ingressClassList, client.MatchingFields{ - indexer.IngressClassParametersRef: indexer.GenIndexKey(gatewayProxy.GetNamespace(), gatewayProxy.GetName()), - }); err != nil { - r.Log.Error(err, "failed to list ingress classes for gateway proxy", "gatewayproxy", gatewayProxy.GetName()) - return nil - } - - var requests []reconcile.Request - for _, ingressClass := range ingressClassList.Items { - requests = append(requests, r.listApisixTlsForIngressClass(ctx, &ingressClass)...) - } - - // Remove duplicates - uniqueRequests := make(map[string]reconcile.Request) - for _, request := range requests { - uniqueRequests[request.String()] = request - } - - distinctRequests := make([]reconcile.Request, 0, len(uniqueRequests)) - for _, request := range uniqueRequests { - distinctRequests = append(distinctRequests, request) - } - - return distinctRequests + return listIngressClassRequestsForGatewayProxy(ctx, r.Client, obj, r.Log, r.listApisixTlsForIngressClass) } diff --git a/internal/controller/consumer_controller.go b/internal/controller/consumer_controller.go index d82c8fe91..8f03496c0 100644 --- a/internal/controller/consumer_controller.go +++ b/internal/controller/consumer_controller.go @@ -100,23 +100,15 @@ func (r *ConsumerReconciler) listConsumersForSecret(ctx context.Context, obj cli r.Log.Error(nil, "failed to convert to Secret", "object", obj) return nil } - consumerList := &v1alpha1.ConsumerList{} - if err := r.List(ctx, consumerList, client.MatchingFields{ - indexer.SecretIndexRef: indexer.GenIndexKey(secret.GetNamespace(), secret.GetName()), - }); err != nil { - r.Log.Error(err, "failed to list consumers") - return nil - } - requests := make([]reconcile.Request, 0, len(consumerList.Items)) - for _, consumer := range consumerList.Items { - requests = append(requests, reconcile.Request{ - NamespacedName: client.ObjectKey{ - Name: consumer.Name, - Namespace: consumer.Namespace, - }, - }) - } - return requests + return ListRequests( + ctx, + r.Client, + r.Log, + &v1alpha1.ConsumerList{}, + client.MatchingFields{ + indexer.SecretIndexRef: indexer.GenIndexKey(secret.GetNamespace(), secret.GetName()), + }, + ) } func (r *ConsumerReconciler) listConsumersForGateway(ctx context.Context, obj client.Object) []reconcile.Request { diff --git a/internal/controller/httproutepolicy.go b/internal/controller/httproutepolicy.go index 863555f60..8c2eee147 100644 --- a/internal/controller/httproutepolicy.go +++ b/internal/controller/httproutepolicy.go @@ -202,7 +202,7 @@ func (r *IngressReconciler) updateHTTPRoutePolicyStatusOnDeleting(ctx context.Co if err := r.Get(ctx, namespacedName, &ingress); err != nil { continue } - ingressClass, err := r.getIngressClass(&ingress) + ingressClass, err := r.getIngressClass(ctx, &ingress) if err != nil { continue } diff --git a/internal/controller/indexer/indexer.go b/internal/controller/indexer/indexer.go index 1f0354aa0..6bc8d13b2 100644 --- a/internal/controller/indexer/indexer.go +++ b/internal/controller/indexer/indexer.go @@ -16,6 +16,7 @@ import ( "cmp" "context" + corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" @@ -57,6 +58,7 @@ func SetupIndexer(mgr ctrl.Manager) error { setupApisixRouteIndexer, setupApisixPluginConfigIndexer, setupApisixTlsIndexer, + setupApisixConsumerIndexer, } { if err := setup(mgr); err != nil { return err @@ -125,6 +127,18 @@ func setupApisixPluginConfigIndexer(mgr ctrl.Manager) error { return nil } +func setupApisixConsumerIndexer(mgr ctrl.Manager) error { + if err := mgr.GetFieldIndexer().IndexField( + context.Background(), + &apiv2.ApisixConsumer{}, + SecretIndexRef, + ApisixConsumerSecretIndexFunc, + ); err != nil { + return err + } + return nil +} + func ConsumerSecretIndexFunc(rawObj client.Object) []string { consumer := rawObj.(*v1alpha1.Consumer) secretKeys := make([]string, 0) @@ -640,6 +654,28 @@ func ApisixPluginConfigSecretIndexFunc(obj client.Object) (keys []string) { return } +func ApisixConsumerSecretIndexFunc(rawObj client.Object) (keys []string) { + ac := rawObj.(*apiv2.ApisixConsumer) + var secretRef *corev1.LocalObjectReference + if ac.Spec.AuthParameter.KeyAuth != nil { + secretRef = ac.Spec.AuthParameter.KeyAuth.SecretRef + } else if ac.Spec.AuthParameter.BasicAuth != nil { + secretRef = ac.Spec.AuthParameter.BasicAuth.SecretRef + } else if ac.Spec.AuthParameter.JwtAuth != nil { + secretRef = ac.Spec.AuthParameter.JwtAuth.SecretRef + } else if ac.Spec.AuthParameter.WolfRBAC != nil { + secretRef = ac.Spec.AuthParameter.WolfRBAC.SecretRef + } else if ac.Spec.AuthParameter.HMACAuth != nil { + secretRef = ac.Spec.AuthParameter.HMACAuth.SecretRef + } else if ac.Spec.AuthParameter.LDAPAuth != nil { + secretRef = ac.Spec.AuthParameter.LDAPAuth.SecretRef + } + if secretRef != nil { + keys = append(keys, GenIndexKey(ac.GetNamespace(), secretRef.Name)) + } + return +} + func setupApisixTlsIndexer(mgr ctrl.Manager) error { // Create secret index for ApisixTls if err := mgr.GetFieldIndexer().IndexField( diff --git a/internal/controller/ingress_controller.go b/internal/controller/ingress_controller.go index 74403089a..8380d3650 100644 --- a/internal/controller/ingress_controller.go +++ b/internal/controller/ingress_controller.go @@ -14,7 +14,6 @@ package controller import ( "context" - "errors" "fmt" "reflect" @@ -39,7 +38,6 @@ import ( gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/apache/apisix-ingress-controller/api/v1alpha1" - "github.com/apache/apisix-ingress-controller/internal/controller/config" "github.com/apache/apisix-ingress-controller/internal/controller/indexer" "github.com/apache/apisix-ingress-controller/internal/controller/status" "github.com/apache/apisix-ingress-controller/internal/provider" @@ -144,7 +142,7 @@ func (r *IngressReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct // create a translate context tctx := provider.NewDefaultTranslateContext(ctx) - ingressClass, err := r.getIngressClass(ingress) + ingressClass, err := r.getIngressClass(ctx, ingress) if err != nil { r.Log.Error(err, "failed to get IngressClass") return ctrl.Result{}, err @@ -157,7 +155,7 @@ func (r *IngressReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct }) // process IngressClass parameters if they reference GatewayProxy - if err := r.processIngressClassParameters(ctx, tctx, ingress, ingressClass); err != nil { + if err := ProcessIngressClassParameters(tctx, r.Client, r.Log, ingress, ingressClass); err != nil { r.Log.Error(err, "failed to process IngressClass parameters", "ingressClass", ingressClass.Name) return ctrl.Result{}, err } @@ -201,48 +199,18 @@ func (r *IngressReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct } // getIngressClass get the ingress class for the ingress -func (r *IngressReconciler) getIngressClass(obj client.Object) (*networkingv1.IngressClass, error) { +func (r *IngressReconciler) getIngressClass(ctx context.Context, obj client.Object) (*networkingv1.IngressClass, error) { ingress := obj.(*networkingv1.Ingress) - - if ingress.Spec.IngressClassName == nil { - // handle the case where IngressClassName is not specified - // find all ingress classes and check if any of them is marked as default - ingressClassList := &networkingv1.IngressClassList{} - if err := r.List(context.Background(), ingressClassList, client.MatchingFields{ - indexer.IngressClass: config.GetControllerName(), - }); err != nil { - r.Log.Error(err, "failed to list ingress classes") - return nil, err - } - - // find the ingress class that is marked as default - for _, ic := range ingressClassList.Items { - if IsDefaultIngressClass(&ic) && matchesController(ic.Spec.Controller) { - log.Debugw("match the default ingress class") - return &ic, nil - } - } - - log.Debugw("no default ingress class found") - return nil, errors.New("no default ingress class found") + var ingressClassName string + if ingress.Spec.IngressClassName != nil { + ingressClassName = *ingress.Spec.IngressClassName } - - // if it does not match, check if the ingress class is controlled by us - ingressClass := networkingv1.IngressClass{} - if err := r.Get(context.Background(), client.ObjectKey{Name: *ingress.Spec.IngressClassName}, &ingressClass); err != nil { - return nil, err - } - - if matchesController(ingressClass.Spec.Controller) { - return &ingressClass, nil - } - - return nil, errors.New("ingress class is not controlled by us") + return GetIngressClass(ctx, r.Client, r.Log, ingressClassName) } // checkIngressClass check if the ingress uses the ingress class that we control func (r *IngressReconciler) checkIngressClass(obj client.Object) bool { - _, err := r.getIngressClass(obj) + _, err := r.getIngressClass(context.Background(), obj) return err == nil } @@ -680,118 +648,7 @@ func (r *IngressReconciler) updateStatus(ctx context.Context, tctx *provider.Tra return nil } -// processIngressClassParameters processes the IngressClass parameters that reference GatewayProxy -func (r *IngressReconciler) processIngressClassParameters(ctx context.Context, tctx *provider.TranslateContext, ingress *networkingv1.Ingress, ingressClass *networkingv1.IngressClass) error { - if ingressClass.Spec.Parameters == nil { - return nil - } - - ingressClassKind := utils.NamespacedNameKind(ingressClass) - ingressKind := utils.NamespacedNameKind(ingress) - - parameters := ingressClass.Spec.Parameters - // check if the parameters reference GatewayProxy - if parameters.APIGroup != nil && *parameters.APIGroup == v1alpha1.GroupVersion.Group && parameters.Kind == KindGatewayProxy { - ns := ingress.GetNamespace() - if parameters.Namespace != nil { - ns = *parameters.Namespace - } - - gatewayProxy := &v1alpha1.GatewayProxy{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: ns, - Name: parameters.Name, - }, gatewayProxy); err != nil { - r.Log.Error(err, "failed to get GatewayProxy", "namespace", ns, "name", parameters.Name) - return err - } - - r.Log.Info("found GatewayProxy for IngressClass", "ingressClass", ingressClass.Name, "gatewayproxy", gatewayProxy.Name) - tctx.GatewayProxies[ingressClassKind] = *gatewayProxy - tctx.ResourceParentRefs[ingressKind] = append(tctx.ResourceParentRefs[ingressKind], ingressClassKind) - - // check if the provider field references a secret - if gatewayProxy.Spec.Provider != nil && gatewayProxy.Spec.Provider.Type == v1alpha1.ProviderTypeControlPlane { - if gatewayProxy.Spec.Provider.ControlPlane != nil && - gatewayProxy.Spec.Provider.ControlPlane.Auth.Type == v1alpha1.AuthTypeAdminKey && - gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey != nil && - gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom != nil && - gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom.SecretKeyRef != nil { - - secretRef := gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom.SecretKeyRef - secret := &corev1.Secret{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: ns, - Name: secretRef.Name, - }, secret); err != nil { - r.Log.Error(err, "failed to get secret for GatewayProxy provider", - "namespace", ns, - "name", secretRef.Name) - return err - } - - r.Log.Info("found secret for GatewayProxy provider", - "ingressClass", ingressClass.Name, - "gatewayproxy", gatewayProxy.Name, - "secret", secretRef.Name) - - tctx.Secrets[types.NamespacedName{ - Namespace: ns, - Name: secretRef.Name, - }] = secret - } - } - } - - // if gateway proxy is not found, return error - _, ok := tctx.GatewayProxies[ingressClassKind] - if !ok { - r.Log.Error(fmt.Errorf("no gateway proxy found for ingress class"), "failed to process IngressClass parameters", "ingressClass", ingressClass.Name) - return fmt.Errorf("no gateway proxy found for ingress class") - } - - return nil -} - // listIngressesForGatewayProxy list all ingresses that use a specific gateway proxy func (r *IngressReconciler) listIngressesForGatewayProxy(ctx context.Context, obj client.Object) []reconcile.Request { - gatewayProxy, ok := obj.(*v1alpha1.GatewayProxy) - if !ok { - r.Log.Error(fmt.Errorf("unexpected object type"), "failed to convert object to GatewayProxy") - return nil - } - - // find all ingress classes that reference this gateway proxy - ingressClassList := &networkingv1.IngressClassList{} - if err := r.List(ctx, ingressClassList, client.MatchingFields{ - indexer.IngressClassParametersRef: indexer.GenIndexKey(gatewayProxy.GetNamespace(), gatewayProxy.GetName()), - }); err != nil { - r.Log.Error(err, "failed to list ingress classes for gateway proxy", "gatewayproxy", gatewayProxy.GetName()) - return nil - } - - var requests []reconcile.Request - - for _, ingressClass := range ingressClassList.Items { - requests = append(requests, r.listIngressForIngressClass(ctx, &ingressClass)...) - } - - // the requests may contain duplicates, distinct the requests - requests = distinctRequests(requests) - - return requests -} - -// distinctRequests distinct the requests -func distinctRequests(requests []reconcile.Request) []reconcile.Request { - uniqueRequests := make(map[string]reconcile.Request) - for _, request := range requests { - uniqueRequests[request.String()] = request - } - - distinctRequests := make([]reconcile.Request, 0, len(uniqueRequests)) - for _, request := range uniqueRequests { - distinctRequests = append(distinctRequests, request) - } - return distinctRequests + return listIngressClassRequestsForGatewayProxy(ctx, r.Client, obj, r.Log, r.listIngressForIngressClass) } diff --git a/internal/controller/utils.go b/internal/controller/utils.go index 6be766041..049774455 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -24,10 +24,12 @@ import ( "strings" "github.com/api7/gopkg/pkg/log" + "github.com/go-logr/logr" "github.com/samber/lo" "go.uber.org/zap" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" k8stypes "k8s.io/apimachinery/pkg/types" @@ -41,6 +43,7 @@ import ( "github.com/apache/apisix-ingress-controller/api/v1alpha1" apiv2 "github.com/apache/apisix-ingress-controller/api/v2" "github.com/apache/apisix-ingress-controller/internal/controller/config" + "github.com/apache/apisix-ingress-controller/internal/controller/indexer" "github.com/apache/apisix-ingress-controller/internal/provider" "github.com/apache/apisix-ingress-controller/internal/types" "github.com/apache/apisix-ingress-controller/internal/utils" @@ -59,6 +62,7 @@ const ( KindApisixGlobalRule = "ApisixGlobalRule" KindApisixPluginConfig = "ApisixPluginConfig" KindApisixTls = "ApisixTls" + KindApisixConsumer = "ApisixConsumer" ) const defaultIngressClassAnnotation = "ingressclass.kubernetes.io/is-default-class" @@ -1172,3 +1176,241 @@ func checkReferenceGrant(ctx context.Context, cli client.Client, obj v1beta1.Ref } return false } + +func ListRequests( + ctx context.Context, + c client.Client, + logger logr.Logger, + listObj client.ObjectList, + opts ...client.ListOption, +) []reconcile.Request { + return ListMatchingRequests( + ctx, + c, + logger, + listObj, + func(obj client.Object) bool { + return true + }, + opts..., + ) +} + +func ListMatchingRequests( + ctx context.Context, + c client.Client, + logger logr.Logger, + listObj client.ObjectList, + matchFunc func(obj client.Object) bool, + opts ...client.ListOption, +) []reconcile.Request { + if err := c.List(ctx, listObj); err != nil { + logger.Error(err, "failed to list resource") + return nil + } + + items, err := meta.ExtractList(listObj) + if err != nil { + logger.Error(err, "failed to extract list items") + return nil + } + + var requests []reconcile.Request + for _, item := range items { + obj, ok := item.(client.Object) + if !ok { + continue + } + + if matchFunc(obj) { + requests = append(requests, reconcile.Request{ + NamespacedName: utils.NamespacedName(obj), + }) + } + } + return requests +} + +func listIngressClassRequestsForGatewayProxy( + ctx context.Context, + c client.Client, + obj client.Object, + logger logr.Logger, + listFunc func(context.Context, client.Object) []reconcile.Request, +) []reconcile.Request { + gatewayProxy, ok := obj.(*v1alpha1.GatewayProxy) + if !ok { + return nil + } + + ingressClassList := &networkingv1.IngressClassList{} + if err := c.List(ctx, ingressClassList, client.MatchingFields{ + indexer.IngressClassParametersRef: indexer.GenIndexKey(gatewayProxy.GetNamespace(), gatewayProxy.GetName()), + }); err != nil { + logger.Error(err, "failed to list ingress classes for gateway proxy", "gatewayproxy", gatewayProxy.GetName()) + return nil + } + + requestSet := make(map[string]reconcile.Request) + for _, ingressClass := range ingressClassList.Items { + for _, req := range listFunc(ctx, &ingressClass) { + requestSet[req.String()] = req + } + } + + requests := make([]reconcile.Request, 0, len(requestSet)) + for _, req := range requestSet { + requests = append(requests, req) + } + return requests +} + +func matchesIngressController(obj client.Object) bool { + ingressClass, ok := obj.(*networkingv1.IngressClass) + if !ok { + return false + } + return matchesController(ingressClass.Spec.Controller) +} + +func matchesIngressClass(c client.Client, log logr.Logger, ingressClassName string) bool { + if ingressClassName == "" { + // Check for default ingress class + ingressClassList := &networkingv1.IngressClassList{} + if err := c.List(context.Background(), ingressClassList, client.MatchingFields{ + indexer.IngressClass: config.GetControllerName(), + }); err != nil { + log.Error(err, "failed to list ingress classes") + return false + } + + // Find the ingress class that is marked as default + for _, ic := range ingressClassList.Items { + if IsDefaultIngressClass(&ic) && matchesController(ic.Spec.Controller) { + return true + } + } + return false + } + + // Check if the specified ingress class is controlled by us + var ingressClass networkingv1.IngressClass + if err := c.Get(context.Background(), client.ObjectKey{Name: ingressClassName}, &ingressClass); err != nil { + log.Error(err, "failed to get ingress class", "ingressClass", ingressClassName) + return false + } + + return matchesController(ingressClass.Spec.Controller) +} + +func ProcessIngressClassParameters(tctx *provider.TranslateContext, c client.Client, log logr.Logger, object client.Object, ingressClass *networkingv1.IngressClass) error { + if ingressClass == nil || ingressClass.Spec.Parameters == nil { + return nil + } + + ingressClassKind := utils.NamespacedNameKind(ingressClass) + objKind := utils.NamespacedNameKind(object) + + parameters := ingressClass.Spec.Parameters + // check if the parameters reference GatewayProxy + if parameters.APIGroup != nil && *parameters.APIGroup == v1alpha1.GroupVersion.Group && parameters.Kind == KindGatewayProxy { + ns := object.GetNamespace() + if parameters.Namespace != nil { + ns = *parameters.Namespace + } + + gatewayProxy := &v1alpha1.GatewayProxy{} + if err := c.Get(tctx, client.ObjectKey{ + Namespace: ns, + Name: parameters.Name, + }, gatewayProxy); err != nil { + log.Error(err, "failed to get GatewayProxy", "namespace", ns, "name", parameters.Name) + return err + } + + log.Info("found GatewayProxy for IngressClass", "ingressClass", ingressClass.Name, "gatewayproxy", gatewayProxy.Name) + tctx.GatewayProxies[ingressClassKind] = *gatewayProxy + tctx.ResourceParentRefs[objKind] = append(tctx.ResourceParentRefs[objKind], ingressClassKind) + + // check if the provider field references a secret + if gatewayProxy.Spec.Provider != nil && gatewayProxy.Spec.Provider.Type == v1alpha1.ProviderTypeControlPlane { + if gatewayProxy.Spec.Provider.ControlPlane != nil && + gatewayProxy.Spec.Provider.ControlPlane.Auth.Type == v1alpha1.AuthTypeAdminKey && + gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey != nil && + gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom != nil && + gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom.SecretKeyRef != nil { + + secretRef := gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom.SecretKeyRef + secret := &corev1.Secret{} + if err := c.Get(tctx, client.ObjectKey{ + Namespace: ns, + Name: secretRef.Name, + }, secret); err != nil { + log.Error(err, "failed to get secret for GatewayProxy provider", + "namespace", ns, + "name", secretRef.Name) + return err + } + + log.Info("found secret for GatewayProxy provider", + "ingressClass", ingressClass.Name, + "gatewayproxy", gatewayProxy.Name, + "secret", secretRef.Name) + + tctx.Secrets[k8stypes.NamespacedName{ + Namespace: ns, + Name: secretRef.Name, + }] = secret + } + } + } + + return nil +} + +func GetIngressClass(ctx context.Context, c client.Client, log logr.Logger, ingressClassName string) (*networkingv1.IngressClass, error) { + if ingressClassName == "" { + // Check for default ingress class + ingressClassList := &networkingv1.IngressClassList{} + if err := c.List(ctx, ingressClassList, client.MatchingFields{ + indexer.IngressClass: config.GetControllerName(), + }); err != nil { + log.Error(err, "failed to list ingress classes") + return nil, err + } + + // Find the ingress class that is marked as default + for _, ic := range ingressClassList.Items { + if IsDefaultIngressClass(&ic) && matchesController(ic.Spec.Controller) { + return &ic, nil + } + } + return nil, errors.New("no default ingress class found") + } + + // Check if the specified ingress class is controlled by us + var ingressClass networkingv1.IngressClass + if err := c.Get(ctx, client.ObjectKey{Name: ingressClassName}, &ingressClass); err != nil { + return nil, err + } + + if matchesController(ingressClass.Spec.Controller) { + return &ingressClass, nil + } + + return nil, errors.New("ingress class is not controlled by us") +} + +// distinctRequests distinct the requests +func distinctRequests(requests []reconcile.Request) []reconcile.Request { + uniqueRequests := make(map[string]reconcile.Request) + for _, request := range requests { + uniqueRequests[request.String()] = request + } + + distinctRequests := make([]reconcile.Request, 0, len(uniqueRequests)) + for _, request := range uniqueRequests { + distinctRequests = append(distinctRequests, request) + } + return distinctRequests +} diff --git a/internal/manager/controllers.go b/internal/manager/controllers.go index 5fb93bbcd..c56e23dac 100644 --- a/internal/manager/controllers.go +++ b/internal/manager/controllers.go @@ -134,6 +134,13 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro Provider: pro, Updater: updater, }, + &controller.ApisixConsumerReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixConsumer"), + Provider: pro, + Updater: updater, + }, &controller.ApisixPluginConfigReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), diff --git a/internal/provider/adc/adc.go b/internal/provider/adc/adc.go index 0b426feb7..463202bd2 100644 --- a/internal/provider/adc/adc.go +++ b/internal/provider/adc/adc.go @@ -125,6 +125,9 @@ func (d *adcClient) Update(ctx context.Context, tctx *provider.TranslateContext, case *apiv2.ApisixTls: result, err = d.translator.TranslateApisixTls(tctx, t.DeepCopy()) resourceTypes = append(resourceTypes, "ssl") + case *apiv2.ApisixConsumer: + result, err = d.translator.TranslateApisixConsumer(tctx, t.DeepCopy()) + resourceTypes = append(resourceTypes, "consumer") } if err != nil { return err @@ -223,6 +226,9 @@ func (d *adcClient) Delete(ctx context.Context, obj client.Object) error { case *apiv2.ApisixTls: resourceTypes = append(resourceTypes, "ssl") labels = label.GenLabel(obj) + case *apiv2.ApisixConsumer: + resourceTypes = append(resourceTypes, "consumer") + labels = label.GenLabel(obj) } rk := utils.NamespacedNameKind(obj) diff --git a/internal/provider/adc/translator/apisixconsumer.go b/internal/provider/adc/translator/apisixconsumer.go new file mode 100644 index 000000000..5c3f44066 --- /dev/null +++ b/internal/provider/adc/translator/apisixconsumer.go @@ -0,0 +1,360 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package translator + +import ( + "fmt" + "strconv" + + "github.com/pkg/errors" + k8stypes "k8s.io/apimachinery/pkg/types" + + adctypes "github.com/apache/apisix-ingress-controller/api/adc" + v2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/internal/controller/label" + "github.com/apache/apisix-ingress-controller/internal/provider" +) + +var ( + _errKeyNotFoundOrInvalid = errors.New("key \"key\" not found or invalid in secret") + _errUsernameNotFoundOrInvalid = errors.New("key \"username\" not found or invalid in secret") + _errPasswordNotFoundOrInvalid = errors.New("key \"password\" not found or invalid in secret") +) + +const ( + _jwtAuthExpDefaultValue = 86400 + + _hmacAuthAlgorithmDefaultValue = "hmac-sha256" + _hmacAuthClockSkewDefaultValue = int64(0) + _hmacAuthKeepHeadersDefaultValue = false + _hmacAuthEncodeURIParamsDefaultValue = true + _hmacAuthValidateRequestBodyDefaultValue = false + _hmacAuthMaxReqBodyDefaultValue = 524288 + + _true = "true" +) + +func (t *Translator) TranslateApisixConsumer(tctx *provider.TranslateContext, ac *v2.ApisixConsumer) (*TranslateResult, error) { + result := &TranslateResult{} + plugins := make(adctypes.Plugins) + if ac.Spec.AuthParameter.KeyAuth != nil { + cfg, err := t.translateConsumerKeyAuthPlugin(tctx, ac.Namespace, ac.Spec.AuthParameter.KeyAuth) + if err != nil { + return nil, fmt.Errorf("invalid key auth config: %s", err) + } + plugins["key-auth"] = cfg + } else if ac.Spec.AuthParameter.BasicAuth != nil { + cfg, err := t.translateConsumerBasicAuthPlugin(tctx, ac.Namespace, ac.Spec.AuthParameter.BasicAuth) + if err != nil { + return nil, fmt.Errorf("invalid basic auth config: %s", err) + } + plugins["basic-auth"] = cfg + } else if ac.Spec.AuthParameter.JwtAuth != nil { + cfg, err := t.translateConsumerJwtAuthPlugin(tctx, ac.Namespace, ac.Spec.AuthParameter.JwtAuth) + if err != nil { + return nil, fmt.Errorf("invalid jwt auth config: %s", err) + } + plugins["jwt-auth"] = cfg + } else if ac.Spec.AuthParameter.WolfRBAC != nil { + cfg, err := t.translateConsumerWolfRBACPlugin(tctx, ac.Namespace, ac.Spec.AuthParameter.WolfRBAC) + if err != nil { + return nil, fmt.Errorf("invalid wolf rbac config: %s", err) + } + plugins["wolf-rbac"] = cfg + } else if ac.Spec.AuthParameter.HMACAuth != nil { + cfg, err := t.translateConsumerHMACAuthPlugin(tctx, ac.Namespace, ac.Spec.AuthParameter.HMACAuth) + if err != nil { + return nil, fmt.Errorf("invalid hmac auth config: %s", err) + } + plugins["hmac-auth"] = cfg + } else if ac.Spec.AuthParameter.LDAPAuth != nil { + cfg, err := t.translateConsumerLDAPAuthPlugin(tctx, ac.Namespace, ac.Spec.AuthParameter.LDAPAuth) + if err != nil { + return nil, fmt.Errorf("invalid ldap auth config: %s", err) + } + plugins["ldap-auth"] = cfg + } + + username := adctypes.ComposeConsumerName(ac.Namespace, ac.Name) + consumer := &adctypes.Consumer{ + Username: username, + } + consumer.Plugins = plugins + consumer.Labels = label.GenLabel(ac) + result.Consumers = append(result.Consumers, consumer) + return result, nil +} + +func (t *Translator) translateConsumerKeyAuthPlugin(tctx *provider.TranslateContext, consumerNamespace string, cfg *v2.ApisixConsumerKeyAuth) (*adctypes.KeyAuthConsumerConfig, error) { + if cfg.Value != nil { + return &adctypes.KeyAuthConsumerConfig{Key: cfg.Value.Key}, nil + } + + sec := tctx.Secrets[k8stypes.NamespacedName{ + Namespace: consumerNamespace, + Name: cfg.SecretRef.Name, + }] + if sec == nil { + return nil, fmt.Errorf("secret %s/%s not found", consumerNamespace, cfg.SecretRef.Name) + } + raw, ok := sec.Data["key"] + if !ok || len(raw) == 0 { + return nil, _errKeyNotFoundOrInvalid + } + return &adctypes.KeyAuthConsumerConfig{Key: string(raw)}, nil +} + +func (t *Translator) translateConsumerBasicAuthPlugin(tctx *provider.TranslateContext, consumerNamespace string, cfg *v2.ApisixConsumerBasicAuth) (*adctypes.BasicAuthConsumerConfig, error) { + if cfg.Value != nil { + return &adctypes.BasicAuthConsumerConfig{ + Username: cfg.Value.Username, + Password: cfg.Value.Password, + }, nil + } + + sec := tctx.Secrets[k8stypes.NamespacedName{ + Namespace: consumerNamespace, + Name: cfg.SecretRef.Name, + }] + if sec == nil { + return nil, fmt.Errorf("secret %s/%s not found", consumerNamespace, cfg.SecretRef.Name) + } + raw1, ok := sec.Data["username"] + if !ok || len(raw1) == 0 { + return nil, _errUsernameNotFoundOrInvalid + } + raw2, ok := sec.Data["password"] + if !ok || len(raw2) == 0 { + return nil, _errPasswordNotFoundOrInvalid + } + return &adctypes.BasicAuthConsumerConfig{ + Username: string(raw1), + Password: string(raw2), + }, nil +} + +func (t *Translator) translateConsumerWolfRBACPlugin(tctx *provider.TranslateContext, consumerNamespace string, cfg *v2.ApisixConsumerWolfRBAC) (*adctypes.WolfRBACConsumerConfig, error) { + if cfg.Value != nil { + return &adctypes.WolfRBACConsumerConfig{ + Server: cfg.Value.Server, + Appid: cfg.Value.Appid, + HeaderPrefix: cfg.Value.HeaderPrefix, + }, nil + } + sec := tctx.Secrets[k8stypes.NamespacedName{ + Namespace: consumerNamespace, + Name: cfg.SecretRef.Name, + }] + if sec == nil { + return nil, fmt.Errorf("secret %s/%s not found", consumerNamespace, cfg.SecretRef.Name) + } + raw1 := sec.Data["server"] + raw2 := sec.Data["appid"] + raw3 := sec.Data["header_prefix"] + return &adctypes.WolfRBACConsumerConfig{ + Server: string(raw1), + Appid: string(raw2), + HeaderPrefix: string(raw3), + }, nil +} + +func (t *Translator) translateConsumerJwtAuthPlugin(tctx *provider.TranslateContext, consumerNamespace string, cfg *v2.ApisixConsumerJwtAuth) (*adctypes.JwtAuthConsumerConfig, error) { + if cfg.Value != nil { + // The field exp must be a positive integer, default value 86400. + if cfg.Value.Exp < 1 { + cfg.Value.Exp = _jwtAuthExpDefaultValue + } + return &adctypes.JwtAuthConsumerConfig{ + Key: cfg.Value.Key, + Secret: cfg.Value.Secret, + PublicKey: cfg.Value.PublicKey, + PrivateKey: cfg.Value.PrivateKey, + Algorithm: cfg.Value.Algorithm, + Exp: cfg.Value.Exp, + Base64Secret: cfg.Value.Base64Secret, + LifetimeGracePeriod: cfg.Value.LifetimeGracePeriod, + }, nil + } + + sec := tctx.Secrets[k8stypes.NamespacedName{ + Namespace: consumerNamespace, + Name: cfg.SecretRef.Name, + }] + if sec == nil { + return nil, fmt.Errorf("secret %s/%s not found", consumerNamespace, cfg.SecretRef.Name) + } + keyRaw, ok := sec.Data["key"] + if !ok || len(keyRaw) == 0 { + return nil, _errKeyNotFoundOrInvalid + } + base64SecretRaw := sec.Data["base64_secret"] + var base64Secret bool + if string(base64SecretRaw) == _true { + base64Secret = true + } + expRaw := sec.Data["exp"] + exp, _ := strconv.ParseInt(string(expRaw), 10, 64) + // The field exp must be a positive integer, default value 86400. + if exp < 1 { + exp = _jwtAuthExpDefaultValue + } + lifetimeGracePeriodRaw := sec.Data["lifetime_grace_period"] + lifetimeGracePeriod, _ := strconv.ParseInt(string(lifetimeGracePeriodRaw), 10, 64) + secretRaw := sec.Data["secret"] + publicKeyRaw := sec.Data["public_key"] + privateKeyRaw := sec.Data["private_key"] + algorithmRaw := sec.Data["algorithm"] + + return &adctypes.JwtAuthConsumerConfig{ + Key: string(keyRaw), + Secret: string(secretRaw), + PublicKey: string(publicKeyRaw), + PrivateKey: string(privateKeyRaw), + Algorithm: string(algorithmRaw), + Exp: exp, + Base64Secret: base64Secret, + LifetimeGracePeriod: lifetimeGracePeriod, + }, nil +} + +func (t *Translator) translateConsumerHMACAuthPlugin(tctx *provider.TranslateContext, consumerNamespace string, cfg *v2.ApisixConsumerHMACAuth) (*adctypes.HMACAuthConsumerConfig, error) { + if cfg.Value != nil { + return &adctypes.HMACAuthConsumerConfig{ + AccessKey: cfg.Value.AccessKey, + SecretKey: cfg.Value.SecretKey, + Algorithm: cfg.Value.Algorithm, + ClockSkew: cfg.Value.ClockSkew, + SignedHeaders: cfg.Value.SignedHeaders, + KeepHeaders: cfg.Value.KeepHeaders, + EncodeURIParams: cfg.Value.EncodeURIParams, + ValidateRequestBody: cfg.Value.ValidateRequestBody, + MaxReqBody: cfg.Value.MaxReqBody, + }, nil + } + + sec := tctx.Secrets[k8stypes.NamespacedName{ + Namespace: consumerNamespace, + Name: cfg.SecretRef.Name, + }] + if sec == nil { + return nil, fmt.Errorf("secret %s/%s not found", consumerNamespace, cfg.SecretRef.Name) + } + + accessKeyRaw, ok := sec.Data["access_key"] + if !ok || len(accessKeyRaw) == 0 { + return nil, _errKeyNotFoundOrInvalid + } + + secretKeyRaw, ok := sec.Data["secret_key"] + if !ok || len(secretKeyRaw) == 0 { + return nil, _errKeyNotFoundOrInvalid + } + + algorithmRaw, ok := sec.Data["algorithm"] + var algorithm string + if !ok { + algorithm = _hmacAuthAlgorithmDefaultValue + } else { + algorithm = string(algorithmRaw) + } + + clockSkewRaw := sec.Data["clock_skew"] + clockSkew, _ := strconv.ParseInt(string(clockSkewRaw), 10, 64) + if clockSkew < 0 { + clockSkew = _hmacAuthClockSkewDefaultValue + } + + signedHeadersRaw := sec.Data["signed_headers"] + signedHeaders := make([]string, 0, len(signedHeadersRaw)) + for _, b := range signedHeadersRaw { + signedHeaders = append(signedHeaders, string(b)) + } + + var keepHeader bool + keepHeaderRaw, ok := sec.Data["keep_headers"] + if !ok { + keepHeader = _hmacAuthKeepHeadersDefaultValue + } else { + if string(keepHeaderRaw) == _true { + keepHeader = true + } else { + keepHeader = false + } + } + + var encodeURIParams bool + encodeURIParamsRaw, ok := sec.Data["encode_uri_params"] + if !ok { + encodeURIParams = _hmacAuthEncodeURIParamsDefaultValue + } else { + if string(encodeURIParamsRaw) == _true { + encodeURIParams = true + } else { + encodeURIParams = false + } + } + + var validateRequestBody bool + validateRequestBodyRaw, ok := sec.Data["validate_request_body"] + if !ok { + validateRequestBody = _hmacAuthValidateRequestBodyDefaultValue + } else { + if string(validateRequestBodyRaw) == _true { + validateRequestBody = true + } else { + validateRequestBody = false + } + } + + maxReqBodyRaw := sec.Data["max_req_body"] + maxReqBody, _ := strconv.ParseInt(string(maxReqBodyRaw), 10, 64) + if maxReqBody < 0 { + maxReqBody = _hmacAuthMaxReqBodyDefaultValue + } + + return &adctypes.HMACAuthConsumerConfig{ + AccessKey: string(accessKeyRaw), + SecretKey: string(secretKeyRaw), + Algorithm: algorithm, + ClockSkew: clockSkew, + SignedHeaders: signedHeaders, + KeepHeaders: keepHeader, + EncodeURIParams: encodeURIParams, + ValidateRequestBody: validateRequestBody, + MaxReqBody: maxReqBody, + }, nil +} + +func (t *Translator) translateConsumerLDAPAuthPlugin(tctx *provider.TranslateContext, consumerNamespace string, cfg *v2.ApisixConsumerLDAPAuth) (*adctypes.LDAPAuthConsumerConfig, error) { + if cfg.Value != nil { + return &adctypes.LDAPAuthConsumerConfig{ + UserDN: cfg.Value.UserDN, + }, nil + } + + sec := tctx.Secrets[k8stypes.NamespacedName{ + Namespace: consumerNamespace, + Name: cfg.SecretRef.Name, + }] + if sec == nil { + return nil, fmt.Errorf("secret %s/%s not found", consumerNamespace, cfg.SecretRef.Name) + } + userDNRaw, ok := sec.Data["user_dn"] + if !ok || len(userDNRaw) == 0 { + return nil, _errKeyNotFoundOrInvalid + } + + return &adctypes.LDAPAuthConsumerConfig{ + UserDN: string(userDNRaw), + }, nil +} diff --git a/internal/provider/adc/translator/apisixroute.go b/internal/provider/adc/translator/apisixroute.go index 37ecf7f3b..aec400095 100644 --- a/internal/provider/adc/translator/apisixroute.go +++ b/internal/provider/adc/translator/apisixroute.go @@ -127,9 +127,9 @@ func (t *Translator) loadRoutePlugins(tctx *provider.TranslateContext, ar *apiv2 func (t *Translator) buildPluginConfig(plugin apiv2.ApisixRoutePlugin, namespace string, secrets map[types.NamespacedName]*v1.Secret) map[string]any { config := make(map[string]any) - if plugin.Config != nil { - for key, value := range plugin.Config { - config[key] = json.RawMessage(value.Raw) + if len(plugin.Config.Raw) > 0 { + if err := json.Unmarshal(plugin.Config.Raw, &config); err != nil { + t.Log.Error(err, "failed to unmarshal plugin config") } } if plugin.SecretRef != "" { diff --git a/internal/provider/adc/translator/consumer.go b/internal/provider/adc/translator/consumer.go index fd1a4907b..82841967e 100644 --- a/internal/provider/adc/translator/consumer.go +++ b/internal/provider/adc/translator/consumer.go @@ -71,9 +71,11 @@ func (t *Translator) TranslateConsumerV1alpha1(tctx *provider.TranslateContext, for _, plugin := range consumerV.Spec.Plugins { pluginName := plugin.Name pluginConfig := make(map[string]any) - if err := json.Unmarshal(plugin.Config.Raw, &pluginConfig); err != nil { - t.Log.Error(err, "failed to unmarshal plugin config", "plugin", plugin) - continue + if len(plugin.Config.Raw) > 0 { + if err := json.Unmarshal(plugin.Config.Raw, &pluginConfig); err != nil { + t.Log.Error(err, "failed to unmarshal plugin config", "plugin", plugin) + continue + } } plugins[pluginName] = pluginConfig } diff --git a/internal/provider/adc/translator/gateway.go b/internal/provider/adc/translator/gateway.go index cdef8b97c..0c35f1205 100644 --- a/internal/provider/adc/translator/gateway.go +++ b/internal/provider/adc/translator/gateway.go @@ -209,15 +209,16 @@ func (t *Translator) fillPluginsFromGatewayProxy(plugins adctypes.GlobalRule, ga } pluginName := plugin.Name - var pluginConfig map[string]any - if err := json.Unmarshal(plugin.Config.Raw, &pluginConfig); err != nil { - log.Errorw("gateway proxy plugin config unmarshal failed", zap.Error(err), zap.String("plugin", pluginName)) - continue + pluginConfig := map[string]any{} + if len(plugin.Config.Raw) > 0 { + if err := json.Unmarshal(plugin.Config.Raw, &pluginConfig); err != nil { + log.Errorw("gateway proxy plugin config unmarshal failed", zap.Error(err), zap.String("plugin", pluginName)) + continue + } } - - log.Debugw("fill plugin from gateway proxy", zap.String("plugin", pluginName), zap.Any("config", pluginConfig)) plugins[pluginName] = pluginConfig } + log.Debugw("fill plugins for gateway proxy", zap.Any("plugins", plugins)) } func (t *Translator) fillPluginMetadataFromGatewayProxy(pluginMetadata adctypes.PluginMetadata, gatewayProxy *v1alpha1.GatewayProxy) { diff --git a/internal/provider/adc/translator/globalrule.go b/internal/provider/adc/translator/globalrule.go index 525ba4d21..757831cab 100644 --- a/internal/provider/adc/translator/globalrule.go +++ b/internal/provider/adc/translator/globalrule.go @@ -40,22 +40,11 @@ func (t *Translator) TranslateApisixGlobalRule(tctx *provider.TranslateContext, continue } - // Parse plugin configuration - var pluginConfig map[string]any - if plugin.Config != nil { - pluginConfig = make(map[string]any) - // Convert map[string]apiextensionsv1.JSON to map[string]any - for key, jsonValue := range plugin.Config { - var value any - if err := json.Unmarshal(jsonValue.Raw, &value); err != nil { - log.Errorw("failed to parse plugin config", - zap.String("plugin", plugin.Name), - zap.String("key", key), - zap.Error(err), - ) - return nil, err - } - pluginConfig[key] = value + pluginConfig := make(map[string]any) + if len(plugin.Config.Raw) > 0 { + if err := json.Unmarshal(plugin.Config.Raw, &pluginConfig); err != nil { + log.Errorw("failed to unmarshal plugin config", zap.String("plugin", plugin.Name), zap.Error(err)) + continue } } plugins[plugin.Name] = pluginConfig diff --git a/internal/provider/adc/translator/httproute.go b/internal/provider/adc/translator/httproute.go index 1e8b5973f..12866441c 100644 --- a/internal/provider/adc/translator/httproute.go +++ b/internal/provider/adc/translator/httproute.go @@ -72,7 +72,7 @@ func (t *Translator) fillPluginFromExtensionRef(plugins adctypes.Plugins, namesp } for _, plugin := range pluginconfig.Spec.Plugins { pluginName := plugin.Name - var pluginconfig map[string]any + pluginconfig := make(map[string]any) if len(plugin.Config.Raw) > 0 { if err := json.Unmarshal(plugin.Config.Raw, &pluginconfig); err != nil { log.Errorw("plugin config unmarshal failed", zap.Error(err)) diff --git a/test/e2e/apisix/consumer.go b/test/e2e/apisix/consumer.go new file mode 100644 index 000000000..e750da9d4 --- /dev/null +++ b/test/e2e/apisix/consumer.go @@ -0,0 +1,332 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apisix + +import ( + "fmt" + "net/http" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/types" + + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/test/e2e/framework" + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +type Headers map[string]string + +var _ = Describe("Test ApisixConsumer", func() { + var ( + s = scaffold.NewScaffold(&scaffold.Options{ + ControllerName: "apisix.apache.org/apisix-ingress-controller", + }) + applier = framework.NewApplier(s.GinkgoT, s.K8sClient, s.CreateResourceFromString) + ) + + BeforeEach(func() { + By("create GatewayProxy") + gatewayProxy := fmt.Sprintf(gatewayProxyYaml, s.Deployer.GetAdminEndpoint(), s.AdminKey()) + err := s.CreateResourceFromStringWithNamespace(gatewayProxy, "default") + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("create IngressClass") + err = s.CreateResourceFromStringWithNamespace(ingressClassYaml, "") + Expect(err).NotTo(HaveOccurred(), "creating IngressClass") + time.Sleep(5 * time.Second) + }) + + Context("Test KeyAuth", func() { + const ( + keyAuth = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixConsumer +metadata: + name: test-consumer +spec: + ingressClassName: apisix + authParameter: + keyAuth: + value: + key: test-key +` + defaultApisixRoute = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + hosts: + - httpbin + paths: + - /get + - /headers + - /anything + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + authentication: + enable: true + type: keyAuth +` + secret = ` +apiVersion: v1 +kind: Secret +metadata: + name: keyauth +data: + # foo-key + key: Zm9vLWtleQ== +` + secretUpdated = ` +apiVersion: v1 +kind: Secret +metadata: + name: keyauth +data: + # foo2-key + key: Zm9vMi1rZXk= +` + keyAuthWiwhSecret = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixConsumer +metadata: + name: test-consumer +spec: + ingressClassName: apisix + authParameter: + keyAuth: + secretRef: + name: keyauth +` + ) + request := func(path string, headers Headers) int { + return s.NewAPISIXClient().GET(path).WithHeaders(headers).WithHost("httpbin").Expect().Raw().StatusCode + } + + It("Basic tests", func() { + By("apply ApisixRoute") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apiv2.ApisixRoute{}, defaultApisixRoute) + + By("apply ApisixConsumer") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-consumer"}, &apiv2.ApisixConsumer{}, keyAuth) + + By("verify ApisixRoute with ApisixConsumer") + Eventually(request).WithArguments("/get", Headers{ + "apikey": "invalid-key", + }).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized)) + + Eventually(request).WithArguments("/get", Headers{ + "apikey": "test-key", + }).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("Delete ApisixConsumer") + err := s.DeleteResource("ApisixConsumer", "test-consumer") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixConsumer") + Eventually(request).WithArguments("/get", Headers{ + "apikey": "test-key", + }).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized)) + + By("delete ApisixRoute") + err = s.DeleteResource("ApisixRoute", "default") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + Eventually(request).WithArguments("/headers", Headers{}).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound)) + }) + + It("SecretRef tests", func() { + By("apply ApisixRoute") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apiv2.ApisixRoute{}, defaultApisixRoute) + + By("apply Secret") + err := s.CreateResourceFromString(secret) + Expect(err).ShouldNot(HaveOccurred(), "creating Secret for ApisixConsumer") + + By("apply ApisixConsumer") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-consumer"}, &apiv2.ApisixConsumer{}, keyAuthWiwhSecret) + + By("verify ApisixRoute with ApisixConsumer") + Eventually(request).WithArguments("/get", Headers{ + "apikey": "invalid-key", + }).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized)) + + Eventually(request).WithArguments("/get", Headers{ + "apikey": "foo-key", + }).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("update Secret") + err = s.CreateResourceFromString(secretUpdated) + Expect(err).ShouldNot(HaveOccurred(), "updating Secret for ApisixConsumer") + + Eventually(request).WithArguments("/get", Headers{ + "apikey": "foo-key", + }).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized)) + + Eventually(request).WithArguments("/get", Headers{ + "apikey": "foo2-key", + }).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("Delete ApisixConsumer") + err = s.DeleteResource("ApisixConsumer", "test-consumer") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixConsumer") + Eventually(request).WithArguments("/get", Headers{ + "apikey": "test-key", + }).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized)) + + By("delete ApisixRoute") + err = s.DeleteResource("ApisixRoute", "default") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + Eventually(request).WithArguments("/headers", Headers{}).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound)) + }) + }) + + Context("Test BasicAuth", func() { + const ( + basicAuth = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixConsumer +metadata: + name: test-consumer +spec: + ingressClassName: apisix + authParameter: + basicAuth: + value: + username: test-user + password: test-password +` + defaultApisixRoute = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + hosts: + - httpbin + paths: + - /get + - /headers + - /anything + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + authentication: + enable: true + type: basicAuth +` + + secret = ` +apiVersion: v1 +kind: Secret +metadata: + name: basic +data: + # foo:bar + username: Zm9v + password: YmFy +` + secretUpdated = ` +apiVersion: v1 +kind: Secret +metadata: + name: basic +data: + # foo-new-user:bar-new-password + username: Zm9vLW5ldy11c2Vy + password: YmFyLW5ldy1wYXNzd29yZA== +` + + basicAuthWithSecret = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixConsumer +metadata: + name: test-consumer +spec: + ingressClassName: apisix + authParameter: + basicAuth: + secretRef: + name: basic +` + ) + + request := func(path string, username, password string) int { + return s.NewAPISIXClient().GET(path).WithBasicAuth(username, password).WithHost("httpbin").Expect().Raw().StatusCode + } + It("Basic tests", func() { + By("apply ApisixRoute") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apiv2.ApisixRoute{}, defaultApisixRoute) + + By("apply ApisixConsumer") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-consumer"}, &apiv2.ApisixConsumer{}, basicAuth) + + By("verify ApisixRoute with ApisixConsumer") + Eventually(request).WithArguments("/get", "invalid-username", "invalid-password").WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized)) + + Eventually(request).WithArguments("/get", "test-user", "test-password").WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("Delete ApisixConsumer") + err := s.DeleteResource("ApisixConsumer", "test-consumer") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixConsumer") + Eventually(request).WithArguments("/get", "test-user", "test-password").WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized)) + + By("delete ApisixRoute") + err = s.DeleteResource("ApisixRoute", "default") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + Eventually(request).WithArguments("/headers", "", "").WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound)) + }) + + It("SecretRef tests", func() { + By("apply ApisixRoute") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apiv2.ApisixRoute{}, defaultApisixRoute) + + By("apply Secret") + err := s.CreateResourceFromString(secret) + Expect(err).ShouldNot(HaveOccurred(), "creating Secret for ApisixConsumer") + + By("apply ApisixConsumer") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-consumer"}, &apiv2.ApisixConsumer{}, basicAuthWithSecret) + + By("verify ApisixRoute with ApisixConsumer") + Eventually(request).WithArguments("/get", "", "").WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized)) + Eventually(request).WithArguments("/get", "foo", "bar").WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("update Secret") + err = s.CreateResourceFromString(secretUpdated) + Expect(err).ShouldNot(HaveOccurred(), "updating Secret for ApisixConsumer") + + Eventually(request).WithArguments("/get", "foo", "bar").WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized)) + Eventually(request).WithArguments("/get", "foo-new-user", "bar-new-password").WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("Delete ApisixConsumer") + err = s.DeleteResource("ApisixConsumer", "test-consumer") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixConsumer") + Eventually(request).WithArguments("/get", "foo-new-user", "bar-new-password").WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized)) + + By("delete ApisixRoute") + err = s.DeleteResource("ApisixRoute", "default") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + Eventually(request).WithArguments("/get", "", "").WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound)) + }) + }) +}) diff --git a/test/e2e/scaffold/apisix_deployer.go b/test/e2e/scaffold/apisix_deployer.go index 25c88c136..8d53c8362 100644 --- a/test/e2e/scaffold/apisix_deployer.go +++ b/test/e2e/scaffold/apisix_deployer.go @@ -218,7 +218,7 @@ func (s *APISIXDeployer) deployDataplane(opts *APISIXDeployOptions) *corev1.Serv func (s *APISIXDeployer) DeployIngress() { s.Framework.DeployIngress(framework.IngressDeployOpts{ - ProviderSyncPeriod: time.Second, + ProviderSyncPeriod: 200 * time.Millisecond, ControllerName: s.opts.ControllerName, Namespace: s.namespace, Replicas: 1, @@ -227,7 +227,7 @@ func (s *APISIXDeployer) DeployIngress() { func (s *APISIXDeployer) ScaleIngress(replicas int) { s.Framework.DeployIngress(framework.IngressDeployOpts{ - ProviderSyncPeriod: time.Second, + ProviderSyncPeriod: 200 * time.Millisecond, ControllerName: s.opts.ControllerName, Namespace: s.namespace, Replicas: replicas,