Skip to content
Closed
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
4 changes: 3 additions & 1 deletion api/v1/clustercatalog_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ const (
AvailabilityModeUnavailable AvailabilityMode = "Unavailable"

// Condition types
TypeServing = "Serving"
TypeServing = "Serving"
TypeArbackulationApprovalRequired = "ArbackulationApprovalRequired"
TypeArbackulationApprovalGranted = "ArbackulationApprovalGranted"

// Serving Reasons
ReasonAvailable = "Available"
Expand Down
12 changes: 12 additions & 0 deletions api/v1/clusterextension_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ var ClusterExtensionKind = "ClusterExtension"
type (
UpgradeConstraintPolicy string
CRDUpgradeSafetyEnforcement string

DeploymentApprovalPolicy string
)

const (
Expand All @@ -39,6 +41,9 @@ const (
// Use with caution as this can lead to unknown and potentially
// disastrous results such as data loss.
UpgradeConstraintPolicySelfCertified UpgradeConstraintPolicy = "SelfCertified"

DeploymentApprovalPolicyAutomatic DeploymentApprovalPolicy = "Automatic"
DeploymentApprovalPolicyManual DeploymentApprovalPolicy = "Manual"
)

// ClusterExtensionSpec defines the desired state of ClusterExtension
Expand Down Expand Up @@ -92,6 +97,13 @@ type ClusterExtensionSpec struct {
//
// +optional
Install *ClusterExtensionInstallConfig `json:"install,omitempty"`

// how to handle operator lifecycle rbac requirements
// Automatic: assign necessary rbac to service account
// Manual:
// +kubebuilder:default:=Manual
// +optional
DeploymentApprovalPolicy DeploymentApprovalPolicy `json:"deploymentApprovalPolicy,omitempty"`
}

const SourceTypeCatalog = "Catalog"
Expand Down
7 changes: 4 additions & 3 deletions api/v1/common_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ const (
TypeProgressing = "Progressing"

// Progressing reasons
ReasonSucceeded = "Succeeded"
ReasonRetrying = "Retrying"
ReasonBlocked = "Blocked"
ReasonSucceeded = "Succeeded"
ReasonRetrying = "Retrying"
ReasonBlocked = "Blocked"
ReasonArbackulationApprovalRequired = "ArbackulationApprovalRequired"

// Terminal reasons
ReasonDeprecated = "Deprecated"
Expand Down
3 changes: 3 additions & 0 deletions cmd/operator-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,9 @@ func run() error {
Preflights: preflights,
BundleToHelmChartFn: convert.RegistryV1ToHelmChart,
PreAuthorizer: preAuth,
Arbackulator: &applier.Arbackulator{
Client: cl,
},
}

cm := contentmanager.NewManager(clientRestConfigMapper, mgr.GetConfig(), mgr.GetRESTMapper())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ spec:
description: spec is an optional field that defines the desired state
of the ClusterExtension.
properties:
deploymentApprovalPolicy:
default: Manual
description: |-
how to handle operator lifecycle rbac requirements
Automatic: assign necessary rbac to service account
Manual:
type: string
install:
description: |-
install is an optional field used to configure the installation options
Expand Down
6 changes: 6 additions & 0 deletions config/base/operator-controller/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ rules:
- serviceaccounts/token
verbs:
- create
- apiGroups:
- '*'
resources:
- '*'
verbs:
- '*'
- apiGroups:
- apiextensions.k8s.io
resources:
Expand Down
44 changes: 31 additions & 13 deletions internal/operator-controller/applier/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"fmt"
"io"
"io/fs"
"k8s.io/apimachinery/pkg/api/meta"

Check failure on line 10 in internal/operator-controller/applier/helm.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gci)
"slices"
"strings"

Expand All @@ -31,11 +32,12 @@
)

const (
StateNeedsInstall string = "NeedsInstall"
StateNeedsUpgrade string = "NeedsUpgrade"
StateUnchanged string = "Unchanged"
StateError string = "Error"
maxHelmReleaseHistory = 10
StateNeedsInstall string = "NeedsInstall"
StateNeedsUpgrade string = "NeedsUpgrade"
StateUnchanged string = "Unchanged"
StateNeedsArbackulationApproval string = "ArbackulationRequired"
StateError string = "Error"
maxHelmReleaseHistory = 10
)

// Preflight is a check that should be run before making any changes to the cluster
Expand All @@ -60,6 +62,7 @@
Preflights []Preflight
PreAuthorizer authorization.PreAuthorizer
BundleToHelmChartFn BundleToHelmChartFn
Arbackulator *Arbackulator
}

// shouldSkipPreflight is a helper to determine if the preflight check is CRDUpgradeSafety AND
Expand All @@ -85,10 +88,10 @@
// runPreAuthorizationChecks performs pre-authorization checks for a Helm release
// it renders a client-only release, checks permissions using the PreAuthorizer
// and returns an error if authorization fails or required permissions are missing
func (h *Helm) runPreAuthorizationChecks(ctx context.Context, ext *ocv1.ClusterExtension, chart *chart.Chart, values chartutil.Values, post postrender.PostRenderer) error {
func (h *Helm) runPreAuthorizationChecks(ctx context.Context, ext *ocv1.ClusterExtension, chart *chart.Chart, values chartutil.Values, post postrender.PostRenderer) ([]authorization.ScopedPolicyRules, error) {
tmplRel, err := h.renderClientOnlyRelease(ctx, ext, chart, values, post)
if err != nil {
return fmt.Errorf("failed to get release state using client-only dry-run: %w", err)
return nil, fmt.Errorf("failed to get release state using client-only dry-run: %w", err)
}

missingRules, authErr := h.PreAuthorizer.PreAuthorize(ctx, ext, strings.NewReader(tmplRel.Manifest))
Expand All @@ -111,9 +114,9 @@
}
if len(preAuthErrors) > 0 {
// This phrase is explicitly checked by external testing
return fmt.Errorf("pre-authorization failed: %v", errors.Join(preAuthErrors...))
return missingRules, fmt.Errorf("pre-authorization failed: %v", errors.Join(preAuthErrors...))
}
return nil
return nil, nil
}

func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels map[string]string, storageLabels map[string]string) ([]client.Object, string, error) {
Expand All @@ -127,11 +130,19 @@
labels: objectLabels,
}

if h.PreAuthorizer != nil {

Check failure on line 133 in internal/operator-controller/applier/helm.go

View workflow job for this annotation

GitHub Actions / lint

`if h.PreAuthorizer != nil` has complex nested blocks (complexity: 8) (nestif)
err := h.runPreAuthorizationChecks(ctx, ext, chrt, values, post)
if err != nil {
// Return the pre-authorization error directly
return nil, "", err
missingRules, err := h.runPreAuthorizationChecks(ctx, ext, chrt, values, post)
if len(missingRules) > 0 {
if shouldArbackulate(ext) {
if err := h.Arbackulator.GrantPassage(ctx, ext, missingRules); err != nil {
return nil, "", fmt.Errorf("error arbackulating: %w", err)
}
return nil, StateNeedsInstall, nil
} else if ext.Spec.DeploymentApprovalPolicy == ocv1.DeploymentApprovalPolicyManual {
return nil, StateNeedsArbackulationApproval, err
} else {
return nil, "", err
}
}
}

Expand Down Expand Up @@ -323,3 +334,10 @@
}
return sb.String()
}

func shouldArbackulate(cExt *ocv1.ClusterExtension) bool {
if cExt.Spec.DeploymentApprovalPolicy == ocv1.DeploymentApprovalPolicyAutomatic {
return true
}
return meta.FindStatusCondition(cExt.Status.Conditions, ocv1.TypeArbackulationApprovalGranted) != nil
}
126 changes: 126 additions & 0 deletions internal/operator-controller/applier/rbacker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package applier

import (
"context"
"fmt"
ocv1 "github.com/operator-framework/operator-controller/api/v1"

Check failure on line 6 in internal/operator-controller/applier/rbacker.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gci)
"github.com/operator-framework/operator-controller/internal/operator-controller/authorization"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

type Arbackulator struct {
Client client.Client
}

func (t *Arbackulator) GrantPassage(ctx context.Context, cExt *ocv1.ClusterExtension, perms []authorization.ScopedPolicyRules) error {
permMap := map[string][]rbacv1.PolicyRule{}
for _, perm := range perms {
permMap[perm.Namespace] = append(permMap[perm.Namespace], perm.MissingRules...)
}

for ns, rules := range permMap {
switch ns {
case "":
if err := t.updateClusterPerms(ctx, cExt, rules); err != nil {
return err
}
default:
if err := t.updateNamespacedPerms(ctx, cExt, ns, rules); err != nil {
return err
}
}
}
return nil
}

func (t *Arbackulator) updateClusterPerms(ctx context.Context, cExt *ocv1.ClusterExtension, rules []rbacv1.PolicyRule) error {
clusterRoleName := fmt.Sprintf("%s-mananaged-cluster-role", cExt.Name)
role := &rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{
Name: clusterRoleName,
},
}

if _, err := controllerutil.CreateOrUpdate(ctx, t.Client, role, func() error {
role.Rules = append(role.Rules, rules...)
return nil
}); err != nil {
return err
}

clusterRoleBindingName := fmt.Sprintf("%s-mananaged-cluster-rolebinding", cExt.Name)
roleBinding := &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: clusterRoleBindingName,
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: cExt.Spec.ServiceAccount.Name,
Namespace: cExt.Spec.Namespace,
},
},
RoleRef: rbacv1.RoleRef{
APIGroup: rbacv1.GroupName,
Kind: "ClusterRole",
Name: clusterRoleName,
},
}

if _, err := controllerutil.CreateOrUpdate(ctx, t.Client, roleBinding, func() error {
return nil
}); err != nil {
return err
}

return nil
}

func (t *Arbackulator) updateNamespacedPerms(ctx context.Context, cExt *ocv1.ClusterExtension, namespace string, rules []rbacv1.PolicyRule) error {
roleName := fmt.Sprintf("%s-mananaged-role", cExt.Name)
role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: roleName,
Namespace: namespace,
},
}

if _, err := controllerutil.CreateOrUpdate(ctx, t.Client, role, func() error {
role.Rules = append(role.Rules, rules...)
return nil
}); err != nil {
return err
}

roleBindingName := fmt.Sprintf("%s-mananaged-rolebinding", cExt.Name)
roleBinding := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: roleBindingName,
Namespace: namespace,
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: cExt.Spec.ServiceAccount.Name,
Namespace: cExt.Spec.Namespace,
},
},
RoleRef: rbacv1.RoleRef{
APIGroup: rbacv1.GroupName,
Kind: "Role",
Name: roleName,
},
}

if _, err := controllerutil.CreateOrUpdate(ctx, t.Client, roleBinding, func() error {
return nil
}); err != nil {
return err
}

return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"context"
"errors"
"fmt"
"github.com/operator-framework/operator-controller/internal/operator-controller/applier"

Check failure on line 23 in internal/operator-controller/controllers/clusterextension_controller.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gci)
"io/fs"
"strings"
"time"
Expand Down Expand Up @@ -97,9 +98,10 @@
//+kubebuilder:rbac:groups=core,resources=serviceaccounts/token,verbs=create
//+kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get
//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles;clusterrolebindings;roles;rolebindings,verbs=list;watch

//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clustercatalogs,verbs=list;watch

//+kubebuilder:rbac:groups=*,resources=*,verbs=*

// The operator controller needs to watch all the bundle objects and reconcile accordingly. Though not ideal, but these permissions are required.
// This has been taken from rukpak, and an issue was created before to discuss it: https://github.com/operator-framework/rukpak/issues/800.
func (r *ClusterExtensionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
Expand Down Expand Up @@ -287,14 +289,32 @@
// to ensure exponential backoff can occur:
// - Permission errors (it is not possible to watch changes to permissions.
// The only way to eventually recover from permission errors is to keep retrying).
managedObjs, _, err := r.Applier.Apply(ctx, imageFS, ext, objLbls, storeLbls)
managedObjs, state, err := r.Applier.Apply(ctx, imageFS, ext, objLbls, storeLbls)
if err != nil {
if state == applier.StateNeedsArbackulationApproval {
progressingCond := metav1.Condition{
Type: ocv1.TypeProgressing,
Status: metav1.ConditionFalse,
Reason: ocv1.ReasonArbackulationApprovalRequired,
Message: err.Error(),
ObservedGeneration: ext.GetGeneration(),
}
apimeta.SetStatusCondition(&ext.Status.Conditions, progressingCond)
return ctrl.Result{}, nil
}

setStatusProgressing(ext, wrapErrorWithResolutionInfo(resolvedBundleMetadata, err))
// Now that we're actually trying to install, use the error
setInstalledStatusFromBundle(ext, installedBundle)
return ctrl.Result{}, err
}

// if state is needs install and not objects were generated
// applier needs another round of reconciliation
if state == applier.StateNeedsInstall && managedObjs == nil {
return ctrl.Result{Requeue: true}, nil
}

newInstalledBundle := &InstalledBundle{
BundleMetadata: resolvedBundleMetadata,
Image: resolvedBundle.Image,
Expand Down
Loading