Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ metadata:
capabilities: Deep Insights
console.openshift.io/plugins: '["gitops-plugin"]'
containerImage: quay.io/redhat-developer/gitops-operator
createdAt: "2026-07-31T05:20:33Z"
createdAt: "2026-08-18T19:13:31Z"
description: Enables teams to adopt GitOps principles for managing cluster configurations
and application delivery across hybrid multi-cluster Kubernetes environments.
features.operators.openshift.io/disconnected: "true"
Expand Down
11 changes: 9 additions & 2 deletions common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,17 @@ const (
InfraNodeLabelSelector = "node-role.kubernetes.io/infra"
// Default console plugin image
DefaultConsoleImage = "quay.io/redhat-user-workloads/rh-openshift-gitops-tenant/console-plugin-rhel9"
// DefaultConsoleImagePF5 is Default console plugin image for PatternFly 5
DefaultConsoleImagePF5 = "quay.io/redhat-user-workloads/rh-openshift-gitops-tenant/console-plugin-4.18-rhel9"
// Default console plugin version
DefaultConsoleVersion = "main"
// Default console plugin installation OCP version
DefaultDynamicPluginStartOCPVersion = "4.15.0"
// DefaultConsoleVersionPF5 is a Default console plugin version for PatternFly 5
DefaultConsoleVersionPF5 = "main"
// DefaultDynamicPluginStartOCPVersion is the minimum OCP version that supports the console plugin
DefaultDynamicPluginStartOCPVersion = "4.18.0"
// PluginPF6MinOCPVersion is the minimum OCP version that should use the PF6-based plugin;
// OCP versions >= 4.18 and < 4.19 use the PF5-based plugin instead.
PluginPF6MinOCPVersion = "4.19.0"
// ImagePullPolicyEnvVar is the environment variable for configuring image pull policy
ImagePullPolicy = "IMAGE_PULL_POLICY"
// InfraNodeSelectorAnnotation is the OpenShift namespace annotation that applies a default node selector to all pods
Expand Down
38 changes: 23 additions & 15 deletions controllers/consoleplugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const (
gitopsPluginSvcName = gitopsPluginName + "-service"
proxyAlias = "gitops"
pluginImageEnv = "GITOPS_CONSOLE_PLUGIN_IMAGE"
pluginImageEnvPF5 = "GITOPS_CONSOLE_PLUGIN_IMAGE_PF5"
servicePort = 9001
pluginServingCertName = "console-serving-cert"
kubeAppLabelApp = "app"
Expand All @@ -49,12 +50,18 @@ const (
kubeAppLabelName = "app.kubernetes.io/name"
)

func getPluginPodSpec(crImagePullPolicy corev1.PullPolicy) corev1.PodSpec {
consolePluginImage := os.Getenv(pluginImageEnv)
if consolePluginImage == "" {
image := common.DefaultConsoleImage
version := common.DefaultConsoleVersion
consolePluginImage = image + ":" + version
func getPluginPodSpec(crImagePullPolicy corev1.PullPolicy, isPF5 bool) corev1.PodSpec {
var consolePluginImage string
if isPF5 {
consolePluginImage = os.Getenv(pluginImageEnvPF5)
if consolePluginImage == "" {
consolePluginImage = common.DefaultConsoleImagePF5 + ":" + common.DefaultConsoleVersionPF5
}
} else {
consolePluginImage = os.Getenv(pluginImageEnv)
if consolePluginImage == "" {
consolePluginImage = common.DefaultConsoleImage + ":" + common.DefaultConsoleVersion
}
}

podSpec := corev1.PodSpec{
Expand Down Expand Up @@ -138,8 +145,8 @@ func getPluginPodSpec(crImagePullPolicy corev1.PullPolicy) corev1.PodSpec {
return podSpec
}

func pluginDeployment(crImagePullPolicy corev1.PullPolicy) *appsv1.Deployment {
podSpec := getPluginPodSpec(crImagePullPolicy)
func pluginDeployment(crImagePullPolicy corev1.PullPolicy, isPF5 bool) *appsv1.Deployment {
podSpec := getPluginPodSpec(crImagePullPolicy, isPF5)
template := corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
Expand Down Expand Up @@ -362,9 +369,9 @@ func sortTolerations(tolerations []corev1.Toleration) []corev1.Toleration {
return sorted
}

func (r *ReconcileGitopsService) reconcileDeployment(cr *pipelinesv1alpha1.GitopsService, request reconcile.Request, newPluginConfigMap *corev1.ConfigMap) (reconcile.Result, error) {
func (r *ReconcileGitopsService) reconcileDeployment(cr *pipelinesv1alpha1.GitopsService, request reconcile.Request, newPluginConfigMap *corev1.ConfigMap, isPF5 bool) (reconcile.Result, error) {
reqLogger := logs.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
newPluginDeployment := pluginDeployment(cr.Spec.ImagePullPolicy)
newPluginDeployment := pluginDeployment(cr.Spec.ImagePullPolicy, isPF5)

if err := controllerutil.SetControllerReference(cr, newPluginDeployment, r.Scheme); err != nil {
return reconcile.Result{}, err
Expand Down Expand Up @@ -481,7 +488,7 @@ func (r *ReconcileGitopsService) reconcileService(instance *pipelinesv1alpha1.Gi
existingServiceRef.Labels = pluginServiceRef.Labels
existingServiceRef.Spec.Selector = pluginServiceRef.Spec.Selector
existingServiceRef.Spec.Ports = pluginServiceRef.Spec.Ports
return reconcile.Result{}, r.Client.Update(context.TODO(), pluginServiceRef)
return reconcile.Result{}, r.Client.Update(context.TODO(), existingServiceRef)
}
}
return reconcile.Result{}, nil
Expand Down Expand Up @@ -518,7 +525,7 @@ func (r *ReconcileGitopsService) reconcileConsolePlugin(instance *pipelinesv1alp
reqLogger.Info("Reconciling Console Plugin", "Namespace", existingPlugin.Namespace, "Name", existingPlugin.Name)
existingPlugin.Spec.DisplayName = newConsolePlugin.Spec.DisplayName
existingPlugin.Spec.Backend.Service = newConsolePlugin.Spec.Backend.Service
return reconcile.Result{}, r.Client.Update(context.TODO(), newConsolePlugin)
return reconcile.Result{}, r.Client.Update(context.TODO(), existingPlugin)
}
}
return reconcile.Result{}, nil
Expand Down Expand Up @@ -576,8 +583,9 @@ func (r *ReconcileGitopsService) reconcileConfigMap(instance *pipelinesv1alpha1.
return reconcile.Result{}, nil
}

// is this func the reconciler enty point to reconcile the current plugin state?
func (r *ReconcileGitopsService) reconcilePlugin(instance *pipelinesv1alpha1.GitopsService, request reconcile.Request) (reconcile.Result, error) {
// reconcilePlugin is the entry point for reconciling all console plugin resources.
// isPF5 selects the PatternFly 5 image (OCP 4.18.x) vs the PatternFly 6 image (OCP >= 4.19).
func (r *ReconcileGitopsService) reconcilePlugin(instance *pipelinesv1alpha1.GitopsService, request reconcile.Request, isPF5 bool) (reconcile.Result, error) {
reqLogger := logs.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
if !util.IsConsoleAPIFound() {
reqLogger.Info("Skip console plugin reconcile: OpenShift Console API not found")
Expand All @@ -595,7 +603,7 @@ func (r *ReconcileGitopsService) reconcilePlugin(instance *pipelinesv1alpha1.Git
return result, err
}

if result, err := r.reconcileDeployment(instance, request, newPluginConfigMap); err != nil {
if result, err := r.reconcileDeployment(instance, request, newPluginConfigMap, isPF5); err != nil {
return result, err
}

Expand Down
99 changes: 79 additions & 20 deletions controllers/consoleplugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ func TestPlugin_reconcileDeployment_changedTemplateLabels(t *testing.T) {
fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(newGitopsService(), d).Build()
reconciler := newReconcileGitOpsService(fakeClient, s)

_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap())
_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap(), false)
assertNoError(t, err)

deployment := &appsv1.Deployment{}
Expand Down Expand Up @@ -647,7 +647,7 @@ func TestPlugin_reconcileDeployment_changedContainers(t *testing.T) {
assert.DeepEqual(t, deployment.Spec.Template.Spec.Containers[0].SecurityContext, securityContextForPlugin())
}

_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap())
_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap(), false)
assertNoError(t, err)

// There should be a new console plugin deployment created
Expand All @@ -672,7 +672,7 @@ func TestPlugin_reconcileDeployment_changedContainers(t *testing.T) {
assertNoError(t, err)

// Verify if the containers are reconciled back to the default values
_, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap())
_, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap(), false)
assertNoError(t, err)

deployment = &appsv1.Deployment{}
Expand Down Expand Up @@ -979,7 +979,7 @@ func TestPlugin_reconcileDeployment_infraNodeSelectorNotInPodSpec(t *testing.T)
fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(gitopsService).Build()
reconciler := newReconcileGitOpsService(fakeClient, s)

_, err := reconciler.reconcileDeployment(gitopsService, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap())
_, err := reconciler.reconcileDeployment(gitopsService, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap(), false)
assertNoError(t, err)

deployment := &appsv1.Deployment{}
Expand All @@ -999,7 +999,7 @@ func TestPlugin_reconcileDeployment(t *testing.T) {
reconciler := newReconcileGitOpsService(fakeClient, s)
instance := &pipelinesv1alpha1.GitopsService{}

_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap())
_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap(), false)
assertNoError(t, err)

deployment := &appsv1.Deployment{}
Expand Down Expand Up @@ -1036,7 +1036,7 @@ func TestPlugin_reconcileDeployment_ChangedResources(t *testing.T) {
},
}

_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap())
_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap(), false)
assertNoError(t, err)

deployment := &appsv1.Deployment{}
Expand All @@ -1056,7 +1056,7 @@ func TestPlugin_ReconcileDeployment_DefaultResourceValues(t *testing.T) {
fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(newGitopsService()).Build()
reconciler := newReconcileGitOpsService(fakeClient, s)
instance := &pipelinesv1alpha1.GitopsService{}
_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap())
_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap(), false)
assertNoError(t, err)

deployment := &appsv1.Deployment{}
Expand Down Expand Up @@ -1096,7 +1096,7 @@ func TestPlugin_ReconcileDeployment_ChangeExistingResourceValues(t *testing.T) {
Resources: Resources,
},
}
_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap())
_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap(), false)
assertNoError(t, err)

deployment := &appsv1.Deployment{}
Expand All @@ -1120,7 +1120,7 @@ func TestPlugin_ReconcileDeployment_ChangeExistingResourceValues(t *testing.T) {
}
instance.Spec.ConsolePlugin.Backend.Resources, instance.Spec.ConsolePlugin.GitopsPlugin.Resources = updatedResources, updatedResources

_, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap())
_, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap(), false)
assertNoError(t, err)

deployment = &appsv1.Deployment{}
Expand Down Expand Up @@ -1669,7 +1669,7 @@ func TestReconcileDeployment_NoUpdateWhenContainersOrderDiffers(t *testing.T) {
instance := &pipelinesv1alpha1.GitopsService{}

// Create deployment
_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap())
_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap(), false)
assertNoError(t, err)

// Get the deployment and capture initial ResourceVersion and Generation
Expand All @@ -1692,7 +1692,7 @@ func TestReconcileDeployment_NoUpdateWhenContainersOrderDiffers(t *testing.T) {
}

// Reconcile again - should NOT trigger an update
_, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap())
_, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap(), false)
assertNoError(t, err)

// Verify no update was triggered
Expand All @@ -1715,7 +1715,7 @@ func TestReconcileDeployment_NoUpdateWhenVolumesOrderDiffers(t *testing.T) {
instance := &pipelinesv1alpha1.GitopsService{}

// Create deployment
_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap())
_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap(), false)
assertNoError(t, err)

// Get the deployment
Expand All @@ -1740,7 +1740,7 @@ func TestReconcileDeployment_NoUpdateWhenVolumesOrderDiffers(t *testing.T) {
genAfterManualUpdate := deployment.Generation

// Reconcile again - should NOT trigger an update
_, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap())
_, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap(), false)
assertNoError(t, err)

// Verify no update was triggered
Expand Down Expand Up @@ -1786,7 +1786,7 @@ func TestReconcileDeployment_NoUpdateWhenTolerationsOrderDiffers(t *testing.T) {
reconciler := newReconcileGitOpsService(fakeClient, s)

// Create deployment
_, err := reconciler.reconcileDeployment(gitopsService, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap())
_, err := reconciler.reconcileDeployment(gitopsService, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap(), false)
assertNoError(t, err)

// Get the deployment
Expand All @@ -1811,7 +1811,7 @@ func TestReconcileDeployment_NoUpdateWhenTolerationsOrderDiffers(t *testing.T) {
genAfterManualUpdate := deployment.Generation

// Reconcile again - should NOT trigger an update
_, err = reconciler.reconcileDeployment(gitopsService, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap())
_, err = reconciler.reconcileDeployment(gitopsService, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap(), false)
assertNoError(t, err)

// Verify no update was triggered
Expand All @@ -1837,7 +1837,7 @@ func TestReconcileDeployment_UpdateWhenActualChange(t *testing.T) {
instance := &pipelinesv1alpha1.GitopsService{}

// Create deployment
_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap())
_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap(), false)
assertNoError(t, err)

// Get the deployment and capture initial ResourceVersion and Generation
Expand All @@ -1852,7 +1852,7 @@ func TestReconcileDeployment_UpdateWhenActualChange(t *testing.T) {
assertNoError(t, err)

// Reconcile again - should trigger an update
_, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap())
_, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap(), false)
assertNoError(t, err)

// Verify update was triggered
Expand Down Expand Up @@ -2023,7 +2023,7 @@ func TestReconcileDeployment_AddsHashAnnotation(t *testing.T) {
},
}
r := &ReconcileGitopsService{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(instance).Build(), Scheme: scheme}
_, err := r.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsService), cm)
_, err := r.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsService), cm, false)
assert.NilError(t, err)
deployment := &appsv1.Deployment{}
err = r.Client.Get(context.TODO(), types.NamespacedName{Name: gitopsPluginName, Namespace: serviceNamespace}, deployment)
Expand Down Expand Up @@ -2056,12 +2056,71 @@ func TestReconcileDeployment_UpdatesHashAnnotationWhenConfigChanges(t *testing.T
},
}
r := &ReconcileGitopsService{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(instance).Build(), Scheme: scheme}
_, err := r.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsService), cm1)
_, err := r.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsService), cm1, false)
assert.NilError(t, err)
_, err = r.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsService), cm2)
_, err = r.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsService), cm2, false)
assert.NilError(t, err)
deployment := &appsv1.Deployment{}
err = r.Client.Get(context.TODO(), types.NamespacedName{Name: gitopsPluginName, Namespace: serviceNamespace}, deployment)
assert.NilError(t, err)
assert.Equal(t, getConfigMapHash(cm2), deployment.Spec.Template.Annotations["httpd-cfg-hash"])
}

func TestPlugin_reconcileDeployment_PluginImage(t *testing.T) {
tests := []struct {
name string
isPF5 bool
envKey string
envValue string
expectedImage string
}{
{
name: "PF6 default image when env var is not set",
isPF5: false,
expectedImage: common.DefaultConsoleImage + ":" + common.DefaultConsoleVersion,
},
{
name: "PF6 image from env var when set",
isPF5: false,
envKey: pluginImageEnv,
envValue: "custom-pf6-image:v1.2.3",
expectedImage: "custom-pf6-image:v1.2.3",
},
{
name: "PF5 default image when env var is not set",
isPF5: true,
expectedImage: common.DefaultConsoleImagePF5 + ":" + common.DefaultConsoleVersionPF5,
},
{
name: "PF5 image from env var when set",
isPF5: true,
envKey: pluginImageEnvPF5,
envValue: "custom-pf5-image:v1.2.3",
expectedImage: "custom-pf5-image:v1.2.3",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.envKey != "" {
t.Setenv(test.envKey, test.envValue)
}

s := scheme.Scheme
addKnownTypesToScheme(s)

fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(newGitopsService()).Build()
reconciler := newReconcileGitOpsService(fakeClient, s)
instance := &pipelinesv1alpha1.GitopsService{}

_, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap(), test.isPF5)
assertNoError(t, err)

deployment := &appsv1.Deployment{}
err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: gitopsPluginName, Namespace: serviceNamespace}, deployment)
assertNoError(t, err)

assert.Equal(t, deployment.Spec.Template.Spec.Containers[0].Image, test.expectedImage)
})
}
}
Loading
Loading