atecontroller: drop the ActorTemplate CRD controller

Nothing creates ActorTemplate CRDs anymore: demos, e2e fixtures, and
ate-setup create substrate ActorTemplate resources directly, and the
apiserver resolves templates from the store only. Delete the
CRD-to-substrate reconciler and its tests, and drop the corresponding
RBAC from the generated role.
This commit is contained in:
zoezhao
2026-08-31 17:45:53 -07:00
parent e502cf60e3
commit 8f6c1adb01
5 changed files with 9 additions and 472 deletions
@@ -1,213 +0,0 @@
// Copyright 2026 Google LLC
//
// 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 controllers
import (
"context"
"fmt"
"time"
"github.com/agent-substrate/substrate/internal/resources"
atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1"
"github.com/agent-substrate/substrate/pkg/proto/ateapipb"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
k8errors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const (
GoldenSnapshotCreationReason = "GoldenSnapshotCreation"
// goldenSnapshotWarmup is the default wall-clock delay between resuming
// the golden actor and taking its snapshot, used as a coarse "give the
// workload time to finish initializing" fallback for templates without
// a readiness probe. Templates whose containers all declare readyz skip
// this wait — ResumeActor only returns once readyz reports 200, so the
// workload is already initialized by the time we get here.
goldenSnapshotWarmup = 20 * time.Second
)
type ActorTemplateReconciler struct {
client.Client
Scheme *runtime.Scheme
AteClient ateapipb.ControlClient
}
//+kubebuilder:rbac:groups=ate.dev,resources=actortemplates,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=ate.dev,resources=actortemplates/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=ate.dev,resources=actortemplates/finalizers,verbs=update
//+kubebuilder:rbac:groups=ate.dev,resources=workerpools,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch
//+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch
//+kubebuilder:rbac:groups=discovery.k8s.io,resources=endpointslices,verbs=get;list;watch,namespace=ate-system
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
func (r *ActorTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// Fetch actor template
at := &atev1alpha1.ActorTemplate{}
if err := r.Get(ctx, req.NamespacedName, at); err != nil {
if k8errors.IsNotFound(err) {
return ctrl.Result{}, nil
}
return ctrl.Result{}, fmt.Errorf("failed to get actor template %q: %w", req.NamespacedName, err)
}
// Handle deletion
if !at.GetDeletionTimestamp().IsZero() {
return ctrl.Result{}, nil
}
switch at.Status.Phase {
case atev1alpha1.PhaseInitial:
actorName := string(at.UID)
// Golden actors live in the reserved ate-golden system atespace.
_, err := r.AteClient.CreateAtespace(ctx, &ateapipb.CreateAtespaceRequest{
Atespace: &ateapipb.Atespace{
Metadata: &ateapipb.ResourceMetadata{
Name: resources.GoldenActorAtespace,
},
},
})
if err != nil && status.Code(err) != codes.AlreadyExists {
return ctrl.Result{}, fmt.Errorf("while ensuring atespace %q: %w", resources.GoldenActorAtespace, err)
}
createReq := &ateapipb.CreateActorRequest{
Actor: &ateapipb.Actor{
Metadata: &ateapipb.ResourceMetadata{
Atespace: resources.GoldenActorAtespace,
Name: actorName,
},
ActorTemplate: &ateapipb.ObjectRef{
Atespace: at.ObjectMeta.Namespace,
Name: at.ObjectMeta.Name,
},
},
}
_, err = r.AteClient.CreateActor(ctx, createReq)
if err != nil && status.Code(err) != codes.AlreadyExists {
return ctrl.Result{}, fmt.Errorf("while creating golden actor: %w", err)
}
at.Status.Phase = atev1alpha1.PhaseResumeGoldenActor
at.Status.GoldenActorID = actorName
if err := r.Status().Update(ctx, at); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
case atev1alpha1.PhaseResumeGoldenActor:
// TODO(ateom): If resumption fails because the ateom or atelet is not
// quite ready, we can end up leaking a worker that thinks it's assigned
// to the golden actor. We should persist the golden actor ID first,
// then drive resume as a separate step.
// Resuming when the ActorTemplate has no golden snapshot results in the
// workload being freshly booted.
//
// TODO: Maybe this should go through a different RPC dedicated to
// booting an actor from scratch.
resumeReq := &ateapipb.ResumeActorRequest{
Actor: &ateapipb.ObjectRef{Atespace: resources.GoldenActorAtespace, Name: at.Status.GoldenActorID},
}
_, err := r.AteClient.ResumeActor(ctx, resumeReq)
if err != nil {
return ctrl.Result{}, fmt.Errorf("while resuming golden actor: %w", err)
}
at.Status.Phase = atev1alpha1.PhaseWaitGoldenActor
at.Status.TakeGoldenSnapshotAt = metav1.NewTime(time.Now().Add(goldenSnapshotWarmupFor(at)))
if err := r.Status().Update(ctx, at); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
case atev1alpha1.PhaseWaitGoldenActor:
// Wait until the snapshot time.
rem := time.Until(at.Status.TakeGoldenSnapshotAt.Time)
if rem >= 0 {
return ctrl.Result{RequeueAfter: rem}, nil
}
// TODO: Need to be more resilient --- if suspendactor tells us
// conflict, we should fetch the suspended actor and read the snapshot
// from it.
req := &ateapipb.SuspendActorRequest{
Actor: &ateapipb.ObjectRef{Atespace: resources.GoldenActorAtespace, Name: at.Status.GoldenActorID},
}
resp, err := r.AteClient.SuspendActor(ctx, req)
if err != nil {
return ctrl.Result{}, fmt.Errorf("while suspending golden actor: %w", err)
}
snapshot := resp.GetActor().GetStatus().GetLatestSnapshot()
if snapshot == nil {
return ctrl.Result{}, fmt.Errorf("suspending golden actor returned no ActorSnapshot")
}
// Transition to PhaseReady
at.Status.GoldenSnapshot = snapshot.GetName()
at.Status.Phase = atev1alpha1.PhaseReady
meta.SetStatusCondition(&at.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionTrue,
Reason: "Ready",
Message: "Actor template is ready for use",
})
if err := r.Status().Update(ctx, at); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
case atev1alpha1.PhaseReady:
return ctrl.Result{}, nil
default:
return ctrl.Result{}, fmt.Errorf("unrecognized phase %q", at.Status.Phase)
}
}
// SetupWithManager sets up the controller with the Manager.
func (r *ActorTemplateReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).For(&atev1alpha1.ActorTemplate{}).Complete(r)
}
// goldenSnapshotWarmupFor returns 0 when every container in the template has
// a readyz probe (so ResumeActor already blocked until the workload reported
// 200), and the default warmup otherwise. A template with no containers
// keeps the default — there is nothing to gate on.
func goldenSnapshotWarmupFor(at *atev1alpha1.ActorTemplate) time.Duration {
containers := at.Spec.Containers
if len(containers) == 0 {
return goldenSnapshotWarmup
}
for i := range containers {
if containers[i].Readyz == nil {
return goldenSnapshotWarmup
}
}
return 0
}
@@ -1,234 +0,0 @@
// Copyright 2026 Google LLC
//
// 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 controllers
import (
"context"
"testing"
"github.com/agent-substrate/substrate/pkg/proto/ateapipb"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1"
)
func TestGoldenSnapshotWarmupFor(t *testing.T) {
probe := &atev1alpha1.ContainerReadyz{
HTTPGet: &atev1alpha1.HTTPGetAction{Port: 80},
}
tests := []struct {
name string
containers []atev1alpha1.Container
wantZero bool
}{
{
name: "no containers keeps default warmup",
containers: nil,
wantZero: false,
},
{
name: "all containers have readyz skips warmup",
containers: []atev1alpha1.Container{
{Name: "a", Readyz: probe},
{Name: "b", Readyz: probe},
},
wantZero: true,
},
{
name: "single container with readyz skips warmup",
containers: []atev1alpha1.Container{
{Name: "a", Readyz: probe},
},
wantZero: true,
},
{
name: "mixed containers keep warmup",
containers: []atev1alpha1.Container{
{Name: "a", Readyz: probe},
{Name: "b"},
},
wantZero: false,
},
{
name: "no readyz anywhere keeps warmup",
containers: []atev1alpha1.Container{
{Name: "a"},
{Name: "b"},
},
wantZero: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
at := &atev1alpha1.ActorTemplate{
Spec: atev1alpha1.ActorTemplateSpec{Containers: tt.containers},
}
got := goldenSnapshotWarmupFor(at)
if tt.wantZero && got != 0 {
t.Errorf("goldenSnapshotWarmupFor = %v, want 0", got)
}
if !tt.wantZero && got != goldenSnapshotWarmup {
t.Errorf("goldenSnapshotWarmupFor = %v, want %v", got, goldenSnapshotWarmup)
}
})
}
}
type mockControlClient struct {
ateapipb.ControlClient
createAtespaceFn func(ctx context.Context, req *ateapipb.CreateAtespaceRequest, opts ...grpc.CallOption) (*ateapipb.Atespace, error)
createActorFn func(ctx context.Context, req *ateapipb.CreateActorRequest, opts ...grpc.CallOption) (*ateapipb.Actor, error)
}
func (m *mockControlClient) CreateAtespace(ctx context.Context, req *ateapipb.CreateAtespaceRequest, opts ...grpc.CallOption) (*ateapipb.Atespace, error) {
if m.createAtespaceFn != nil {
return m.createAtespaceFn(ctx, req, opts...)
}
return &ateapipb.Atespace{}, nil
}
func (m *mockControlClient) CreateActor(ctx context.Context, req *ateapipb.CreateActorRequest, opts ...grpc.CallOption) (*ateapipb.Actor, error) {
if m.createActorFn != nil {
return m.createActorFn(ctx, req, opts...)
}
return &ateapipb.Actor{}, nil
}
func TestActorTemplateReconciler_Reconcile_PhaseInitial(t *testing.T) {
scheme := runtime.NewScheme()
if err := atev1alpha1.AddToScheme(scheme); err != nil {
t.Fatalf("failed to add scheme: %v", err)
}
const templateUID = "test-uid-12345"
const expectedActorName = templateUID
t.Run("creates golden actor using template UID", func(t *testing.T) {
template := &atev1alpha1.ActorTemplate{
ObjectMeta: metav1.ObjectMeta{
Name: "my-template",
Namespace: "default",
UID: types.UID(templateUID),
},
Status: atev1alpha1.ActorTemplateStatus{
Phase: atev1alpha1.PhaseInitial,
},
}
fakeK8sClient := fake.NewClientBuilder().
WithScheme(scheme).
WithStatusSubresource(&atev1alpha1.ActorTemplate{}).
WithObjects(template).
Build()
var createdActorName string
fakeAteClient := &mockControlClient{
createActorFn: func(ctx context.Context, req *ateapipb.CreateActorRequest, opts ...grpc.CallOption) (*ateapipb.Actor, error) {
createdActorName = req.GetActor().GetMetadata().GetName()
return &ateapipb.Actor{}, nil
},
}
reconciler := &ActorTemplateReconciler{
Client: fakeK8sClient,
Scheme: scheme,
AteClient: fakeAteClient,
}
ctx := context.Background()
req := ctrl.Request{NamespacedName: types.NamespacedName{Name: "my-template", Namespace: "default"}}
res, err := reconciler.Reconcile(ctx, req)
if err != nil {
t.Fatalf("Reconcile returned error: %v", err)
}
if !res.IsZero() {
t.Errorf("unexpected requeue result: %v", res)
}
if createdActorName != expectedActorName {
t.Errorf("created actor name = %q, want %q", createdActorName, expectedActorName)
}
reconciledTemplate := &atev1alpha1.ActorTemplate{}
if err := fakeK8sClient.Get(ctx, req.NamespacedName, reconciledTemplate); err != nil {
t.Fatalf("failed to get reconciled ActorTemplate: %v", err)
}
if reconciledTemplate.Status.GoldenActorID != expectedActorName {
t.Errorf("status.GoldenActorID = %q, want %q", reconciledTemplate.Status.GoldenActorID, expectedActorName)
}
if reconciledTemplate.Status.Phase != atev1alpha1.PhaseResumeGoldenActor {
t.Errorf("status.Phase = %q, want %q", reconciledTemplate.Status.Phase, atev1alpha1.PhaseResumeGoldenActor)
}
})
t.Run("handles AlreadyExists error when golden actor was created on prior attempt", func(t *testing.T) {
template := &atev1alpha1.ActorTemplate{
ObjectMeta: metav1.ObjectMeta{
Name: "my-template-retry",
Namespace: "default",
UID: types.UID(templateUID),
},
Status: atev1alpha1.ActorTemplateStatus{
Phase: atev1alpha1.PhaseInitial,
},
}
fakeK8sClient := fake.NewClientBuilder().
WithScheme(scheme).
WithStatusSubresource(&atev1alpha1.ActorTemplate{}).
WithObjects(template).
Build()
fakeAteClient := &mockControlClient{
createActorFn: func(ctx context.Context, req *ateapipb.CreateActorRequest, opts ...grpc.CallOption) (*ateapipb.Actor, error) {
return nil, status.Error(codes.AlreadyExists, "actor already exists in ateapi")
},
}
reconciler := &ActorTemplateReconciler{
Client: fakeK8sClient,
Scheme: scheme,
AteClient: fakeAteClient,
}
ctx := context.Background()
req := ctrl.Request{NamespacedName: types.NamespacedName{Name: "my-template-retry", Namespace: "default"}}
_, err := reconciler.Reconcile(ctx, req)
if err != nil {
t.Fatalf("Reconcile returned error on AlreadyExists retry: %v", err)
}
reconciledTemplate := &atev1alpha1.ActorTemplate{}
if err := fakeK8sClient.Get(ctx, req.NamespacedName, reconciledTemplate); err != nil {
t.Fatalf("failed to get reconciled ActorTemplate: %v", err)
}
if reconciledTemplate.Status.GoldenActorID != expectedActorName {
t.Errorf("status.GoldenActorID = %q, want %q", reconciledTemplate.Status.GoldenActorID, expectedActorName)
}
if reconciledTemplate.Status.Phase != atev1alpha1.PhaseResumeGoldenActor {
t.Errorf("status.Phase = %q, want %q", reconciledTemplate.Status.Phase, atev1alpha1.PhaseResumeGoldenActor)
}
})
}
@@ -14,4 +14,12 @@
package controllers
// RBAC needed by atecontroller components outside this package, which
// controller-gen (paths="./...") does not scan:
// - internal/workersync's pod informer lists and watches worker pods.
// - internal/k8sresolver watches ateapi's EndpointSlices to dial it.
//
//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
//+kubebuilder:rbac:groups=discovery.k8s.io,resources=endpointslices,verbs=get;list;watch,namespace=ate-system
//go:generate bash ../../../../hack/run-tool.sh controller-gen rbac:headerFile=../../../../hack/boilerplate/sh.txt,roleName=ate-controller paths="./..." output:rbac:artifacts:config=../../../../manifests/ate-install/generated/
-9
View File
@@ -193,15 +193,6 @@ func main() {
os.Exit(1)
}
if err = (&controllers.ActorTemplateReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
AteClient: ateapiClient,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ActorTemplate")
os.Exit(1)
}
if err = (&controllers.EgressMITMTrustReconciler{
Client: mgr.GetClient(),
}).SetupWithManager(mgr); err != nil {
+1 -16
View File
@@ -21,24 +21,12 @@ rules:
- apiGroups:
- ""
resources:
- configmaps
- pods
- secrets
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- pods
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- apps
resources:
@@ -54,7 +42,6 @@ rules:
- apiGroups:
- ate.dev
resources:
- actortemplates
- workerpools
verbs:
- create
@@ -67,14 +54,12 @@ rules:
- apiGroups:
- ate.dev
resources:
- actortemplates/finalizers
- workerpools/finalizers
verbs:
- update
- apiGroups:
- ate.dev
resources:
- actortemplates/status
- workerpools/status
verbs:
- get