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

✨ Single/Own Namespace Install Mode Support #1724

Merged
merged 7 commits into from
Feb 27, 2025
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
6 changes: 4 additions & 2 deletions cmd/operator-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import (
"github.com/operator-framework/operator-controller/internal/operator-controller/features"
"github.com/operator-framework/operator-controller/internal/operator-controller/finalizers"
"github.com/operator-framework/operator-controller/internal/operator-controller/resolve"
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert"
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety"
"github.com/operator-framework/operator-controller/internal/operator-controller/scheme"
fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs"
Expand Down Expand Up @@ -407,8 +408,9 @@ func run() error {
}

helmApplier := &applier.Helm{
ActionClientGetter: acg,
Preflights: preflights,
ActionClientGetter: acg,
Preflights: preflights,
BundleToHelmChartFn: convert.RegistryV1ToHelmChart,
}

cm := contentmanager.NewManager(clientRestConfigMapper, mgr.GetConfig(), mgr.GetRESTMapper())
Expand Down
105 changes: 105 additions & 0 deletions docs/draft/howto/single-ownnamespace-install.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
## Description

!!! note
This feature is still in *alpha* the `SingleOwnNamespaceInstallSupport` feature-gate must be enabled to make use of it.
See the instructions below on how to enable it.

---

A component of OLMv0's multi-tenancy feature is its support of four [*installModes*](https://olm.operatorframework.io/docs/advanced-tasks/operator-scoping-with-operatorgroups/#targetnamespaces-and-their-relationship-to-installmodes):
for operator installation:

- *OwnNamespace*: If supported, the operator can be configured to watch for events in the namespace it is deployed in.
- *SingleNamespace*: If supported, the operator can be configured to watch for events in a single namespace that the operator is not deployed in.
- *MultiNamespace*: If supported, the operator can be configured to watch for events in more than one namespace.
- *AllNamespaces*: If supported, the operator can be configured to watch for events in all namespaces.

OLMv1 will not attempt multi-tenancy (see [design decisions document](../../project/olmv1_design_decisions.md)) and will think of operators
as globally installed, i.e. in OLMv0 parlance, as installed in *AllNamespaces* mode. However, there are operators that
were intended only for the *SingleNamespace* and *OwnNamespace* install modes. In order to make these operators installable in v1 while they
transition to the new model, v1 is adding support for these two new *installModes*. It should be noted that, in line with v1's no multi-tenancy policy,
users will not be able to install the same operator multiple times, and that in future iterations of the registry bundle format will not
include *installModes*.

## Demos

### SingleNamespace Install

[![SingleNamespace Install Demo](https://asciinema.org/a/w1IW0xWi1S9cKQFb9jnR07mgh.svg)](https://asciinema.org/a/w1IW0xWi1S9cKQFb9jnR07mgh)

### OwnNamespace Install

[![OwnNamespace Install Demo](https://asciinema.org/a/Rxx6WUwAU016bXFDW74XLcM5i.svg)](https://asciinema.org/a/Rxx6WUwAU016bXFDW74XLcM5i)

## Enabling the Feature-Gate

!!! tip

This guide assumes OLMv1 is already installed. If that is not the case,
you can follow the [getting started](../../getting-started/olmv1_getting_started.md) guide to install OLMv1.

---

Patch the `operator-controller` `Deployment` adding `--feature-gates=SingleOwnNamespaceInstallSupport=true` to the
controller container arguments:

```terminal title="Enable SingleOwnNamespaceInstallSupport feature-gate"
kubectl patch deployment -n olmv1-system operator-controller-controller-manager --type='json' -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--feature-gates=SingleOwnNamespaceInstallSupport=true"}]'
```

Wait for `Deployment` rollout:

```terminal title="Wait for Deployment rollout"
kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager
```

## Configuring the `ClusterExtension`

A `ClusterExtension` can be configured to install bundle in `Single-` or `OwnNamespace` mode through the
`olm.operatorframework.io/watch-namespace: <namespace>` annotation. The *installMode* is inferred in the following way:

- *AllNamespaces*: `<namespace>` is empty, or the annotation is not present
- *OwnNamespace*: `<namespace>` is the install namespace (i.e. `.spec.namespace`)
- *SingleNamespace*: `<namespace>` not the install namespace

### Examples

``` terminal title="SingleNamespace install mode example"
kubectl apply -f - <<EOF
apiVersion: olm.operatorframework.io/v1
kind: ClusterExtension
metadata:
name: argocd
annotations:
olm.operatorframework.io/watch-namespace: argocd-watch
spec:
namespace: argocd
serviceAccount:
name: argocd-installer
source:
sourceType: Catalog
catalog:
packageName: argocd-operator
version: 0.2.1 # Update to version 0.2.1
EOF
```

``` terminal title="OwnNamespace install mode example"
kubectl apply -f - <<EOF
apiVersion: olm.operatorframework.io/v1
kind: ClusterExtension
metadata:
name: argocd
annotations:
olm.operatorframework.io/watch-namespace: argocd
spec:
namespace: argocd
serviceAccount:
name: argocd-installer
source:
sourceType: Catalog
catalog:
packageName: argocd-operator
version: 0.2.1 # Update to version 0.2.1
EOF
```
2 changes: 1 addition & 1 deletion hack/demo/generate-asciidemo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
trap cleanup SIGINT SIGTERM EXIT

SCRIPTPATH="$( cd -- "$(dirname "$0")" > /dev/null 2>&1 ; pwd -P )"
export DEMO_RESOURCE_DIR="${SCRIPTPATH}/resources"

check_prereq() {
prog=$1
Expand Down Expand Up @@ -80,7 +81,6 @@ for prereq in "asciinema curl"; do
check_prereq ${prereq}
done


curl https://raw.githubusercontent.com/zechris/asciinema-rec_script/main/bin/asciinema-rec_script -o ${WKDIR}/asciinema-rec_script
chmod +x ${WKDIR}/asciinema-rec_script
screencast=${WKDIR}/${DEMO_NAME}.cast ${WKDIR}/asciinema-rec_script ${SCRIPTPATH}/${DEMO_SCRIPT}
Expand Down
43 changes: 43 additions & 0 deletions hack/demo/own-namespace-demo.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash

#
# Welcome to the OwnNamespace install mode demo
#
trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT

# enable 'SingleOwnNamespaceInstallSupport' feature gate
kubectl patch deployment -n olmv1-system operator-controller-controller-manager --type='json' -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--feature-gates=SingleOwnNamespaceInstallSupport=true"}]'

# wait for operator-controller to become available
kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager

# create install namespace
kubectl create ns argocd-system

# create installer service account
kubectl create serviceaccount -n argocd-system argocd-installer

# give installer service account admin privileges (not for production environments)
kubectl create clusterrolebinding argocd-installer-crb --clusterrole=cluster-admin --serviceaccount=argocd-system:argocd-installer

# install cluster extension in own namespace install mode (watch-namespace == install namespace == argocd-system)
cat ${DEMO_RESOURCE_DIR}/own-namespace-demo.yaml

# apply cluster extension
kubectl apply -f ${DEMO_RESOURCE_DIR}/own-namespace-demo.yaml

# wait for cluster extension installation to succeed
kubectl wait --for=condition=Installed clusterextension/argocd-operator --timeout="60s"

# check argocd-operator controller deployment pod template olm.targetNamespaces annotation
kubectl get deployments -n argocd-system argocd-operator-controller-manager -o jsonpath="{.spec.template.metadata.annotations.olm\.targetNamespaces}"

# check for argocd-operator rbac in watch namespace
kubectl get roles,rolebindings -n argocd-system -o name

# get controller service-account name
kubectl get deployments -n argocd-system argocd-operator-controller-manager -o jsonpath="{.spec.template.spec.serviceAccount}"

# check service account for role binding is the same as controller service-account
rolebinding=$(kubectl get rolebindings -n argocd-system -o name | grep 'argocd-operator' | head -n 1)
kubectl get -n argocd-system $rolebinding -o jsonpath='{.subjects}' | jq .[0]
16 changes: 16 additions & 0 deletions hack/demo/resources/own-namespace-demo.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: olm.operatorframework.io/v1
kind: ClusterExtension
metadata:
name: argocd-operator
annotations:
# watch namespace is the same as intall namespace
olm.operatorframework.io/watch-namespace: argocd-system
spec:
namespace: argocd-system
serviceAccount:
name: argocd-installer
source:
sourceType: Catalog
catalog:
packageName: argocd-operator
version: 0.6.0
16 changes: 16 additions & 0 deletions hack/demo/resources/single-namespace-demo.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: olm.operatorframework.io/v1
kind: ClusterExtension
metadata:
name: argocd-operator
annotations:
# watch-namespace is different from install namespace
olm.operatorframework.io/watch-namespace: argocd
spec:
namespace: argocd-system
serviceAccount:
name: argocd-installer
source:
sourceType: Catalog
catalog:
packageName: argocd-operator
version: 0.6.0
46 changes: 46 additions & 0 deletions hack/demo/single-own-namespace.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env bash

#
# Welcome to the SingleNamespace install mode demo
#
trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT

# enable 'SingleOwnNamespaceInstallSupport' feature gate
kubectl patch deployment -n olmv1-system operator-controller-controller-manager --type='json' -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--feature-gates=SingleOwnNamespaceInstallSupport=true"}]'

# wait for operator-controller to become available
kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager

# create install namespace
kubectl create ns argocd-system

# create installer service account
kubectl create serviceaccount -n argocd-system argocd-installer

# give installer service account admin privileges (not for production environments)
kubectl create clusterrolebinding argocd-installer-crb --clusterrole=cluster-admin --serviceaccount=argocd-system:argocd-installer

# create watch namespace
kubectl create namespace argocd

# install cluster extension in single namespace install mode (watch namespace != install namespace)
cat ${DEMO_RESOURCE_DIR}/single-namespace-demo.yaml

# apply cluster extension
kubectl apply -f ${DEMO_RESOURCE_DIR}/single-namespace-demo.yaml

# wait for cluster extension installation to succeed
kubectl wait --for=condition=Installed clusterextension/argocd-operator --timeout="60s"

# check argocd-operator controller deployment pod template olm.targetNamespaces annotation
kubectl get deployments -n argocd-system argocd-operator-controller-manager -o jsonpath="{.spec.template.metadata.annotations.olm\.targetNamespaces}"

# check for argocd-operator rbac in watch namespace
kubectl get roles,rolebindings -n argocd -o name

# get controller service-account name
kubectl get deployments -n argocd-system argocd-operator-controller-manager -o jsonpath="{.spec.template.spec.serviceAccount}"

# check service account for role binding is the controller deployment service account
rolebinding=$(kubectl get rolebindings -n argocd -o name | grep 'argocd-operator' | head -n 1)
kubectl get -n argocd $rolebinding -o jsonpath='{.subjects}' | jq .[0]
22 changes: 17 additions & 5 deletions internal/operator-controller/applier/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"helm.sh/helm/v3/pkg/postrender"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage/driver"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
apimachyaml "k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand All @@ -25,7 +24,6 @@ import (

ocv1 "github.com/operator-framework/operator-controller/api/v1"
"github.com/operator-framework/operator-controller/internal/operator-controller/features"
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert"
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety"
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util"
)
Expand Down Expand Up @@ -53,9 +51,12 @@ type Preflight interface {
Upgrade(context.Context, *release.Release) error
}

type BundleToHelmChartFn func(rv1 fs.FS, installNamespace string, watchNamespace string) (*chart.Chart, error)

type Helm struct {
ActionClientGetter helmclient.ActionClientGetter
Preflights []Preflight
ActionClientGetter helmclient.ActionClientGetter
Preflights []Preflight
BundleToHelmChartFn BundleToHelmChartFn
}

// shouldSkipPreflight is a helper to determine if the preflight check is CRDUpgradeSafety AND
Expand All @@ -79,7 +80,7 @@ func shouldSkipPreflight(ctx context.Context, preflight Preflight, ext *ocv1.Clu
}

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) {
chrt, err := convert.RegistryV1ToHelmChart(ctx, contentFS, ext.Spec.Namespace, []string{corev1.NamespaceAll})
chrt, err := h.buildHelmChart(contentFS, ext)
if err != nil {
return nil, "", err
}
Expand Down Expand Up @@ -152,6 +153,17 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte
return relObjects, state, nil
}

func (h *Helm) buildHelmChart(bundleFS fs.FS, ext *ocv1.ClusterExtension) (*chart.Chart, error) {
if h.BundleToHelmChartFn == nil {
return nil, errors.New("BundleToHelmChartFn is nil")
}
watchNamespace, err := GetWatchNamespace(ext)
if err != nil {
return nil, err
}
return h.BundleToHelmChartFn(bundleFS, ext.Spec.Namespace, watchNamespace)
}

func (h *Helm) getReleaseState(cl helmclient.ActionInterface, ext *ocv1.ClusterExtension, chrt *chart.Chart, values chartutil.Values, post postrender.PostRenderer) (*release.Release, *release.Release, string, error) {
currentRelease, err := cl.Get(ext.GetName())
if errors.Is(err, driver.ErrReleaseNotFound) {
Expand Down
Loading
Loading