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
8 changes: 4 additions & 4 deletions api/v1alpha1/worker_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,24 @@ type WorkerOptions struct {
// The Temporal namespace for the worker to connect to.
// +kubebuilder:validation:MinLength=1
TemporalNamespace string `json:"temporalNamespace"`
// CustomBuildID optionally overrides the auto-generated build ID for this worker deployment.
// UnsafeCustomBuildID optionally overrides the auto-generated build ID for this worker deployment.
// When set, the controller uses this value instead of computing a build ID from the
// pod template hash. This enables rolling updates for non-workflow code changes
// (bug fixes, config changes) while preserving the same build ID.
//
// WARNING: Using a custom build ID requires careful management. If workflow code changes
// but CustomBuildID stays the same, pinned workflows may execute on workers running incompatible
// but UnsafeCustomBuildID stays the same, pinned workflows may execute on workers running incompatible
// code. Only use this when you have a reliable way to detect changes in your workflow
// definitions (e.g., hashing workflow source files in CI/CD).
//
// When the CustomBuildID is stable but pod template spec changes, the controller triggers
// When the UnsafeCustomBuildID is stable but pod template spec changes, the controller triggers
// a rolling update instead of creating a new deployment version. The controller uses
// a hash of the user-provided pod template spec to detect ANY changes, including
// container images, env vars, commands, volumes, resources, and all other fields.
// +optional
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:Pattern=`^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$`
CustomBuildID string `json:"customBuildID,omitempty"`
UnsafeCustomBuildID string `json:"unsafeCustomBuildID,omitempty"`
}

// TemporalWorkerDeploymentSpec defines the desired state of TemporalWorkerDeployment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3950,7 +3950,7 @@ spec:
required:
- name
type: object
customBuildID:
unsafeCustomBuildID:
maxLength: 63
pattern: ^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$
type: string
Expand Down
4 changes: 2 additions & 2 deletions internal/k8s/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ func NewObjectRef(obj client.Object) *corev1.ObjectReference {
}

func ComputeBuildID(w *temporaliov1alpha1.TemporalWorkerDeployment) string {
// Check for user-provided build ID in spec.workerOptions.customBuildID
if override := w.Spec.WorkerOptions.CustomBuildID; override != "" {
// Check for user-provided build ID in spec.workerOptions.unsafeCustomBuildID
if override := w.Spec.WorkerOptions.UnsafeCustomBuildID; override != "" {
cleaned := cleanBuildID(override)
if cleaned != "" {
return TruncateString(cleaned, MaxBuildIdLen)
Expand Down
14 changes: 7 additions & 7 deletions internal/k8s/deployments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ func TestGenerateBuildID(t *testing.T) {
name: "spec buildID override",
generateInputs: func() (*temporaliov1alpha1.TemporalWorkerDeployment, *temporaliov1alpha1.TemporalWorkerDeployment) {
twd := testhelpers.MakeTWDWithImage("", "", "some-image")
twd.Spec.WorkerOptions.CustomBuildID = "manual-override-v1"
twd.Spec.WorkerOptions.UnsafeCustomBuildID = "manual-override-v1"
return twd, nil
},
expectedPrefix: "manual-override-v1",
Expand All @@ -369,10 +369,10 @@ func TestGenerateBuildID(t *testing.T) {
generateInputs: func() (*temporaliov1alpha1.TemporalWorkerDeployment, *temporaliov1alpha1.TemporalWorkerDeployment) {
// Two TWDs with DIFFERENT images but SAME buildID
twd1 := testhelpers.MakeTWDWithImage("", "", "image-v1")
twd1.Spec.WorkerOptions.CustomBuildID = "stable-id"
twd1.Spec.WorkerOptions.UnsafeCustomBuildID = "stable-id"

twd2 := testhelpers.MakeTWDWithImage("", "", "image-v2")
twd2.Spec.WorkerOptions.CustomBuildID = "stable-id"
twd2.Spec.WorkerOptions.UnsafeCustomBuildID = "stable-id"
return twd1, twd2
},
expectedPrefix: "stable-id",
Expand All @@ -385,7 +385,7 @@ func TestGenerateBuildID(t *testing.T) {
// 72 char buildID - should be truncated to 63
longBuildID := "this-is-a-very-long-build-id-value-that-exceeds-63-characters-limit"
twd := testhelpers.MakeTWDWithImage("", "", "some-image")
twd.Spec.WorkerOptions.CustomBuildID = longBuildID
twd.Spec.WorkerOptions.UnsafeCustomBuildID = longBuildID
return twd, nil
},
expectedPrefix: "this-is-a-very-long-build-id-value-that-exceeds-63-characters-l",
Expand All @@ -396,7 +396,7 @@ func TestGenerateBuildID(t *testing.T) {
name: "spec buildID override with empty value falls back to hash",
generateInputs: func() (*temporaliov1alpha1.TemporalWorkerDeployment, *temporaliov1alpha1.TemporalWorkerDeployment) {
twd := testhelpers.MakeTWDWithImage("", "", "fallback-image")
twd.Spec.WorkerOptions.CustomBuildID = "" // empty customBuildID
twd.Spec.WorkerOptions.UnsafeCustomBuildID = "" // empty UnsafeCustomBuildID
return twd, nil
},
expectedPrefix: "fallback-image", // Falls back to image-based build ID
Expand All @@ -407,7 +407,7 @@ func TestGenerateBuildID(t *testing.T) {
name: "spec buildID override with only invalid chars falls back to hash",
generateInputs: func() (*temporaliov1alpha1.TemporalWorkerDeployment, *temporaliov1alpha1.TemporalWorkerDeployment) {
twd := testhelpers.MakeTWDWithImage("", "", "fallback-image2")
twd.Spec.WorkerOptions.CustomBuildID = "###$$$%%%" // all invalid chars
twd.Spec.WorkerOptions.UnsafeCustomBuildID = "###$$$%%%" // all invalid chars
return twd, nil
},
expectedPrefix: "fallback-image2", // Falls back to image-based build ID
Expand All @@ -418,7 +418,7 @@ func TestGenerateBuildID(t *testing.T) {
name: "spec buildID override trims leading and trailing separators",
generateInputs: func() (*temporaliov1alpha1.TemporalWorkerDeployment, *temporaliov1alpha1.TemporalWorkerDeployment) {
twd := testhelpers.MakeTWDWithImage("", "", "some-image")
twd.Spec.WorkerOptions.CustomBuildID = "---my-build-id---" // leading/trailing dashes
twd.Spec.WorkerOptions.UnsafeCustomBuildID = "---my-build-id---" // leading/trailing dashes
return twd, nil
},
expectedPrefix: "my-build-id", // dashes trimmed
Expand Down
4 changes: 2 additions & 2 deletions internal/planner/planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@ func checkAndUpdateDeploymentPodTemplateSpec(
return nil
}

// Only check for drift when customBuildID is explicitly set by the user.
// Only check for drift when UnsafeCustomBuildID is explicitly set by the user.
// If buildID is auto-generated, any spec change would generate a new buildID anyway.
if spec.WorkerOptions.CustomBuildID == "" {
if spec.WorkerOptions.UnsafeCustomBuildID == "" {
return nil
}

Expand Down
26 changes: 13 additions & 13 deletions internal/planner/planner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2229,8 +2229,8 @@ func TestCheckAndUpdateDeploymentPodTemplateSpec(t *testing.T) {
},
},
WorkerOptions: temporaliov1alpha1.WorkerOptions{
TemporalNamespace: "test-namespace",
CustomBuildID: "stable-build-id",
TemporalNamespace: "test-namespace",
UnsafeCustomBuildID: "stable-build-id",
},
},
connection: createDefaultConnectionSpec(),
Expand All @@ -2250,8 +2250,8 @@ func TestCheckAndUpdateDeploymentPodTemplateSpec(t *testing.T) {
},
},
WorkerOptions: temporaliov1alpha1.WorkerOptions{
TemporalNamespace: "test-namespace",
CustomBuildID: "stable-build-id",
TemporalNamespace: "test-namespace",
UnsafeCustomBuildID: "stable-build-id",
},
},
connection: createDefaultConnectionSpec(),
Expand All @@ -2273,8 +2273,8 @@ func TestCheckAndUpdateDeploymentPodTemplateSpec(t *testing.T) {
},
},
WorkerOptions: temporaliov1alpha1.WorkerOptions{
TemporalNamespace: "test-namespace",
CustomBuildID: "stable-build-id",
TemporalNamespace: "test-namespace",
UnsafeCustomBuildID: "stable-build-id",
},
},
connection: createDefaultConnectionSpec(),
Expand All @@ -2301,8 +2301,8 @@ func TestCheckAndUpdateDeploymentPodTemplateSpec(t *testing.T) {
},
},
WorkerOptions: temporaliov1alpha1.WorkerOptions{
TemporalNamespace: "test-namespace",
CustomBuildID: "stable-build-id",
TemporalNamespace: "test-namespace",
UnsafeCustomBuildID: "stable-build-id",
},
},
connection: createDefaultConnectionSpec(),
Expand All @@ -2323,8 +2323,8 @@ func TestCheckAndUpdateDeploymentPodTemplateSpec(t *testing.T) {
},
},
WorkerOptions: temporaliov1alpha1.WorkerOptions{
TemporalNamespace: "test-namespace",
CustomBuildID: "stable-build-id",
TemporalNamespace: "test-namespace",
UnsafeCustomBuildID: "stable-build-id",
},
},
connection: createDefaultConnectionSpec(),
Expand Down Expand Up @@ -2619,7 +2619,7 @@ func createDefaultWorkerSpec() *temporaliov1alpha1.TemporalWorkerDeploymentSpec
}
}

// createWorkerSpecWithBuildID creates a worker spec with an explicit customBuildID set
// createWorkerSpecWithBuildID creates a worker spec with an explicit UnsafeCustomBuildID set
func createWorkerSpecWithBuildID(buildID string) *temporaliov1alpha1.TemporalWorkerDeploymentSpec {
return &temporaliov1alpha1.TemporalWorkerDeploymentSpec{
Replicas: int32Ptr(1),
Expand All @@ -2634,8 +2634,8 @@ func createWorkerSpecWithBuildID(buildID string) *temporaliov1alpha1.TemporalWor
},
},
WorkerOptions: temporaliov1alpha1.WorkerOptions{
TemporalNamespace: "test-namespace",
CustomBuildID: buildID,
TemporalNamespace: "test-namespace",
UnsafeCustomBuildID: buildID,
},
}
}
Expand Down
25 changes: 15 additions & 10 deletions internal/testhelpers/make.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,10 @@ func MakeTWDWithImage(name, namespace, imageName string) *temporaliov1alpha1.Tem
// MakeBuildId computes a build id based on the image and
// If no podSpec is provided, defaults to HelloWorldPodSpec with the given image name.
// If you provide your own podSpec, make sure to give the first container your desired image name if success is expected.
func MakeBuildId(twdName, imageName string, podSpec *corev1.PodTemplateSpec) string {
func MakeBuildId(twdName, imageName, unsafeCustomBuildID string, podSpec *corev1.PodTemplateSpec) string {
if unsafeCustomBuildID != "" {
return unsafeCustomBuildID
}
return k8s.ComputeBuildID(
ModifyObj(
MakeTWDWithName(twdName, ""),
Expand All @@ -135,12 +138,12 @@ func MakeTWDWithName(name, namespace string) *temporaliov1alpha1.TemporalWorkerD
return twd
}

func MakeCurrentVersion(namespace, twdName, imageName string, healthy, createDeployment bool) *temporaliov1alpha1.CurrentWorkerDeploymentVersion {
func MakeCurrentVersion(namespace, twdName, imageName, unsafeCustomBuildID string, healthy, createDeployment bool) *temporaliov1alpha1.CurrentWorkerDeploymentVersion {
if imageName == worker_versioning.UnversionedVersionId { // empty build id == nil current version == unversioned
return nil
}
ret := &temporaliov1alpha1.CurrentWorkerDeploymentVersion{
BaseWorkerDeploymentVersion: MakeBaseVersion(namespace, twdName, imageName, temporaliov1alpha1.VersionStatusCurrent, createDeployment, true),
BaseWorkerDeploymentVersion: MakeBaseVersion(namespace, twdName, imageName, unsafeCustomBuildID, temporaliov1alpha1.VersionStatusCurrent, createDeployment, true),
}

if healthy {
Expand All @@ -150,9 +153,9 @@ func MakeCurrentVersion(namespace, twdName, imageName string, healthy, createDep
return ret
}

func MakeTargetVersion(namespace, twdName, imageName string, status temporaliov1alpha1.VersionStatus, rampPercentage int32, healthy, createDeployment bool) temporaliov1alpha1.TargetWorkerDeploymentVersion {
func MakeTargetVersion(namespace, twdName, imageName, unsafeCustomBuildID string, status temporaliov1alpha1.VersionStatus, rampPercentage int32, healthy, createDeployment bool) temporaliov1alpha1.TargetWorkerDeploymentVersion {
ret := temporaliov1alpha1.TargetWorkerDeploymentVersion{
BaseWorkerDeploymentVersion: MakeBaseVersion(namespace, twdName, imageName, status, createDeployment, true),
BaseWorkerDeploymentVersion: MakeBaseVersion(namespace, twdName, imageName, unsafeCustomBuildID, status, createDeployment, true),
}

if rampPercentage >= 0 {
Expand All @@ -167,9 +170,9 @@ func MakeTargetVersion(namespace, twdName, imageName string, status temporaliov1
return ret
}

func MakeDeprecatedVersion(namespace, twdName, imageName string, status temporaliov1alpha1.VersionStatus, healthy, createDeployment, hasDeployment bool) *temporaliov1alpha1.DeprecatedWorkerDeploymentVersion {
func MakeDeprecatedVersion(namespace, twdName, imageName, unsafeCustomBuildID string, status temporaliov1alpha1.VersionStatus, healthy, createDeployment, hasDeployment bool) *temporaliov1alpha1.DeprecatedWorkerDeploymentVersion {
ret := &temporaliov1alpha1.DeprecatedWorkerDeploymentVersion{
BaseWorkerDeploymentVersion: MakeBaseVersion(namespace, twdName, imageName, status, createDeployment, hasDeployment),
BaseWorkerDeploymentVersion: MakeBaseVersion(namespace, twdName, imageName, unsafeCustomBuildID, status, createDeployment, hasDeployment),
}
if status == temporaliov1alpha1.VersionStatusDrained {
t := metav1.NewTime(time.Now())
Expand All @@ -183,9 +186,11 @@ func MakeDeprecatedVersion(namespace, twdName, imageName string, status temporal
return ret
}

func MakeBaseVersion(namespace, twdName, imageName string, status temporaliov1alpha1.VersionStatus, createDeployment, hasDeployment bool) temporaliov1alpha1.BaseWorkerDeploymentVersion {
func MakeBaseVersion(namespace, twdName, imageName, unsafeCustomBuildID string, status temporaliov1alpha1.VersionStatus, createDeployment, hasDeployment bool) temporaliov1alpha1.BaseWorkerDeploymentVersion {
buildID := MakeBuildId(twdName, imageName, unsafeCustomBuildID, nil)

ret := temporaliov1alpha1.BaseWorkerDeploymentVersion{
BuildID: MakeBuildId(twdName, imageName, nil),
BuildID: buildID,
Status: status,
HealthySince: nil,
TaskQueues: []temporaliov1alpha1.TaskQueue{
Expand All @@ -198,7 +203,7 @@ func MakeBaseVersion(namespace, twdName, imageName string, status temporaliov1al
Namespace: namespace,
Name: k8s.ComputeVersionedDeploymentName(
twdName,
MakeBuildId(twdName, imageName, nil),
buildID,
),
}
}
Expand Down
40 changes: 33 additions & 7 deletions internal/testhelpers/test_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ func (b *TemporalWorkerDeploymentBuilder) WithTargetTemplate(imageName string) *
return b
}

// WithUnsafeCustomBuildID sets the optional custom build id of the TWD, thus defining a stable target version separate from the hash of the pod spec.
func (b *TemporalWorkerDeploymentBuilder) WithUnsafeCustomBuildID(buildID string) *TemporalWorkerDeploymentBuilder {
b.twd.Spec.WorkerOptions.UnsafeCustomBuildID = buildID
return b
}

// WithTemporalConnection sets the temporal connection name
func (b *TemporalWorkerDeploymentBuilder) WithTemporalConnection(connectionName string) *TemporalWorkerDeploymentBuilder {
b.twd.Spec.WorkerOptions.TemporalConnectionRef = temporaliov1alpha1.TemporalConnectionReference{Name: connectionName}
Expand Down Expand Up @@ -161,7 +167,7 @@ func (sb *StatusBuilder) WithNamespace(k8sNamespace string) *StatusBuilder {
// WithCurrentVersion sets the current version in the status
func (sb *StatusBuilder) WithCurrentVersion(imageName string, healthy, createDeployment bool) *StatusBuilder {
sb.currentVersionBuilder = func(twdName string, namespace string) *temporaliov1alpha1.CurrentWorkerDeploymentVersion {
return MakeCurrentVersion(namespace, twdName, imageName, healthy, createDeployment)
return MakeCurrentVersion(namespace, twdName, imageName, "", healthy, createDeployment)
}
return sb
}
Expand All @@ -171,7 +177,18 @@ func (sb *StatusBuilder) WithCurrentVersion(imageName string, healthy, createDep
// Target Version is required.
func (sb *StatusBuilder) WithTargetVersion(imageName string, status temporaliov1alpha1.VersionStatus, rampPercentage int32, healthy bool, createDeployment bool) *StatusBuilder {
sb.targetVersionBuilder = func(twdName string, namespace string) temporaliov1alpha1.TargetWorkerDeploymentVersion {
return MakeTargetVersion(namespace, twdName, imageName, status, rampPercentage, healthy, createDeployment)
return MakeTargetVersion(namespace, twdName, imageName, "", status, rampPercentage, healthy, createDeployment)
}
return sb
}

// WithTargetVersionWithCustomBuild sets the target version in the status with a custom build id not based on the pod spec.
// Set createDeployment to true if the test runner should create the Deployment, or false if you expect the controller to create it..
// Target Version is required.
func (sb *StatusBuilder) WithTargetVersionWithCustomBuild(imageName, unsafeCustomBuildID string, status temporaliov1alpha1.VersionStatus, rampPercentage int32, healthy bool, createDeployment bool) *StatusBuilder {
sb.targetVersionBuilder = func(twdName string, namespace string) temporaliov1alpha1.TargetWorkerDeploymentVersion {
tv := MakeTargetVersion(namespace, twdName, imageName, unsafeCustomBuildID, status, rampPercentage, healthy, createDeployment)
return tv
}
return sb
}
Expand All @@ -184,7 +201,7 @@ func (sb *StatusBuilder) WithDeprecatedVersions(infos ...DeprecatedVersionInfo)
sb.deprecatedVersionsBuilder = func(twdName string, namespace string) []*temporaliov1alpha1.DeprecatedWorkerDeploymentVersion {
ret := make([]*temporaliov1alpha1.DeprecatedWorkerDeploymentVersion, len(infos))
for i, info := range infos {
ret[i] = MakeDeprecatedVersion(namespace, twdName, info.image, info.status, info.healthy, info.createDeployment, info.hasDeployment)
ret[i] = MakeDeprecatedVersion(namespace, twdName, info.image, "", info.status, info.healthy, info.createDeployment, info.hasDeployment)

}
return ret
Expand Down Expand Up @@ -368,8 +385,9 @@ func NewDeprecatedVersionInfo(imageName string, status temporaliov1alpha1.Versio
// DeploymentInfo defines the necessary information about a Deployment, so that tests can
// recreate and validate state that is not visible in the TemporalWorkerDeployment status
type DeploymentInfo struct {
image string
replicas int32
image string
replicas int32
unsafeCustomBuildID string
}

func NewDeploymentInfo(imageName string, replicas int32) DeploymentInfo {
Expand All @@ -379,6 +397,14 @@ func NewDeploymentInfo(imageName string, replicas int32) DeploymentInfo {
}
}

func NewDeploymentInfoWithUnsafeCustomBuildID(imageName, unsafeCustomBuildID string, replicas int32) DeploymentInfo {
return DeploymentInfo{
image: imageName,
replicas: replicas,
unsafeCustomBuildID: unsafeCustomBuildID,
}
}

// WithExistingDeployments adds info to create existing deployments, indexed by the build id that the given image would result in
func (tcb *TestCaseBuilder) WithExistingDeployments(existingDeploymentInfos ...DeploymentInfo) *TestCaseBuilder {
tcb.existingDeploymentInfos = existingDeploymentInfos
Expand Down Expand Up @@ -418,12 +444,12 @@ func (tcb *TestCaseBuilder) Build() TestCase {
expectedDeploymentReplicas: make(map[string]int32),
}
for _, info := range tcb.existingDeploymentInfos {
buildId := MakeBuildId(tcb.name, info.image, nil)
buildId := MakeBuildId(tcb.name, info.image, info.unsafeCustomBuildID, nil)
ret.existingDeploymentReplicas[buildId] = info.replicas
ret.existingDeploymentImages[buildId] = info.image
}
for _, info := range tcb.expectedDeploymentInfos {
buildId := MakeBuildId(tcb.name, info.image, nil)
buildId := MakeBuildId(tcb.name, info.image, info.unsafeCustomBuildID, nil)
ret.expectedDeploymentReplicas[buildId] = info.replicas
}
ret.twd.Spec.Template = SetTaskQueue(ret.twd.Spec.Template, tcb.name)
Expand Down
Loading