diff --git a/api/v1beta1/temporalcluster_types.go b/api/v1beta1/temporalcluster_types.go
index a0677e64..16df18fd 100644
--- a/api/v1beta1/temporalcluster_types.go
+++ b/api/v1beta1/temporalcluster_types.go
@@ -557,6 +557,17 @@ type TemporalUISpec struct {
// Image defines the temporal ui docker image the instance should run.
// +optional
Image string `json:"image"`
+ // ImageTag overrides the tag used for the temporal ui image. When set, it
+ // is used instead of Version as the image tag.
+ // +optional
+ ImageTag string `json:"imageTag,omitempty"`
+ // ImageDigest pins the temporal ui image to an immutable digest
+ // (e.g. "sha256:abc..."). When set, it is appended to the image reference
+ // (repository:tag@digest) so the running image cannot change behind a
+ // mutable tag.
+ // +kubebuilder:validation:Pattern=`^sha256:[a-fA-F0-9]{64}$`
+ // +optional
+ ImageDigest string `json:"imageDigest,omitempty"`
// Number of desired replicas for the ui. Default to 1.
// +kubebuilder:validation:Minimum=1
// +optional
@@ -589,6 +600,17 @@ type TemporalAdminToolsSpec struct {
// Version defines the temporal admin tools version the instance should run.
// +optional
Version string `json:"version"`
+ // ImageTag overrides the tag used for the temporal admin tools image. When
+ // set, it is used instead of Version as the image tag.
+ // +optional
+ ImageTag string `json:"imageTag,omitempty"`
+ // ImageDigest pins the temporal admin tools image to an immutable digest
+ // (e.g. "sha256:abc..."). When set, it is appended to the image reference
+ // (repository:tag@digest) so the running image cannot change behind a
+ // mutable tag.
+ // +kubebuilder:validation:Pattern=`^sha256:[a-fA-F0-9]{64}$`
+ // +optional
+ ImageDigest string `json:"imageDigest,omitempty"`
// Compute Resources required by the ui.
// More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
// +optional
@@ -1015,6 +1037,18 @@ type TemporalClusterSpec struct {
// This version impacts the underlying persistence schemas versions.
// +optional
Version *version.Version `json:"version"`
+ // ImageTag overrides the tag used for the temporal server image. When set,
+ // it is used instead of Version as the image tag. Version still drives the
+ // persistence schema version regardless of this override.
+ // +optional
+ ImageTag string `json:"imageTag,omitempty"`
+ // ImageDigest pins the temporal server image to an immutable digest
+ // (e.g. "sha256:abc..."). When set, it is appended to the image reference
+ // (repository:tag@digest) so the running image cannot change behind a
+ // mutable tag.
+ // +kubebuilder:validation:Pattern=`^sha256:[a-fA-F0-9]{64}$`
+ // +optional
+ ImageDigest string `json:"imageDigest,omitempty"`
// Log defines temporal cluster's logger configuration.
// +optional
Log *LogSpec `json:"log,omitempty"`
diff --git a/config/crd/bases/temporal.io_temporalclusters.yaml b/config/crd/bases/temporal.io_temporalclusters.yaml
index 7eebc60f..32d2f61e 100644
--- a/config/crd/bases/temporal.io_temporalclusters.yaml
+++ b/config/crd/bases/temporal.io_temporalclusters.yaml
@@ -58,6 +58,19 @@ spec:
image:
description: Image defines the temporal admin tools docker image the instance should run.
type: string
+ imageDigest:
+ description: |-
+ ImageDigest pins the temporal admin tools image to an immutable digest
+ (e.g. "sha256:abc..."). When set, it is appended to the image reference
+ (repository:tag@digest) so the running image cannot change behind a
+ mutable tag.
+ pattern: ^sha256:[a-fA-F0-9]{64}$
+ type: string
+ imageTag:
+ description: |-
+ ImageTag overrides the tag used for the temporal admin tools image. When
+ set, it is used instead of Version as the image tag.
+ type: string
overrides:
description: Overrides adds some overrides to the resources deployed for the ui.
properties:
@@ -450,6 +463,14 @@ spec:
image:
description: Image defines the temporal server docker image the cluster should use for each services.
type: string
+ imageDigest:
+ description: |-
+ ImageDigest pins the temporal server image to an immutable digest
+ (e.g. "sha256:abc..."). When set, it is appended to the image reference
+ (repository:tag@digest) so the running image cannot change behind a
+ mutable tag.
+ pattern: ^sha256:[a-fA-F0-9]{64}$
+ type: string
imagePullSecrets:
description: |-
An optional list of references to secrets in the same namespace
@@ -471,6 +492,12 @@ spec:
type: object
x-kubernetes-map-type: atomic
type: array
+ imageTag:
+ description: |-
+ ImageTag overrides the tag used for the temporal server image. When set,
+ it is used instead of Version as the image tag. Version still drives the
+ persistence schema version regardless of this override.
+ type: string
jobInitContainers:
description: JobInitContainers adds a list of init containers to the setup's jobs.
items:
@@ -3876,6 +3903,19 @@ spec:
image:
description: Image defines the temporal ui docker image the instance should run.
type: string
+ imageDigest:
+ description: |-
+ ImageDigest pins the temporal ui image to an immutable digest
+ (e.g. "sha256:abc..."). When set, it is appended to the image reference
+ (repository:tag@digest) so the running image cannot change behind a
+ mutable tag.
+ pattern: ^sha256:[a-fA-F0-9]{64}$
+ type: string
+ imageTag:
+ description: |-
+ ImageTag overrides the tag used for the temporal ui image. When set, it
+ is used instead of Version as the image tag.
+ type: string
ingress:
description: |-
Ingress is an optional ingress configuration for the UI.
diff --git a/docs/api/v1beta1.md b/docs/api/v1beta1.md
index cec32b63..bc88ed62 100644
--- a/docs/api/v1beta1.md
+++ b/docs/api/v1beta1.md
@@ -89,6 +89,35 @@ This version impacts the underlying persistence schemas versions.
+imageTag
+
+string
+
+ |
+
+(Optional)
+ ImageTag overrides the tag used for the temporal server image. When set,
+it is used instead of Version as the image tag. Version still drives the
+persistence schema version regardless of this override.
+ |
+
+
+
+imageDigest
+
+string
+
+ |
+
+(Optional)
+ ImageDigest pins the temporal server image to an immutable digest
+(e.g. “sha256:abc…”). When set, it is appended to the image reference
+(repository:tag@digest) so the running image cannot change behind a
+mutable tag.
+ |
+
+
+
log
@@ -4612,6 +4641,34 @@ string
|
+imageTag
+
+string
+
+ |
+
+(Optional)
+ ImageTag overrides the tag used for the temporal admin tools image. When
+set, it is used instead of Version as the image tag.
+ |
+
+
+
+imageDigest
+
+string
+
+ |
+
+(Optional)
+ ImageDigest pins the temporal admin tools image to an immutable digest
+(e.g. “sha256:abc…”). When set, it is appended to the image reference
+(repository:tag@digest) so the running image cannot change behind a
+mutable tag.
+ |
+
+
+
resources
@@ -4838,6 +4895,35 @@ This version impacts the underlying persistence schemas versions.
|
+imageTag
+
+string
+
+ |
+
+(Optional)
+ ImageTag overrides the tag used for the temporal server image. When set,
+it is used instead of Version as the image tag. Version still drives the
+persistence schema version regardless of this override.
+ |
+
+
+
+imageDigest
+
+string
+
+ |
+
+(Optional)
+ ImageDigest pins the temporal server image to an immutable digest
+(e.g. “sha256:abc…”). When set, it is appended to the image reference
+(repository:tag@digest) so the running image cannot change behind a
+mutable tag.
+ |
+
+
+
log
@@ -5297,6 +5383,32 @@ TemporalNamespaceArchivalSpec
If not set, the default cluster configuration is used.
|
+
+
+customSearchAttributes
+
+map[string]string
+
+ |
+
+(Optional)
+ CustomSearchAttributes is an optional mapping of custom search attribute names to types.
+Supported types: Text, Keyword, Int, Double, Bool, DateTime, KeywordList.
+ |
+
+
+
+allowSearchAttributeDeletion
+
+bool
+
+ |
+
+(Optional)
+ AllowSearchAttributeDeletion makes the controller remove custom search attributes
+from the Temporal server if they are not present in the spec.
+ |
+
@@ -5521,6 +5633,32 @@ TemporalNamespaceArchivalSpec
If not set, the default cluster configuration is used.
+
+
+customSearchAttributes
+
+map[string]string
+
+ |
+
+(Optional)
+ CustomSearchAttributes is an optional mapping of custom search attribute names to types.
+Supported types: Text, Keyword, Int, Double, Bool, DateTime, KeywordList.
+ |
+
+
+
+allowSearchAttributeDeletion
+
+bool
+
+ |
+
+(Optional)
+ AllowSearchAttributeDeletion makes the controller remove custom search attributes
+from the Temporal server if they are not present in the spec.
+ |
+
@@ -6082,6 +6220,34 @@ string
+imageTag
+
+string
+
+ |
+
+(Optional)
+ ImageTag overrides the tag used for the temporal ui image. When set, it
+is used instead of Version as the image tag.
+ |
+
+
+
+imageDigest
+
+string
+
+ |
+
+(Optional)
+ ImageDigest pins the temporal ui image to an immutable digest
+(e.g. “sha256:abc…”). When set, it is appended to the image reference
+(repository:tag@digest) so the running image cannot change behind a
+mutable tag.
+ |
+
+
+
replicas
int32
diff --git a/docs/features/image-pinning.md b/docs/features/image-pinning.md
new file mode 100644
index 00000000..04de9974
--- /dev/null
+++ b/docs/features/image-pinning.md
@@ -0,0 +1,68 @@
+# Pinning image tags and digests
+
+By default the operator derives every container image tag from the configured
+`version`: the server, web UI and admin tools images are all deployed as
+`:`. This is convenient, but it means:
+
+- the deployed tag is coupled to `version`, even though `version` also drives
+ the persistence schema migrations, and
+- a mutable tag can be repointed upstream, so the running image can change
+ without any change to the `TemporalCluster` resource.
+
+Each component (`spec`, `spec.ui`, `spec.admintools`) therefore exposes two
+optional fields:
+
+- **`imageTag`** — overrides the tag used for the image. When set, it is used
+ instead of `version`. For the server, `version` still drives the persistence
+ schema version regardless of this override — only the deployed image tag
+ changes. This is useful when an image is published with a tag that differs
+ from the Temporal version (for example admin-tools images).
+- **`imageDigest`** — pins the image to an immutable digest (`sha256:...`). When
+ set, it is appended to the reference as `:@`. The tag is
+ kept for readability while the digest guarantees the running image cannot
+ change behind a mutable tag. This is recommended for security-sensitive or
+ compliance-driven deployments (e.g. supply-chain / image-provenance
+ requirements).
+
+Both fields are optional and can be combined. The resulting image reference is:
+
+| `imageTag` | `imageDigest` | Resulting reference |
+| ---------- | ------------- | ---------------------------- |
+| unset | unset | `image:version` |
+| set | unset | `image:imageTag` |
+| unset | set | `image:version@digest` |
+| set | set | `image:imageTag@digest` |
+
+## Example: pin the server and UI to a digest
+
+```yaml
+apiVersion: temporal.io/v1beta1
+kind: TemporalCluster
+metadata:
+ name: prod
+spec:
+ version: 1.28.2
+ image: temporalio/server
+ imageDigest: sha256:459aa877ef82c9a7f0ce688f2451ec8e576142c4efefecb2988d2f5f8b12699e
+ ui:
+ enabled: true
+ version: 2.44.1
+ image: temporalio/ui
+ imageDigest: sha256:0000000000000000000000000000000000000000000000000000000000000000
+```
+
+## Example: override the admin-tools tag
+
+```yaml
+apiVersion: temporal.io/v1beta1
+kind: TemporalCluster
+metadata:
+ name: prod
+spec:
+ version: 1.28.2
+ admintools:
+ enabled: true
+ image: temporalio/admin-tools
+ # the admin-tools image for this version is published under a different tag
+ imageTag: 1.28.2-tctl-1.18.1-cli-0.13.2
+```
diff --git a/internal/resource/admintools/deployment_builder.go b/internal/resource/admintools/deployment_builder.go
index 7bec4487..378e32a4 100644
--- a/internal/resource/admintools/deployment_builder.go
+++ b/internal/resource/admintools/deployment_builder.go
@@ -134,7 +134,7 @@ func (b *DeploymentBuilder) Update(object client.Object) error {
Containers: []corev1.Container{
{
Name: "admintools",
- Image: fmt.Sprintf("%s:%s", b.instance.Spec.AdminTools.Image, b.instance.Spec.AdminTools.Version),
+ Image: meta.ContainerImage(b.instance.Spec.AdminTools.Image, b.instance.Spec.AdminTools.Version, b.instance.Spec.AdminTools.ImageTag, b.instance.Spec.AdminTools.ImageDigest),
ImagePullPolicy: corev1.PullIfNotPresent,
TerminationMessagePath: corev1.TerminationMessagePathDefault,
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
diff --git a/internal/resource/base/deployment_builder.go b/internal/resource/base/deployment_builder.go
index 03a7bafe..a7fc7736 100644
--- a/internal/resource/base/deployment_builder.go
+++ b/internal/resource/base/deployment_builder.go
@@ -349,7 +349,7 @@ func (b *DeploymentBuilder) Update(object client.Object) error {
Containers: []corev1.Container{
{
Name: "service", // name "service" is here to simplify overrides
- Image: fmt.Sprintf("%s:%s", b.instance.Spec.Image, b.instance.Spec.Version),
+ Image: meta.ContainerImage(b.instance.Spec.Image, fmt.Sprintf("%s", b.instance.Spec.Version), b.instance.Spec.ImageTag, b.instance.Spec.ImageDigest),
ImagePullPolicy: corev1.PullIfNotPresent,
Resources: b.service.Resources,
TerminationMessagePath: corev1.TerminationMessagePathDefault,
diff --git a/internal/resource/meta/image.go b/internal/resource/meta/image.go
new file mode 100644
index 00000000..7029f910
--- /dev/null
+++ b/internal/resource/meta/image.go
@@ -0,0 +1,45 @@
+// Licensed to Alexandre VILAIN under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Alexandre VILAIN licenses this file to you 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 meta
+
+import "fmt"
+
+// ContainerImage builds a container image reference from a repository and a
+// default tag, optionally overriding the tag and/or pinning the image to an
+// immutable digest. The result follows the OCI "repository:tag[@digest]" form:
+//
+// - repository:defaultTag when neither override is set
+// - repository:tagOverride when tagOverride is set (defaultTag ignored)
+// - repository:tag@digest when digest is set (the tag is kept for
+// readability while the digest guarantees immutability)
+//
+// The digest is expected to already carry its algorithm prefix (e.g.
+// "sha256:abc..."); CRD validation enforces the format on the API fields.
+func ContainerImage(repository, defaultTag, tagOverride, digest string) string {
+ tag := defaultTag
+ if tagOverride != "" {
+ tag = tagOverride
+ }
+
+ image := fmt.Sprintf("%s:%s", repository, tag)
+ if digest != "" {
+ image = fmt.Sprintf("%s@%s", image, digest)
+ }
+
+ return image
+}
diff --git a/internal/resource/meta/image_test.go b/internal/resource/meta/image_test.go
new file mode 100644
index 00000000..7a6a9d0a
--- /dev/null
+++ b/internal/resource/meta/image_test.go
@@ -0,0 +1,65 @@
+// Licensed to Alexandre VILAIN under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Alexandre VILAIN licenses this file to you 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 meta_test
+
+import (
+ "testing"
+
+ "github.com/alexandrevilain/temporal-operator/internal/resource/meta"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestContainerImage(t *testing.T) {
+ const (
+ repository = "temporalio/server"
+ digest = "sha256:459aa877ef82c9a7f0ce688f2451ec8e576142c4efefecb2988d2f5f8b12699e"
+ )
+
+ for name, tc := range map[string]struct {
+ defaultTag string
+ tagOverride string
+ digest string
+ expected string
+ }{
+ "default tag only": {
+ defaultTag: "1.28.2",
+ expected: "temporalio/server:1.28.2",
+ },
+ "tag override replaces default": {
+ defaultTag: "1.28.2",
+ tagOverride: "1.28.2-custom",
+ expected: "temporalio/server:1.28.2-custom",
+ },
+ "digest pins default tag": {
+ defaultTag: "1.28.2",
+ digest: digest,
+ expected: "temporalio/server:1.28.2@" + digest,
+ },
+ "tag override and digest combined": {
+ defaultTag: "1.28.2",
+ tagOverride: "1.28.2-custom",
+ digest: digest,
+ expected: "temporalio/server:1.28.2-custom@" + digest,
+ },
+ } {
+ t.Run(name, func(t *testing.T) {
+ got := meta.ContainerImage(repository, tc.defaultTag, tc.tagOverride, tc.digest)
+ assert.Equal(t, tc.expected, got)
+ })
+ }
+}
diff --git a/internal/resource/persistence/schema_setup_job_builder.go b/internal/resource/persistence/schema_setup_job_builder.go
index 1d504c41..ae6d9639 100644
--- a/internal/resource/persistence/schema_setup_job_builder.go
+++ b/internal/resource/persistence/schema_setup_job_builder.go
@@ -22,6 +22,7 @@ import (
"github.com/alexandrevilain/temporal-operator/api/v1beta1"
"github.com/alexandrevilain/temporal-operator/internal/metadata"
+ "github.com/alexandrevilain/temporal-operator/internal/resource/meta"
"github.com/alexandrevilain/temporal-operator/internal/resource/mtls/istio"
"github.com/alexandrevilain/temporal-operator/internal/resource/mtls/linkerd"
"github.com/alexandrevilain/temporal-operator/pkg/version"
@@ -124,7 +125,7 @@ func (b *SchemaJobBuilder) Build() client.Object {
Containers: []corev1.Container{
{
Name: "schema-script-runner",
- Image: fmt.Sprintf("%s:%s", b.instance.Spec.AdminTools.Image, version.DefaultAdminToolTag(b.instance.Spec.Version)),
+ Image: meta.ContainerImage(b.instance.Spec.AdminTools.Image, version.DefaultAdminToolTag(b.instance.Spec.Version), b.instance.Spec.AdminTools.ImageTag, b.instance.Spec.AdminTools.ImageDigest),
ImagePullPolicy: corev1.PullIfNotPresent,
Resources: b.instance.Spec.JobResources,
TerminationMessagePath: corev1.TerminationMessagePathDefault,
diff --git a/internal/resource/ui/deployment_builder.go b/internal/resource/ui/deployment_builder.go
index a5dbec42..3f144e7e 100644
--- a/internal/resource/ui/deployment_builder.go
+++ b/internal/resource/ui/deployment_builder.go
@@ -127,7 +127,7 @@ func (b *DeploymentBuilder) Update(object client.Object) error {
Containers: []corev1.Container{
{
Name: "ui",
- Image: fmt.Sprintf("%s:%s", b.instance.Spec.UI.Image, b.instance.Spec.UI.Version),
+ Image: meta.ContainerImage(b.instance.Spec.UI.Image, b.instance.Spec.UI.Version, b.instance.Spec.UI.ImageTag, b.instance.Spec.UI.ImageDigest),
ImagePullPolicy: corev1.PullIfNotPresent,
Resources: b.instance.Spec.UI.Resources,
TerminationMessagePath: corev1.TerminationMessagePathDefault,
diff --git a/mkdocs.yml b/mkdocs.yml
index d28a3f9a..fc64cb11 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -26,6 +26,7 @@ nav:
- Archival: features/archival.md
- Temporal UI: features/temporal-ui.md
- Admin Tools: features/admin-tools.md
+ - Image tag & digest: features/image-pinning.md
- mTLS:
- Using Cert-Manager: features/mtls/cert-manager.md
- Using Istio: features/mtls/istio.md
|