diff --git a/api/adc/types.go b/api/adc/types.go index 8d7b6c4bc..d9b0dab6a 100644 --- a/api/adc/types.go +++ b/api/adc/types.go @@ -103,6 +103,13 @@ func (g *GlobalRule) DeepCopy() GlobalRule { return GlobalRule(copied) } +// +k8s:deepcopy-gen=true +type GlobalRuleItem struct { + Metadata `json:",inline" yaml:",inline"` + + Plugins Plugins `json:"plugins" yaml:"plugins"` +} + type PluginMetadata Plugins func (p *PluginMetadata) DeepCopy() PluginMetadata { diff --git a/api/adc/zz_generated.deepcopy.go b/api/adc/zz_generated.deepcopy.go index cb805efbb..8051a98bc 100644 --- a/api/adc/zz_generated.deepcopy.go +++ b/api/adc/zz_generated.deepcopy.go @@ -133,6 +133,23 @@ 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 *GlobalRuleItem) DeepCopyInto(out *GlobalRuleItem) { + *out = *in + in.Metadata.DeepCopyInto(&out.Metadata) + out.Plugins = in.Plugins.DeepCopy() +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GlobalRuleItem. +func (in *GlobalRuleItem) DeepCopy() *GlobalRuleItem { + if in == nil { + return nil + } + out := new(GlobalRuleItem) + 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 diff --git a/api/v2/apisixroute_types.go b/api/v2/apisixroute_types.go index cea41e4eb..e50131d96 100644 --- a/api/v2/apisixroute_types.go +++ b/api/v2/apisixroute_types.go @@ -130,10 +130,13 @@ type ApisixRoutePlugin struct { // The plugin name. Name string `json:"name" yaml:"name"` // Whether this plugin is in use, default is true. + // +kubebuilder:default=true Enable bool `json:"enable" yaml:"enable"` // Plugin configuration. + // +kubebuilder:validation:Optional Config ApisixRoutePluginConfig `json:"config" yaml:"config"` // Plugin configuration secretRef. + // +kubebuilder:validation:Optional SecretRef string `json:"secretRef" yaml:"secretRef"` } diff --git a/api/v2/reason.go b/api/v2/reason.go new file mode 100644 index 000000000..723388f7a --- /dev/null +++ b/api/v2/reason.go @@ -0,0 +1,19 @@ +// 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 v2 + +type Reason string + +const ( + ReasonSyncFailed Reason = "SyncFailed" +) diff --git a/config/crd/bases/apisix.apache.org_apisixglobalrules.yaml b/config/crd/bases/apisix.apache.org_apisixglobalrules.yaml index 25a1558a5..797528d2b 100644 --- a/config/crd/bases/apisix.apache.org_apisixglobalrules.yaml +++ b/config/crd/bases/apisix.apache.org_apisixglobalrules.yaml @@ -55,6 +55,7 @@ spec: description: Plugin configuration. type: object enable: + default: true description: Whether this plugin is in use, default is true. type: boolean name: @@ -64,10 +65,8 @@ spec: description: Plugin configuration secretRef. type: string required: - - config - enable - name - - secretRef type: object type: array required: diff --git a/config/crd/bases/apisix.apache.org_apisixpluginconfigs.yaml b/config/crd/bases/apisix.apache.org_apisixpluginconfigs.yaml index 1b956192b..083e76871 100644 --- a/config/crd/bases/apisix.apache.org_apisixpluginconfigs.yaml +++ b/config/crd/bases/apisix.apache.org_apisixpluginconfigs.yaml @@ -56,6 +56,7 @@ spec: description: Plugin configuration. type: object enable: + default: true description: Whether this plugin is in use, default is true. type: boolean name: @@ -65,10 +66,8 @@ spec: description: Plugin configuration secretRef. type: string required: - - config - enable - name - - secretRef type: object type: array required: diff --git a/config/crd/bases/apisix.apache.org_apisixroutes.yaml b/config/crd/bases/apisix.apache.org_apisixroutes.yaml index 078157f12..d62986b43 100644 --- a/config/crd/bases/apisix.apache.org_apisixroutes.yaml +++ b/config/crd/bases/apisix.apache.org_apisixroutes.yaml @@ -255,6 +255,7 @@ spec: description: Plugin configuration. type: object enable: + default: true description: Whether this plugin is in use, default is true. type: boolean @@ -265,10 +266,8 @@ spec: description: Plugin configuration secretRef. type: string required: - - config - enable - name - - secretRef type: object type: array priority: @@ -374,6 +373,7 @@ spec: description: Plugin configuration. type: object enable: + default: true description: Whether this plugin is in use, default is true. type: boolean @@ -384,10 +384,8 @@ spec: description: Plugin configuration secretRef. type: string required: - - config - enable - name - - secretRef type: object type: array protocol: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 33f71415b..552fc1c41 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -54,44 +54,6 @@ rules: verbs: - get - update -- apiGroups: - - apisix.apache.org.github.com - resources: - - apisixconsumers - - apisixglobalrules - - apisixroutes - - apisixtls - - apisixupstreams - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - apisix.apache.org.github.com - resources: - - apisixconsumers/finalizers - - apisixglobalrules/finalizers - - apisixroutes/finalizers - - apisixtls/finalizers - - apisixupstreams/finalizers - verbs: - - update -- apiGroups: - - apisix.apache.org.github.com - resources: - - apisixconsumers/status - - apisixglobalrules/status - - apisixroutes/status - - apisixtls/status - - apisixupstreams/status - verbs: - - get - - patch - - update - apiGroups: - coordination.k8s.io resources: diff --git a/config/samples/config.yaml b/config/samples/config.yaml index 4b68bcf54..55a74e3a5 100644 --- a/config/samples/config.yaml +++ b/config/samples/config.yaml @@ -25,7 +25,7 @@ enable_http2: false # Whether to enable HTTP/2 for the serve probe_addr: ":8081" # The address the probe endpoint binds to. # The default value is ":8081". -secure_metrics: "" # The secure metrics configuration. +secure_metrics: false # The secure metrics configuration. # The default value is "" (empty). exec_adc_timeout: 15s # The timeout for the ADC to execute. diff --git a/docs/crd/api.md b/docs/crd/api.md index 7b25a4341..4b137c93f 100644 --- a/docs/crd/api.md +++ b/docs/crd/api.md @@ -1632,6 +1632,8 @@ them if they are set on the port level. _Appears in:_ - [ApisixUpstreamSpec](#apisixupstreamspec) + + #### UpstreamTimeout diff --git a/go.mod b/go.mod index 77b149974..05aeda446 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,6 @@ require ( go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/net v0.28.0 - google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.15.4 k8s.io/api v0.31.1 @@ -202,6 +201,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect google.golang.org/grpc v1.66.2 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/internal/controller/apisixconsumer_controller.go b/internal/controller/apisixconsumer_controller.go index fe4e05380..97a247593 100644 --- a/internal/controller/apisixconsumer_controller.go +++ b/internal/controller/apisixconsumer_controller.go @@ -32,10 +32,6 @@ type ApisixConsumerReconciler struct { Log logr.Logger } -// +kubebuilder:rbac:groups=apisix.apache.org.github.com,resources=apisixconsumers,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=apisix.apache.org.github.com,resources=apisixconsumers/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=apisix.apache.org.github.com,resources=apisixconsumers/finalizers,verbs=update - // 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) diff --git a/internal/controller/apisixglobalrule_controller.go b/internal/controller/apisixglobalrule_controller.go index 92ffbef0b..f529dbe2e 100644 --- a/internal/controller/apisixglobalrule_controller.go +++ b/internal/controller/apisixglobalrule_controller.go @@ -14,60 +14,370 @@ 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" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + "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" + "github.com/apache/apisix-ingress-controller/internal/utils" ) // ApisixGlobalRuleReconciler reconciles a ApisixGlobalRule object type ApisixGlobalRuleReconciler struct { client.Client - Scheme *runtime.Scheme - Log logr.Logger + Scheme *runtime.Scheme + Log logr.Logger + Provider provider.Provider + Updater status.Updater } -// +kubebuilder:rbac:groups=apisix.apache.org.github.com,resources=apisixglobalrules,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=apisix.apache.org.github.com,resources=apisixglobalrules/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=apisix.apache.org.github.com,resources=apisixglobalrules/finalizers,verbs=update - -// Reconcile FIXME: implement the reconcile logic (For now, it dose nothing other than directly accepting) +// Reconcile implements the reconciliation logic for ApisixGlobalRule func (r *ApisixGlobalRuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - r.Log.Info("reconcile", "request", req.NamespacedName) + var globalRule apiv2.ApisixGlobalRule + if err := r.Get(ctx, req.NamespacedName, &globalRule); err != nil { + if client.IgnoreNotFound(err) == nil { + // Create a minimal object for deletion + globalRule.Namespace = req.Namespace + globalRule.Name = req.Name + globalRule.TypeMeta = metav1.TypeMeta{ + Kind: KindApisixGlobalRule, + APIVersion: apiv2.GroupVersion.String(), + } + // Delete from provider + if err := r.Provider.Delete(ctx, &globalRule); err != nil { + r.Log.Error(err, "failed to delete global rule from provider") + return ctrl.Result{}, err + } + r.Log.Info("deleted global rule", "globalrule", globalRule.Name) + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + r.Log.Info("reconciling global rule", "globalrule", globalRule.Name) + + // create a translate context + tctx := provider.NewDefaultTranslateContext(ctx) - var obj apiv2.ApisixGlobalRule - if err := r.Get(ctx, req.NamespacedName, &obj); err != nil { - r.Log.Error(err, "failed to get ApisixConsumer", "request", req.NamespacedName) + // get the ingress class + ingressClass, err := r.getIngressClass(&globalRule) + if err != nil { + log.Error(err, "failed to get IngressClass") 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), - }, + // process IngressClass parameters if they reference GatewayProxy + if err := r.processIngressClassParameters(ctx, tctx, &globalRule, ingressClass); err != nil { + log.Error(err, "failed to process IngressClass parameters", "ingressClass", ingressClass.Name) + return ctrl.Result{}, err } - if err := r.Status().Update(ctx, &obj); err != nil { - r.Log.Error(err, "failed to update status", "request", req.NamespacedName) + // Sync the global rule to APISIX + if err := r.Provider.Update(ctx, tctx, &globalRule); err != nil { + log.Error(err, "failed to sync global rule to provider") + // Update status with failure condition + r.updateStatus(&globalRule, metav1.Condition{ + Type: string(gatewayv1.RouteConditionAccepted), + Status: metav1.ConditionFalse, + ObservedGeneration: globalRule.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(apiv2.ReasonSyncFailed), + Message: err.Error(), + }) return ctrl.Result{}, err } + // Update status with success condition + r.updateStatus(&globalRule, metav1.Condition{ + Type: string(gatewayv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + ObservedGeneration: globalRule.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatewayv1.RouteReasonAccepted), + Message: "The global rule has been accepted and synced to APISIX", + }) + return ctrl.Result{}, nil } // SetupWithManager sets up the controller with the Manager. func (r *ApisixGlobalRuleReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&apiv2.ApisixGlobalRule{}). - Named("apisixglobalrule"). + For(&apiv2.ApisixGlobalRule{}, + builder.WithPredicates( + predicate.NewPredicateFuncs(r.checkIngressClass), + ), + ). + WithEventFilter( + predicate.Or( + predicate.GenerationChangedPredicate{}, + predicate.AnnotationChangedPredicate{}, + ), + ). + Watches( + &networkingv1.IngressClass{}, + handler.EnqueueRequestsFromMapFunc(r.listGlobalRulesForIngressClass), + builder.WithPredicates( + predicate.NewPredicateFuncs(r.matchesIngressController), + ), + ). + Watches(&v1alpha1.GatewayProxy{}, + handler.EnqueueRequestsFromMapFunc(r.listGlobalRulesForGatewayProxy), + ). Complete(r) } + +// checkIngressClass checks if the ApisixGlobalRule uses the ingress class that we control +func (r *ApisixGlobalRuleReconciler) checkIngressClass(obj client.Object) bool { + globalRule, ok := obj.(*apiv2.ApisixGlobalRule) + if !ok { + return false + } + + return r.matchesIngressClass(globalRule.Spec.IngressClassName) +} + +// matchesIngressClass checks if the given ingress class name matches our controlled classes +func (r *ApisixGlobalRuleReconciler) matchesIngressClass(ingressClassName string) bool { + if 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 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 := r.Get(context.Background(), client.ObjectKey{Name: ingressClassName}, &ingressClass); err != nil { + r.Log.Error(err, "failed to get ingress class", "ingressClass", ingressClassName) + return false + } + + 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) + if !ok { + 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 + } + } + 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") +} + +// 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 +} + +// updateStatus updates the ApisixGlobalRule status with the given condition +func (r *ApisixGlobalRuleReconciler) updateStatus(globalRule *apiv2.ApisixGlobalRule, condition metav1.Condition) { + r.Updater.Update(status.Update{ + NamespacedName: NamespacedName(globalRule), + Resource: &apiv2.ApisixGlobalRule{}, + Mutator: status.MutatorFunc(func(obj client.Object) client.Object { + gr, ok := obj.(*apiv2.ApisixGlobalRule) + if !ok { + err := fmt.Errorf("unsupported object type %T", obj) + panic(err) + } + grCopy := gr.DeepCopy() + grCopy.Status.Conditions = []metav1.Condition{condition} + return grCopy + }), + }) +} diff --git a/internal/controller/apisixroute_controller.go b/internal/controller/apisixroute_controller.go index 6febd1249..d2dbd47b7 100644 --- a/internal/controller/apisixroute_controller.go +++ b/internal/controller/apisixroute_controller.go @@ -32,10 +32,6 @@ type ApisixRouteReconciler struct { Log logr.Logger } -// +kubebuilder:rbac:groups=apisix.apache.org.github.com,resources=apisixroutes,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=apisix.apache.org.github.com,resources=apisixroutes/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=apisix.apache.org.github.com,resources=apisixroutes/finalizers,verbs=update - func (r *ApisixRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { r.Log.Info("reconcile", "request", req.NamespacedName) diff --git a/internal/controller/apisixtls_controller.go b/internal/controller/apisixtls_controller.go index e4050e39a..a34ed232f 100644 --- a/internal/controller/apisixtls_controller.go +++ b/internal/controller/apisixtls_controller.go @@ -32,10 +32,6 @@ type ApisixTlsReconciler struct { Log logr.Logger } -// +kubebuilder:rbac:groups=apisix.apache.org.github.com,resources=apisixtls,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=apisix.apache.org.github.com,resources=apisixtls/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=apisix.apache.org.github.com,resources=apisixtls/finalizers,verbs=update - // Reconcile FIXME: implement the reconcile logic (For now, it dose nothing other than directly accepting) func (r *ApisixTlsReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { r.Log.Info("reconcile", "request", req.NamespacedName) diff --git a/internal/controller/apisixupstream_controller.go b/internal/controller/apisixupstream_controller.go index 24c9b55b2..2dce86a06 100644 --- a/internal/controller/apisixupstream_controller.go +++ b/internal/controller/apisixupstream_controller.go @@ -32,10 +32,6 @@ type ApisixUpstreamReconciler struct { Log logr.Logger } -// +kubebuilder:rbac:groups=apisix.apache.org.github.com,resources=apisixupstreams,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=apisix.apache.org.github.com,resources=apisixupstreams/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=apisix.apache.org.github.com,resources=apisixupstreams/finalizers,verbs=update - // Reconcile FIXME: implement the reconcile logic (For now, it dose nothing other than directly accepting) func (r *ApisixUpstreamReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { r.Log.Info("reconcile", "request", req.NamespacedName) diff --git a/internal/controller/utils.go b/internal/controller/utils.go index 319f6a72a..96f9dc9df 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -46,14 +46,15 @@ import ( ) const ( - KindGateway = "Gateway" - KindHTTPRoute = "HTTPRoute" - KindGatewayClass = "GatewayClass" - KindIngress = "Ingress" - KindIngressClass = "IngressClass" - KindGatewayProxy = "GatewayProxy" - KindSecret = "Secret" - KindService = "Service" + KindGateway = "Gateway" + KindHTTPRoute = "HTTPRoute" + KindGatewayClass = "GatewayClass" + KindIngress = "Ingress" + KindIngressClass = "IngressClass" + KindGatewayProxy = "GatewayProxy" + KindSecret = "Secret" + KindService = "Service" + KindApisixGlobalRule = "ApisixGlobalRule" ) const defaultIngressClassAnnotation = "ingressclass.kubernetes.io/is-default-class" diff --git a/internal/manager/controllers.go b/internal/manager/controllers.go index 8a02a3b45..f68ef7335 100644 --- a/internal/manager/controllers.go +++ b/internal/manager/controllers.go @@ -120,30 +120,12 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("IngressClass"), Provider: pro, }, - &controller.ApisixConsumerReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixConsumer"), - }, &controller.ApisixGlobalRuleReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixGlobalRule"), - }, - &controller.ApisixRouteReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixRoute"), - }, - &controller.ApisixTlsReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixTls"), - }, - &controller.ApisixUpstreamReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixUpstream"), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixGlobalRule"), + Provider: pro, + Updater: updater, }, }, nil } diff --git a/internal/provider/adc/adc.go b/internal/provider/adc/adc.go index 902a2891b..5d065df62 100644 --- a/internal/provider/adc/adc.go +++ b/internal/provider/adc/adc.go @@ -28,6 +28,7 @@ import ( adctypes "github.com/apache/apisix-ingress-controller/api/adc" "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/label" "github.com/apache/apisix-ingress-controller/internal/provider" "github.com/apache/apisix-ingress-controller/internal/provider/adc/translator" @@ -52,6 +53,8 @@ const ( type adcClient struct { sync.Mutex + syncLock sync.Mutex + translator *translator.Translator // gateway/ingressclass -> adcConfig configs map[types.NamespacedNameKind]adcConfig @@ -113,6 +116,9 @@ func (d *adcClient) Update(ctx context.Context, tctx *provider.TranslateContext, case *networkingv1.IngressClass: result, err = d.translator.TranslateIngressClass(tctx, t.DeepCopy()) resourceTypes = append(resourceTypes, "global_rule", "plugin_metadata") + case *apiv2.ApisixGlobalRule: + result, err = d.translator.TranslateApisixGlobalRule(tctx, t.DeepCopy()) + resourceTypes = append(resourceTypes, "global_rule") } if err != nil { return err @@ -158,6 +164,7 @@ func (d *adcClient) Update(ctx context.Context, tctx *provider.TranslateContext, SSLs: result.SSL, Consumers: result.Consumers, } + log.Debugw("update resources", zap.Any("resources", resources)) for _, config := range configs { if err := d.store.Insert(config.Name, resourceTypes, resources, label.GenLabel(obj)); err != nil { @@ -176,6 +183,12 @@ func (d *adcClient) Update(ctx context.Context, tctx *provider.TranslateContext, // and triggered by a timer for synchronization return nil case BackendModeAPI7EE: + // if api version is v2, then skip sync + if obj.GetObjectKind().GroupVersionKind().GroupVersion() == apiv2.GroupVersion { + log.Debugw("api version is v2, skip sync", zap.Any("obj", obj)) + return nil + } + return d.sync(ctx, Task{ Name: obj.GetName(), Labels: label.GenLabel(obj), @@ -208,6 +221,9 @@ func (d *adcClient) Delete(ctx context.Context, obj client.Object) error { labels = label.GenLabel(obj) case *networkingv1.IngressClass: // delete all resources + case *apiv2.ApisixGlobalRule: + resourceTypes = append(resourceTypes, "global_rule") + labels = label.GenLabel(obj) } rk := utils.NamespacedNameKind(obj) @@ -278,6 +294,9 @@ func (d *adcClient) Start(ctx context.Context) error { } func (d *adcClient) Sync(ctx context.Context) error { + d.syncLock.Lock() + defer d.syncLock.Unlock() + log.Debug("syncing all resources") if len(d.configs) == 0 { diff --git a/internal/provider/adc/cache/cache.go b/internal/provider/adc/cache/cache.go index 7f133b150..780e25c02 100644 --- a/internal/provider/adc/cache/cache.go +++ b/internal/provider/adc/cache/cache.go @@ -26,6 +26,8 @@ type Cache interface { InsertService(*types.Service) error // InsertConsumer adds or updates consumer to cache. InsertConsumer(*types.Consumer) error + // InsertGlobalRule adds or updates global rule to cache. + InsertGlobalRule(*types.GlobalRuleItem) error // GetSSL finds the ssl from cache according to the primary index (id). GetSSL(string) (*types.SSL, error) @@ -33,13 +35,17 @@ type Cache interface { GetService(string) (*types.Service, error) // GetConsumer finds the consumer from cache according to the primary index (username). GetConsumer(string) (*types.Consumer, error) + // GetGlobalRule finds the global rule from cache according to the primary index (id). + GetGlobalRule(string) (*types.GlobalRuleItem, error) // DeleteSSL deletes the specified ssl in cache. DeleteSSL(*types.SSL) error // DeleteUpstream deletes the specified upstream in cache. DeleteService(*types.Service) error - // DeleteGlobalRule deletes the specified stream_route in cache. + // DeleteConsumer deletes the specified consumer in cache. DeleteConsumer(*types.Consumer) error + // DeleteGlobalRule deletes the specified global rule in cache. + DeleteGlobalRule(*types.GlobalRuleItem) error // ListSSL lists all ssl objects in cache. ListSSL(...ListOption) ([]*types.SSL, error) @@ -47,6 +53,8 @@ type Cache interface { ListServices(...ListOption) ([]*types.Service, error) // ListConsumers lists all consumer objects in cache. ListConsumers(...ListOption) ([]*types.Consumer, error) + // ListGlobalRules lists all global rule objects in cache. + ListGlobalRules(...ListOption) ([]*types.GlobalRuleItem, error) } type ListOption interface { diff --git a/internal/provider/adc/cache/memdb.go b/internal/provider/adc/cache/memdb.go index 8fcc6edd9..326d14c99 100644 --- a/internal/provider/adc/cache/memdb.go +++ b/internal/provider/adc/cache/memdb.go @@ -50,6 +50,8 @@ func (c *dbCache) Insert(obj any) error { return c.InsertService(t) case *types.Consumer: return c.InsertConsumer(t) + case *types.GlobalRuleItem: + return c.InsertGlobalRule(t) default: return errors.New("unsupported type") } @@ -65,6 +67,8 @@ func (c *dbCache) Delete(obj any) error { return c.DeleteService(t) case *types.Consumer: return c.DeleteConsumer(t) + case *types.GlobalRuleItem: + return c.DeleteGlobalRule(t) default: return errors.New("unsupported type") } @@ -87,6 +91,10 @@ func (c *dbCache) InsertConsumer(consumer *types.Consumer) error { return c.insert("consumer", consumer.DeepCopy()) } +func (c *dbCache) InsertGlobalRule(globalRule *types.GlobalRuleItem) error { + return c.insert("global_rule", globalRule.DeepCopy()) +} + func (c *dbCache) insert(table string, obj any) error { txn := c.db.Txn(true) defer txn.Abort() @@ -129,6 +137,14 @@ func (c *dbCache) GetConsumer(username string) (*types.Consumer, error) { return obj.(*types.Consumer).DeepCopy(), nil } +func (c *dbCache) GetGlobalRule(id string) (*types.GlobalRuleItem, error) { + obj, err := c.get("global_rule", id) + if err != nil { + return nil, err + } + return obj.(*types.GlobalRuleItem).DeepCopy(), nil +} + func (c *dbCache) GetStreamRoute(id string) (*types.StreamRoute, error) { obj, err := c.get("stream_route", id) if err != nil { @@ -201,6 +217,18 @@ func (c *dbCache) ListConsumers(opts ...ListOption) ([]*types.Consumer, error) { return consumers, nil } +func (c *dbCache) ListGlobalRules(opts ...ListOption) ([]*types.GlobalRuleItem, error) { + raws, err := c.list("global_rule", opts...) + if err != nil { + return nil, err + } + globalRules := make([]*types.GlobalRuleItem, 0, len(raws)) + for _, raw := range raws { + globalRules = append(globalRules, raw.(*types.GlobalRuleItem).DeepCopy()) + } + return globalRules, nil +} + func (c *dbCache) list(table string, opts ...ListOption) ([]any, error) { txn := c.db.Txn(false) defer txn.Abort() @@ -239,6 +267,10 @@ func (c *dbCache) DeleteConsumer(consumer *types.Consumer) error { return c.delete("consumer", consumer) } +func (c *dbCache) DeleteGlobalRule(globalRule *types.GlobalRuleItem) error { + return c.delete("global_rule", globalRule) +} + func (c *dbCache) delete(table string, obj any) error { txn := c.db.Txn(true) defer txn.Abort() diff --git a/internal/provider/adc/cache/noop_db.go b/internal/provider/adc/cache/noop_db.go index 298653a3b..4e85bbb25 100644 --- a/internal/provider/adc/cache/noop_db.go +++ b/internal/provider/adc/cache/noop_db.go @@ -40,7 +40,7 @@ func (c *noopCache) InsertService(u *types.Service) error { return nil } -func (c *noopCache) InsertGlobalRule(gr *types.GlobalRule) error { +func (c *noopCache) InsertGlobalRule(gr *types.GlobalRuleItem) error { return nil } @@ -56,7 +56,7 @@ func (c *noopCache) GetService(id string) (*types.Service, error) { return nil, nil } -func (c *noopCache) GetGlobalRule(id string) (*types.GlobalRule, error) { +func (c *noopCache) GetGlobalRule(id string) (*types.GlobalRuleItem, error) { return nil, nil } @@ -76,7 +76,7 @@ func (c *noopCache) ListStreamRoutes(...ListOption) ([]*types.StreamRoute, error return nil, nil } -func (c *noopCache) ListGlobalRules(...ListOption) ([]*types.GlobalRule, error) { +func (c *noopCache) ListGlobalRules(...ListOption) ([]*types.GlobalRuleItem, error) { return nil, nil } @@ -92,7 +92,7 @@ func (c *noopCache) DeleteService(u *types.Service) error { return nil } -func (c *noopCache) DeleteGlobalRule(gr *types.GlobalRule) error { +func (c *noopCache) DeleteGlobalRule(gr *types.GlobalRuleItem) error { return nil } diff --git a/internal/provider/adc/cache/schema.go b/internal/provider/adc/cache/schema.go index e50bf3117..a076e026e 100644 --- a/internal/provider/adc/cache/schema.go +++ b/internal/provider/adc/cache/schema.go @@ -73,6 +73,22 @@ var ( }, }, }, + "global_rule": { + Name: "global_rule", + Indexes: map[string]*memdb.IndexSchema{ + "id": { + Name: "id", + Unique: true, + Indexer: &memdb.StringFieldIndex{Field: "ID"}, + }, + KindLabelIndex: { + Name: KindLabelIndex, + Unique: false, + AllowMissing: true, + Indexer: &KindLabelIndexer, + }, + }, + }, }, } ) diff --git a/internal/provider/adc/executor.go b/internal/provider/adc/executor.go index 89242f581..bf6f2a033 100644 --- a/internal/provider/adc/executor.go +++ b/internal/provider/adc/executor.go @@ -45,10 +45,9 @@ func (e *DefaultADCExecutor) Execute(ctx context.Context, mode string, config ad } func (e *DefaultADCExecutor) runADC(ctx context.Context, mode string, config adcConfig, args []string) error { - ctxWithTimeout, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - for _, addr := range config.ServerAddrs { + ctxWithTimeout, cancel := context.WithTimeout(ctx, 15*time.Second) + defer cancel() if err := e.runForSingleServer(ctxWithTimeout, addr, mode, config, args); err != nil { return err } @@ -62,6 +61,8 @@ func (e *DefaultADCExecutor) runForSingleServer(ctx context.Context, serverAddr, cmdArgs = append(cmdArgs, "--tls-skip-verify") } + cmdArgs = append(cmdArgs, "--timeout", "15s") + env := e.prepareEnv(serverAddr, mode, config.Token) var stdout, stderr bytes.Buffer diff --git a/internal/provider/adc/store.go b/internal/provider/adc/store.go index f29931380..7a1800f18 100644 --- a/internal/provider/adc/store.go +++ b/internal/provider/adc/store.go @@ -16,6 +16,8 @@ import ( "sync" "github.com/api7/gopkg/pkg/log" + "github.com/google/uuid" + "go.uber.org/zap" adctypes "github.com/apache/apisix-ingress-controller/api/adc" "github.com/apache/apisix-ingress-controller/internal/controller/label" @@ -24,7 +26,6 @@ import ( type Store struct { cacheMap map[string]cache.Cache - globalruleMap map[string]adctypes.GlobalRule pluginMetadataMap map[string]adctypes.PluginMetadata sync.Mutex @@ -33,7 +34,6 @@ type Store struct { func NewStore() *Store { return &Store{ cacheMap: make(map[string]cache.Cache), - globalruleMap: make(map[string]adctypes.GlobalRule), pluginMetadataMap: make(map[string]adctypes.PluginMetadata), } } @@ -105,7 +105,32 @@ func (s *Store) Insert(name string, resourceTypes []string, resources adctypes.R } } case "global_rule": - s.globalruleMap[name] = resources.GlobalRules + // List existing global rules that match the selector + globalRules, err := targetCache.ListGlobalRules(selector) + if err != nil { + return err + } + // Delete existing matching global rules + for _, globalRule := range globalRules { + if err := targetCache.DeleteGlobalRule(globalRule); err != nil { + return err + } + } + // Convert GlobalRule (Plugins) to GlobalRuleItem and insert + if len(resources.GlobalRules) > 0 { + id := name + "-" + uuid.NewString() + globalRuleItem := &adctypes.GlobalRuleItem{ + Metadata: adctypes.Metadata{ + ID: id, + Name: id, + Labels: Labels, + }, + Plugins: adctypes.Plugins(resources.GlobalRules), + } + if err := targetCache.InsertGlobalRule(globalRuleItem); err != nil { + return err + } + } case "plugin_metadata": s.pluginMetadataMap[name] = resources.PluginMetadata default: @@ -160,7 +185,15 @@ func (s *Store) Delete(name string, resourceTypes []string, Labels map[string]st } } case "global_rule": - delete(s.globalruleMap, name) + globalRules, err := targetCache.ListGlobalRules(selector) + if err != nil { + log.Errorf("failed to list global rules: %v", err) + } + for _, globalRule := range globalRules { + if err := targetCache.DeleteGlobalRule(globalRule); err != nil { + log.Errorf("failed to delete global rule %s: %v", globalRule.ID, err) + } + } case "plugin_metadata": delete(s.pluginMetadataMap, name) } @@ -180,9 +213,18 @@ func (s *Store) GetResources(name string) (*adctypes.Resources, error) { } var globalrule adctypes.GlobalRule var metadata adctypes.PluginMetadata - if global, ok := s.globalruleMap[name]; ok { - globalrule = global.DeepCopy() + // Get all global rules from cache and merge them + globalRuleItems, _ := targetCache.ListGlobalRules() + if len(globalRuleItems) > 0 { + merged := make(adctypes.Plugins) + for _, item := range globalRuleItems { + for k, v := range item.Plugins { + merged[k] = v + } + } + globalrule = adctypes.GlobalRule(merged) } + log.Debugw("get resources global rule items", zap.Any("globalRuleItems", globalRuleItems)) if meta, ok := s.pluginMetadataMap[name]; ok { metadata = meta.DeepCopy() } diff --git a/internal/provider/adc/translator/globalrule.go b/internal/provider/adc/translator/globalrule.go new file mode 100644 index 000000000..525ba4d21 --- /dev/null +++ b/internal/provider/adc/translator/globalrule.go @@ -0,0 +1,67 @@ +// 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 ( + "encoding/json" + + "github.com/api7/gopkg/pkg/log" + "go.uber.org/zap" + + adctypes "github.com/apache/apisix-ingress-controller/api/adc" + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/internal/provider" +) + +// TranslateApisixGlobalRule translates ApisixGlobalRule to APISIX GlobalRule +func (t *Translator) TranslateApisixGlobalRule(tctx *provider.TranslateContext, obj *apiv2.ApisixGlobalRule) (*TranslateResult, error) { + log.Debugw("translating ApisixGlobalRule", + zap.String("namespace", obj.Namespace), + zap.String("name", obj.Name), + ) + + // Create global rule plugins + plugins := make(adctypes.Plugins) + + // Translate each plugin from the spec + for _, plugin := range obj.Spec.Plugins { + // Check if plugin is enabled (default to true if not specified) + if !plugin.Enable { + 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 + } + } + plugins[plugin.Name] = pluginConfig + } + + return &TranslateResult{ + GlobalRules: adctypes.GlobalRule(plugins), + }, nil +} diff --git a/internal/utils/k8s.go b/internal/utils/k8s.go index eb22f5172..2f32c30a3 100644 --- a/internal/utils/k8s.go +++ b/internal/utils/k8s.go @@ -13,9 +13,10 @@ package utils import ( - "github.com/apache/apisix-ingress-controller/internal/types" k8stypes "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/apache/apisix-ingress-controller/internal/types" ) func NamespacedName(obj client.Object) k8stypes.NamespacedName { diff --git a/test/e2e/apisix/globalrule.go b/test/e2e/apisix/globalrule.go new file mode 100644 index 000000000..9702d6422 --- /dev/null +++ b/test/e2e/apisix/globalrule.go @@ -0,0 +1,337 @@ +// 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" + + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = Describe("Test GlobalRule", func() { + s := scaffold.NewScaffold(&scaffold.Options{ + ControllerName: "apisix.apache.org/apisix-ingress-controller", + }) + + var gatewayProxyYaml = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: apisix-proxy-config + namespace: default +spec: + provider: + type: ControlPlane + controlPlane: + endpoints: + - %s + auth: + type: AdminKey + adminKey: + value: "%s" +` + + var ingressClassYaml = ` +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + name: apisix +spec: + controller: "apisix.apache.org/apisix-ingress-controller" + parameters: + apiGroup: "apisix.apache.org" + kind: "GatewayProxy" + name: "apisix-proxy-config" + namespace: "default" + scope: "Namespace" +` + + var ingressYaml = ` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: test-ingress +spec: + ingressClassName: apisix + rules: + - host: globalrule.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: httpbin-service-e2e-test + port: + number: 80 +` + + Context("ApisixGlobalRule Basic Operations", func() { + 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) + + By("create Ingress") + err = s.CreateResourceFromString(ingressYaml) + Expect(err).NotTo(HaveOccurred(), "creating Ingress") + time.Sleep(5 * time.Second) + + By("verify Ingress works") + Eventually(func() int { + return s.NewAPISIXClient(). + GET("/get"). + WithHost("globalrule.example.com"). + Expect().Raw().StatusCode + }).WithTimeout(8 * time.Second).ProbeEvery(time.Second). + Should(Equal(http.StatusOK)) + }) + + It("Test GlobalRule with response-rewrite plugin", func() { + globalRuleYaml := ` +apiVersion: apisix.apache.org/v2 +kind: ApisixGlobalRule +metadata: + name: test-global-rule-response-rewrite +spec: + ingressClassName: apisix + plugins: + - name: response-rewrite + enable: true + config: + headers: + X-Global-Rule: "test-response-rewrite" + X-Global-Test: "enabled" +` + + By("create ApisixGlobalRule with response-rewrite plugin") + err := s.CreateResourceFromString(globalRuleYaml) + Expect(err).NotTo(HaveOccurred(), "creating ApisixGlobalRule") + + By("verify ApisixGlobalRule status condition") + time.Sleep(5 * time.Second) + gryaml, err := s.GetResourceYaml("ApisixGlobalRule", "test-global-rule-response-rewrite") + Expect(err).NotTo(HaveOccurred(), "getting ApisixGlobalRule yaml") + Expect(gryaml).To(ContainSubstring(`status: "True"`)) + Expect(gryaml).To(ContainSubstring("message: The global rule has been accepted and synced to APISIX")) + + By("verify global rule is applied - response should have custom headers") + resp := s.NewAPISIXClient(). + GET("/get"). + WithHost("globalrule.example.com"). + Expect(). + Status(http.StatusOK) + resp.Header("X-Global-Rule").IsEqual("test-response-rewrite") + resp.Header("X-Global-Test").IsEqual("enabled") + + By("delete ApisixGlobalRule") + err = s.DeleteResource("ApisixGlobalRule", "test-global-rule-response-rewrite") + Expect(err).NotTo(HaveOccurred(), "deleting ApisixGlobalRule") + time.Sleep(5 * time.Second) + + By("verify global rule is removed - response should not have custom headers") + resp = s.NewAPISIXClient(). + GET("/get"). + WithHost("globalrule.example.com"). + Expect(). + Status(http.StatusOK) + resp.Header("X-Global-Rule").IsEmpty() + resp.Header("X-Global-Test").IsEmpty() + }) + + It("Test GlobalRule update", func() { + globalRuleYaml := ` +apiVersion: apisix.apache.org/v2 +kind: ApisixGlobalRule +metadata: + name: test-global-rule-update +spec: + ingressClassName: apisix + plugins: + - name: response-rewrite + enable: true + config: + headers: + X-Update-Test: "version1" +` + + updatedGlobalRuleYaml := ` +apiVersion: apisix.apache.org/v2 +kind: ApisixGlobalRule +metadata: + name: test-global-rule-update +spec: + ingressClassName: apisix + plugins: + - name: response-rewrite + enable: true + config: + headers: + X-Update-Test: "version2" + X-New-Header: "added" +` + + By("create initial ApisixGlobalRule") + err := s.CreateResourceFromString(globalRuleYaml) + Expect(err).NotTo(HaveOccurred(), "creating ApisixGlobalRule") + + By("verify initial ApisixGlobalRule status condition") + time.Sleep(5 * time.Second) + gryaml, err := s.GetResourceYaml("ApisixGlobalRule", "test-global-rule-update") + Expect(err).NotTo(HaveOccurred(), "getting ApisixGlobalRule yaml") + Expect(gryaml).To(ContainSubstring(`status: "True"`)) + Expect(gryaml).To(ContainSubstring("message: The global rule has been accepted and synced to APISIX")) + + By("verify initial configuration") + resp := s.NewAPISIXClient(). + GET("/get"). + WithHost("globalrule.example.com"). + Expect(). + Status(http.StatusOK) + resp.Header("X-Update-Test").IsEqual("version1") + resp.Header("X-New-Header").IsEmpty() + + By("update ApisixGlobalRule") + err = s.CreateResourceFromString(updatedGlobalRuleYaml) + Expect(err).NotTo(HaveOccurred(), "updating ApisixGlobalRule") + + By("verify updated ApisixGlobalRule status condition") + time.Sleep(5 * time.Second) + gryaml, err = s.GetResourceYaml("ApisixGlobalRule", "test-global-rule-update") + Expect(err).NotTo(HaveOccurred(), "getting updated ApisixGlobalRule yaml") + Expect(gryaml).To(ContainSubstring(`status: "True"`)) + Expect(gryaml).To(ContainSubstring("message: The global rule has been accepted and synced to APISIX")) + Expect(gryaml).To(ContainSubstring("observedGeneration: 2")) + + By("verify updated configuration") + resp = s.NewAPISIXClient(). + GET("/get"). + WithHost("globalrule.example.com"). + Expect(). + Status(http.StatusOK) + resp.Header("X-Update-Test").IsEqual("version2") + resp.Header("X-New-Header").IsEqual("added") + + By("delete ApisixGlobalRule") + err = s.DeleteResource("ApisixGlobalRule", "test-global-rule-update") + Expect(err).NotTo(HaveOccurred(), "deleting ApisixGlobalRule") + }) + + It("Test multiple GlobalRules with different plugins", func() { + proxyRewriteGlobalRuleYaml := ` +apiVersion: apisix.apache.org/v2 +kind: ApisixGlobalRule +metadata: + name: test-global-rule-proxy-rewrite +spec: + ingressClassName: apisix + plugins: + - name: proxy-rewrite + enable: true + config: + headers: + add: + X-Global-Proxy: "test" +` + + responseRewriteGlobalRuleYaml := ` +apiVersion: apisix.apache.org/v2 +kind: ApisixGlobalRule +metadata: + name: test-global-rule-response-rewrite-multi +spec: + ingressClassName: apisix + plugins: + - name: response-rewrite + enable: true + config: + headers: + X-Global-Multi: "test-multi-rule" + X-Response-Type: "rewrite" +` + + By("create ApisixGlobalRule with proxy-rewrite plugin") + err := s.CreateResourceFromString(proxyRewriteGlobalRuleYaml) + Expect(err).NotTo(HaveOccurred(), "creating ApisixGlobalRule with proxy-rewrite") + + By("create ApisixGlobalRule with response-rewrite plugin") + err = s.CreateResourceFromString(responseRewriteGlobalRuleYaml) + Expect(err).NotTo(HaveOccurred(), "creating ApisixGlobalRule with response-rewrite") + + By("verify both ApisixGlobalRule status conditions") + time.Sleep(5 * time.Second) + + proxyRewriteYaml, err := s.GetResourceYaml("ApisixGlobalRule", "test-global-rule-proxy-rewrite") + Expect(err).NotTo(HaveOccurred(), "getting proxy-rewrite ApisixGlobalRule yaml") + Expect(proxyRewriteYaml).To(ContainSubstring(`status: "True"`)) + Expect(proxyRewriteYaml).To(ContainSubstring("message: The global rule has been accepted and synced to APISIX")) + + responseRewriteYaml, err := s.GetResourceYaml("ApisixGlobalRule", "test-global-rule-response-rewrite-multi") + Expect(err).NotTo(HaveOccurred(), "getting response-rewrite ApisixGlobalRule yaml") + Expect(responseRewriteYaml).To(ContainSubstring(`status: "True"`)) + Expect(responseRewriteYaml).To(ContainSubstring("message: The global rule has been accepted and synced to APISIX")) + + By("verify both global rules are applied on GET request") + getResp := s.NewAPISIXClient(). + GET("/get"). + WithHost("globalrule.example.com"). + Expect(). + Status(http.StatusOK) + getResp.Header("X-Global-Multi").IsEqual("test-multi-rule") + getResp.Header("X-Response-Type").IsEqual("rewrite") + getResp.Body().Contains(`"X-Global-Proxy": "test"`) + + By("delete proxy-rewrite ApisixGlobalRule") + err = s.DeleteResource("ApisixGlobalRule", "test-global-rule-proxy-rewrite") + Expect(err).NotTo(HaveOccurred(), "deleting proxy-rewrite ApisixGlobalRule") + time.Sleep(5 * time.Second) + + By("verify only response-rewrite global rule remains - proxy-rewrite headers should be removed") + getRespAfterProxyDelete := s.NewAPISIXClient(). + GET("/get"). + WithHost("globalrule.example.com"). + Expect(). + Status(http.StatusOK) + getRespAfterProxyDelete.Header("X-Global-Multi").IsEqual("test-multi-rule") + getRespAfterProxyDelete.Header("X-Response-Type").IsEqual("rewrite") + getRespAfterProxyDelete.Body().NotContains(`"X-Global-Proxy": "test"`) + + By("delete response-rewrite ApisixGlobalRule") + err = s.DeleteResource("ApisixGlobalRule", "test-global-rule-response-rewrite-multi") + Expect(err).NotTo(HaveOccurred(), "deleting response-rewrite ApisixGlobalRule") + time.Sleep(5 * time.Second) + + By("verify all global rules are removed") + finalResp := s.NewAPISIXClient(). + GET("/get"). + WithHost("globalrule.example.com"). + Expect(). + Status(http.StatusOK) + finalResp.Header("X-Global-Multi").IsEmpty() + finalResp.Header("X-Response-Type").IsEmpty() + finalResp.Body().NotContains(`"X-Global-Proxy": "test"`) + }) + }) +}) diff --git a/test/e2e/apiv2/apisixconsumer.go b/test/e2e/apiv2/apisixconsumer.go index d5087cf5f..b3587e360 100644 --- a/test/e2e/apiv2/apisixconsumer.go +++ b/test/e2e/apiv2/apisixconsumer.go @@ -11,35 +11,3 @@ // limitations under the License. package apiv2 - -import ( - . "github.com/onsi/ginkgo/v2" - "k8s.io/apimachinery/pkg/types" - - "github.com/apache/apisix-ingress-controller/test/e2e/framework" - "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" -) - -var _ = Describe("Test ApisixConsumer", func() { - var ( - s = scaffold.NewDefaultScaffold() - applier = framework.NewApplier(s.GinkgoT, s.K8sClient, s.CreateResourceFromString) - ) - Context("Test ApisixConsumer", func() { - It("Test ApisixConsumer", func() { - var apisixConsumerSpec = ` -apiVersion: apisix.apache.org/v2 -kind: ApisixConsumer -metadata: - name: defaultapisixconsumer -spec: - authParameter: - basicAuth: - value: - username: jack - password: jack-password -` - applier.MustApplyApisixConsumer(types.NamespacedName{Name: "defaultapisixconsumer", Namespace: s.Namespace()}, apisixConsumerSpec) - }) - }) -})