diff --git a/api/v2/apisixupstream_types.go b/api/v2/apisixupstream_types.go index 27c80c70c..552164218 100644 --- a/api/v2/apisixupstream_types.go +++ b/api/v2/apisixupstream_types.go @@ -36,14 +36,19 @@ type ApisixUpstreamSpec struct { PortLevelSettings []PortLevelSettings `json:"portLevelSettings,omitempty" yaml:"portLevelSettings,omitempty"` } +// ApisixUpstreamStatus defines the observed state of ApisixUpstream. +type ApisixUpstreamStatus = ApisixStatus + // +kubebuilder:object:root=true +// +kubebuilder:subresource:status // ApisixUpstream is the Schema for the apisixupstreams API. type ApisixUpstream struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec ApisixUpstreamSpec `json:"spec,omitempty"` + Spec ApisixUpstreamSpec `json:"spec,omitempty"` + Status ApisixUpstreamStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index e353bbf75..c97bfe340 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -1208,6 +1208,7 @@ func (in *ApisixUpstream) DeepCopyInto(out *ApisixUpstream) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixUpstream. diff --git a/config/crd/bases/apisix.apache.org_apisixupstreams.yaml b/config/crd/bases/apisix.apache.org_apisixupstreams.yaml index c4efa840c..542a0c5f3 100644 --- a/config/crd/bases/apisix.apache.org_apisixupstreams.yaml +++ b/config/crd/bases/apisix.apache.org_apisixupstreams.yaml @@ -490,6 +490,68 @@ spec: the pass_host is set to rewrite type: string type: object + status: + description: ApisixStatus is the status report for Apisix ingress Resources + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object type: object served: true storage: true + subresources: + status: {} diff --git a/docs/crd/api.md b/docs/crd/api.md index 37032cdc8..a337c2db5 100644 --- a/docs/crd/api.md +++ b/docs/crd/api.md @@ -1300,6 +1300,8 @@ _Appears in:_ + + #### ApisixTlsSpec diff --git a/internal/controller/apisixpluginconfig_controller.go b/internal/controller/apisixpluginconfig_controller.go index ba57cac01..332b0df2c 100644 --- a/internal/controller/apisixpluginconfig_controller.go +++ b/internal/controller/apisixpluginconfig_controller.go @@ -30,7 +30,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/status" - "github.com/apache/apisix-ingress-controller/internal/provider" "github.com/apache/apisix-ingress-controller/internal/utils" ) @@ -46,11 +45,7 @@ type ApisixPluginConfigReconciler struct { func (r *ApisixPluginConfigReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&apiv2.ApisixPluginConfig{}). - WithEventFilter( - predicate.Or( - predicate.GenerationChangedPredicate{}, - ), - ). + WithEventFilter(predicate.GenerationChangedPredicate{}). Watches(&networkingv1.IngressClass{}, handler.EnqueueRequestsFromMapFunc(r.listApisixPluginConfigForIngressClass), builder.WithPredicates( @@ -67,23 +62,12 @@ func (r *ApisixPluginConfigReconciler) SetupWithManager(mgr ctrl.Manager) error func (r *ApisixPluginConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { var pc apiv2.ApisixPluginConfig if err := r.Get(ctx, req.NamespacedName, &pc); err != nil { - if client.IgnoreNotFound(err) == nil { - pc.Namespace = req.Namespace - pc.Name = req.Name - pc.TypeMeta = metav1.TypeMeta{ - Kind: KindApisixPluginConfig, - APIVersion: apiv2.GroupVersion.String(), - } - - return ctrl.Result{}, nil - } - return ctrl.Result{}, err + return ctrl.Result{}, client.IgnoreNotFound(err) } var ( - tctx = provider.NewDefaultTranslateContext(ctx) - ic *networkingv1.IngressClass - err error + ic *networkingv1.IngressClass + err error ) defer func() { r.updateStatus(&pc, err) @@ -92,7 +76,7 @@ func (r *ApisixPluginConfigReconciler) Reconcile(ctx context.Context, req ctrl.R if ic, err = r.getIngressClass(&pc); err != nil { return ctrl.Result{}, err } - if err = r.processIngressClassParameters(ctx, tctx, &pc, ic); err != nil { + if err = r.processIngressClassParameters(ctx, &pc, ic); err != nil { return ctrl.Result{}, err } return ctrl.Result{}, nil @@ -174,15 +158,13 @@ func (r *ApisixPluginConfigReconciler) getDefaultIngressClass() (*networkingv1.I } // processIngressClassParameters processes the IngressClass parameters that reference GatewayProxy -func (r *ApisixPluginConfigReconciler) processIngressClassParameters(ctx context.Context, tc *provider.TranslateContext, pc *apiv2.ApisixPluginConfig, ingressClass *networkingv1.IngressClass) error { +func (r *ApisixPluginConfigReconciler) processIngressClassParameters(ctx context.Context, pc *apiv2.ApisixPluginConfig, ingressClass *networkingv1.IngressClass) error { if ingressClass == nil || ingressClass.Spec.Parameters == nil { return nil } var ( - ingressClassKind = utils.NamespacedNameKind(ingressClass) - pcKind = utils.NamespacedNameKind(pc) - parameters = ingressClass.Spec.Parameters + parameters = ingressClass.Spec.Parameters ) if parameters.APIGroup == nil || *parameters.APIGroup != v1alpha1.GroupVersion.Group || parameters.Kind != KindGatewayProxy { return nil @@ -197,15 +179,7 @@ func (r *ApisixPluginConfigReconciler) processIngressClassParameters(ctx context ns = &pc.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[pcKind] = append(tc.ResourceParentRefs[pcKind], ingressClassKind) - - return nil + return r.Get(ctx, client.ObjectKey{Namespace: *ns, Name: parameters.Name}, &gatewayProxy) } func (r *ApisixPluginConfigReconciler) updateStatus(pc *apiv2.ApisixPluginConfig, err error) { diff --git a/internal/controller/apisixupstream_controller.go b/internal/controller/apisixupstream_controller.go new file mode 100644 index 000000000..0da4fc196 --- /dev/null +++ b/internal/controller/apisixupstream_controller.go @@ -0,0 +1,192 @@ +// 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 controller + +import ( + "cmp" + "context" + + "github.com/go-logr/logr" + networkingv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + 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" + + "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/status" + "github.com/apache/apisix-ingress-controller/internal/utils" +) + +// ApisixUpstreamReconciler reconciles a ApisixUpstream object +type ApisixUpstreamReconciler struct { + client.Client + Scheme *runtime.Scheme + Log logr.Logger + Updater status.Updater +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ApisixUpstreamReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&apiv2.ApisixUpstream{}). + WithEventFilter(predicate.GenerationChangedPredicate{}). + Watches(&networkingv1.IngressClass{}, + handler.EnqueueRequestsFromMapFunc(r.listApisixUpstreamForIngressClass), + builder.WithPredicates( + predicate.NewPredicateFuncs(r.matchesIngressController), + ), + ). + Watches(&v1alpha1.GatewayProxy{}, + handler.EnqueueRequestsFromMapFunc(r.listApisixUpstreamForGatewayProxy), + ). + Named("apisixupstream"). + Complete(r) +} + +func (r *ApisixUpstreamReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var au apiv2.ApisixUpstream + if err := r.Get(ctx, req.NamespacedName, &au); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + var ( + ic *networkingv1.IngressClass + err error + ) + defer func() { + r.updateStatus(&au, err) + }() + + if ic, err = r.getIngressClass(&au); err != nil { + return ctrl.Result{}, err + } + if err = r.processIngressClassParameters(ctx, &au, ic); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil +} + +func (r *ApisixUpstreamReconciler) listApisixUpstreamForIngressClass(ctx context.Context, object client.Object) (requests []reconcile.Request) { + ic, ok := object.(*networkingv1.IngressClass) + if !ok { + return nil + } + + isDefaultIngressClass := IsDefaultIngressClass(ic) + var auList apiv2.ApisixUpstreamList + if err := r.List(ctx, &auList); err != nil { + return nil + } + for _, pc := range auList.Items { + if pc.Spec.IngressClassName == ic.Name || (isDefaultIngressClass && pc.Spec.IngressClassName == "") { + requests = append(requests, reconcile.Request{NamespacedName: utils.NamespacedName(&pc)}) + } + } + return requests +} + +func (r *ApisixUpstreamReconciler) listApisixUpstreamForGatewayProxy(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); 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.listApisixUpstreamForIngressClass(ctx, &ic)...) + } + + return requests +} + +func (r *ApisixUpstreamReconciler) matchesIngressController(obj client.Object) bool { + ingressClass, ok := obj.(*networkingv1.IngressClass) + if !ok { + return false + } + return matchesController(ingressClass.Spec.Controller) +} + +func (r *ApisixUpstreamReconciler) getIngressClass(au *apiv2.ApisixUpstream) (*networkingv1.IngressClass, error) { + if au.Spec.IngressClassName == "" { + return r.getDefaultIngressClass() + } + + var ic networkingv1.IngressClass + if err := r.Get(context.Background(), client.ObjectKey{Name: au.Spec.IngressClassName}, &ic); err != nil { + return nil, err + } + return &ic, nil +} + +func (r *ApisixUpstreamReconciler) processIngressClassParameters(ctx context.Context, au *apiv2.ApisixUpstream, ic *networkingv1.IngressClass) error { + if ic == nil || ic.Spec.Parameters == nil { + return nil + } + + var ( + parameters = ic.Spec.Parameters + ) + if parameters.APIGroup == nil || *parameters.APIGroup != v1alpha1.GroupVersion.Group || parameters.Kind != KindGatewayProxy { + return nil + } + + // check if the parameters reference GatewayProxy + var ( + gp v1alpha1.GatewayProxy + ns = cmp.Or(parameters.Namespace, &au.Namespace) + ) + + return r.Get(ctx, client.ObjectKey{Namespace: *ns, Name: parameters.Name}, &gp) +} + +func (r *ApisixUpstreamReconciler) getDefaultIngressClass() (*networkingv1.IngressClass, error) { + var icList networkingv1.IngressClassList + if err := r.List(context.Background(), &icList); 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 does not match the controller", + } +} + +func (r *ApisixUpstreamReconciler) updateStatus(au *apiv2.ApisixUpstream, err error) { + SetApisixCRDConditionAccepted(&au.Status, au.GetGeneration(), err) + r.Updater.Update(status.Update{ + NamespacedName: utils.NamespacedName(au), + Resource: &apiv2.ApisixUpstream{}, + Mutator: status.MutatorFunc(func(obj client.Object) client.Object { + cp := obj.(*apiv2.ApisixUpstream).DeepCopy() + cp.Status = au.Status + return cp + }), + }) +} diff --git a/internal/manager/controllers.go b/internal/manager/controllers.go index ac7fcf4f3..0a52499c2 100644 --- a/internal/manager/controllers.go +++ b/internal/manager/controllers.go @@ -140,5 +140,11 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixPluginConfig"), Updater: updater, }, + &controller.ApisixUpstreamReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixUpstream"), + Updater: updater, + }, }, nil } diff --git a/test/e2e/apisix/route.go b/test/e2e/apisix/route.go index 6c08536a1..67e1e5ffb 100644 --- a/test/e2e/apisix/route.go +++ b/test/e2e/apisix/route.go @@ -49,6 +49,7 @@ var _ = Describe("Test ApisixRoute", func() { }) Context("Test ApisixRoute", func() { + It("Basic tests", func() { const apisixRouteSpec = ` apiVersion: apisix.apache.org/v2 @@ -298,7 +299,6 @@ spec: }) Context("Test ApisixRoute reference ApisixUpstream", func() { - It("Test reference ApisixUpstream", func() { const apisixRouteSpec = ` apiVersion: apisix.apache.org/v2 @@ -349,8 +349,7 @@ spec: By("create Service, ApisixUpstream and ApisixRoute") err := s.CreateResourceFromString(serviceSpec) Expect(err).ShouldNot(HaveOccurred(), "apply service") - err = s.CreateResourceFromString(apisixUpstreamSpec0) - Expect(err).ShouldNot(HaveOccurred(), "apply apisixUpstreamSpec") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default-upstream"}, new(apiv2.ApisixUpstream), apisixUpstreamSpec0) var apisxiRoute apiv2.ApisixRoute applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisxiRoute, apisixRouteSpec) @@ -362,8 +361,7 @@ spec: Eventually(request).WithArguments("/get").WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusServiceUnavailable)) By("verify that ApisixUpstream reference a Service which is ExternalName should request OK") - err = s.CreateResourceFromString(apisixUpstreamSpec1) - Expect(err).ShouldNot(HaveOccurred(), "update apisixUpstream") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default-upstream"}, new(apiv2.ApisixUpstream), apisixUpstreamSpec1) Eventually(request).WithArguments("/get").WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) }) @@ -409,12 +407,10 @@ spec: "X-Upstream-Host": "$upstream_addr" ` By("apply ApisixUpstream") - err := s.CreateResourceFromString(apisixUpstreamSpec) - Expect(err).ShouldNot(HaveOccurred(), "apply ApisixUpstream") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default-upstream"}, new(apiv2.ApisixUpstream), apisixUpstreamSpec) By("apply ApisixRoute") - var apisixRoute apiv2.ApisixRoute - applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpec) + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, new(apiv2.ApisixRoute), apisixRouteSpec) By("verify ApisixRoute works") request := func(path string) int {