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
3 changes: 3 additions & 0 deletions api/v1alpha1/gitopsservice_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ type GitopsServiceSpec struct {
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
// ConsolePlugin defines the Resource configuration for the Console Plugin components
ConsolePlugin *ConsolePluginStruct `json:"consolePlugin,omitempty"`
// ImagePullPolicy defines the image pull policy for GitOps workloads
// +kubebuilder:validation:Enum=Always;IfNotPresent;Never
ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"`
}

// ConsolePluginStruct defines the resource configuration for the Console Plugin components
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ metadata:
capabilities: Deep Insights
console.openshift.io/plugins: '["gitops-plugin"]'
containerImage: quay.io/redhat-developer/gitops-operator
createdAt: "2025-10-01T09:01:02Z"
createdAt: "2025-10-24T17:54:33Z"
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
8 changes: 8 additions & 0 deletions bundle/manifests/pipelines.openshift.io_gitopsservices.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ spec:
type: object
type: object
type: object
imagePullPolicy:
description: ImagePullPolicy defines the image pull policy for GitOps
workloads
enum:
- Always
- IfNotPresent
- Never
type: string
nodeSelector:
additionalProperties:
type: string
Expand Down
32 changes: 31 additions & 1 deletion common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ limitations under the License.

package common

import "os"
import (
"os"

corev1 "k8s.io/api/core/v1"
)

const (
// ArgoCDInstanceName is the default Argo CD instance name
Expand All @@ -33,6 +37,8 @@ const (
DefaultConsoleVersion = "v0.1.0"
// Default console plugin installation OCP version
DefaultDynamicPluginStartOCPVersion = "4.15.0"
// ImagePullPolicyEnvVar is the environment variable for configuring image pull policy
ImagePullPolicy = "IMAGE_PULL_POLICY"
)

// InfraNodeSelector returns openshift label for infrastructure nodes
Expand All @@ -48,3 +54,27 @@ func StringFromEnv(env string, defaultValue string) string {
}
return defaultValue
}

// GetImagePullPolicy returns the image pull policy based on precedence:
// 1. CR-level override (highest precedence)
// 2. Environment variable (middle precedence)
// 3. Default fallback (lowest precedence - IfNotPresent)
func GetImagePullPolicy(crImagePullPolicy corev1.PullPolicy) corev1.PullPolicy {
// If CR specifies a policy, use it (highest precedence)
if crImagePullPolicy != "" {
return crImagePullPolicy
}

// Check environment variable (middle precedence)
envPolicy := os.Getenv(ImagePullPolicy)
switch envPolicy {
case "Always":
return corev1.PullAlways
case "IfNotPresent":
return corev1.PullIfNotPresent
case "Never":
return corev1.PullNever
default:
return corev1.PullPolicy("IfNotPresent")
}
}
87 changes: 87 additions & 0 deletions common/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
Copyright 2021.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package common

import (
"os"
"testing"

corev1 "k8s.io/api/core/v1"
)

func TestGetImagePullPolicy(t *testing.T) {
tests := []struct {
name string
crPolicy corev1.PullPolicy
envValue string
expectedPolicy corev1.PullPolicy
}{
{
name: "CR policy takes precedence",
crPolicy: corev1.PullIfNotPresent,
envValue: "Never",
expectedPolicy: corev1.PullIfNotPresent,
},
{
name: "Environment variable used when CR policy empty",
crPolicy: "",
envValue: "IfNotPresent",
expectedPolicy: corev1.PullIfNotPresent,
},
{
name: "Environment variable Never",
crPolicy: "",
envValue: "Never",
expectedPolicy: corev1.PullNever,
},
{
name: "Environment variable Always",
crPolicy: "",
envValue: "Always",
expectedPolicy: corev1.PullAlways,
},
{
name: "Default to IfNotPresent when no config",
crPolicy: "",
envValue: "",
expectedPolicy: corev1.PullIfNotPresent,
},
{
name: "Invalid env value defaults to IfNotPresent",
crPolicy: "",
envValue: "InvalidValue",
expectedPolicy: corev1.PullIfNotPresent,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set environment variable
if tt.envValue != "" {
os.Setenv(ImagePullPolicy, tt.envValue)
} else {
os.Unsetenv(ImagePullPolicy)
}
defer os.Unsetenv(ImagePullPolicy)

result := GetImagePullPolicy(tt.crPolicy)
if result != tt.expectedPolicy {
t.Errorf("GetImagePullPolicy() = %v, want %v", result, tt.expectedPolicy)
}
})
}
}
8 changes: 8 additions & 0 deletions config/crd/bases/pipelines.openshift.io_gitopsservices.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ spec:
type: object
type: object
type: object
imagePullPolicy:
description: ImagePullPolicy defines the image pull policy for GitOps
workloads
enum:
- Always
- IfNotPresent
- Never
type: string
nodeSelector:
additionalProperties:
type: string
Expand Down
10 changes: 5 additions & 5 deletions controllers/consoleplugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const (
kubeAppLabelName = "app.kubernetes.io/name"
)

func getPluginPodSpec() corev1.PodSpec {
func getPluginPodSpec(crImagePullPolicy corev1.PullPolicy) corev1.PodSpec {
consolePluginImage := os.Getenv(pluginImageEnv)
if consolePluginImage == "" {
image := common.DefaultConsoleImage
Expand All @@ -58,7 +58,7 @@ func getPluginPodSpec() corev1.PodSpec {
Env: util.ProxyEnvVars(),
Name: gitopsPluginName,
Image: consolePluginImage,
ImagePullPolicy: corev1.PullAlways,
ImagePullPolicy: common.GetImagePullPolicy(crImagePullPolicy),
Ports: []corev1.ContainerPort{
{
Name: "http",
Expand Down Expand Up @@ -133,8 +133,8 @@ func getPluginPodSpec() corev1.PodSpec {
return podSpec
}

func pluginDeployment() *appsv1.Deployment {
podSpec := getPluginPodSpec()
func pluginDeployment(crImagePullPolicy corev1.PullPolicy) *appsv1.Deployment {
podSpec := getPluginPodSpec(crImagePullPolicy)
template := corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
Expand Down Expand Up @@ -269,7 +269,7 @@ func pluginConfigMap() *corev1.ConfigMap {

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

if err := controllerutil.SetControllerReference(cr, newPluginDeployment, r.Scheme); err != nil {
return reconcile.Result{}, err
Expand Down
2 changes: 1 addition & 1 deletion controllers/consoleplugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ func TestPlugin_reconcileDeployment_changedContainers(t *testing.T) {
consoleImage := common.DefaultConsoleImage + ":" + common.DefaultConsoleVersion
assert.Equal(t, deployment.Spec.Template.Spec.Containers[0].Name, "gitops-plugin")
assert.Equal(t, deployment.Spec.Template.Spec.Containers[0].Image, consoleImage)
assert.Equal(t, deployment.Spec.Template.Spec.Containers[0].ImagePullPolicy, corev1.PullAlways)
assert.Equal(t, deployment.Spec.Template.Spec.Containers[0].ImagePullPolicy, corev1.PullIfNotPresent)
assert.Equal(t, deployment.Spec.Template.Spec.Containers[0].Ports[0].Name, "http")
assert.Equal(t, deployment.Spec.Template.Spec.Containers[0].Ports[0].Protocol, corev1.ProtocolTCP)
assert.Equal(t, deployment.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort, int32(9001))
Expand Down
13 changes: 9 additions & 4 deletions controllers/gitopsservice_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ func (r *ReconcileGitopsService) reconcileBackend(gitopsserviceNamespacedName ty

// Define a new backend Deployment
{
deploymentObj := newBackendDeployment(gitopsserviceNamespacedName)
deploymentObj := newBackendDeployment(gitopsserviceNamespacedName, instance.Spec.ImagePullPolicy)

// Add SeccompProfile based on cluster version
util.AddSeccompProfileForOpenShift(r.Client, &deploymentObj.Spec.Template.Spec)
Expand Down Expand Up @@ -654,6 +654,10 @@ func (r *ReconcileGitopsService) reconcileBackend(gitopsserviceNamespacedName ty
found.Spec.Template.Spec.Containers[0].Env = deploymentObj.Spec.Template.Spec.Containers[0].Env
changed = true
}
if !reflect.DeepEqual(found.Spec.Template.Spec.Containers[0].ImagePullPolicy, deploymentObj.Spec.Template.Spec.Containers[0].ImagePullPolicy) {
found.Spec.Template.Spec.Containers[0].ImagePullPolicy = deploymentObj.Spec.Template.Spec.Containers[0].ImagePullPolicy
changed = true
}
if !reflect.DeepEqual(found.Spec.Template.Spec.Containers[0].Resources, deploymentObj.Spec.Template.Spec.Containers[0].Resources) {
found.Spec.Template.Spec.Containers[0].Resources = deploymentObj.Spec.Template.Spec.Containers[0].Resources
changed = true
Expand Down Expand Up @@ -744,16 +748,17 @@ func objectMeta(resourceName string, namespace string, opts ...func(*metav1.Obje
return objectMeta
}

func newBackendDeployment(ns types.NamespacedName) *appsv1.Deployment {
func newBackendDeployment(ns types.NamespacedName, crImagePullPolicy corev1.PullPolicy) *appsv1.Deployment {
image := os.Getenv(backendImageEnvName)
if image == "" {
image = backendImage
}
podSpec := corev1.PodSpec{
Containers: []corev1.Container{
{
Name: ns.Name,
Image: image,
Name: ns.Name,
Image: image,
ImagePullPolicy: common.GetImagePullPolicy(crImagePullPolicy),
Ports: []corev1.ContainerPort{
{
Name: "http",
Expand Down
4 changes: 2 additions & 2 deletions controllers/gitopsservice_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@ func TestImageFromEnvVariable(t *testing.T) {
image := "quay.io/org/test"
t.Setenv(backendImageEnvName, image)

deployment := newBackendDeployment(ns)
deployment := newBackendDeployment(ns, corev1.PullPolicy(corev1.PullIfNotPresent))

got := deployment.Spec.Template.Spec.Containers[0].Image
if got != image {
t.Errorf("Image mismatch: got %s, want %s", got, image)
}
})
t.Run("env variable for image not found", func(t *testing.T) {
deployment := newBackendDeployment(ns)
deployment := newBackendDeployment(ns, corev1.PullPolicy(corev1.PullIfNotPresent))

got := deployment.Spec.Template.Spec.Containers[0].Image
if got != backendImage {
Expand Down
Loading