diff --git a/.github/codecov.yaml b/.github/codecov.yaml index 13a5999da..f0da968ba 100644 --- a/.github/codecov.yaml +++ b/.github/codecov.yaml @@ -2,10 +2,21 @@ codecov: bot: "Codecov Bot" max_report_age: 12 - require_ci_to_pass: yes + require_ci_to_pass: true notify: after_n_builds: 1 - wait_for_ci: yes + wait_for_ci: true + +coverage: + status: + project: + default: + target: auto + threshold: 1% + patch: + default: + target: auto + threshold: 1% # Layout of the PR comment produced by Codecov bot comment: @@ -13,8 +24,8 @@ comment: # Find more at https://docs.codecov.com/docs/ignoring-paths ignore: - - api/external/** # ignoring external vendor code - - "**/*.deepcopy.go" # ignore controller-gen generated code + - api/external/** # ignoring external vendor code + - "**/*.deepcopy.go" # ignore controller-gen generated code flag_management: individual_flags: diff --git a/controllers/gateway_kuadrant_controller.go b/controllers/gateway_kuadrant_controller.go new file mode 100644 index 000000000..9f39c529a --- /dev/null +++ b/controllers/gateway_kuadrant_controller.go @@ -0,0 +1,136 @@ +/* +Copyright 2021 Red Hat, Inc. + +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 controllers + +import ( + "context" + "encoding/json" + "strings" + + "github.com/go-logr/logr" + apierrors "k8s.io/apimachinery/pkg/api/errors" + 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/predicate" + gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/reconcilers" +) + +// GatewayKuadrantReconciler reconciles Gateway object with kuadrant metadata +type GatewayKuadrantReconciler struct { + *reconcilers.BaseReconciler +} + +//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=kuadrant.io,resources=kuadrants,verbs=get;list + +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.10.0/pkg/reconcile +func (r *GatewayKuadrantReconciler) Reconcile(eventCtx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := r.Logger().WithValues("Gateway", req.NamespacedName) + logger.Info("Reconciling Kuadrant annotations") + ctx := logr.NewContext(eventCtx, logger) + + gw := &gatewayapiv1beta1.Gateway{} + if err := r.Client().Get(ctx, req.NamespacedName, gw); err != nil { + if apierrors.IsNotFound(err) { + logger.Info("no gateway found") + return ctrl.Result{}, nil + } + logger.Error(err, "failed to get gateway") + return ctrl.Result{}, err + } + + if logger.V(1).Enabled() { + jsonData, err := json.MarshalIndent(gw, "", " ") + if err != nil { + return ctrl.Result{}, err + } + logger.V(1).Info(string(jsonData)) + } + + err := r.reconcileGatewayKuadrantMetadata(ctx, gw) + + if err != nil { + return ctrl.Result{}, err + } + + logger.Info("Gateway kuadrant annotations reconciled successfully") + return ctrl.Result{}, nil +} + +func (r *GatewayKuadrantReconciler) reconcileGatewayKuadrantMetadata(ctx context.Context, gw *gatewayapiv1beta1.Gateway) error { + updated, err := r.reconcileKuadrantNamespaceAnnotation(ctx, gw) + if err != nil { + return err + } + + if updated { + if err := r.Client().Update(ctx, gw); err != nil { + return err + } + } + + return nil +} + +func (r *GatewayKuadrantReconciler) reconcileKuadrantNamespaceAnnotation(ctx context.Context, gw *gatewayapiv1beta1.Gateway) (bool, error) { + logger, err := logr.FromContext(ctx) + if err != nil { + return false, err + } + + if common.IsKuadrantManaged(gw) { + return false, nil + } + + kuadrantList := &kuadrantv1beta1.KuadrantList{} + if err := r.Client().List(ctx, kuadrantList); err != nil { + return false, err + } + if len(kuadrantList.Items) == 0 { + // Kuadrant was not found + logger.Info("Kuadrant instance not found in the cluster") + return false, nil + } + + if len(kuadrantList.Items) > 1 { + // multiple kuadrant instances? not supported + keys := make([]string, len(kuadrantList.Items)) + for idx := range kuadrantList.Items { + keys[idx] = client.ObjectKeyFromObject(&kuadrantList.Items[idx]).String() + } + logger.Info("Multiple kuadrant instances found", "num", len(kuadrantList.Items), "keys", strings.Join(keys[:], ",")) + return false, nil + } + + common.AnnotateObject(gw, kuadrantList.Items[0].Namespace) + + return true, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *GatewayKuadrantReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + // Gateway Kuadrant controller only cares about the annotations + For(&gatewayapiv1beta1.Gateway{}, builder.WithPredicates(predicate.AnnotationChangedPredicate{})). + Complete(r) +} diff --git a/controllers/gateway_kuadrant_controller_test.go b/controllers/gateway_kuadrant_controller_test.go new file mode 100644 index 000000000..443ec7b50 --- /dev/null +++ b/controllers/gateway_kuadrant_controller_test.go @@ -0,0 +1,128 @@ +//go:build integration + +package controllers + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + "github.com/kuadrant/kuadrant-operator/pkg/common" +) + +var _ = Describe("Kuadrant Gateway controller", func() { + var ( + testNamespace string + gwName = "toystore-gw" + ) + + beforeEachCallback := func() { + CreateNamespace(&testNamespace) + + ApplyKuadrantCR(testNamespace) + } + + BeforeEach(beforeEachCallback) + AfterEach(DeleteNamespaceCallback(&testNamespace)) + + Context("Gateway created after Kuadrant instance", func() { + It("gateway should have required annotation", func() { + gateway := testBuildBasicGateway(gwName, testNamespace) + err := k8sClient.Create(context.Background(), gateway) + Expect(err).ToNot(HaveOccurred()) + + Eventually(func() bool { + existingGateway := &gatewayapiv1beta1.Gateway{} + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(gateway), existingGateway) + if err != nil { + logf.Log.V(1).Info("[WARN] Getting gateway failed", "error", err) + return false + } + + if meta.IsStatusConditionFalse(existingGateway.Status.Conditions, common.GatewayProgrammedConditionType) { + logf.Log.V(1).Info("[WARN] Gateway not ready") + return false + } + + return true + }, 15*time.Second, 5*time.Second).Should(BeTrue()) + + // Check gateway is annotated with kuadrant annotation + Eventually(func() bool { + existingGateway := &gatewayapiv1beta1.Gateway{} + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(gateway), existingGateway) + if err != nil { + logf.Log.V(1).Info("[WARN] Getting gateway failed", "error", err) + return false + } + return common.IsKuadrantManaged(existingGateway) + }, 15*time.Second, 5*time.Second).Should(BeTrue()) + }) + }) + + Context("Two kuadrant instances", func() { + + BeforeEach(func() { + newKuadrantName := "second" + newKuadrant := &kuadrantv1beta1.Kuadrant{ + TypeMeta: metav1.TypeMeta{APIVersion: "v1beta1", Kind: "Kuadrant"}, + ObjectMeta: metav1.ObjectMeta{Name: newKuadrantName, Namespace: testNamespace}, + } + err := testClient().Create(context.Background(), newKuadrant) + Expect(err).ToNot(HaveOccurred()) + + Eventually(func() bool { + kuadrant := &kuadrantv1beta1.Kuadrant{} + err := k8sClient.Get(context.Background(), client.ObjectKey{Name: newKuadrantName, Namespace: testNamespace}, kuadrant) + if err != nil { + return false + } + if !meta.IsStatusConditionTrue(kuadrant.Status.Conditions, "Ready") { + return false + } + return true + }, time.Minute, 5*time.Second).Should(BeTrue()) + }) + + It("new gateway should not be annotated", func() { + gateway := testBuildBasicGateway(gwName, testNamespace) + err := k8sClient.Create(context.Background(), gateway) + Expect(err).ToNot(HaveOccurred()) + + Eventually(func() bool { + existingGateway := &gatewayapiv1beta1.Gateway{} + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(gateway), existingGateway) + if err != nil { + logf.Log.V(1).Info("[WARN] Getting gateway failed", "error", err) + return false + } + + if meta.IsStatusConditionFalse(existingGateway.Status.Conditions, common.GatewayProgrammedConditionType) { + logf.Log.V(1).Info("[WARN] Gateway not ready") + return false + } + + return true + }, 15*time.Second, 5*time.Second).Should(BeTrue()) + + // Check gateway is not annotated with kuadrant annotation + Eventually(func() bool { + existingGateway := &gatewayapiv1beta1.Gateway{} + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(gateway), existingGateway) + if err != nil { + logf.Log.V(1).Info("[WARN] Getting gateway failed", "error", err) + return false + } + return !common.IsKuadrantManaged(existingGateway) + }, 15*time.Second, 5*time.Second).Should(BeTrue()) + }) + }) +}) diff --git a/controllers/suite_test.go b/controllers/suite_test.go index e31da642b..d6b3d999e 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -168,6 +168,18 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) + gatewayKuadrantBaseReconciler := reconcilers.NewBaseReconciler( + mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), + log.Log.WithName("kuadrant").WithName("gateway"), + mgr.GetEventRecorderFor("GatewayKuadrant"), + ) + + err = (&GatewayKuadrantReconciler{ + BaseReconciler: gatewayKuadrantBaseReconciler, + }).SetupWithManager(mgr) + + Expect(err).NotTo(HaveOccurred()) + go func() { defer GinkgoRecover() err = mgr.Start(ctrl.SetupSignalHandler()) diff --git a/main.go b/main.go index dbf7c6880..664497cc9 100644 --- a/main.go +++ b/main.go @@ -182,6 +182,19 @@ func main() { os.Exit(1) } + gatewayKuadrantBaseReconciler := reconcilers.NewBaseReconciler( + mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), + log.Log.WithName("kuadrant").WithName("gateway"), + mgr.GetEventRecorderFor("GatewayKuadrant"), + ) + + if err = (&controllers.GatewayKuadrantReconciler{ + BaseReconciler: gatewayKuadrantBaseReconciler, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "GatewayKuadrant") + os.Exit(1) + } + //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {