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
10 changes: 6 additions & 4 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
suite:
description: Which suite(s) to run
type: choice
options: [default, devserver, mtls, upgrade, opensearch, cassandra, multicluster, all]
options: [default, devserver, workflowrun, mtls, upgrade, opensearch, cassandra, multicluster, all]
default: default

permissions:
Expand All @@ -32,22 +32,24 @@ jobs:
run: |
postgres='{"temporal":"1.31.1","persistence":"postgres","suite":"postgres/lifecycle"}'
devserver='{"suite":"devserver"}'
workflowrun='{"suite":"workflowrun"}'
mtls='{"temporal":"1.31.1","persistence":"mtls","suite":"mtls"}'
upgrade='{"temporal":"1.30.0","persistence":"upgrade","suite":"upgrade"}'
opensearch='{"temporal":"1.31.1","persistence":"opensearch","suite":"opensearch"}'
cassandra='{"temporal":"1.31.1","persistence":"cassandra","suite":"cassandra"}'
multicluster='{"temporal":"1.31.1","persistence":"multicluster","suite":"multicluster"}'
if [ "$EVENT" = "schedule" ]; then
echo "combos=[$postgres,$devserver,$mtls,$upgrade,$opensearch,$cassandra,$multicluster]" >> "$GITHUB_OUTPUT"
echo "combos=[$postgres,$devserver,$workflowrun,$mtls,$upgrade,$opensearch,$cassandra,$multicluster]" >> "$GITHUB_OUTPUT"
elif [ "$EVENT" = "workflow_dispatch" ]; then
case "$SUITE" in
devserver) echo "combos=[$devserver]" >> "$GITHUB_OUTPUT" ;;
workflowrun) echo "combos=[$workflowrun]" >> "$GITHUB_OUTPUT" ;;
mtls) echo "combos=[$mtls]" >> "$GITHUB_OUTPUT" ;;
upgrade) echo "combos=[$upgrade]" >> "$GITHUB_OUTPUT" ;;
opensearch) echo "combos=[$opensearch]" >> "$GITHUB_OUTPUT" ;;
cassandra) echo "combos=[$cassandra]" >> "$GITHUB_OUTPUT" ;;
multicluster) echo "combos=[$multicluster]" >> "$GITHUB_OUTPUT" ;;
all) echo "combos=[$postgres,$devserver,$mtls,$upgrade,$opensearch,$cassandra,$multicluster]" >> "$GITHUB_OUTPUT" ;;
all) echo "combos=[$postgres,$devserver,$workflowrun,$mtls,$upgrade,$opensearch,$cassandra,$multicluster]" >> "$GITHUB_OUTPUT" ;;
*) echo "combos=[$postgres,$devserver]" >> "$GITHUB_OUTPUT" ;;
esac
else
Expand Down Expand Up @@ -119,7 +121,7 @@ jobs:
| grep -oE '1\.[0-9]+\.[0-9]+' | sort -u || true)
images=""
for v in $versions; do
if echo "${{ matrix.combo.suite }}" | grep -q "^devserver"; then
if echo "${{ matrix.combo.suite }}" | grep -qE "^(devserver|workflowrun)"; then
# The dev server's spec.version is a Temporal *server* version, but
# it runs the temporalio/temporal *CLI* image, which is versioned
# independently (its tags are CLI releases, e.g. 1.7.x, not server
Expand Down
11 changes: 11 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,15 @@ resources:
kind: TemporalClusterClient
path: github.com/bmorton/temporal-operator/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
domain: bmor10.com
group: temporal
kind: TemporalWorkflowRun
path: github.com/bmorton/temporal-operator/api/v1alpha1
version: v1alpha1
webhooks:
validation: true
webhookVersion: v1
version: "3"
10 changes: 10 additions & 0 deletions api/v1alpha1/conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,14 @@ const (
ReasonReplicationDrift = "ReplicationConfigDrift"
// ReasonActiveClusterInvalid indicates the active cluster selection is invalid.
ReasonActiveClusterInvalid = "ActiveClusterInvalid"
// ReasonWorkflowRunNotPermitted indicates the target's WorkflowRunPolicy denied the run.
ReasonWorkflowRunNotPermitted = "WorkflowRunNotPermitted"
// ReasonWorkflowRunning indicates the workflow is currently running.
ReasonWorkflowRunning = "WorkflowRunning"
// ReasonWorkflowFinished indicates the workflow reached a terminal state.
ReasonWorkflowFinished = "WorkflowFinished"
// ReasonClusterNotFound indicates the referenced Temporal target was not found.
ReasonClusterNotFound = "ClusterNotFound"
// ReasonClusterNotReady indicates the referenced Temporal target is not ready.
ReasonClusterNotReady = "ClusterNotReady"
)
5 changes: 5 additions & 0 deletions api/v1alpha1/temporalcluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ type TemporalClusterSpec struct {
// validating webhook as a safety measure.
// +optional
PreventDeletion bool `json:"preventDeletion,omitempty"`

// WorkflowRunPolicy gates operator-initiated TemporalWorkflowRun executions
// against this cluster. Absent means disabled (closed by default).
// +optional
WorkflowRunPolicy *WorkflowRunPolicy `json:"workflowRunPolicy,omitempty"`
}

// ServicesSpec configures each Temporal service plus shared overrides.
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/temporaldevserver_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ type TemporalDevServerSpec struct {
// Affinity applied to the dev server pod.
// +optional
Affinity *corev1.Affinity `json:"affinity,omitempty"`

// WorkflowRunPolicy gates operator-initiated TemporalWorkflowRun executions
// against this dev server. Absent means enabled with no allowlist.
// +optional
WorkflowRunPolicy *WorkflowRunPolicy `json:"workflowRunPolicy,omitempty"`
}

// DevServerUISpec controls the bundled Web UI.
Expand Down
154 changes: 154 additions & 0 deletions api/v1alpha1/temporalworkflowrun_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
Copyright 2026 Brian Morton.

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 v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// WorkflowRunPolicy is a per-target opt-in gate controlling operator-initiated
// workflow runs. It is declared on a TemporalCluster or TemporalDevServer by the
// cluster owner. Defaults differ per kind and are applied by the controller's
// target resolver, not by kubebuilder defaults, so an absent policy is
// meaningful: disabled for TemporalCluster, enabled (no allowlist) for
// TemporalDevServer.
type WorkflowRunPolicy struct {
// Enabled permits operator-initiated workflow runs against this target.
// +optional
Enabled bool `json:"enabled,omitempty"`
// AllowedNamespaces optionally restricts which Temporal namespaces runs may
// target. Empty means any namespace is allowed.
// +optional
AllowedNamespaces []string `json:"allowedNamespaces,omitempty"`
// AllowedTaskQueues optionally restricts which task queues runs may use.
// Empty means any task queue is allowed.
// +optional
AllowedTaskQueues []string `json:"allowedTaskQueues,omitempty"`
}

// TemporalWorkflowRunSpec defines the desired state of a one-off workflow run.
type TemporalWorkflowRunSpec struct {
// ClusterRef references the TemporalCluster or TemporalDevServer that runs
// the workflow. Resolved in the same Kubernetes namespace as this CR.
ClusterRef ClusterReference `json:"clusterRef"`

// Namespace is the Temporal namespace to start the workflow in.
Namespace string `json:"namespace"`

// Workflow describes the one-off workflow to start. Immutable after create.
Workflow StartWorkflowAction `json:"workflow"`

// TTLSecondsAfterFinished, when set, deletes this CR that many seconds after
// the workflow reaches a terminal state. Unset keeps the CR indefinitely.
// +optional
TTLSecondsAfterFinished *int32 `json:"ttlSecondsAfterFinished,omitempty"`

// CancellationPolicy controls what happens to a still-running workflow when
// this CR is deleted.
// +kubebuilder:validation:Enum=Abandon;Cancel;Terminate
// +kubebuilder:default=Abandon
// +optional
CancellationPolicy string `json:"cancellationPolicy,omitempty"`
}

// Cancellation policies applied to a still-running workflow when its
// TemporalWorkflowRun is deleted.
const (
// CancellationPolicyAbandon leaves the workflow running.
CancellationPolicyAbandon = "Abandon"
// CancellationPolicyCancel requests graceful cancellation of the workflow.
CancellationPolicyCancel = "Cancel"
// CancellationPolicyTerminate forcefully terminates the workflow.
CancellationPolicyTerminate = "Terminate"
)

// WorkflowRunFailure carries failure detail for non-success terminal states.
type WorkflowRunFailure struct {
// +optional
Message string `json:"message,omitempty"`
// +optional
Type string `json:"type,omitempty"`
}

// TemporalWorkflowRunStatus is the observed state of a workflow run.
type TemporalWorkflowRunStatus struct {
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// Phase is a friendly lifecycle summary: Pending, Running, Completed,
// Failed, Terminated, Canceled, TimedOut, or ContinuedAsNew.
// +optional
Phase string `json:"phase,omitempty"`
// +optional
WorkflowID string `json:"workflowID,omitempty"`
// +optional
RunID string `json:"runID,omitempty"`
// +optional
WorkflowType string `json:"workflowType,omitempty"`
// +optional
TaskQueue string `json:"taskQueue,omitempty"`
// +optional
StartTime *metav1.Time `json:"startTime,omitempty"`
// +optional
CloseTime *metav1.Time `json:"closeTime,omitempty"`
// CompletionTime is when the operator first observed a terminal state; it
// drives the TTL countdown.
// +optional
CompletionTime *metav1.Time `json:"completionTime,omitempty"`
// +optional
HistoryLength int64 `json:"historyLength,omitempty"`
// Failure carries the failure message/type for non-success terminal states.
// +optional
Failure *WorkflowRunFailure `json:"failure,omitempty"`
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Namespaced,shortName=twr
// +kubebuilder:subresource:status
// +kubebuilder:storageversion
// +kubebuilder:printcolumn:name="Cluster",type=string,JSONPath=`.spec.clusterRef.name`
// +kubebuilder:printcolumn:name="Namespace",type=string,JSONPath=`.spec.namespace`
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`

// TemporalWorkflowRun is the Schema for the temporalworkflowruns API.
type TemporalWorkflowRun struct {
metav1.TypeMeta `json:",inline"`
// +optional
metav1.ObjectMeta `json:"metadata,omitzero"`
// +required
Spec TemporalWorkflowRunSpec `json:"spec"`
// +optional
Status TemporalWorkflowRunStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// TemporalWorkflowRunList contains a list of TemporalWorkflowRun.
type TemporalWorkflowRunList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []TemporalWorkflowRun `json:"items"`
}

func init() {
registerType(&TemporalWorkflowRun{}, &TemporalWorkflowRunList{})
}
Loading
Loading