mirror of
https://github.com/agent-substrate/substrate.git
synced 2026-10-02 03:24:42 +08:00
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:
@@ -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/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user