Skip to content

Commit

Permalink
Introduce OperatorConditions
Browse files Browse the repository at this point in the history
This commit introduces two new controllers:

The first controller is the OperatorConditionGenerator Controller
which reconciles ClusterServiceVersions and creates an OperatorCondtion
CR for the CSV.

The second controller is the OperatorCondition Controller which
reconciles OperatorConditions, Roles and RoleBindings for
ServiceAccounts defined in its spec and injecting the
OPERATOR_CONDITION_NAME Environment Variable into Deployments defined in
its Spec.
  • Loading branch information
awgreene committed Dec 2, 2020
1 parent 2b104d9 commit f571cd8
Show file tree
Hide file tree
Showing 9 changed files with 883 additions and 1 deletion.
26 changes: 26 additions & 0 deletions cmd/olm/manager.go
Expand Up @@ -23,6 +23,32 @@ func Manager(ctx context.Context) (ctrl.Manager, error) {
return nil, err
}

operatorConditionReconciler, err := operators.NewOperatorConditionReconciler(
mgr.GetClient(),
ctrl.Log.WithName("controllers").WithName("operatorcondition"),
mgr.GetScheme(),
)
if err != nil {
return nil, err
}

if err = operatorConditionReconciler.SetupWithManager(mgr); err != nil {
return nil, err
}

operatorConditionGeneratorReconciler, err := operators.NewOperatorConditionGeneratorReconciler(
mgr.GetClient(),
ctrl.Log.WithName("controllers").WithName("operatorcondition-generator"),
mgr.GetScheme(),
)
if err != nil {
return nil, err
}

if err = operatorConditionGeneratorReconciler.SetupWithManager(mgr); err != nil {
return nil, err
}

// Setup a new controller to reconcile Operators
setupLog.Info("configuring controller")
if feature.Gate.Enabled(feature.OperatorLifecycleManagerV1) {
Expand Down
2 changes: 2 additions & 0 deletions pkg/controller/operators/adoption_controller.go
Expand Up @@ -82,6 +82,7 @@ func (r *AdoptionReconciler) SetupWithManager(mgr ctrl.Manager) error {
Watches(&source.Kind{Type: &apiextensionsv1.CustomResourceDefinition{}}, enqueueProviders).
Watches(&source.Kind{Type: &apiregistrationv1.APIService{}}, enqueueCSV).
Watches(&source.Kind{Type: &operatorsv1alpha1.Subscription{}}, enqueueCSV).
Watches(&source.Kind{Type: &operatorsv1.OperatorCondition{}}, enqueueCSV).
Complete(reconcile.Func(r.ReconcileClusterServiceVersion))
if err != nil {
return err
Expand Down Expand Up @@ -324,6 +325,7 @@ func (r *AdoptionReconciler) adoptees(ctx context.Context, operator decorators.O
&operatorsv1alpha1.SubscriptionList{},
&operatorsv1alpha1.InstallPlanList{},
&operatorsv1alpha1.ClusterServiceVersionList{},
&operatorsv1.OperatorConditionList{},
}

// Only resources that aren't already labelled are adoption candidates
Expand Down
3 changes: 2 additions & 1 deletion pkg/controller/operators/operator_controller.go
Expand Up @@ -33,7 +33,6 @@ var (
apiregistrationv1.AddToScheme,
operatorsv1alpha1.AddToScheme,
operatorsv1.AddToScheme,
operatorsv1.AddToScheme,
)

// AddToScheme adds all types necessary for the controller to operate.
Expand Down Expand Up @@ -81,6 +80,7 @@ func (r *OperatorReconciler) SetupWithManager(mgr ctrl.Manager) error {
Watches(&source.Kind{Type: &operatorsv1alpha1.Subscription{}}, enqueueOperator).
Watches(&source.Kind{Type: &operatorsv1alpha1.InstallPlan{}}, enqueueOperator).
Watches(&source.Kind{Type: &operatorsv1alpha1.ClusterServiceVersion{}}, enqueueOperator).
Watches(&source.Kind{Type: &operatorsv1.OperatorCondition{}}, enqueueOperator).
// TODO(njhale): Add WebhookConfigurations and ConfigMaps
Complete(r)
}
Expand Down Expand Up @@ -205,6 +205,7 @@ func (r *OperatorReconciler) listComponents(ctx context.Context, selector labels
&operatorsv1alpha1.SubscriptionList{},
&operatorsv1alpha1.InstallPlanList{},
&operatorsv1alpha1.ClusterServiceVersionList{},
&operatorsv1.OperatorConditionList{},
}

opt := client.MatchingLabelsSelector{Selector: selector}
Expand Down
236 changes: 236 additions & 0 deletions pkg/controller/operators/operatorcondition_controller.go
@@ -0,0 +1,236 @@
package operators

import (
"context"
"reflect"

"github.com/go-logr/logr"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"

operatorsv1 "github.com/operator-framework/api/pkg/operators/v1"
"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/ownerutil"
)

const (
OperatorConditionEnvVarKey = "OPERATOR_CONDITION_NAME"
)

// OperatorConditionReconciler reconciles an OperatorCondition object.
type OperatorConditionReconciler struct {
client.Client
log logr.Logger
}

// +kubebuilder:rbac:groups=operators.coreos.com,resources=operatorconditions,verbs=get;list;update;patch;delete
// +kubebuilder:rbac:groups=operators.coreos.com,resources=operatorconditions/status,verbs=update;patch

// SetupWithManager adds the OperatorCondition Reconciler reconciler to the given controller manager.
func (r *OperatorConditionReconciler) SetupWithManager(mgr ctrl.Manager) error {
handler := &handler.EnqueueRequestForOwner{
IsController: true,
OwnerType: &operatorsv1.OperatorCondition{},
}

return ctrl.NewControllerManagedBy(mgr).
For(&operatorsv1.OperatorCondition{}).
Watches(&source.Kind{Type: &rbacv1.Role{}}, handler).
Watches(&source.Kind{Type: &rbacv1.RoleBinding{}}, handler).
Complete(r)
}

// NewOperatorConditionReconciler constructs and returns an OperatorConditionReconciler.
// As a side effect, the given scheme has operator discovery types added to it.
func NewOperatorConditionReconciler(cli client.Client, log logr.Logger, scheme *runtime.Scheme) (*OperatorConditionReconciler, error) {
// Add watched types to scheme.
if err := AddToScheme(scheme); err != nil {
return nil, err
}

return &OperatorConditionReconciler{
Client: cli,
log: log,
}, nil
}

// Implement reconcile.Reconciler so the controller can reconcile objects
var _ reconcile.Reconciler = &OperatorConditionReconciler{}

func (r *OperatorConditionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
// Set up a convenient log object so we don't have to type request over and over again
log := r.log.WithValues("request", req)
log.V(2).Info("reconciling operatorcondition")

operatorCondition := &operatorsv1.OperatorCondition{}
err := r.Client.Get(context.TODO(), req.NamespacedName, operatorCondition)
if err != nil {
log.V(1).Error(err, "Unable to find operatorcondition")
return ctrl.Result{}, err
}

err = r.ensureOperatorConditionRole(operatorCondition)
if err != nil {
log.V(1).Error(err, "Error ensuring OperatorCondition Role")
return ctrl.Result{Requeue: true}, err
}

err = r.ensureOperatorConditionRoleBinding(operatorCondition)
if err != nil {
log.V(1).Error(err, "Error ensuring OperatorCondition RoleBinding")
return ctrl.Result{Requeue: true}, err
}

err = r.ensureDeploymentEnvVars(operatorCondition)
if err != nil {
log.V(1).Error(err, "Error ensuring OperatorCondition Deployment EnvVars")
return ctrl.Result{Requeue: true}, err
}

return ctrl.Result{}, nil
}

func (r *OperatorConditionReconciler) ensureOperatorConditionRole(operatorCondition *operatorsv1.OperatorCondition) error {
r.log.V(4).Info("Ensuring the Role for the OperatorCondition")
role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: operatorCondition.GetName(),
Namespace: operatorCondition.GetNamespace(),
},
Rules: []rbacv1.PolicyRule{
{
Verbs: []string{"get"},
APIGroups: []string{"operators.coreos.com"},
Resources: []string{"operatorconditions"},
ResourceNames: []string{operatorCondition.GetName()},
},
{
Verbs: []string{"get,update,patch"},
APIGroups: []string{"operators.coreos.com"},
Resources: []string{"operatorconditions/status"},
ResourceNames: []string{operatorCondition.GetName()},
},
},
}
ownerutil.AddOwner(role, operatorCondition, false, true)

existingRole := &rbacv1.Role{}
err := r.Client.Get(context.TODO(), client.ObjectKey{Name: role.GetName(), Namespace: role.GetNamespace()}, existingRole)
if err != nil {
if !k8serrors.IsNotFound(err) {
return err
}
return r.Client.Create(context.TODO(), role)
}

if ownerutil.IsOwnedBy(existingRole, operatorCondition) &&
reflect.DeepEqual(role.Rules, existingRole.Rules) {
r.log.V(5).Info("Existing Role does not need to be updated")
return nil
}
r.log.V(5).Info("Existing Role needs to be updated")

existingRole.OwnerReferences = role.OwnerReferences
existingRole.Rules = role.Rules
return r.Client.Update(context.TODO(), existingRole)
}

func (r *OperatorConditionReconciler) ensureOperatorConditionRoleBinding(operatorCondition *operatorsv1.OperatorCondition) error {
r.log.V(4).Info("Ensuring the RoleBinding for the OperatorCondition")
subjects := []rbacv1.Subject{}
for _, serviceAccount := range operatorCondition.Spec.ServiceAccounts {
subjects = append(subjects, rbacv1.Subject{
Kind: rbacv1.ServiceAccountKind,
Name: serviceAccount,
APIGroup: "",
})
}

roleBinding := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: operatorCondition.GetName(),
Namespace: operatorCondition.GetNamespace(),
},
Subjects: subjects,
RoleRef: rbacv1.RoleRef{
Kind: "Role",
Name: operatorCondition.GetName(),
APIGroup: "rbac.authorization.k8s.io",
},
}
ownerutil.AddOwner(roleBinding, operatorCondition, false, true)

existingRoleBinding := &rbacv1.RoleBinding{}
err := r.Client.Get(context.TODO(), client.ObjectKey{Name: roleBinding.GetName(), Namespace: roleBinding.GetNamespace()}, existingRoleBinding)
if err != nil {
if !k8serrors.IsNotFound(err) {
return err
}
return r.Client.Create(context.TODO(), roleBinding)
}

if ownerutil.IsOwnedBy(existingRoleBinding, operatorCondition) &&
existingRoleBinding.RoleRef == roleBinding.RoleRef &&
reflect.DeepEqual(roleBinding.Subjects, existingRoleBinding.Subjects) {
r.log.V(5).Info("Existing RoleBinding does not need to be updated")
return nil
}

r.log.V(5).Info("Existing RoleBinding needs to be updated")
existingRoleBinding.OwnerReferences = roleBinding.OwnerReferences
existingRoleBinding.Subjects = roleBinding.Subjects
existingRoleBinding.RoleRef = roleBinding.RoleRef

return r.Client.Update(context.TODO(), existingRoleBinding)
}

func (r *OperatorConditionReconciler) ensureDeploymentEnvVars(operatorCondition *operatorsv1.OperatorCondition) error {
r.log.V(4).Info("Ensuring that deployments have the OPERATOR_CONDITION_NAME variable")
for _, deploymentName := range operatorCondition.Spec.Deployments {
deployment := &appsv1.Deployment{}
err := r.Client.Get(context.TODO(), types.NamespacedName{Name: deploymentName, Namespace: operatorCondition.GetNamespace()}, deployment)
if err != nil {
return err
}
deploymentNeedsUpdate := false
for i := range deployment.Spec.Template.Spec.Containers {
envVars, containedEnvVar := ensureEnvVarIsPresent(deployment.Spec.Template.Spec.Containers[i].Env, corev1.EnvVar{Name: OperatorConditionEnvVarKey, Value: operatorCondition.GetName()})
if !containedEnvVar {
deploymentNeedsUpdate = true
}
deployment.Spec.Template.Spec.Containers[i].Env = envVars
}
if !deploymentNeedsUpdate {
r.log.V(5).Info("Existing deployment does not need to be updated")
continue
}
r.log.V(5).Info("Existing deployment needs to be updated")
err = r.Client.Update(context.TODO(), deployment)
if err != nil {
return err
}
}
return nil
}

func ensureEnvVarIsPresent(envVars []corev1.EnvVar, envVar corev1.EnvVar) ([]corev1.EnvVar, bool) {
for i, each := range envVars {
if each.Name == envVar.Name {
if each.Value == envVar.Value {
return envVars, true
}
envVars[i].Value = envVar.Value
return envVars, false
}
}
return append(envVars, envVar), false
}

0 comments on commit f571cd8

Please sign in to comment.