Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow OperatorPolicy to create OLM subscriptions #162

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
JustinKuli marked this conversation as resolved.
Show resolved Hide resolved

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