Conversation
e1fda24 to
f49a562
Compare
Replace the deprecated mgr.GetEventRecorderFor (returns record.EventRecorder) with mgr.GetEventRecorder (returns events.EventRecorder). The new API's Eventf signature adds 'related' and 'action' parameters; 'related' is nil for all call sites and 'action' reuses the reason string (already UpperCamelCase). Test fake recorder switched from record.NewFakeRecorder to events.NewFakeRecorder.
f49a562 to
7361be9
Compare
jaypipes
left a comment
There was a problem hiding this comment.
Phew, this is a lot to digest :) Some suggestions inline for you to take a look at. Will do a more in-depth review later on.
| // IsClusterScoped reports whether this kind is cluster-scoped. Used to | ||
| // scope the watch mapper: a namespaced kind lists WorkerDeployments in the | ||
| // connection's own namespace; a cluster-scoped kind lists across all. | ||
| IsClusterScoped() bool |
There was a problem hiding this comment.
I don't believe this interface method is necessary. It's possible to ask the discovery client whether a GroupKind is cluster-scoped or not:
func kindIsNamespaced(
disco discovery.CachedDiscoveryInterface, // See: https://github.com/kubernetes/client-go/blob/master/discovery/cached/disk/cached_discovery.go#L45
gvr schema.GroupVersionResource, // See: https://pkg.go.dev/k8s.io/apimachinery/pkg/runtime/schema#GroupVersionResource
) bool {
apiResources, err := disco.ServerResourcesForGroupVersion(
gvr.GroupVersion().String(),
)
if err != nil {
return false
}
for _, apiResource := range apiResources.APIResources {
if apiResource.Name == gvr.Resource {
return apiResource.Namespaced
}
}
return false
}| // this by delegating to Fingerprint(). A provider whose kind owns pod config | ||
| // out of band implements the mutation as a no-op but must still write the | ||
| // fingerprint annotation. | ||
| ApplyWorkerPodSpec(podSpec *corev1.PodSpec, annotations map[string]string, opts PodSpecApplyOpts) error |
There was a problem hiding this comment.
Do we need to pass the annotations map? How about just passing the fingerprint that we will set on the PodSpec?
| func (r *WorkerDeploymentReconciler) findTWDsUsingConnection(ctx context.Context, tc client.Object) []reconcile.Request { | ||
| return r.findTWDsUsingConnectionKind(connectionprovider.RefGroupKind(temporaliov1alpha1.ConnectionReference{Name: tc.GetName()}), false)(ctx, tc) | ||
| } | ||
|
|
||
| func (r *WorkerDeploymentReconciler) findTWDsUsingClusterConnection( | ||
| ctx context.Context, | ||
| cc client.Object, | ||
| ) []reconcile.Request { | ||
| return r.findTWDsUsingConnectionKind(connectionprovider.RefGroupKind(temporaliov1alpha1.ConnectionReference{ObjectRef: &corev1.TypedObjectReference{APIGroup: ptr(temporaliov1alpha1.GroupVersion.Group), Kind: "ClusterConnection", Name: cc.GetName()}}), true)(ctx, cc) | ||
| } | ||
|
|
||
| // getConnectionByRef returns the underlying object for ref via the provider | ||
| // registry, for tests that need the concrete object. | ||
| func (r *WorkerDeploymentReconciler) getConnectionByRef( | ||
| ctx context.Context, | ||
| ref temporaliov1alpha1.ConnectionReference, | ||
| namespace string, | ||
| ) (client.Object, error) { | ||
| prov, err := connectionprovider.LookupProvider(r.Providers, ref) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| connection, err := prov.Fetch(ctx, ref, namespace) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return connection.Object(), nil | ||
| } |
There was a problem hiding this comment.
Any reason these methods are defined on WorkerDeploymentReconciler in the clusterconnection_test.go file instead of a non-test file like the newly-added internal/controller/connection_helpers.go file?
| Namespace: twd.Namespace, | ||
| }, | ||
| }) | ||
| // findTWDsUsingConnectionKind returns a mapper that enqueues every WorkerDeployment |
There was a problem hiding this comment.
This should probably be moved to the new internal/controller/connection_helpers.go file along with the other connection-related helpers.
| sigs.k8s.io/yaml v1.6.0 // indirect | ||
| ) | ||
|
|
||
| replace github.com/temporalio/temporal-worker-controller => ../.. |
| // If an update is required, it rebuilds the deployment spec and returns a pointer to that Deployment. | ||
| // If no update is needed or the Deployment does not exist, it returns nil. | ||
| func checkAndUpdateDeploymentPodTemplateSpec( | ||
| ctx context.Context, |
There was a problem hiding this comment.
Any reason to add the context parameter here? I don't see it being used anywhere.
| continue | ||
| } | ||
| r.Recorder.Eventf(workerDeploy, corev1.EventTypeWarning, ReasonGateWorkflowFailed, | ||
| r.Recorder.Eventf(workerDeploy, nil, corev1.EventTypeWarning, ReasonGateWorkflowFailed, ReasonGateWorkflowFailed, |
There was a problem hiding this comment.
Hmmm.... I'm wondering how this code was previously compiling. It seems we were not calling the EventRecorder.Eventf interface method properly... there should have been a compile-time failure.
Summary
Related to issue #246
This is step 1 of making the worker controller usable as a library: it introduces a clean seam between the
WorkerDeploymentreconciler and the connection resources it references, so the OSS controller no longer hardcodes thetemporal.io/Connectionandtemporal.io/ClusterConnectionkinds.A wrapper binary can now register additional connection kinds at process start — without upstream CRD changes — by appending its own
ConnectionProviderto the startup-built slice. The reconciler resolves aConnectionReferenceto a registered provider, which supplies the referenced object (for finalizer management), a cached SDK client, and a fingerprint (for drift detection).What changed
New seam:
internal/controller/connectionproviderConnectionProvider— the group-kind-level factory. One instance is registered perGroupKindat startup; the default registry shipsConnectionandClusterConnection. ProvidesFetch,NewObject,IsClusterScoped, andGroupKind.ResolvedConnection— the per-reconcile handle bound to a fetched object. Carries the behavior for that connection:Object(),GetClient(),Fingerprint(),ApplyWorkerPodSpec(),Evict().*UnknownKindError→ReasonUnknownConnectionKind,*AuthError→ReasonAuthSecretInvalid,*DialError→ReasonTemporalClientCreationFailed.sameConnectionRef— so a namespacedConnection"foo" and a cluster-scoped connection "foo" are distinct.Default provider:
internal/controller/clientpool/default_provider.goDefaultProviderimplementsConnectionProviderfor the two built-in kinds, sharing aClientPool.defaultResolvedimplementsResolvedConnection; theConnectionSpecis opaque to the reconciler and never leaves this package.Reconciler wiring
WorkerDeploymentReconcilernow holds a startup-builtProviders []ConnectionProvider(never mutated) instead of a concreteClientPool.main.gowiresclientpool.NewDefaultProviders.connection_helpers.gocentralizes finalizer add/release logic, scoped correctly per kind.API
ConnectionReferencegains anObjectRef(full type info: apiGroup + kind + name) alongside the shorthandNameform. CEL validation enforces exactly-one-of andapiGroup == temporal.io; kind is intentionally not validated at admission — an unregistered kind surfaces asReasonUnknownConnectionKindon the next reconcile.Design notes
Fingerprint/ApplyWorkerPodSpecinvariant:ApplyWorkerPodSpecwritesFingerprint()into theconnection-spec-hashannotation, so the two cannot drift apart — the planner comparesFingerprint()against that annotation to decide whether an in-place pod-spec update is needed. The default provider guarantees this by delegating toFingerprint(); the contract is documented on both methods and locked by a test.PodSpecApplyOpts: the per-worker context (TemporalNamespace,WorkerDeploymentName,BuildID) is passed as a struct rather than positionalstrings, removing the risk of swapping indistinguishable arguments at call sites.connectionRefnever wedges deletion.Why "step 1"
The concrete
ConnectionSpecstill lives inapi/v1alpha1and the default provider is the only one shipped. This PR establishes the interface and the registration mechanism; subsequent steps will let a wrapper binary supply its own provider implementations and (eventually) its own connection CRDs without forking the controller.Test coverage
connectionprovider:LookupProvider(unknown/found/empty),RefGroupKind,RefName,UnknownKindError.Is.clientpool(default provider):Fetch(namespaced/cluster-scoped/not-found),GetClient(auth/dial/cache-hit),Fingerprintstability,ApplyWorkerPodSpec(inject + idempotent + annotation-equals-fingerprint),Evict,NewObject,IsClusterScoped,NewDefaultProviders.go build ./...,go vet, andgofmtare clean; affected package tests pass.Checklist
go build ./...gofmtclean