From 37b89f721724928841c78e09a5b854023d694d96 Mon Sep 17 00:00:00 2001 From: Jason Zhang Date: Tue, 5 Sep 2023 11:11:04 -0400 Subject: [PATCH] Allow OperatorPolicy to create OLM subscriptions Modified the controller logic such that the OperatorPolicy controller can create OLM subscriptions based on the policy spec. Currently, an OperatorGroup is created for each Subscription. Future implementation will support creating OperatorGroups based on installModes supported by the generated CSVs. ref: https://issues.redhat.com/browse/ACM-6597 Signed-off-by: Jason Zhang --- controllers/operatorpolicy_controller.go | 153 +++++++++++++++++- controllers/operatorpolicy_controller_test.go | 87 ++++++++++ controllers/suite_test.go | 4 + main.go | 4 + 4 files changed, 240 insertions(+), 8 deletions(-) create mode 100644 controllers/operatorpolicy_controller_test.go diff --git a/controllers/operatorpolicy_controller.go b/controllers/operatorpolicy_controller.go index 5eda072f..92c64a07 100644 --- a/controllers/operatorpolicy_controller.go +++ b/controllers/operatorpolicy_controller.go @@ -6,9 +6,13 @@ package controllers import ( "context" "fmt" + "strings" "time" + operatorv1 "github.com/operator-framework/api/pkg/operators/v1" + operatorv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -113,24 +117,103 @@ func (r *OperatorPolicyReconciler) PeriodicallyExecOperatorPolicies(freq uint, e } // handleSinglePolicy encapsulates the logic for processing a single operatorPolicy. -// Currently, this just means setting the status to Compliant and logging the name -// of the operatorPolicy. +// Currently, the controller is able to create an OLM Subscription from the operatorPolicy specs, +// create an OperatorGroup in the ns as the Subscription, set the compliance status, and log the policy. // // In the future, more reconciliation logic will be added. For reference: // https://github.com/JustinKuli/ocm-enhancements/blob/89-operator-policy/enhancements/sig-policy/89-operator-policy-kind/README.md func (r *OperatorPolicyReconciler) handleSinglePolicy( policy *policyv1beta1.OperatorPolicy, ) error { - policy.Status.ComplianceState = policyv1.Compliant + OpLog.Info("Handling OperatorPolicy", "policy", policy.Name) + + subscriptionSpec := new(operatorv1alpha1.Subscription) + err := r.Get(context.TODO(), + types.NamespacedName{Namespace: subscriptionSpec.Namespace, Name: subscriptionSpec.Name}, + subscriptionSpec) + exists := !errors.IsNotFound(err) + shouldExist := strings.EqualFold(string(policy.Spec.ComplianceType), string(policyv1.MustHave)) + + // Object does not exist but it should exist, create object + if !exists && shouldExist { + if strings.EqualFold(string(policy.Spec.RemediationAction), string(policyv1.Enforce)) { + OpLog.Info("creating kind " + subscriptionSpec.Kind + " in ns " + subscriptionSpec.Namespace) + subscriptionSpec := buildSubscription(policy, subscriptionSpec) + err = r.Create(context.TODO(), subscriptionSpec) - err := r.updatePolicyStatus(policy) - if err != nil { - OpLog.Info("error while updating policy status") + if err != nil { + r.setCompliance(policy, policyv1.NonCompliant) + OpLog.Error(err, "Could not handle missing musthave object") - return err + return err + } + + // Currently creates an OperatorGroup for every Subscription + // in the same ns, and defaults to targeting all ns. + // Future implementations will enable targeting ns based on + // installModes supported by the CSV. Also, only one OperatorGroup + // should exist in each ns + operatorGroup := buildOperatorGroup(policy) + err = r.Create(context.TODO(), operatorGroup) + + if err != nil { + r.setCompliance(policy, policyv1.NonCompliant) + OpLog.Error(err, "Could not handle missing musthave object") + + return err + } + + r.setCompliance(policy, policyv1.Compliant) + + return nil + } + + // Inform + r.setCompliance(policy, policyv1.NonCompliant) + + return nil + } + + // Object exists but it should not exist, delete object + // Deleting related objects will be added in the future + if exists && !shouldExist { + if strings.EqualFold(string(policy.Spec.RemediationAction), string(policyv1.Enforce)) { + OpLog.Info("deleting kind " + subscriptionSpec.Kind + " in ns " + subscriptionSpec.Namespace) + err = r.Delete(context.TODO(), subscriptionSpec) + + if err != nil { + r.setCompliance(policy, policyv1.NonCompliant) + OpLog.Error(err, "Could not handle existing musthave object") + + return err + } + + r.setCompliance(policy, policyv1.Compliant) + + return nil + } + + // Inform + r.setCompliance(policy, policyv1.NonCompliant) + + return nil } - OpLog.Info("Logging operator policy", "policy", policy.Name) + // Object does not exist and it should not exist, emit success event + if !exists && !shouldExist { + OpLog.Info("The object does not exist and is compliant with the mustnothave compliance type") + // Future implementation: Possibly emit a success event + + return nil + } + + // Object exists, now need to validate field to make sure they match + if exists { + OpLog.Info("The object already exists. Checking fields to verify matching specs") + // Future implementation: Verify the specs of the object matches the one on the cluster + + return nil + } return nil } @@ -176,3 +259,57 @@ func (r *OperatorPolicyReconciler) shouldEvaluatePolicy( return true } + +// buildSubscription bootstraps the subscription spec defined in the operator policy +// with the apiversion and kind in preparation for resource creation +func buildSubscription( + policy *policyv1beta1.OperatorPolicy, + subscription *operatorv1alpha1.Subscription, +) *operatorv1alpha1.Subscription { + gvk := schema.GroupVersionKind{ + Group: "operators.coreos.com", + Version: "v1alpha1", + Kind: "Subscription", + } + + subscription.SetGroupVersionKind(gvk) + subscription.ObjectMeta.Name = policy.Spec.Subscription.Package + subscription.ObjectMeta.Namespace = policy.Spec.Subscription.Namespace + subscription.Spec = policy.Spec.Subscription.SubscriptionSpec.DeepCopy() + + return subscription +} + +// Sets the compliance of the policy +func (r *OperatorPolicyReconciler) setCompliance( + policy *policyv1beta1.OperatorPolicy, + compliance policyv1.ComplianceState, +) { + policy.Status.ComplianceState = compliance + + err := r.updatePolicyStatus(policy) + if err != nil { + OpLog.Error(err, "error while updating policy status") + } +} + +// buildOperatorGroup bootstraps the OperatorGroup spec defined in the operator policy +// with the apiversion and kind in preparation for resource creation +func buildOperatorGroup( + policy *policyv1beta1.OperatorPolicy, +) *operatorv1.OperatorGroup { + operatorGroup := new(operatorv1.OperatorGroup) + + gvk := schema.GroupVersionKind{ + Group: "operators.coreos.com", + Version: "v1", + Kind: "OperatorGroup", + } + + operatorGroup.SetGroupVersionKind(gvk) + operatorGroup.ObjectMeta.SetName(policy.Spec.Subscription.Package + "-operator-group") + operatorGroup.ObjectMeta.SetNamespace(policy.Spec.Subscription.Namespace) + operatorGroup.Spec.TargetNamespaces = []string{"*"} + + return operatorGroup +} diff --git a/controllers/operatorpolicy_controller_test.go b/controllers/operatorpolicy_controller_test.go new file mode 100644 index 00000000..9eb6fd2a --- /dev/null +++ b/controllers/operatorpolicy_controller_test.go @@ -0,0 +1,87 @@ +package controllers + +import ( + "testing" + + operatorv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + policyv1beta1 "open-cluster-management.io/config-policy-controller/api/v1beta1" +) + +func TestBuildSubscription(t *testing.T) { + testSubscription := new(operatorv1alpha1.Subscription) + testPolicy := &policyv1beta1.OperatorPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "default", + }, + Spec: policyv1beta1.OperatorPolicySpec{ + Severity: "low", + RemediationAction: "enforce", + ComplianceType: "musthave", + Subscription: policyv1beta1.SubscriptionSpec{ + SubscriptionSpec: operatorv1alpha1.SubscriptionSpec{ + Channel: "stable", + Package: "my-operator", + InstallPlanApproval: "Automatic", + CatalogSource: "my-catalog", + CatalogSourceNamespace: "my-ns", + StartingCSV: "my-operator-v1", + }, + Namespace: "default", + }, + }, + } + desiredGVK := schema.GroupVersionKind{ + Group: "operators.coreos.com", + Version: "v1alpha1", + Kind: "Subscription", + } + + // Check values are correctly bootstrapped to the Subscription + ret := buildSubscription(testPolicy, testSubscription) + assert.Equal(t, ret.GetObjectKind().GroupVersionKind(), desiredGVK) + assert.Equal(t, ret.ObjectMeta.Name, "my-operator") + assert.Equal(t, ret.ObjectMeta.Namespace, "default") + assert.Equal(t, ret.Spec, &testPolicy.Spec.Subscription.SubscriptionSpec) +} + +func TestBuildOperatorGroup(t *testing.T) { + testPolicy := &policyv1beta1.OperatorPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "default", + }, + Spec: policyv1beta1.OperatorPolicySpec{ + Severity: "low", + RemediationAction: "enforce", + ComplianceType: "musthave", + Subscription: policyv1beta1.SubscriptionSpec{ + SubscriptionSpec: operatorv1alpha1.SubscriptionSpec{ + Channel: "stable", + Package: "my-operator", + InstallPlanApproval: "Automatic", + CatalogSource: "my-catalog", + CatalogSourceNamespace: "my-ns", + StartingCSV: "my-operator-v1", + }, + Namespace: "default", + }, + }, + } + desiredGVK := schema.GroupVersionKind{ + Group: "operators.coreos.com", + Version: "v1", + Kind: "OperatorGroup", + } + + // Ensure OperatorGroup values are populated correctly + ret := buildOperatorGroup(testPolicy) + assert.Equal(t, ret.GetObjectKind().GroupVersionKind(), desiredGVK) + assert.Equal(t, ret.ObjectMeta.GetName(), "my-operator-operator-group") + assert.Equal(t, ret.ObjectMeta.GetNamespace(), "default") + assert.Equal(t, ret.Spec.TargetNamespaces, []string{"*"}) +} diff --git a/controllers/suite_test.go b/controllers/suite_test.go index e8bfc7e0..179e58a5 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -9,6 +9,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + operatorv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" @@ -54,6 +55,9 @@ var _ = BeforeSuite(func() { err = policyv1beta1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = operatorv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + //+kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) diff --git a/main.go b/main.go index 11b0798f..b3b174a8 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,8 @@ import ( "time" "github.com/go-logr/zapr" + operatorv1 "github.com/operator-framework/api/pkg/operators/v1" + operatorv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" "github.com/spf13/pflag" "github.com/stolostron/go-log-utils/zaputil" appsv1 "k8s.io/api/apps/v1" @@ -68,6 +70,8 @@ func init() { utilruntime.Must(policyv1beta1.AddToScheme(scheme)) utilruntime.Must(extensionsv1.AddToScheme(scheme)) utilruntime.Must(extensionsv1beta1.AddToScheme(scheme)) + utilruntime.Must(operatorv1alpha1.AddToScheme(scheme)) + utilruntime.Must(operatorv1.AddToScheme(scheme)) } type ctrlOpts struct {