Skip to content
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
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export OPERATOR_VERSION ?= 4.2.0-dev
IMAGE_VERSION ?= $(OPERATOR_VERSION)
BUNDLE_VERSION ?= $(IMAGE_VERSION)
DEFAULT_NAMESPACE ?= quay.io/cryostat

# Minimum and Maximum OpenShift versions for Console Plugin support
export MIN_OPENSHIFT_VERSION ?= 4.19.0
export MAX_OPENSHIFT_VERSION ?= 99.99.0
IMAGE_NAMESPACE ?= $(DEFAULT_NAMESPACE)
OPERATOR_NAME ?= cryostat-operator
OPERATOR_SDK_VERSION ?= v1.31.0
Expand Down Expand Up @@ -372,6 +376,7 @@ manifests: controller-gen ## Generate manifests e.g. CRD, RBAC, etc.
$(CONTROLLER_GEN) rbac:roleName=role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
envsubst < hack/image_tag_patch.yaml.in > config/default/image_tag_patch.yaml
envsubst < hack/image_pull_patch.yaml.in > config/default/image_pull_patch.yaml
envsubst < hack/openshift_version_patch.yaml.in > config/default/openshift_version_patch.yaml
envsubst < hack/plugin_image_pull_patch.yaml.in > config/openshift/plugin_image_pull_patch.yaml
envsubst < hack/insights_patch.yaml.in > config/overlays/insights/insights_patch.yaml
envsubst < hack/insights_image_pull_patch.yaml.in > config/insights/insights_image_pull_patch.yaml
Expand Down
1 change: 1 addition & 0 deletions config/default/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ commonLabels:
patchesStrategicMerge:
- image_tag_patch.yaml
- image_pull_patch.yaml
- openshift_version_patch.yaml
- manager_webhook_patch.yaml
- webhookcainjection_patch.yaml
- webhook_object_selector_patch.yaml
Expand Down
15 changes: 15 additions & 0 deletions config/default/openshift_version_patch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller
namespace: system
spec:
template:
spec:
containers:
- name: manager
env:
- name: MIN_OPENSHIFT_VERSION
value: "4.19.0"
- name: MAX_OPENSHIFT_VERSION
value: "99.99.0"
15 changes: 15 additions & 0 deletions hack/openshift_version_patch.yaml.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller
namespace: system
spec:
template:
spec:
containers:
- name: manager
env:
- name: MIN_OPENSHIFT_VERSION
value: "${MIN_OPENSHIFT_VERSION}"
- name: MAX_OPENSHIFT_VERSION
value: "${MAX_OPENSHIFT_VERSION}"
109 changes: 97 additions & 12 deletions internal/console/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
openshiftoperatorv1 "github.com/openshift/api/operator/v1"
appsv1 "k8s.io/api/apps/v1"
rbacv1 "k8s.io/api/rbac/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
Expand All @@ -40,21 +41,23 @@ import (
)

type PluginInstaller struct {
Client client.Client
Namespace string
Scheme *runtime.Scheme
Log logr.Logger
Client client.Client
Namespace string
Scheme *runtime.Scheme
Log logr.Logger
MinOpenShiftVersion string
MaxOpenShiftVersion string
}

// Verify that *PluginInstaller implements manager.Runnable and manager.LeaderElectionRunnable.
var _ manager.Runnable = (*PluginInstaller)(nil)
var _ manager.LeaderElectionRunnable = (*PluginInstaller)(nil)

// Minimum OpenShift version that supports the plugin
const minOpenShiftVersion = "4.15.0"
// Default minimum OpenShift version that supports the plugin (0.0.0 = accept any version if not set)
const defaultMinOpenShiftVersion = "0.0.0"

// Maximum OpenShift version that supports the plugin
const maxOpenShiftVersion = "99.99.0" // Placeholder until needed
// Default maximum OpenShift version that supports the plugin
const defaultMaxOpenShiftVersion = "99.99.0"

// Start implements manager.Runnable.
func (r *PluginInstaller) Start(ctx context.Context) error {
Expand All @@ -72,8 +75,8 @@ func (r *PluginInstaller) installConsolePlugin(ctx context.Context) error {
return err
}
if !compat {
// Return early if this OpenShift cluster is not compatible
return nil
// Uninstall plugin if it exists on incompatible cluster
return r.uninstallConsolePlugin(ctx)
}
err = r.createConsolePlugin(ctx)
if err != nil {
Expand Down Expand Up @@ -222,9 +225,27 @@ func (r *PluginInstaller) findOwner(ctx context.Context) (*rbacv1.ClusterRoleBin
}

func (r *PluginInstaller) isOpenShiftCompatible(ctx context.Context) (bool, error) {
minVersionStr := r.MinOpenShiftVersion
if minVersionStr == "" {
minVersionStr = defaultMinOpenShiftVersion
}

maxVersionStr := r.MaxOpenShiftVersion
if maxVersionStr == "" {
maxVersionStr = defaultMaxOpenShiftVersion
}

// Build a semver.Version from the minimum/maximum version
minVersion := semver.MustParse(minOpenShiftVersion)
maxVersion := semver.MustParse(maxOpenShiftVersion)
minVersion, err := semver.Parse(minVersionStr)
if err != nil {
r.Log.Error(err, "Invalid MIN_OPENSHIFT_VERSION, using default", "value", minVersionStr, "default", defaultMinOpenShiftVersion)
minVersion = semver.MustParse(defaultMinOpenShiftVersion)
}
maxVersion, err := semver.Parse(maxVersionStr)
if err != nil {
r.Log.Error(err, "Invalid MAX_OPENSHIFT_VERSION, using default", "value", maxVersionStr, "default", defaultMaxOpenShiftVersion)
maxVersion = semver.MustParse(defaultMaxOpenShiftVersion)
}

// Look up the cluster's version
version, err := r.getOpenShiftVersion(ctx)
Expand Down Expand Up @@ -263,3 +284,67 @@ func (r *PluginInstaller) getOpenShiftVersion(ctx context.Context) (*semver.Vers
version := semver.MustParse(trimmedVer)
return &version, nil
}

func (r *PluginInstaller) uninstallConsolePlugin(ctx context.Context) error {
r.Log.Info("Uninstalling Console Plugin due to incompatible OpenShift version")

err := r.unregisterConsolePlugin(ctx)
if err != nil {
return err
}

err = r.deleteConsolePlugin(ctx)
if err != nil {
return err
}

r.Log.Info("Console Plugin uninstalled successfully")
return nil
}

func (r *PluginInstaller) unregisterConsolePlugin(ctx context.Context) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
console := &openshiftoperatorv1.Console{}
err := r.Client.Get(ctx, types.NamespacedName{Name: constants.ConsoleCRName}, console)
if err != nil {
if kerrors.IsNotFound(err) {
return nil
}
return err
}

idx := slices.Index(console.Spec.Plugins, constants.ConsolePluginName)
if idx == -1 {
return nil
}

console.Spec.Plugins = slices.Delete(console.Spec.Plugins, idx, idx+1)

err = r.Client.Update(ctx, console)
if err != nil {
return err
}

r.Log.Info("Console Plugin unregistered from Console", "name", console.Name)
return nil
})
}

func (r *PluginInstaller) deleteConsolePlugin(ctx context.Context) error {
plugin := &consolev1.ConsolePlugin{
ObjectMeta: metav1.ObjectMeta{
Name: constants.ConsolePluginName,
},
}

err := r.Client.Delete(ctx, plugin)
if err != nil {
if kerrors.IsNotFound(err) {
return nil
}
return err
}

r.Log.Info("ConsolePlugin deleted", "name", plugin.Name)
return nil
}
117 changes: 117 additions & 0 deletions internal/console/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ var _ = Describe("Plugin", func() {
Namespace: t.Namespace,
Scheme: k8sScheme,
Log: logger,
// Hardcode test bounds
MinOpenShiftVersion: "4.19.0",
MaxOpenShiftVersion: "99.99.0",
}
})

Expand Down Expand Up @@ -248,6 +251,120 @@ var _ = Describe("Plugin", func() {
expectNoChanges()
})
})
Context("uninstalling plugin", func() {
Context("with existing plugin on incompatible OpenShift (too old)", func() {
BeforeEach(func() {
t.objs = append(t.objs, t.NewConsoleExisting(), t.NewConsolePlugin(), t.NewPluginClusterRoleBinding(), t.NewOperatorDeployment(), t.NewClusterVersionOld())
})
JustBeforeEach(func() {
t.updateClusterVersionStatus(t.NewClusterVersionOld())
err := installer.Start(context.Background())
Expect(err).ToNot(HaveOccurred())
})
It("should delete ConsolePlugin", func() {
plugin := &consolev1.ConsolePlugin{}
err := t.client.Get(context.Background(), types.NamespacedName{Name: t.NewConsolePlugin().Name}, plugin)
Expect(kerrors.IsNotFound(err)).To(BeTrue())
})
It("should unregister from Console", func() {
console := t.getConsole(t.NewConsole())
Expect(console.Spec.Plugins).ToNot(ContainElement("cryostat-plugin"))
Expect(console.Spec.Plugins).To(ContainElement("other-plugin"))
})
})
Context("with existing plugin on incompatible OpenShift (too new)", func() {
BeforeEach(func() {
t.objs = append(t.objs, t.NewConsoleExisting(), t.NewConsolePlugin(), t.NewPluginClusterRoleBinding(), t.NewOperatorDeployment(), t.NewClusterVersionNew())
})
JustBeforeEach(func() {
t.updateClusterVersionStatus(t.NewClusterVersionNew())
err := installer.Start(context.Background())
Expect(err).ToNot(HaveOccurred())
})
It("should delete ConsolePlugin", func() {
plugin := &consolev1.ConsolePlugin{}
err := t.client.Get(context.Background(), types.NamespacedName{Name: t.NewConsolePlugin().Name}, plugin)
Expect(kerrors.IsNotFound(err)).To(BeTrue())
})
It("should unregister from Console", func() {
console := t.getConsole(t.NewConsole())
Expect(console.Spec.Plugins).ToNot(ContainElement("cryostat-plugin"))
Expect(console.Spec.Plugins).To(ContainElement("other-plugin"))
})
})
Context("with no existing plugin on incompatible OpenShift", func() {
BeforeEach(func() {
t.objs = append(t.objs, t.NewConsole(), t.NewPluginClusterRoleBinding(), t.NewOperatorDeployment(), t.NewClusterVersionOld())
})
JustBeforeEach(func() {
t.updateClusterVersionStatus(t.NewClusterVersionOld())
err := installer.Start(context.Background())
Expect(err).ToNot(HaveOccurred())
})
It("should not error when plugin already uninstalled", func() {
// Test passes if no error in JustBeforeEach
})
It("should not create ConsolePlugin", func() {
plugin := &consolev1.ConsolePlugin{}
err := t.client.Get(context.Background(), types.NamespacedName{Name: t.NewConsolePlugin().Name}, plugin)
Expect(kerrors.IsNotFound(err)).To(BeTrue())
})
})
Context("with ConsolePlugin CR but not registered in Console", func() {
BeforeEach(func() {
t.objs = append(t.objs, t.NewConsole(), t.NewConsolePlugin(), t.NewPluginClusterRoleBinding(), t.NewOperatorDeployment(), t.NewClusterVersionOld())
})
JustBeforeEach(func() {
t.updateClusterVersionStatus(t.NewClusterVersionOld())
err := installer.Start(context.Background())
Expect(err).ToNot(HaveOccurred())
})
It("should delete ConsolePlugin", func() {
plugin := &consolev1.ConsolePlugin{}
err := t.client.Get(context.Background(), types.NamespacedName{Name: t.NewConsolePlugin().Name}, plugin)
Expect(kerrors.IsNotFound(err)).To(BeTrue())
})
It("should not error when plugin not registered", func() {
// Test passes if no error in JustBeforeEach
})
})
Context("with plugin registered in Console but no CR", func() {
BeforeEach(func() {
t.objs = append(t.objs, t.NewConsoleExisting(), t.NewPluginClusterRoleBinding(), t.NewOperatorDeployment(), t.NewClusterVersionOld())
})
JustBeforeEach(func() {
t.updateClusterVersionStatus(t.NewClusterVersionOld())
err := installer.Start(context.Background())
Expect(err).ToNot(HaveOccurred())
})
It("should unregister from Console", func() {
console := t.getConsole(t.NewConsole())
Expect(console.Spec.Plugins).ToNot(ContainElement("cryostat-plugin"))
Expect(console.Spec.Plugins).To(ContainElement("other-plugin"))
})
It("should not error when ConsolePlugin CR missing", func() {
// Test passes if no error in JustBeforeEach
})
})
Context("with missing Console CR during uninstall", func() {
BeforeEach(func() {
t.objs = append(t.objs, t.NewConsolePlugin(), t.NewPluginClusterRoleBinding(), t.NewOperatorDeployment(), t.NewClusterVersionOld())
})
JustBeforeEach(func() {
t.updateClusterVersionStatus(t.NewClusterVersionOld())
err := installer.Start(context.Background())
Expect(err).ToNot(HaveOccurred())
})
It("should still delete ConsolePlugin", func() {
plugin := &consolev1.ConsolePlugin{}
err := t.client.Get(context.Background(), types.NamespacedName{Name: t.NewConsolePlugin().Name}, plugin)
Expect(kerrors.IsNotFound(err)).To(BeTrue())
})
It("should not error when Console CR missing", func() {
// Test passes if no error in JustBeforeEach
})
})
})
})
Context("as runnable", func() {
It("should need leader election", func() {
Expand Down
2 changes: 1 addition & 1 deletion internal/console/test/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ func (r *PluginTestResources) NewOperatorDeploymentMissingLabels() *appsv1.Deplo
}

func (r *PluginTestResources) NewClusterVersion() *configv1.ClusterVersion {
return r.newClusterVersion("4.17.0-foo+bar")
return r.newClusterVersion("4.19.0-foo+bar")
}

func (r *PluginTestResources) NewClusterVersionOld() *configv1.ClusterVersion {
Expand Down
12 changes: 8 additions & 4 deletions internal/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,15 @@ func main() {
setupLog.Error(err, "could not determine operator's namespace")
os.Exit(1)
}
minOpenShiftVersion := os.Getenv("MIN_OPENSHIFT_VERSION")
maxOpenShiftVersion := os.Getenv("MAX_OPENSHIFT_VERSION")
installer := &console.PluginInstaller{
Client: mgr.GetClient(),
Namespace: namespace,
Scheme: mgr.GetScheme(),
Log: setupLog,
Client: mgr.GetClient(),
Namespace: namespace,
Scheme: mgr.GetScheme(),
Log: setupLog,
MinOpenShiftVersion: minOpenShiftVersion,
MaxOpenShiftVersion: maxOpenShiftVersion,
}
err := installer.SetupWithManager(mgr)
if err != nil {
Expand Down
Loading