feat: queue-operator auto-registers Temporal namespace before creating TemporalWorker

A Queue's temporal.io/namespace label was trusted as-is -- if the referenced
Temporal namespace was never registered (or typo'd), the failure only
surfaced as a worker pod silently polling a namespace that doesn't exist.
Now reconcileTemporalWorker calls RegisterNamespace (idempotent, ignores
AlreadyExists) via a direct WorkflowService gRPC client before creating the
TemporalWorker, so namespace and worker always come into existence together.

Also grant queue-operator's ClusterRole create/delete on temporalworkers
(previously missing, causing forbidden errors on the create-then-delete path).
This commit is contained in:
Story Crater Bot
2026-07-13 13:02:40 -07:00
parent a584fb4462
commit 52b86ab8e8
10 changed files with 299 additions and 15 deletions
+55
View File
@@ -0,0 +1,55 @@
// Package temporal wraps the narrow slice of Temporal's WorkflowService that
// queue-operator needs (namespace registration) directly over gRPC, instead
// of pulling in the full Temporal Go SDK for one RPC.
package temporal
import (
"context"
"fmt"
"time"
"go.temporal.io/api/workflowservice/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/durationpb"
)
// defaultRetentionPeriod is applied to namespaces queue-operator registers on
// a Queue's behalf. Namespaces created deliberately by an operator (e.g. via
// the temporal CLI) can still override this by re-registering with different
// settings — RegisterNamespace on an existing namespace is a no-op here.
const defaultRetentionPeriod = 72 * time.Hour
// Client wraps a Temporal frontend's WorkflowService.
type Client struct {
svc workflowservice.WorkflowServiceClient
}
// NewClient dials a Temporal frontend at address (e.g.
// "temporal-frontend.temporal.svc.cluster.local:7233"). The connection is
// plaintext, matching how TemporalWorkerReconciler's worker pods already
// talk to the same frontend (see temporal_worker_controller.go).
func NewClient(address string) (*Client, error) {
conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, fmt.Errorf("dial temporal frontend %s: %w", address, err)
}
return &Client{svc: workflowservice.NewWorkflowServiceClient(conn)}, nil
}
// RegisterNamespace registers namespace, treating AlreadyExists as success.
func (c *Client) RegisterNamespace(ctx context.Context, namespace string) error {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
_, err := c.svc.RegisterNamespace(ctx, &workflowservice.RegisterNamespaceRequest{
Namespace: namespace,
WorkflowExecutionRetentionPeriod: durationpb.New(defaultRetentionPeriod),
})
if err != nil && status.Code(err) != codes.AlreadyExists {
return fmt.Errorf("register temporal namespace %s: %w", namespace, err)
}
return nil
}