diff --git a/api/v1beta1/temporalcluster_types.go b/api/v1beta1/temporalcluster_types.go
index 555c405b..69b3481a 100644
--- a/api/v1beta1/temporalcluster_types.go
+++ b/api/v1beta1/temporalcluster_types.go
@@ -174,7 +174,8 @@ type DeploymentOverride struct {
*ObjectMetaOverride `json:"metadata,omitempty"`
// Specification of the desired behavior of the Deployment.
// +optional
- Spec *DeploymentOverrideSpec `json:"spec,omitempty"`
+ Spec *DeploymentOverrideSpec `json:"spec,omitempty"`
+ JsonPatch *apiextensionsv1.JSON `json:"jsonPatch,omitempty"`
}
// DeploymentOverrideSpec provides the ability to override a Deployment Spec.
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index 4ae28587..a9278619 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -382,6 +382,11 @@ func (in *DeploymentOverride) DeepCopyInto(out *DeploymentOverride) {
*out = new(DeploymentOverrideSpec)
(*in).DeepCopyInto(*out)
}
+ if in.JsonPatch != nil {
+ in, out := &in.JsonPatch, &out.JsonPatch
+ *out = new(apiextensionsv1.JSON)
+ (*in).DeepCopyInto(*out)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentOverride.
diff --git a/config/crd/bases/temporal.io_temporalclusters.yaml b/config/crd/bases/temporal.io_temporalclusters.yaml
index f4afcd5a..1a5d1d2c 100644
--- a/config/crd/bases/temporal.io_temporalclusters.yaml
+++ b/config/crd/bases/temporal.io_temporalclusters.yaml
@@ -64,6 +64,8 @@ spec:
deployment:
description: Override configuration for the temporal service Deployment.
properties:
+ jsonPatch:
+ x-kubernetes-preserve-unknown-fields: true
metadata:
description: |-
ObjectMetaOverride provides the ability to override an object metadata.
@@ -2910,6 +2912,8 @@ spec:
deployment:
description: Override configuration for the temporal service Deployment.
properties:
+ jsonPatch:
+ x-kubernetes-preserve-unknown-fields: true
metadata:
description: |-
ObjectMetaOverride provides the ability to override an object metadata.
@@ -3075,6 +3079,8 @@ spec:
deployment:
description: Override configuration for the temporal service Deployment.
properties:
+ jsonPatch:
+ x-kubernetes-preserve-unknown-fields: true
metadata:
description: |-
ObjectMetaOverride provides the ability to override an object metadata.
@@ -3246,6 +3252,8 @@ spec:
deployment:
description: Override configuration for the temporal service Deployment.
properties:
+ jsonPatch:
+ x-kubernetes-preserve-unknown-fields: true
metadata:
description: |-
ObjectMetaOverride provides the ability to override an object metadata.
@@ -3411,6 +3419,8 @@ spec:
deployment:
description: Override configuration for the temporal service Deployment.
properties:
+ jsonPatch:
+ x-kubernetes-preserve-unknown-fields: true
metadata:
description: |-
ObjectMetaOverride provides the ability to override an object metadata.
@@ -3549,6 +3559,8 @@ spec:
deployment:
description: Override configuration for the temporal service Deployment.
properties:
+ jsonPatch:
+ x-kubernetes-preserve-unknown-fields: true
metadata:
description: |-
ObjectMetaOverride provides the ability to override an object metadata.
@@ -3637,6 +3649,8 @@ spec:
deployment:
description: Override configuration for the temporal service Deployment.
properties:
+ jsonPatch:
+ x-kubernetes-preserve-unknown-fields: true
metadata:
description: |-
ObjectMetaOverride provides the ability to override an object metadata.
@@ -3829,6 +3843,8 @@ spec:
deployment:
description: Override configuration for the temporal service Deployment.
properties:
+ jsonPatch:
+ x-kubernetes-preserve-unknown-fields: true
metadata:
description: |-
ObjectMetaOverride provides the ability to override an object metadata.
diff --git a/docs/api/v1beta1.md b/docs/api/v1beta1.md
index 1a4c6147..27248913 100644
--- a/docs/api/v1beta1.md
+++ b/docs/api/v1beta1.md
@@ -1400,6 +1400,18 @@ DeploymentOverrideSpec
+
+
+jsonPatch
+
+
+k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON
+
+
+ |
+
+ |
+
@@ -2388,7 +2400,7 @@ map[string]string
override
-
+
github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1.ServiceMonitorSpec
@@ -2403,7 +2415,7 @@ All fields can be overwritten except “endpoints”, “selector&rd
|
metricRelabelings
-
+
[]github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1.RelabelConfig
diff --git a/go.mod b/go.mod
index b45a7ebb..ef9971c2 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
github.com/alexandrevilain/controller-tools v0.3.0
github.com/cert-manager/cert-manager v1.16.1
github.com/elliotchance/orderedmap/v2 v2.4.0
+ github.com/evanphx/json-patch/v5 v5.9.0
github.com/go-logr/logr v1.4.2
github.com/gocql/gocql v1.7.0
github.com/google/uuid v1.6.0
@@ -53,7 +54,6 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
- github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
diff --git a/pkg/kubernetes/overrides.go b/pkg/kubernetes/overrides.go
index 1f80ad80..3d9cdf69 100644
--- a/pkg/kubernetes/overrides.go
+++ b/pkg/kubernetes/overrides.go
@@ -23,6 +23,7 @@ import (
"github.com/alexandrevilain/temporal-operator/api/v1beta1"
"github.com/alexandrevilain/temporal-operator/internal/metadata"
+ jsonpatch "github.com/evanphx/json-patch/v5"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/strategicpatch"
@@ -110,6 +111,24 @@ func ApplyDeploymentOverrides(deployment *appsv1.Deployment, override *v1beta1.D
}
}
+ if override.JsonPatch != nil {
+ patch, err := jsonpatch.DecodePatch(override.JsonPatch.Raw)
+ if err != nil {
+ return fmt.Errorf("can't decode json patch: %w", err)
+ }
+
+ original, err := json.Marshal(deployment)
+ if err != nil {
+ return fmt.Errorf("can't marshal deployment spec: %w", err)
+ }
+
+ patched, err := patch.Apply(original)
+ if err != nil {
+ return fmt.Errorf("can't apply json patch: %w", err)
+ }
+ return json.Unmarshal(patched, &deployment)
+ }
+
return nil
}
diff --git a/pkg/kubernetes/overrides_test.go b/pkg/kubernetes/overrides_test.go
index bf8b1c70..b8f715b0 100644
--- a/pkg/kubernetes/overrides_test.go
+++ b/pkg/kubernetes/overrides_test.go
@@ -338,6 +338,166 @@ func TestApplyDeploymentOverrides(t *testing.T) {
},
},
},
+ "add env var to existing env": {
+ original: &appsv1.Deployment{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test",
+ },
+ Spec: appsv1.DeploymentSpec{
+ Template: corev1.PodTemplateSpec{
+ Spec: corev1.PodSpec{
+ Containers: []corev1.Container{
+ {
+ Name: "test",
+ Env: []corev1.EnvVar{
+ {
+ Name: "a",
+ ValueFrom: &corev1.EnvVarSource{
+ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: "test",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ override: &v1beta1.DeploymentOverride{
+ JsonPatch: &apiextensionsv1.JSON{
+ Raw: []byte(`[{"op":"add", "path":"/spec/template/spec/containers/0/env/-", "value":{"name":"b","value":"c"}}]`),
+ },
+ },
+ expected: &appsv1.Deployment{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test",
+ },
+ Spec: appsv1.DeploymentSpec{
+ Template: corev1.PodTemplateSpec{
+ Spec: corev1.PodSpec{
+ Containers: []corev1.Container{
+ {
+ Name: "test",
+ Env: []corev1.EnvVar{
+ {
+ Name: "a",
+ ValueFrom: &corev1.EnvVarSource{
+ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: "test",
+ },
+ },
+ },
+ },
+ {
+ Name: "b",
+ Value: "c",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "add secret volume to existing volumes": {
+ original: &appsv1.Deployment{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test",
+ },
+ Spec: appsv1.DeploymentSpec{
+ Template: corev1.PodTemplateSpec{
+ Spec: corev1.PodSpec{
+ Containers: []corev1.Container{
+ {
+ Name: "test",
+ VolumeMounts: []corev1.VolumeMount{
+ {
+ Name: "a",
+ ReadOnly: true,
+ MountPath: "/a",
+ },
+ },
+ },
+ },
+ Volumes: []corev1.Volume{
+ {
+ Name: "a",
+ VolumeSource: corev1.VolumeSource{
+ ConfigMap: &corev1.ConfigMapVolumeSource{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: "test",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ override: &v1beta1.DeploymentOverride{
+ JsonPatch: &apiextensionsv1.JSON{
+ Raw: []byte(`[
+ {"op": "add", "path": "/spec/template/spec/containers/0/volumeMounts/-", "value": {"name": "b", "readOnly": true, "mountPath": "/b"}},
+ {"op": "add", "path": "/spec/template/spec/volumes/-", "value": {"name": "b", "secret": {"secretName": "test"}}}
+ ]`),
+ },
+ },
+ expected: &appsv1.Deployment{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test",
+ },
+ Spec: appsv1.DeploymentSpec{
+ Template: corev1.PodTemplateSpec{
+ Spec: corev1.PodSpec{
+ Containers: []corev1.Container{
+ {
+ Name: "test",
+ VolumeMounts: []corev1.VolumeMount{
+ {
+ Name: "a",
+ ReadOnly: true,
+ MountPath: "/a",
+ },
+ {
+ Name: "b",
+ ReadOnly: true,
+ MountPath: "/b",
+ },
+ },
+ },
+ },
+ Volumes: []corev1.Volume{
+ {
+ Name: "a",
+ VolumeSource: corev1.VolumeSource{
+ ConfigMap: &corev1.ConfigMapVolumeSource{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: "test",
+ },
+ },
+ },
+ },
+ {
+ Name: "b",
+ VolumeSource: corev1.VolumeSource{
+ Secret: &corev1.SecretVolumeSource{
+ SecretName: "test",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
}
for name, test := range tests {
diff --git a/webhooks/temporalcluster_webhook.go b/webhooks/temporalcluster_webhook.go
index 4b47f10e..1edb197e 100644
--- a/webhooks/temporalcluster_webhook.go
+++ b/webhooks/temporalcluster_webhook.go
@@ -28,6 +28,7 @@ import (
"github.com/alexandrevilain/temporal-operator/pkg/version"
enumspb "go.temporal.io/api/enums/v1"
enumsspb "go.temporal.io/server/api/enums/v1"
+ "go.temporal.io/server/common/primitives"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
@@ -317,6 +318,46 @@ func (w *TemporalClusterWebhook) validateCluster(cluster *v1beta1.TemporalCluste
}
}
+ if cluster.Spec.Services != nil {
+ overrides := cluster.Spec.Services.Overrides
+ if overrides != nil && overrides.Deployment != nil && overrides.Deployment.Spec != nil && overrides.Deployment.JsonPatch != nil {
+ errs = append(errs,
+ field.Forbidden(
+ field.NewPath("spec", "services", "overrides", "deployment", "jsonPatch"),
+ fmt.Sprintf("Can't set JsonPatch when Spec is set on Deployment override"),
+ ),
+ )
+ }
+
+ services := []primitives.ServiceName{
+ primitives.FrontendService,
+ primitives.HistoryService,
+ primitives.MatchingService,
+ primitives.WorkerService,
+ primitives.InternalFrontendService,
+ }
+
+ for _, service := range services {
+ spec, err := cluster.Spec.Services.GetServiceSpec(service)
+ if err != nil {
+ errs = append(errs,
+ field.Invalid(
+ field.NewPath("spec", "services", string(service)),
+ string(service),
+ fmt.Sprintf("Invalid service: %s", string(service)),
+ ),
+ )
+ }
+ if spec != nil && spec.Overrides != nil && spec.Overrides.Deployment != nil && spec.Overrides.Deployment.Spec != nil && spec.Overrides.Deployment.JsonPatch != nil {
+ errs = append(errs,
+ field.Forbidden(
+ field.NewPath("spec", "services", string(service), "overrides", "deployment", "jsonPatch"),
+ fmt.Sprintf("Can't set JsonPatch when Spec is set on Deployment override"),
+ ),
+ )
+ }
+ }
+ }
return warns, errs
}
diff --git a/webhooks/temporalcluster_webhook_test.go b/webhooks/temporalcluster_webhook_test.go
index 66fe3cfa..c9e4180f 100644
--- a/webhooks/temporalcluster_webhook_test.go
+++ b/webhooks/temporalcluster_webhook_test.go
@@ -26,6 +26,7 @@ import (
"github.com/alexandrevilain/temporal-operator/pkg/version"
"github.com/alexandrevilain/temporal-operator/webhooks"
"github.com/stretchr/testify/assert"
+ apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/utils/ptr"
@@ -302,6 +303,39 @@ func TestValidateCreate(t *testing.T) {
},
expectedErr: "TemporalCluster.temporal.io \"fake\" is invalid: spec.persistence.visibilityStore.cassandra: Forbidden: Support for Cassandra as a Visibility database has been removed with Temporal Server v1.24.",
},
+ "error with override spec and JSON patch": {
+ object: &v1beta1.TemporalCluster{
+ TypeMeta: v1beta1.TemporalClusterTypeMeta,
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "fake",
+ },
+ Spec: v1beta1.TemporalClusterSpec{
+ Version: version.MustNewVersionFromString("1.18.4"),
+ Services: &v1beta1.ServicesSpec{
+ Overrides: &v1beta1.ServiceSpecOverride{
+ Deployment: &v1beta1.DeploymentOverride{
+ JsonPatch: &apiextensionsv1.JSON{
+ Raw: []byte(`{ "op": "replace", "path": "/spec/replicas", "value": 3 }`),
+ },
+ Spec: &v1beta1.DeploymentOverrideSpec{
+ Template: &v1beta1.PodTemplateSpecOverride{
+ Spec: &apiextensionsv1.JSON{Raw: []byte(`{ "replicas": 2 }`)},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ wh: &webhooks.TemporalClusterWebhook{
+ AvailableAPIs: &discovery.AvailableAPIs{
+ Istio: false,
+ CertManager: false,
+ PrometheusOperator: false,
+ },
+ },
+ expectedErr: "Forbidden: Can't set JsonPatch when Spec is set on Deployment override",
+ },
}
for name, test := range tests {
|