Skip to content

Commit

Permalink
Allow OperatorPolicy to create OLM subscriptions
Browse files Browse the repository at this point in the history
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 <jaszhang@redhat.com>
  • Loading branch information
zyjjay authored and openshift-merge-robot committed Sep 12, 2023
1 parent 406ccf7 commit de86a78
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 8 deletions.
153 changes: 145 additions & 8 deletions controllers/operatorpolicy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
87 changes: 87 additions & 0 deletions controllers/operatorpolicy_controller_test.go
Original file line number Diff line number Diff line change
@@ -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{"*"})
}
4 changes: 4 additions & 0 deletions controllers/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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})
Expand Down
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit de86a78

Please sign in to comment.