diff --git a/client-go/clientset/versioned/typed/runtime/v1alpha1/fake/fake_runtime_client.go b/client-go/clientset/versioned/typed/runtime/v1alpha1/fake/fake_runtime_client.go index 91c3617f..91d5cc71 100644 --- a/client-go/clientset/versioned/typed/runtime/v1alpha1/fake/fake_runtime_client.go +++ b/client-go/clientset/versioned/typed/runtime/v1alpha1/fake/fake_runtime_client.go @@ -36,6 +36,18 @@ func (c *FakeRuntimeV1alpha1) CodeInterpreters(namespace string) v1alpha1.CodeIn return newFakeCodeInterpreters(c, namespace) } +func (c *FakeRuntimeV1alpha1) SandboxSnapshots(namespace string) v1alpha1.SandboxSnapshotInterface { + return newFakeSandboxSnapshots(c, namespace) +} + +func (c *FakeRuntimeV1alpha1) SandboxSnapshotTasks(namespace string) v1alpha1.SandboxSnapshotTaskInterface { + return newFakeSandboxSnapshotTasks(c, namespace) +} + +func (c *FakeRuntimeV1alpha1) SnapshotClasses() v1alpha1.SnapshotClassInterface { + return newFakeSnapshotClasses(c) +} + // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeRuntimeV1alpha1) RESTClient() rest.Interface { diff --git a/client-go/clientset/versioned/typed/runtime/v1alpha1/fake/fake_sandboxsnapshot.go b/client-go/clientset/versioned/typed/runtime/v1alpha1/fake/fake_sandboxsnapshot.go new file mode 100644 index 00000000..f333633e --- /dev/null +++ b/client-go/clientset/versioned/typed/runtime/v1alpha1/fake/fake_sandboxsnapshot.go @@ -0,0 +1,52 @@ +/* +Copyright The Volcano Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + runtimev1alpha1 "github.com/volcano-sh/agentcube/client-go/clientset/versioned/typed/runtime/v1alpha1" + v1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeSandboxSnapshots implements SandboxSnapshotInterface +type fakeSandboxSnapshots struct { + *gentype.FakeClientWithList[*v1alpha1.SandboxSnapshot, *v1alpha1.SandboxSnapshotList] + Fake *FakeRuntimeV1alpha1 +} + +func newFakeSandboxSnapshots(fake *FakeRuntimeV1alpha1, namespace string) runtimev1alpha1.SandboxSnapshotInterface { + return &fakeSandboxSnapshots{ + gentype.NewFakeClientWithList[*v1alpha1.SandboxSnapshot, *v1alpha1.SandboxSnapshotList]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("sandboxsnapshots"), + v1alpha1.SchemeGroupVersion.WithKind("SandboxSnapshot"), + func() *v1alpha1.SandboxSnapshot { return &v1alpha1.SandboxSnapshot{} }, + func() *v1alpha1.SandboxSnapshotList { return &v1alpha1.SandboxSnapshotList{} }, + func(dst, src *v1alpha1.SandboxSnapshotList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.SandboxSnapshotList) []*v1alpha1.SandboxSnapshot { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.SandboxSnapshotList, items []*v1alpha1.SandboxSnapshot) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/client-go/clientset/versioned/typed/runtime/v1alpha1/fake/fake_sandboxsnapshottask.go b/client-go/clientset/versioned/typed/runtime/v1alpha1/fake/fake_sandboxsnapshottask.go new file mode 100644 index 00000000..16dbab61 --- /dev/null +++ b/client-go/clientset/versioned/typed/runtime/v1alpha1/fake/fake_sandboxsnapshottask.go @@ -0,0 +1,52 @@ +/* +Copyright The Volcano Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + runtimev1alpha1 "github.com/volcano-sh/agentcube/client-go/clientset/versioned/typed/runtime/v1alpha1" + v1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeSandboxSnapshotTasks implements SandboxSnapshotTaskInterface +type fakeSandboxSnapshotTasks struct { + *gentype.FakeClientWithList[*v1alpha1.SandboxSnapshotTask, *v1alpha1.SandboxSnapshotTaskList] + Fake *FakeRuntimeV1alpha1 +} + +func newFakeSandboxSnapshotTasks(fake *FakeRuntimeV1alpha1, namespace string) runtimev1alpha1.SandboxSnapshotTaskInterface { + return &fakeSandboxSnapshotTasks{ + gentype.NewFakeClientWithList[*v1alpha1.SandboxSnapshotTask, *v1alpha1.SandboxSnapshotTaskList]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("sandboxsnapshottasks"), + v1alpha1.SchemeGroupVersion.WithKind("SandboxSnapshotTask"), + func() *v1alpha1.SandboxSnapshotTask { return &v1alpha1.SandboxSnapshotTask{} }, + func() *v1alpha1.SandboxSnapshotTaskList { return &v1alpha1.SandboxSnapshotTaskList{} }, + func(dst, src *v1alpha1.SandboxSnapshotTaskList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.SandboxSnapshotTaskList) []*v1alpha1.SandboxSnapshotTask { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.SandboxSnapshotTaskList, items []*v1alpha1.SandboxSnapshotTask) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/client-go/clientset/versioned/typed/runtime/v1alpha1/fake/fake_snapshotclass.go b/client-go/clientset/versioned/typed/runtime/v1alpha1/fake/fake_snapshotclass.go new file mode 100644 index 00000000..05f70400 --- /dev/null +++ b/client-go/clientset/versioned/typed/runtime/v1alpha1/fake/fake_snapshotclass.go @@ -0,0 +1,52 @@ +/* +Copyright The Volcano Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + runtimev1alpha1 "github.com/volcano-sh/agentcube/client-go/clientset/versioned/typed/runtime/v1alpha1" + v1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeSnapshotClasses implements SnapshotClassInterface +type fakeSnapshotClasses struct { + *gentype.FakeClientWithList[*v1alpha1.SnapshotClass, *v1alpha1.SnapshotClassList] + Fake *FakeRuntimeV1alpha1 +} + +func newFakeSnapshotClasses(fake *FakeRuntimeV1alpha1) runtimev1alpha1.SnapshotClassInterface { + return &fakeSnapshotClasses{ + gentype.NewFakeClientWithList[*v1alpha1.SnapshotClass, *v1alpha1.SnapshotClassList]( + fake.Fake, + "", + v1alpha1.SchemeGroupVersion.WithResource("snapshotclasses"), + v1alpha1.SchemeGroupVersion.WithKind("SnapshotClass"), + func() *v1alpha1.SnapshotClass { return &v1alpha1.SnapshotClass{} }, + func() *v1alpha1.SnapshotClassList { return &v1alpha1.SnapshotClassList{} }, + func(dst, src *v1alpha1.SnapshotClassList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.SnapshotClassList) []*v1alpha1.SnapshotClass { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.SnapshotClassList, items []*v1alpha1.SnapshotClass) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/client-go/clientset/versioned/typed/runtime/v1alpha1/generated_expansion.go b/client-go/clientset/versioned/typed/runtime/v1alpha1/generated_expansion.go index 1ac68773..8850c62d 100644 --- a/client-go/clientset/versioned/typed/runtime/v1alpha1/generated_expansion.go +++ b/client-go/clientset/versioned/typed/runtime/v1alpha1/generated_expansion.go @@ -21,3 +21,9 @@ package v1alpha1 type AgentRuntimeExpansion interface{} type CodeInterpreterExpansion interface{} + +type SandboxSnapshotExpansion interface{} + +type SandboxSnapshotTaskExpansion interface{} + +type SnapshotClassExpansion interface{} diff --git a/client-go/clientset/versioned/typed/runtime/v1alpha1/runtime_client.go b/client-go/clientset/versioned/typed/runtime/v1alpha1/runtime_client.go index 0da62b40..cea7d51d 100644 --- a/client-go/clientset/versioned/typed/runtime/v1alpha1/runtime_client.go +++ b/client-go/clientset/versioned/typed/runtime/v1alpha1/runtime_client.go @@ -30,6 +30,9 @@ type RuntimeV1alpha1Interface interface { RESTClient() rest.Interface AgentRuntimesGetter CodeInterpretersGetter + SandboxSnapshotsGetter + SandboxSnapshotTasksGetter + SnapshotClassesGetter } // RuntimeV1alpha1Client is used to interact with features provided by the runtime group. @@ -45,6 +48,18 @@ func (c *RuntimeV1alpha1Client) CodeInterpreters(namespace string) CodeInterpret return newCodeInterpreters(c, namespace) } +func (c *RuntimeV1alpha1Client) SandboxSnapshots(namespace string) SandboxSnapshotInterface { + return newSandboxSnapshots(c, namespace) +} + +func (c *RuntimeV1alpha1Client) SandboxSnapshotTasks(namespace string) SandboxSnapshotTaskInterface { + return newSandboxSnapshotTasks(c, namespace) +} + +func (c *RuntimeV1alpha1Client) SnapshotClasses() SnapshotClassInterface { + return newSnapshotClasses(c) +} + // NewForConfig creates a new RuntimeV1alpha1Client for the given config. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). diff --git a/client-go/clientset/versioned/typed/runtime/v1alpha1/sandboxsnapshot.go b/client-go/clientset/versioned/typed/runtime/v1alpha1/sandboxsnapshot.go new file mode 100644 index 00000000..4302a31c --- /dev/null +++ b/client-go/clientset/versioned/typed/runtime/v1alpha1/sandboxsnapshot.go @@ -0,0 +1,70 @@ +/* +Copyright The Volcano Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + scheme "github.com/volcano-sh/agentcube/client-go/clientset/versioned/scheme" + runtimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// SandboxSnapshotsGetter has a method to return a SandboxSnapshotInterface. +// A group's client should implement this interface. +type SandboxSnapshotsGetter interface { + SandboxSnapshots(namespace string) SandboxSnapshotInterface +} + +// SandboxSnapshotInterface has methods to work with SandboxSnapshot resources. +type SandboxSnapshotInterface interface { + Create(ctx context.Context, sandboxSnapshot *runtimev1alpha1.SandboxSnapshot, opts v1.CreateOptions) (*runtimev1alpha1.SandboxSnapshot, error) + Update(ctx context.Context, sandboxSnapshot *runtimev1alpha1.SandboxSnapshot, opts v1.UpdateOptions) (*runtimev1alpha1.SandboxSnapshot, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, sandboxSnapshot *runtimev1alpha1.SandboxSnapshot, opts v1.UpdateOptions) (*runtimev1alpha1.SandboxSnapshot, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*runtimev1alpha1.SandboxSnapshot, error) + List(ctx context.Context, opts v1.ListOptions) (*runtimev1alpha1.SandboxSnapshotList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *runtimev1alpha1.SandboxSnapshot, err error) + SandboxSnapshotExpansion +} + +// sandboxSnapshots implements SandboxSnapshotInterface +type sandboxSnapshots struct { + *gentype.ClientWithList[*runtimev1alpha1.SandboxSnapshot, *runtimev1alpha1.SandboxSnapshotList] +} + +// newSandboxSnapshots returns a SandboxSnapshots +func newSandboxSnapshots(c *RuntimeV1alpha1Client, namespace string) *sandboxSnapshots { + return &sandboxSnapshots{ + gentype.NewClientWithList[*runtimev1alpha1.SandboxSnapshot, *runtimev1alpha1.SandboxSnapshotList]( + "sandboxsnapshots", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *runtimev1alpha1.SandboxSnapshot { return &runtimev1alpha1.SandboxSnapshot{} }, + func() *runtimev1alpha1.SandboxSnapshotList { return &runtimev1alpha1.SandboxSnapshotList{} }, + ), + } +} diff --git a/client-go/clientset/versioned/typed/runtime/v1alpha1/sandboxsnapshottask.go b/client-go/clientset/versioned/typed/runtime/v1alpha1/sandboxsnapshottask.go new file mode 100644 index 00000000..1406162b --- /dev/null +++ b/client-go/clientset/versioned/typed/runtime/v1alpha1/sandboxsnapshottask.go @@ -0,0 +1,70 @@ +/* +Copyright The Volcano Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + scheme "github.com/volcano-sh/agentcube/client-go/clientset/versioned/scheme" + runtimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// SandboxSnapshotTasksGetter has a method to return a SandboxSnapshotTaskInterface. +// A group's client should implement this interface. +type SandboxSnapshotTasksGetter interface { + SandboxSnapshotTasks(namespace string) SandboxSnapshotTaskInterface +} + +// SandboxSnapshotTaskInterface has methods to work with SandboxSnapshotTask resources. +type SandboxSnapshotTaskInterface interface { + Create(ctx context.Context, sandboxSnapshotTask *runtimev1alpha1.SandboxSnapshotTask, opts v1.CreateOptions) (*runtimev1alpha1.SandboxSnapshotTask, error) + Update(ctx context.Context, sandboxSnapshotTask *runtimev1alpha1.SandboxSnapshotTask, opts v1.UpdateOptions) (*runtimev1alpha1.SandboxSnapshotTask, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, sandboxSnapshotTask *runtimev1alpha1.SandboxSnapshotTask, opts v1.UpdateOptions) (*runtimev1alpha1.SandboxSnapshotTask, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*runtimev1alpha1.SandboxSnapshotTask, error) + List(ctx context.Context, opts v1.ListOptions) (*runtimev1alpha1.SandboxSnapshotTaskList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *runtimev1alpha1.SandboxSnapshotTask, err error) + SandboxSnapshotTaskExpansion +} + +// sandboxSnapshotTasks implements SandboxSnapshotTaskInterface +type sandboxSnapshotTasks struct { + *gentype.ClientWithList[*runtimev1alpha1.SandboxSnapshotTask, *runtimev1alpha1.SandboxSnapshotTaskList] +} + +// newSandboxSnapshotTasks returns a SandboxSnapshotTasks +func newSandboxSnapshotTasks(c *RuntimeV1alpha1Client, namespace string) *sandboxSnapshotTasks { + return &sandboxSnapshotTasks{ + gentype.NewClientWithList[*runtimev1alpha1.SandboxSnapshotTask, *runtimev1alpha1.SandboxSnapshotTaskList]( + "sandboxsnapshottasks", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *runtimev1alpha1.SandboxSnapshotTask { return &runtimev1alpha1.SandboxSnapshotTask{} }, + func() *runtimev1alpha1.SandboxSnapshotTaskList { return &runtimev1alpha1.SandboxSnapshotTaskList{} }, + ), + } +} diff --git a/client-go/clientset/versioned/typed/runtime/v1alpha1/snapshotclass.go b/client-go/clientset/versioned/typed/runtime/v1alpha1/snapshotclass.go new file mode 100644 index 00000000..0623aa2d --- /dev/null +++ b/client-go/clientset/versioned/typed/runtime/v1alpha1/snapshotclass.go @@ -0,0 +1,68 @@ +/* +Copyright The Volcano Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + scheme "github.com/volcano-sh/agentcube/client-go/clientset/versioned/scheme" + runtimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// SnapshotClassesGetter has a method to return a SnapshotClassInterface. +// A group's client should implement this interface. +type SnapshotClassesGetter interface { + SnapshotClasses() SnapshotClassInterface +} + +// SnapshotClassInterface has methods to work with SnapshotClass resources. +type SnapshotClassInterface interface { + Create(ctx context.Context, snapshotClass *runtimev1alpha1.SnapshotClass, opts v1.CreateOptions) (*runtimev1alpha1.SnapshotClass, error) + Update(ctx context.Context, snapshotClass *runtimev1alpha1.SnapshotClass, opts v1.UpdateOptions) (*runtimev1alpha1.SnapshotClass, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*runtimev1alpha1.SnapshotClass, error) + List(ctx context.Context, opts v1.ListOptions) (*runtimev1alpha1.SnapshotClassList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *runtimev1alpha1.SnapshotClass, err error) + SnapshotClassExpansion +} + +// snapshotClasses implements SnapshotClassInterface +type snapshotClasses struct { + *gentype.ClientWithList[*runtimev1alpha1.SnapshotClass, *runtimev1alpha1.SnapshotClassList] +} + +// newSnapshotClasses returns a SnapshotClasses +func newSnapshotClasses(c *RuntimeV1alpha1Client) *snapshotClasses { + return &snapshotClasses{ + gentype.NewClientWithList[*runtimev1alpha1.SnapshotClass, *runtimev1alpha1.SnapshotClassList]( + "snapshotclasses", + c.RESTClient(), + scheme.ParameterCodec, + "", + func() *runtimev1alpha1.SnapshotClass { return &runtimev1alpha1.SnapshotClass{} }, + func() *runtimev1alpha1.SnapshotClassList { return &runtimev1alpha1.SnapshotClassList{} }, + ), + } +} diff --git a/client-go/informers/externalversions/generic.go b/client-go/informers/externalversions/generic.go index f304a1b7..cd1a3649 100644 --- a/client-go/informers/externalversions/generic.go +++ b/client-go/informers/externalversions/generic.go @@ -57,6 +57,12 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Runtime().V1alpha1().AgentRuntimes().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("codeinterpreters"): return &genericInformer{resource: resource.GroupResource(), informer: f.Runtime().V1alpha1().CodeInterpreters().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("sandboxsnapshots"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Runtime().V1alpha1().SandboxSnapshots().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("sandboxsnapshottasks"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Runtime().V1alpha1().SandboxSnapshotTasks().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("snapshotclasses"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Runtime().V1alpha1().SnapshotClasses().Informer()}, nil } diff --git a/client-go/informers/externalversions/runtime/v1alpha1/interface.go b/client-go/informers/externalversions/runtime/v1alpha1/interface.go index 959f4abe..ca3ad996 100644 --- a/client-go/informers/externalversions/runtime/v1alpha1/interface.go +++ b/client-go/informers/externalversions/runtime/v1alpha1/interface.go @@ -28,6 +28,12 @@ type Interface interface { AgentRuntimes() AgentRuntimeInformer // CodeInterpreters returns a CodeInterpreterInformer. CodeInterpreters() CodeInterpreterInformer + // SandboxSnapshots returns a SandboxSnapshotInformer. + SandboxSnapshots() SandboxSnapshotInformer + // SandboxSnapshotTasks returns a SandboxSnapshotTaskInformer. + SandboxSnapshotTasks() SandboxSnapshotTaskInformer + // SnapshotClasses returns a SnapshotClassInformer. + SnapshotClasses() SnapshotClassInformer } type version struct { @@ -50,3 +56,18 @@ func (v *version) AgentRuntimes() AgentRuntimeInformer { func (v *version) CodeInterpreters() CodeInterpreterInformer { return &codeInterpreterInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } + +// SandboxSnapshots returns a SandboxSnapshotInformer. +func (v *version) SandboxSnapshots() SandboxSnapshotInformer { + return &sandboxSnapshotInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + +// SandboxSnapshotTasks returns a SandboxSnapshotTaskInformer. +func (v *version) SandboxSnapshotTasks() SandboxSnapshotTaskInformer { + return &sandboxSnapshotTaskInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + +// SnapshotClasses returns a SnapshotClassInformer. +func (v *version) SnapshotClasses() SnapshotClassInformer { + return &snapshotClassInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} diff --git a/client-go/informers/externalversions/runtime/v1alpha1/sandboxsnapshot.go b/client-go/informers/externalversions/runtime/v1alpha1/sandboxsnapshot.go new file mode 100644 index 00000000..c17e7f10 --- /dev/null +++ b/client-go/informers/externalversions/runtime/v1alpha1/sandboxsnapshot.go @@ -0,0 +1,102 @@ +/* +Copyright The Volcano Authors. + +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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + versioned "github.com/volcano-sh/agentcube/client-go/clientset/versioned" + internalinterfaces "github.com/volcano-sh/agentcube/client-go/informers/externalversions/internalinterfaces" + runtimev1alpha1 "github.com/volcano-sh/agentcube/client-go/listers/runtime/v1alpha1" + apisruntimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// SandboxSnapshotInformer provides access to a shared informer and lister for +// SandboxSnapshots. +type SandboxSnapshotInformer interface { + Informer() cache.SharedIndexInformer + Lister() runtimev1alpha1.SandboxSnapshotLister +} + +type sandboxSnapshotInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewSandboxSnapshotInformer constructs a new informer for SandboxSnapshot type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewSandboxSnapshotInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredSandboxSnapshotInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredSandboxSnapshotInformer constructs a new informer for SandboxSnapshot type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredSandboxSnapshotInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.RuntimeV1alpha1().SandboxSnapshots(namespace).List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.RuntimeV1alpha1().SandboxSnapshots(namespace).Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.RuntimeV1alpha1().SandboxSnapshots(namespace).List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.RuntimeV1alpha1().SandboxSnapshots(namespace).Watch(ctx, options) + }, + }, + &apisruntimev1alpha1.SandboxSnapshot{}, + resyncPeriod, + indexers, + ) +} + +func (f *sandboxSnapshotInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredSandboxSnapshotInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *sandboxSnapshotInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apisruntimev1alpha1.SandboxSnapshot{}, f.defaultInformer) +} + +func (f *sandboxSnapshotInformer) Lister() runtimev1alpha1.SandboxSnapshotLister { + return runtimev1alpha1.NewSandboxSnapshotLister(f.Informer().GetIndexer()) +} diff --git a/client-go/informers/externalversions/runtime/v1alpha1/sandboxsnapshottask.go b/client-go/informers/externalversions/runtime/v1alpha1/sandboxsnapshottask.go new file mode 100644 index 00000000..4db2447e --- /dev/null +++ b/client-go/informers/externalversions/runtime/v1alpha1/sandboxsnapshottask.go @@ -0,0 +1,102 @@ +/* +Copyright The Volcano Authors. + +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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + versioned "github.com/volcano-sh/agentcube/client-go/clientset/versioned" + internalinterfaces "github.com/volcano-sh/agentcube/client-go/informers/externalversions/internalinterfaces" + runtimev1alpha1 "github.com/volcano-sh/agentcube/client-go/listers/runtime/v1alpha1" + apisruntimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// SandboxSnapshotTaskInformer provides access to a shared informer and lister for +// SandboxSnapshotTasks. +type SandboxSnapshotTaskInformer interface { + Informer() cache.SharedIndexInformer + Lister() runtimev1alpha1.SandboxSnapshotTaskLister +} + +type sandboxSnapshotTaskInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewSandboxSnapshotTaskInformer constructs a new informer for SandboxSnapshotTask type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewSandboxSnapshotTaskInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredSandboxSnapshotTaskInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredSandboxSnapshotTaskInformer constructs a new informer for SandboxSnapshotTask type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredSandboxSnapshotTaskInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.RuntimeV1alpha1().SandboxSnapshotTasks(namespace).List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.RuntimeV1alpha1().SandboxSnapshotTasks(namespace).Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.RuntimeV1alpha1().SandboxSnapshotTasks(namespace).List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.RuntimeV1alpha1().SandboxSnapshotTasks(namespace).Watch(ctx, options) + }, + }, + &apisruntimev1alpha1.SandboxSnapshotTask{}, + resyncPeriod, + indexers, + ) +} + +func (f *sandboxSnapshotTaskInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredSandboxSnapshotTaskInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *sandboxSnapshotTaskInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apisruntimev1alpha1.SandboxSnapshotTask{}, f.defaultInformer) +} + +func (f *sandboxSnapshotTaskInformer) Lister() runtimev1alpha1.SandboxSnapshotTaskLister { + return runtimev1alpha1.NewSandboxSnapshotTaskLister(f.Informer().GetIndexer()) +} diff --git a/client-go/informers/externalversions/runtime/v1alpha1/snapshotclass.go b/client-go/informers/externalversions/runtime/v1alpha1/snapshotclass.go new file mode 100644 index 00000000..6403bde8 --- /dev/null +++ b/client-go/informers/externalversions/runtime/v1alpha1/snapshotclass.go @@ -0,0 +1,101 @@ +/* +Copyright The Volcano Authors. + +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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + versioned "github.com/volcano-sh/agentcube/client-go/clientset/versioned" + internalinterfaces "github.com/volcano-sh/agentcube/client-go/informers/externalversions/internalinterfaces" + runtimev1alpha1 "github.com/volcano-sh/agentcube/client-go/listers/runtime/v1alpha1" + apisruntimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// SnapshotClassInformer provides access to a shared informer and lister for +// SnapshotClasses. +type SnapshotClassInformer interface { + Informer() cache.SharedIndexInformer + Lister() runtimev1alpha1.SnapshotClassLister +} + +type snapshotClassInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewSnapshotClassInformer constructs a new informer for SnapshotClass type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewSnapshotClassInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredSnapshotClassInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredSnapshotClassInformer constructs a new informer for SnapshotClass type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredSnapshotClassInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.RuntimeV1alpha1().SnapshotClasses().List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.RuntimeV1alpha1().SnapshotClasses().Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.RuntimeV1alpha1().SnapshotClasses().List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.RuntimeV1alpha1().SnapshotClasses().Watch(ctx, options) + }, + }, + &apisruntimev1alpha1.SnapshotClass{}, + resyncPeriod, + indexers, + ) +} + +func (f *snapshotClassInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredSnapshotClassInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *snapshotClassInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apisruntimev1alpha1.SnapshotClass{}, f.defaultInformer) +} + +func (f *snapshotClassInformer) Lister() runtimev1alpha1.SnapshotClassLister { + return runtimev1alpha1.NewSnapshotClassLister(f.Informer().GetIndexer()) +} diff --git a/client-go/listers/runtime/v1alpha1/expansion_generated.go b/client-go/listers/runtime/v1alpha1/expansion_generated.go index 6e06c618..ba34114f 100644 --- a/client-go/listers/runtime/v1alpha1/expansion_generated.go +++ b/client-go/listers/runtime/v1alpha1/expansion_generated.go @@ -33,3 +33,23 @@ type CodeInterpreterListerExpansion interface{} // CodeInterpreterNamespaceListerExpansion allows custom methods to be added to // CodeInterpreterNamespaceLister. type CodeInterpreterNamespaceListerExpansion interface{} + +// SandboxSnapshotListerExpansion allows custom methods to be added to +// SandboxSnapshotLister. +type SandboxSnapshotListerExpansion interface{} + +// SandboxSnapshotNamespaceListerExpansion allows custom methods to be added to +// SandboxSnapshotNamespaceLister. +type SandboxSnapshotNamespaceListerExpansion interface{} + +// SandboxSnapshotTaskListerExpansion allows custom methods to be added to +// SandboxSnapshotTaskLister. +type SandboxSnapshotTaskListerExpansion interface{} + +// SandboxSnapshotTaskNamespaceListerExpansion allows custom methods to be added to +// SandboxSnapshotTaskNamespaceLister. +type SandboxSnapshotTaskNamespaceListerExpansion interface{} + +// SnapshotClassListerExpansion allows custom methods to be added to +// SnapshotClassLister. +type SnapshotClassListerExpansion interface{} diff --git a/client-go/listers/runtime/v1alpha1/sandboxsnapshot.go b/client-go/listers/runtime/v1alpha1/sandboxsnapshot.go new file mode 100644 index 00000000..76b17175 --- /dev/null +++ b/client-go/listers/runtime/v1alpha1/sandboxsnapshot.go @@ -0,0 +1,70 @@ +/* +Copyright The Volcano Authors. + +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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// SandboxSnapshotLister helps list SandboxSnapshots. +// All objects returned here must be treated as read-only. +type SandboxSnapshotLister interface { + // List lists all SandboxSnapshots in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*runtimev1alpha1.SandboxSnapshot, err error) + // SandboxSnapshots returns an object that can list and get SandboxSnapshots. + SandboxSnapshots(namespace string) SandboxSnapshotNamespaceLister + SandboxSnapshotListerExpansion +} + +// sandboxSnapshotLister implements the SandboxSnapshotLister interface. +type sandboxSnapshotLister struct { + listers.ResourceIndexer[*runtimev1alpha1.SandboxSnapshot] +} + +// NewSandboxSnapshotLister returns a new SandboxSnapshotLister. +func NewSandboxSnapshotLister(indexer cache.Indexer) SandboxSnapshotLister { + return &sandboxSnapshotLister{listers.New[*runtimev1alpha1.SandboxSnapshot](indexer, runtimev1alpha1.Resource("sandboxsnapshot").GroupResource())} +} + +// SandboxSnapshots returns an object that can list and get SandboxSnapshots. +func (s *sandboxSnapshotLister) SandboxSnapshots(namespace string) SandboxSnapshotNamespaceLister { + return sandboxSnapshotNamespaceLister{listers.NewNamespaced[*runtimev1alpha1.SandboxSnapshot](s.ResourceIndexer, namespace)} +} + +// SandboxSnapshotNamespaceLister helps list and get SandboxSnapshots. +// All objects returned here must be treated as read-only. +type SandboxSnapshotNamespaceLister interface { + // List lists all SandboxSnapshots in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*runtimev1alpha1.SandboxSnapshot, err error) + // Get retrieves the SandboxSnapshot from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*runtimev1alpha1.SandboxSnapshot, error) + SandboxSnapshotNamespaceListerExpansion +} + +// sandboxSnapshotNamespaceLister implements the SandboxSnapshotNamespaceLister +// interface. +type sandboxSnapshotNamespaceLister struct { + listers.ResourceIndexer[*runtimev1alpha1.SandboxSnapshot] +} diff --git a/client-go/listers/runtime/v1alpha1/sandboxsnapshottask.go b/client-go/listers/runtime/v1alpha1/sandboxsnapshottask.go new file mode 100644 index 00000000..d1487ca7 --- /dev/null +++ b/client-go/listers/runtime/v1alpha1/sandboxsnapshottask.go @@ -0,0 +1,70 @@ +/* +Copyright The Volcano Authors. + +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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// SandboxSnapshotTaskLister helps list SandboxSnapshotTasks. +// All objects returned here must be treated as read-only. +type SandboxSnapshotTaskLister interface { + // List lists all SandboxSnapshotTasks in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*runtimev1alpha1.SandboxSnapshotTask, err error) + // SandboxSnapshotTasks returns an object that can list and get SandboxSnapshotTasks. + SandboxSnapshotTasks(namespace string) SandboxSnapshotTaskNamespaceLister + SandboxSnapshotTaskListerExpansion +} + +// sandboxSnapshotTaskLister implements the SandboxSnapshotTaskLister interface. +type sandboxSnapshotTaskLister struct { + listers.ResourceIndexer[*runtimev1alpha1.SandboxSnapshotTask] +} + +// NewSandboxSnapshotTaskLister returns a new SandboxSnapshotTaskLister. +func NewSandboxSnapshotTaskLister(indexer cache.Indexer) SandboxSnapshotTaskLister { + return &sandboxSnapshotTaskLister{listers.New[*runtimev1alpha1.SandboxSnapshotTask](indexer, runtimev1alpha1.Resource("sandboxsnapshottask").GroupResource())} +} + +// SandboxSnapshotTasks returns an object that can list and get SandboxSnapshotTasks. +func (s *sandboxSnapshotTaskLister) SandboxSnapshotTasks(namespace string) SandboxSnapshotTaskNamespaceLister { + return sandboxSnapshotTaskNamespaceLister{listers.NewNamespaced[*runtimev1alpha1.SandboxSnapshotTask](s.ResourceIndexer, namespace)} +} + +// SandboxSnapshotTaskNamespaceLister helps list and get SandboxSnapshotTasks. +// All objects returned here must be treated as read-only. +type SandboxSnapshotTaskNamespaceLister interface { + // List lists all SandboxSnapshotTasks in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*runtimev1alpha1.SandboxSnapshotTask, err error) + // Get retrieves the SandboxSnapshotTask from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*runtimev1alpha1.SandboxSnapshotTask, error) + SandboxSnapshotTaskNamespaceListerExpansion +} + +// sandboxSnapshotTaskNamespaceLister implements the SandboxSnapshotTaskNamespaceLister +// interface. +type sandboxSnapshotTaskNamespaceLister struct { + listers.ResourceIndexer[*runtimev1alpha1.SandboxSnapshotTask] +} diff --git a/client-go/listers/runtime/v1alpha1/snapshotclass.go b/client-go/listers/runtime/v1alpha1/snapshotclass.go new file mode 100644 index 00000000..9c6df789 --- /dev/null +++ b/client-go/listers/runtime/v1alpha1/snapshotclass.go @@ -0,0 +1,48 @@ +/* +Copyright The Volcano Authors. + +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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// SnapshotClassLister helps list SnapshotClasses. +// All objects returned here must be treated as read-only. +type SnapshotClassLister interface { + // List lists all SnapshotClasses in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*runtimev1alpha1.SnapshotClass, err error) + // Get retrieves the SnapshotClass from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*runtimev1alpha1.SnapshotClass, error) + SnapshotClassListerExpansion +} + +// snapshotClassLister implements the SnapshotClassLister interface. +type snapshotClassLister struct { + listers.ResourceIndexer[*runtimev1alpha1.SnapshotClass] +} + +// NewSnapshotClassLister returns a new SnapshotClassLister. +func NewSnapshotClassLister(indexer cache.Indexer) SnapshotClassLister { + return &snapshotClassLister{listers.New[*runtimev1alpha1.SnapshotClass](indexer, runtimev1alpha1.Resource("snapshotclass").GroupResource())} +} diff --git a/cmd/agentd/main.go b/cmd/agentd/main.go index 16e72ac7..164e7451 100644 --- a/cmd/agentd/main.go +++ b/cmd/agentd/main.go @@ -22,12 +22,14 @@ import ( "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" sandboxv1alpha1 "sigs.k8s.io/agent-sandbox/api/v1alpha1" ctrl "sigs.k8s.io/controller-runtime" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "github.com/volcano-sh/agentcube/pkg/agentd" + runtimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" ) var ( @@ -37,6 +39,7 @@ var ( func init() { utilruntime.Must(scheme.AddToScheme(schemeBuilder)) utilruntime.Must(sandboxv1alpha1.AddToScheme(schemeBuilder)) + utilruntime.Must(runtimev1alpha1.AddToScheme(schemeBuilder)) } func main() { @@ -53,14 +56,49 @@ func main() { os.Exit(1) } - err = ctrl.NewControllerManagedBy(mgr). + if err = ctrl.NewControllerManagedBy(mgr). For(&sandboxv1alpha1.Sandbox{}). Complete(&agentd.Reconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - }) + }); err != nil { + fmt.Fprintf(os.Stderr, "unable to create sandbox controller: %v\n", err) + os.Exit(1) + } + + // Read the node name from the downward API environment variable. + nodeName := os.Getenv("NODE_NAME") + if nodeName == "" { + fmt.Fprintf(os.Stderr, "NODE_NAME environment variable is required\n") + os.Exit(1) + } + + registry, err := agentd.BuildDefaultRegistry() + if err != nil { + fmt.Fprintf(os.Stderr, "unable to build driver registry: %v\n", err) + os.Exit(1) + } + + // Advertise snapshot capabilities on the node before controllers start so that + // the workload manager can select this node for snapshot builds immediately. + cs, err := kubernetes.NewForConfig(mgr.GetConfig()) if err != nil { - fmt.Fprintf(os.Stderr, "unable to create controller: %v\n", err) + fmt.Fprintf(os.Stderr, "unable to create kubernetes client: %v\n", err) + os.Exit(1) + } + if err := agentd.AdvertiseDriverCapabilities(ctrl.SetupSignalHandler(), cs, nodeName, registry.Drivers()); err != nil { + fmt.Fprintf(os.Stderr, "unable to advertise driver capabilities: %v\n", err) + os.Exit(1) + } + + snapshotTaskReconciler := &agentd.SnapshotTaskReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + NodeName: nodeName, + Drivers: registry.Drivers(), + } + if err := snapshotTaskReconciler.SetupWithManager(mgr); err != nil { + fmt.Fprintf(os.Stderr, "unable to create snapshot task controller: %v\n", err) os.Exit(1) } diff --git a/cmd/workload-manager/main.go b/cmd/workload-manager/main.go index 7cfe2ea0..c43a46cd 100644 --- a/cmd/workload-manager/main.go +++ b/cmd/workload-manager/main.go @@ -107,7 +107,13 @@ func main() { Scheme: mgr.GetScheme(), } - if err := setupControllers(mgr, sandboxReconciler, codeInterpreterReconciler); err != nil { + snapshotReconciler := &workloadmanager.SandboxSnapshotReconciler{ + Client: mgr.GetClient(), + ArtifactStore: workloadmanager.NewArtifactStoreFromEnv(), + } + defer closeArtifactStore(snapshotReconciler.ArtifactStore) + + if err := setupControllers(mgr, sandboxReconciler, codeInterpreterReconciler, snapshotReconciler); err != nil { fmt.Fprintf(os.Stderr, "unable to setup controllers: %v\n", err) os.Exit(1) } @@ -124,7 +130,7 @@ func main() { } // Create and initialize API server - server, err := workloadmanager.NewServer(config, sandboxReconciler) + server, err := workloadmanager.NewServer(config, sandboxReconciler, mgr.GetClient(), snapshotReconciler.ArtifactStore) if err != nil { klog.Fatalf("Failed to create API server: %v", err) } @@ -184,7 +190,7 @@ func main() { klog.Info("Server stopped") } -func setupControllers(mgr ctrl.Manager, sandboxReconciler *workloadmanager.SandboxReconciler, codeInterpreterReconciler *workloadmanager.CodeInterpreterReconciler) error { +func setupControllers(mgr ctrl.Manager, sandboxReconciler *workloadmanager.SandboxReconciler, codeInterpreterReconciler *workloadmanager.CodeInterpreterReconciler, snapshotReconciler *workloadmanager.SandboxSnapshotReconciler) error { // Setup Sandbox controller if err := ctrl.NewControllerManagedBy(mgr). For(&sandboxv1alpha1.Sandbox{}). @@ -197,9 +203,20 @@ func setupControllers(mgr ctrl.Manager, sandboxReconciler *workloadmanager.Sandb return fmt.Errorf("unable to create codeinterpreter controller: %w", err) } + // Setup SandboxSnapshot controller. + if err := snapshotReconciler.SetupWithManager(mgr); err != nil { + return fmt.Errorf("unable to create snapshot controller: %w", err) + } + return nil } +func closeArtifactStore(store interface{ Close() error }) { + if err := store.Close(); err != nil { + klog.Errorf("artifact store close error: %v", err) + } +} + func validateTLSFlags(enableTLS bool, cfg mtls.Config) error { if cfg.CAFile != "" { if cfg.CertFile == "" || cfg.KeyFile == "" { diff --git a/go.mod b/go.mod index f0a23c80..c637c9f7 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ toolchain go1.24.9 require ( github.com/agiledragon/gomonkey/v2 v2.13.0 github.com/alicebob/miniredis/v2 v2.35.0 - github.com/gin-contrib/gzip v1.0.1 github.com/fsnotify/fsnotify v1.9.0 + github.com/gin-contrib/gzip v1.0.1 github.com/gin-gonic/gin v1.10.0 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/uuid v1.6.0 diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index f39bb861..640ae91d 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -68,13 +68,13 @@ kube::codegen::gen_client \ # This is a workaround for https://github.com/kubernetes/code-generator/issues/XXX echo "Fixing lister-gen GroupResource issue..." find "${SCRIPT_ROOT}/client-go/listers" -name "*.go" -type f | while read -r file; do - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' 's/runtimev1alpha1\.Resource("codeinterpreter")/runtimev1alpha1.Resource("codeinterpreter").GroupResource()/g' "$file" - sed -i '' 's/runtimev1alpha1\.Resource("agentruntime")/runtimev1alpha1.Resource("agentruntime").GroupResource()/g' "$file" - else - sed -i 's/runtimev1alpha1\.Resource("codeinterpreter")/runtimev1alpha1.Resource("codeinterpreter").GroupResource()/g' "$file" - sed -i 's/runtimev1alpha1\.Resource("agentruntime")/runtimev1alpha1.Resource("agentruntime").GroupResource()/g' "$file" - fi + for resource in codeinterpreter agentruntime sandboxsnapshot sandboxsnapshottask snapshotclass; do + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s/runtimev1alpha1\\.Resource(\"${resource}\")/runtimev1alpha1.Resource(\"${resource}\").GroupResource()/g" "$file" + else + sed -i "s/runtimev1alpha1\\.Resource(\"${resource}\")/runtimev1alpha1.Resource(\"${resource}\").GroupResource()/g" "$file" + fi + done done echo "Client-go code generation completed!" diff --git a/manifests/charts/base/crds/runtime.agentcube.volcano.sh_sandboxsnapshots.yaml b/manifests/charts/base/crds/runtime.agentcube.volcano.sh_sandboxsnapshots.yaml new file mode 100644 index 00000000..776d1e00 --- /dev/null +++ b/manifests/charts/base/crds/runtime.agentcube.volcano.sh_sandboxsnapshots.yaml @@ -0,0 +1,210 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: sandboxsnapshots.runtime.agentcube.volcano.sh +spec: + group: runtime.agentcube.volcano.sh + names: + kind: SandboxSnapshot + listKind: SandboxSnapshotList + plural: sandboxsnapshots + singular: sandboxsnapshot + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.snapshotMode + name: Mode + type: string + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .status.readyNodeCount + name: Ready + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: SandboxSnapshot declares a snapshot of an agent-sandbox execution + environment. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: SandboxSnapshotSpec defines the desired state of a SandboxSnapshot. + properties: + forkPolicy: + description: ForkPolicy controls when a Fork snapshot is rebuilt. + properties: + rebuildAfter: + description: |- + RebuildAfter triggers a background replacement after the active artifact set + has been Ready for this duration. The active set continues serving until the + replacement is promoted. + type: string + rebuildOnSourceChange: + description: |- + RebuildOnSourceChange triggers a rebuild when the source SandboxTemplate changes. + Defaults to true when nil. + type: boolean + type: object + snapshotClassName: + description: SnapshotClassName references the SnapshotClass that defines + the provider and placement. + type: string + snapshotMode: + description: SnapshotMode selects the usage mode. + enum: + - Fork + type: string + sourceRef: + description: SourceRef references the source SandboxTemplate for the + snapshot. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + required: + - snapshotClassName + - snapshotMode + - sourceRef + type: object + status: + description: SandboxSnapshotStatus describes the observed state of a SandboxSnapshot. + properties: + conditions: + description: Conditions holds machine-readable status signals. Reserved + for future use. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + creatingNodeCount: + description: CreatingNodeCount is the number of target nodes with + an artifact build in progress. + format: int32 + type: integer + failedNodeCount: + description: FailedNodeCount is the number of target nodes where the + build has reached a terminal failure. + format: int32 + type: integer + message: + description: Message is a human-readable summary of the current status. + type: string + phase: + description: Phase is the aggregate state of the snapshot's active + artifact set. + type: string + readyAt: + description: ReadyAt is the time the snapshot first reached Phase=Ready. + format: date-time + type: string + readyNodeCount: + description: ReadyNodeCount is the number of target nodes with a Ready + artifact. + format: int32 + type: integer + targetNodeCount: + description: TargetNodeCount is the number of nodes the controller + intends to cover. + format: int32 + type: integer + unavailableNodeCount: + description: UnavailableNodeCount is the number of target nodes whose + artifact is not consumable. + format: int32 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/manifests/charts/base/crds/runtime.agentcube.volcano.sh_sandboxsnapshottasks.yaml b/manifests/charts/base/crds/runtime.agentcube.volcano.sh_sandboxsnapshottasks.yaml new file mode 100644 index 00000000..bf596a9c --- /dev/null +++ b/manifests/charts/base/crds/runtime.agentcube.volcano.sh_sandboxsnapshottasks.yaml @@ -0,0 +1,157 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: sandboxsnapshottasks.runtime.agentcube.volcano.sh +spec: + group: runtime.agentcube.volcano.sh + names: + kind: SandboxSnapshotTask + listKind: SandboxSnapshotTaskList + plural: sandboxsnapshottasks + singular: sandboxsnapshottask + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.targetNodeName + name: Node + type: string + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + SandboxSnapshotTask is the internal node-facing task object for snapshot builds. + It is created by SandboxSnapshotController and watched by the node agent. + Users do not create this resource. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: SandboxSnapshotTaskSpec describes the node-local snapshot + build to perform. + properties: + providerName: + description: ProviderName identifies the SnapshotDriver to use on + the target node. + type: string + snapshotHash: + description: SnapshotHash is the hash of the snapshot inputs that + affect artifact contents. + type: string + snapshotKey: + description: SnapshotKey is the logical restore reference generated + by SandboxSnapshotController. + type: string + snapshotMode: + description: SnapshotMode is the mode of this snapshot task. + enum: + - Fork + type: string + snapshotRef: + description: SnapshotRef is the owning SandboxSnapshot for this task. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + snapshotUID: + description: |- + SnapshotUID is the UID of the owning SandboxSnapshot. + Used to reject stale tasks after the snapshot is deleted and recreated. + type: string + targetNodeName: + description: TargetNodeName is the node where the snapshot build must + run. + type: string + targetSandboxRef: + description: TargetSandboxRef is the temporary build Sandbox created + by SandboxSnapshotController. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + required: + - providerName + - snapshotHash + - snapshotKey + - snapshotMode + - snapshotRef + - snapshotUID + - targetNodeName + - targetSandboxRef + type: object + status: + description: SandboxSnapshotTaskStatus is written by the node agent after + the snapshot driver reports. + properties: + message: + description: Message is a human-readable description of the current + phase. + type: string + observedAt: + description: ObservedAt is the time the node agent last wrote this + status. + format: date-time + type: string + phase: + description: Phase is the current artifact phase as reported by the + node agent. + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/manifests/charts/base/crds/runtime.agentcube.volcano.sh_snapshotclasses.yaml b/manifests/charts/base/crds/runtime.agentcube.volcano.sh_snapshotclasses.yaml new file mode 100644 index 00000000..20af105f --- /dev/null +++ b/manifests/charts/base/crds/runtime.agentcube.volcano.sh_snapshotclasses.yaml @@ -0,0 +1,74 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: snapshotclasses.runtime.agentcube.volcano.sh +spec: + group: runtime.agentcube.volcano.sh + names: + kind: SnapshotClass + listKind: SnapshotClassList + plural: snapshotclasses + singular: snapshotclass + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + SnapshotClass describes an infrastructure snapshot capability. + It is cluster-scoped and managed by cluster administrators. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: SnapshotClassSpec defines the snapshot provider and artifact + placement strategy. + properties: + nodeSelector: + additionalProperties: + type: string + description: NodeSelector selects which nodes are eligible for snapshot + builds. + type: object + providerName: + description: |- + ProviderName is the stable identifier for the snapshot provider. + It maps to a SnapshotDriver registered in the node agent. + type: string + supportedSnapshotModes: + description: SupportedSnapshotModes lists the snapshot modes this + class supports. + items: + description: SandboxSnapshotMode selects the snapshot usage mode. + enum: + - Fork + type: string + minItems: 1 + type: array + required: + - providerName + - supportedSnapshotModes + type: object + required: + - spec + type: object + served: true + storage: true diff --git a/pkg/agentd/driver_registry.go b/pkg/agentd/driver_registry.go new file mode 100644 index 00000000..c9a2cbb6 --- /dev/null +++ b/pkg/agentd/driver_registry.go @@ -0,0 +1,84 @@ +/* +Copyright The Volcano Authors. + +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 agentd + +import "fmt" + +// DriverFactory is a constructor function for a SnapshotDriver. Each driver +// file calls RegisterDriverFactory in its init() to enroll itself so that +// BuildDefaultRegistry can instantiate all drivers without main knowing their names. +type DriverFactory func() SnapshotDriver + +var defaultFactories []DriverFactory + +// RegisterDriverFactory enrolls a factory in the package-level default list. +// Call this from driver init() functions; never call it after BuildDefaultRegistry. +func RegisterDriverFactory(f DriverFactory) { + defaultFactories = append(defaultFactories, f) +} + +// BuildDefaultRegistry instantiates every factory registered via RegisterDriverFactory +// and returns a ready-to-use DriverRegistry. Returns an error if two factories +// produce drivers with the same name. +func BuildDefaultRegistry() (*DriverRegistry, error) { + r := NewDriverRegistry() + for _, f := range defaultFactories { + if err := r.Register(f()); err != nil { + return nil, err + } + } + return r, nil +} + +// DriverRegistry holds the set of SnapshotDrivers available on this node. +// Use BuildDefaultRegistry to create one from self-registered drivers, or +// NewDriverRegistry + Register for explicit construction (e.g. in tests). +type DriverRegistry struct { + drivers map[string]SnapshotDriver +} + +// NewDriverRegistry returns an empty registry. +func NewDriverRegistry() *DriverRegistry { + return &DriverRegistry{drivers: make(map[string]SnapshotDriver)} +} + +// Register adds a driver to the registry. Returns an error if a driver with +// the same name has already been registered. +func (r *DriverRegistry) Register(driver SnapshotDriver) error { + name := driver.Name() + if _, exists := r.drivers[name]; exists { + return fmt.Errorf("snapshot driver %q already registered", name) + } + r.drivers[name] = driver + return nil +} + +// Get returns the driver registered under the given name, or false if absent. +func (r *DriverRegistry) Get(name string) (SnapshotDriver, bool) { + d, ok := r.drivers[name] + return d, ok +} + +// Drivers returns a shallow copy of the internal driver map. Callers may +// iterate or pass the result to other components without affecting the registry. +func (r *DriverRegistry) Drivers() map[string]SnapshotDriver { + out := make(map[string]SnapshotDriver, len(r.drivers)) + for k, v := range r.drivers { + out[k] = v + } + return out +} diff --git a/pkg/agentd/kuasar_driver.go b/pkg/agentd/kuasar_driver.go new file mode 100644 index 00000000..ccf7fee4 --- /dev/null +++ b/pkg/agentd/kuasar_driver.go @@ -0,0 +1,292 @@ +/* +Copyright The Volcano Authors. + +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 agentd + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "net" + "time" + + "k8s.io/klog/v2" + + runtimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" +) + +const ( + // KuasarProviderName is the stable provider identifier for the Kuasar SnapStart driver. + KuasarProviderName = "snapstart.kuasar.io" + + // defaultKuasarSocketPath is the default path for the Kuasar admin socket. + defaultKuasarSocketPath = "/run/vmm-sandboxer-admin.sock" + + // kuasarReadinessTimeout is the maximum time to wait for the runtime readiness probe. + kuasarReadinessTimeout = 5 * time.Minute + + // kuasarReadinessPollInterval is the polling interval for the readiness probe. + kuasarReadinessPollInterval = 2 * time.Second +) + +// KuasarDriver implements SnapshotDriver for Kuasar WarmFork snapshots. +// It connects to the node-local Kuasar admin socket to drive snapshot creation. +// +// Integration note: Kuasar uses an inject-socket readiness sequence before accepting +// snapshot commands. The driver performs the CAPABILITIES -> PREPARE -> READY -> COMMIT +// handshake. When Create() returns, the artifact is detached from the build Sandbox +// lifecycle and safe to restore after the Sandbox is deleted. +type KuasarDriver struct { + // SocketPath is the path to the Kuasar admin Unix socket. + SocketPath string +} + +// NewKuasarDriver creates a KuasarDriver with the given socket path. +// Pass an empty string to use the default path. +func NewKuasarDriver(socketPath string) *KuasarDriver { + if socketPath == "" { + socketPath = defaultKuasarSocketPath + } + return &KuasarDriver{SocketPath: socketPath} +} + +func (d *KuasarDriver) Name() string { return KuasarProviderName } + +func (d *KuasarDriver) Capabilities(_ context.Context) SnapshotDriverCapabilities { + return SnapshotDriverCapabilities{ + SnapshotModes: []runtimev1alpha1.SandboxSnapshotMode{ + runtimev1alpha1.SandboxSnapshotModeFork, + }, + } +} + +// Create drives the Kuasar SnapStart snapshot creation for one build Sandbox. +// It blocks until the runtime has reached a fork-safe point and the artifact is ready. +func (d *KuasarDriver) Create(ctx context.Context, req SnapshotDriverCreateRequest) (*SnapshotDriverArtifact, error) { + klog.V(2).InfoS("kuasar driver: creating snapshot", + "snapshotKey", req.SnapshotKey, + "sandbox", req.TargetSandboxRef.Name, + "mode", req.SnapshotMode) + + // Connect to the Kuasar admin socket. + conn, err := d.dialSocket(ctx) + if err != nil { + return nil, fmt.Errorf("kuasar driver: connect to %s: %w", d.SocketPath, err) + } + defer conn.Close() + + // Perform the CAPABILITIES -> PREPARE -> READY -> COMMIT handshake. + // The sandbox name is used to identify the target VM in Kuasar. + sandboxID := req.TargetSandboxRef.Name + if err := d.performHandshake(ctx, conn, sandboxID, req.SnapshotKey, req.SnapshotMode); err != nil { + return nil, fmt.Errorf("kuasar driver: handshake for sandbox %s: %w", sandboxID, err) + } + + klog.V(2).InfoS("kuasar driver: snapshot created", + "snapshotKey", req.SnapshotKey, + "sandbox", sandboxID) + + return &SnapshotDriverArtifact{ + ProviderName: KuasarProviderName, + SnapshotKey: req.SnapshotKey, + SnapshotHash: req.SnapshotHash, + // ProviderRef is the snapshot key; Kuasar resolves it via node-local metadata. + ProviderRef: req.SnapshotKey, + }, nil +} + +func (d *KuasarDriver) Delete(ctx context.Context, artifact SnapshotDriverArtifact) error { + conn, err := d.dialSocket(ctx) + if err != nil { + return fmt.Errorf("kuasar driver: connect for delete: %w", err) + } + defer conn.Close() + return d.sendDeleteCommand(ctx, conn, artifact.SnapshotKey) +} + +func (d *KuasarDriver) List(ctx context.Context) ([]SnapshotDriverArtifact, error) { + conn, err := d.dialSocket(ctx) + if err != nil { + return nil, fmt.Errorf("kuasar driver: connect for list: %w", err) + } + defer conn.Close() + return d.sendListCommand(ctx, conn) +} + +func (d *KuasarDriver) Inspect(ctx context.Context, artifact SnapshotDriverArtifact) (*SnapshotDriverArtifactStatus, error) { + conn, err := d.dialSocket(ctx) + if err != nil { + return nil, fmt.Errorf("kuasar driver: connect for inspect: %w", err) + } + defer conn.Close() + return d.sendInspectCommand(ctx, conn, artifact.SnapshotKey) +} + +// dialSocket establishes a Unix socket connection to the Kuasar admin socket. +func (d *KuasarDriver) dialSocket(ctx context.Context) (net.Conn, error) { + dialer := &net.Dialer{} + conn, err := dialer.DialContext(ctx, "unix", d.SocketPath) + if err != nil { + return nil, fmt.Errorf("dial unix %s: %w", d.SocketPath, err) + } + _ = conn.SetDeadline(time.Now().Add(kuasarReadinessTimeout + time.Minute)) + return conn, nil +} + +// performHandshake executes the Kuasar inject-socket protocol sequence. +// The sequence is: CAPABILITIES -> PREPARE -> READY (wait for runtime) -> COMMIT -> STARTED. +// +// TODO(maintainer): Replace the stub JSON framing below with the actual Kuasar +// admin-socket protocol once the upstream API is stabilized. The steps and +// semantics match the design document (§7.6). +func (d *KuasarDriver) performHandshake(ctx context.Context, conn net.Conn, sandboxID, snapshotKey string, mode runtimev1alpha1.SandboxSnapshotMode) error { + rd := bufio.NewReader(conn) + + // Step 1: CAPABILITIES — exchange supported actions. + if err := d.sendCommand(conn, kuasarCommand{Action: "CAPABILITIES", SandboxID: sandboxID}); err != nil { + return fmt.Errorf("CAPABILITIES send: %w", err) + } + if _, err := d.readResponse(rd); err != nil { + return fmt.Errorf("CAPABILITIES response: %w", err) + } + + // Step 2: PREPARE — declare intent to snapshot. + if err := d.sendCommand(conn, kuasarCommand{ + Action: "PREPARE", + SandboxID: sandboxID, + SnapshotKey: snapshotKey, + Mode: string(mode), + }); err != nil { + return fmt.Errorf("PREPARE send: %w", err) + } + if _, err := d.readResponse(rd); err != nil { + return fmt.Errorf("PREPARE response: %w", err) + } + + // Step 3: READY — wait for the runtime to signal that it is at a fork-safe point. + // The runtime signals readiness via the inject socket; we poll with a timeout. + readinessCtx, cancel := context.WithTimeout(ctx, kuasarReadinessTimeout) + defer cancel() + if err := d.waitForReadiness(readinessCtx, conn, rd, sandboxID); err != nil { + return fmt.Errorf("waiting for runtime readiness: %w", err) + } + + // Step 4: COMMIT — trigger the VMM snapshot. + if err := d.sendCommand(conn, kuasarCommand{Action: "COMMIT", SandboxID: sandboxID}); err != nil { + return fmt.Errorf("COMMIT send: %w", err) + } + if _, err := d.readResponse(rd); err != nil { + return fmt.Errorf("COMMIT response: %w", err) + } + + return nil +} + +// waitForReadiness polls the runtime readiness signal via the inject socket. +// For Fork mode, "ready" means bootstrap is complete and no user state has been loaded. +func (d *KuasarDriver) waitForReadiness(ctx context.Context, conn net.Conn, rd *bufio.Reader, sandboxID string) error { + ticker := time.NewTicker(kuasarReadinessPollInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("timed out waiting for runtime readiness for sandbox %s", sandboxID) + case <-ticker.C: + if err := d.sendCommand(conn, kuasarCommand{Action: "READY", SandboxID: sandboxID}); err != nil { + return fmt.Errorf("READY send: %w", err) + } + resp, err := d.readResponse(rd) + if err != nil { + return fmt.Errorf("READY response: %w", err) + } + if resp.Ready { + return nil + } + } + } +} + +// kuasarCommand is the wire format for commands sent to the Kuasar admin socket. +// The actual wire format is Kuasar-internal; this struct is a placeholder. +type kuasarCommand struct { + Action string `json:"action"` + SandboxID string `json:"sandboxId"` + SnapshotKey string `json:"snapshotKey,omitempty"` + Mode string `json:"mode,omitempty"` +} + +// kuasarResponse is the wire format for responses from the Kuasar admin socket. +type kuasarResponse struct { + Ready bool `json:"ready"` + Error string `json:"error,omitempty"` +} + +func (d *KuasarDriver) sendCommand(conn net.Conn, cmd kuasarCommand) error { + // TODO(maintainer): replace with actual Kuasar wire protocol framing once stabilized. + data, err := json.Marshal(cmd) + if err != nil { + return fmt.Errorf("marshal command: %w", err) + } + data = append(data, '\n') + if _, err := conn.Write(data); err != nil { + return fmt.Errorf("write command: %w", err) + } + return nil +} + +func (d *KuasarDriver) readResponse(rd *bufio.Reader) (*kuasarResponse, error) { + // TODO(maintainer): replace with actual Kuasar wire protocol framing once stabilized. + line, err := rd.ReadString('\n') + if err != nil { + return nil, fmt.Errorf("read response: %w", err) + } + resp := &kuasarResponse{} + if err := json.Unmarshal([]byte(line), resp); err != nil { + return nil, fmt.Errorf("unmarshal response: %w", err) + } + if resp.Error != "" { + return nil, fmt.Errorf("kuasar error: %s", resp.Error) + } + return resp, nil +} + +func (d *KuasarDriver) sendDeleteCommand(_ context.Context, conn net.Conn, snapshotKey string) error { + return d.sendCommand(conn, kuasarCommand{Action: "DELETE", SnapshotKey: snapshotKey}) +} + +func (d *KuasarDriver) sendListCommand(_ context.Context, _ net.Conn) ([]SnapshotDriverArtifact, error) { + // TODO(maintainer): implement list via Kuasar admin socket. + return nil, nil +} + +func init() { + RegisterDriverFactory(func() SnapshotDriver { + return NewKuasarDriver("") + }) +} + +func (d *KuasarDriver) sendInspectCommand(_ context.Context, conn net.Conn, snapshotKey string) (*SnapshotDriverArtifactStatus, error) { + if err := d.sendCommand(conn, kuasarCommand{Action: "INSPECT", SnapshotKey: snapshotKey}); err != nil { + return nil, err + } + if _, err := d.readResponse(bufio.NewReader(conn)); err != nil { + return nil, err + } + // TODO(maintainer): map Kuasar inspect response fields to artifact status. + return &SnapshotDriverArtifactStatus{Phase: runtimev1alpha1.SnapshotArtifactPhaseReady}, nil +} diff --git a/pkg/agentd/node_capability.go b/pkg/agentd/node_capability.go new file mode 100644 index 00000000..872bca8a --- /dev/null +++ b/pkg/agentd/node_capability.go @@ -0,0 +1,85 @@ +/* +Copyright The Volcano Authors. + +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 agentd + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "k8s.io/klog/v2" + + runtimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" +) + +// AdvertiseDriverCapabilities patches the node with snapshot provider capability labels +// derived from the registered SnapshotDrivers. Each driver that reports at least one +// supported mode gets a label of the form: +// +// agentcube.volcano.sh/snapshot-provider.=true +// +// Labels for providers no longer registered are removed so that stale nodes are not +// selected for snapshot builds after a driver is removed. +func AdvertiseDriverCapabilities(ctx context.Context, cs kubernetes.Interface, nodeName string, drivers map[string]SnapshotDriver) error { + // Build the desired provider label set. + desired := make(map[string]string, len(drivers)) + for _, driver := range drivers { + caps := driver.Capabilities(ctx) + if len(caps.SnapshotModes) > 0 { + desired[runtimev1alpha1.SnapshotProviderLabelPrefix+driver.Name()] = "true" + } + } + + // Read the current node to find stale provider labels. + node, err := cs.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("get node %s: %w", nodeName, err) + } + + // Compute the merge-patch: set desired labels, null-delete stale ones. + patchLabels := make(map[string]interface{}, len(desired)) + for k, v := range desired { + patchLabels[k] = v + } + for k := range node.Labels { + if strings.HasPrefix(k, runtimev1alpha1.SnapshotProviderLabelPrefix) { + if _, ok := desired[k]; !ok { + patchLabels[k] = nil // null in merge-patch removes the key + } + } + } + if len(patchLabels) == 0 { + return nil + } + + data, err := json.Marshal(map[string]interface{}{ + "metadata": map[string]interface{}{"labels": patchLabels}, + }) + if err != nil { + return fmt.Errorf("marshal label patch: %w", err) + } + if _, err := cs.CoreV1().Nodes().Patch(ctx, nodeName, types.MergePatchType, data, metav1.PatchOptions{}); err != nil { + return fmt.Errorf("patch node %s labels: %w", nodeName, err) + } + + klog.V(2).InfoS("advertised snapshot driver capabilities on node", "node", nodeName, "labels", patchLabels) + return nil +} diff --git a/pkg/agentd/snapshot_driver.go b/pkg/agentd/snapshot_driver.go new file mode 100644 index 00000000..716fe67d --- /dev/null +++ b/pkg/agentd/snapshot_driver.go @@ -0,0 +1,107 @@ +/* +Copyright The Volcano Authors. + +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 agentd + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + + runtimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" +) + +// SnapshotDriver is an in-process interface implemented by each snapshot provider. +// Node-agent-local implementations call the provider's runtime or VMM directly. +// Each implementation is registered under a stable provider name and selected by +// SandboxSnapshotTask.spec.providerName. +type SnapshotDriver interface { + // Name returns the stable provider name (e.g. "snapstart.kuasar.io"). + Name() string + + // Capabilities returns the capabilities advertised by this driver. + Capabilities(ctx context.Context) SnapshotDriverCapabilities + + // Create performs the snapshot and returns a Ready artifact. + // It must only return successfully after the runtime has reached a fork-safe + // snapshot point (e.g. bootstrap complete, no user state present). + // The artifact must be usable for restore after the build Sandbox is deleted. + Create(ctx context.Context, req SnapshotDriverCreateRequest) (*SnapshotDriverArtifact, error) + + // Delete removes the physical artifact. + Delete(ctx context.Context, artifact SnapshotDriverArtifact) error + + // List enumerates all artifacts managed by this driver on this node. + List(ctx context.Context) ([]SnapshotDriverArtifact, error) + + // Inspect returns the current status of an artifact. + Inspect(ctx context.Context, artifact SnapshotDriverArtifact) (*SnapshotDriverArtifactStatus, error) +} + +// SnapshotDriverCapabilities describes what a driver can do. +type SnapshotDriverCapabilities struct { + // SnapshotModes lists the modes this driver supports. + SnapshotModes []runtimev1alpha1.SandboxSnapshotMode +} + +// SnapshotDriverCreateRequest carries the inputs for a snapshot creation call. +type SnapshotDriverCreateRequest struct { + // TaskRef is the Kubernetes object reference to the SandboxSnapshotTask. + TaskRef corev1.ObjectReference + + // TargetSandboxRef identifies the Sandbox to snapshot. + TargetSandboxRef corev1.TypedLocalObjectReference + + // TargetNodeName is the node where the build Sandbox is running. + TargetNodeName string + + // SnapshotMode is the mode of the snapshot (Fork or Resume). + SnapshotMode runtimev1alpha1.SandboxSnapshotMode + + // ProviderName is the stable provider identifier. + ProviderName string + + // SnapshotKey is the logical restore reference. + SnapshotKey string + + // SnapshotHash is the hash of the snapshot inputs. + SnapshotHash string +} + +// SnapshotDriverArtifact is the driver's representation of a created artifact. +type SnapshotDriverArtifact struct { + // ProviderName identifies the driver that created the artifact. + ProviderName string + + // SnapshotKey is the logical restore reference for this artifact. + SnapshotKey string + + // SnapshotHash is the hash of the snapshot inputs. + SnapshotHash string + + // ProviderRef is a driver-private reference to the physical artifact + // (e.g. a path, an ID, a URL). Opaque to the AgentCube control plane. + ProviderRef string +} + +// SnapshotDriverArtifactStatus describes the current condition of an artifact. +type SnapshotDriverArtifactStatus struct { + // Phase is the current artifact phase as seen by the driver. + Phase runtimev1alpha1.SnapshotArtifactPhase + + // Message is a human-readable description. + Message string +} diff --git a/pkg/agentd/snapshot_mode.go b/pkg/agentd/snapshot_mode.go new file mode 100644 index 00000000..d784d4c5 --- /dev/null +++ b/pkg/agentd/snapshot_mode.go @@ -0,0 +1,75 @@ +/* +Copyright The Volcano Authors. + +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 agentd + +import ( + "context" + "fmt" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + sandboxv1alpha1 "sigs.k8s.io/agent-sandbox/api/v1alpha1" + + runtimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" +) + +// SnapshotModeTaskHandler encapsulates mode-specific logic for executing a SandboxSnapshotTask. +// To add support for a new snapshot mode in the node agent, implement this interface +// and register it in SnapshotTaskReconciler.SetupWithManager. +type SnapshotModeTaskHandler interface { + // ValidateTarget checks whether the target sandbox is in the expected state for snapshotting. + // Returns (result, done, err): done=true means the reconcile should stop for this cycle. + ValidateTarget(ctx context.Context, task *runtimev1alpha1.SandboxSnapshotTask) (ctrl.Result, bool, error) +} + +// ForkModeTaskHandler implements SnapshotModeTaskHandler for Fork-mode snapshot tasks. +// It waits until the build Sandbox is Running before allowing the snapshot driver to proceed. +type ForkModeTaskHandler struct { + client.Client +} + +func (h *ForkModeTaskHandler) ValidateTarget(ctx context.Context, task *runtimev1alpha1.SandboxSnapshotTask) (ctrl.Result, bool, error) { + running, err := h.isSandboxRunning(ctx, task.Namespace, task.Spec.TargetSandboxRef.Name) + if err != nil { + return ctrl.Result{}, true, err + } + if !running { + return ctrl.Result{RequeueAfter: 5 * time.Second}, true, nil + } + return ctrl.Result{}, false, nil +} + +func (h *ForkModeTaskHandler) isSandboxRunning(ctx context.Context, namespace, name string) (bool, error) { + sandbox := &sandboxv1alpha1.Sandbox{} + if err := h.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, sandbox); err != nil { + if apierrors.IsNotFound(err) { + return false, nil + } + return false, fmt.Errorf("get sandbox %s/%s: %w", namespace, name, err) + } + for _, cond := range sandbox.Status.Conditions { + if cond.Type == string(sandboxv1alpha1.SandboxConditionReady) && cond.Status == metav1.ConditionTrue { + return true, nil + } + } + return false, nil +} diff --git a/pkg/agentd/snapshot_task_reconciler.go b/pkg/agentd/snapshot_task_reconciler.go new file mode 100644 index 00000000..49787c28 --- /dev/null +++ b/pkg/agentd/snapshot_task_reconciler.go @@ -0,0 +1,182 @@ +/* +Copyright The Volcano Authors. + +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 agentd + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + 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/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + runtimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" +) + +// SnapshotTaskReconciler watches SandboxSnapshotTask objects assigned to this node +// and drives snapshot creation via the registered SnapshotDriver. +// Mode-specific target validation is delegated to the registered SnapshotModeTaskHandler. +type SnapshotTaskReconciler struct { + client.Client + Scheme *runtime.Scheme + NodeName string + Drivers map[string]SnapshotDriver + ModeHandlers map[runtimev1alpha1.SandboxSnapshotMode]SnapshotModeTaskHandler +} + +func (r *SnapshotTaskReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + task := &runtimev1alpha1.SandboxSnapshotTask{} + if err := r.Get(ctx, req.NamespacedName, task); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Only handle tasks targeting this node. + if task.Spec.TargetNodeName != r.NodeName { + return ctrl.Result{}, nil + } + + // Skip tasks that have already reached a terminal phase. + if task.Status.Phase == runtimev1alpha1.SnapshotArtifactPhaseReady || + task.Status.Phase == runtimev1alpha1.SnapshotArtifactPhaseFailed { + return ctrl.Result{}, nil + } + + if done, err := r.validateSnapshotOwner(ctx, task); done || err != nil { + return ctrl.Result{}, err + } + + // Select driver. + driver, ok := r.Drivers[task.Spec.ProviderName] + if !ok { + return r.reportFailed(ctx, task, fmt.Sprintf("no driver registered for provider %q", task.Spec.ProviderName)) + } + + // Validate driver capabilities. + caps := driver.Capabilities(ctx) + if !containsSnapshotMode(caps.SnapshotModes, task.Spec.SnapshotMode) { + return r.reportFailed(ctx, task, fmt.Sprintf("driver does not support snapshot mode %q", task.Spec.SnapshotMode)) + } + + if result, done, err := r.validateTargetSandbox(ctx, task); done || err != nil { + return result, err + } + + logger.Info("calling snapshot driver", "task", task.Name, "provider", task.Spec.ProviderName) + + artifact, err := driver.Create(ctx, SnapshotDriverCreateRequest{ + TaskRef: corev1.ObjectReference{ + APIVersion: runtimev1alpha1.GroupVersion.String(), + Kind: "SandboxSnapshotTask", + Namespace: task.Namespace, + Name: task.Name, + UID: task.UID, + }, + TargetSandboxRef: task.Spec.TargetSandboxRef, + TargetNodeName: task.Spec.TargetNodeName, + SnapshotMode: task.Spec.SnapshotMode, + ProviderName: task.Spec.ProviderName, + SnapshotKey: task.Spec.SnapshotKey, + SnapshotHash: task.Spec.SnapshotHash, + }) + if err != nil { + logger.Error(err, "snapshot driver create failed", "task", task.Name) + return r.reportFailed(ctx, task, err.Error()) + } + + logger.Info("snapshot driver create succeeded", "task", task.Name, "snapshotKey", artifact.SnapshotKey) + return r.reportReady(ctx, task) +} + +func (r *SnapshotTaskReconciler) validateSnapshotOwner(ctx context.Context, task *runtimev1alpha1.SandboxSnapshotTask) (bool, error) { + logger := log.FromContext(ctx) + snapshot := &runtimev1alpha1.SandboxSnapshot{} + if err := r.Get(ctx, types.NamespacedName{Name: task.Spec.SnapshotRef.Name, Namespace: task.Namespace}, snapshot); err != nil { + if apierrors.IsNotFound(err) { + logger.Info("owning snapshot not found, skipping task", "task", task.Name) + return true, nil + } + return true, err + } + if snapshot.UID != task.Spec.SnapshotUID { + logger.Info("snapshot UID mismatch, skipping stale task", "task", task.Name) + return true, nil + } + return false, nil +} + +func (r *SnapshotTaskReconciler) validateTargetSandbox(ctx context.Context, task *runtimev1alpha1.SandboxSnapshotTask) (ctrl.Result, bool, error) { + handler, ok := r.ModeHandlers[task.Spec.SnapshotMode] + if !ok { + result, err := r.reportFailed(ctx, task, fmt.Sprintf("unsupported snapshot mode %q", task.Spec.SnapshotMode)) + return result, true, err + } + return handler.ValidateTarget(ctx, task) +} + +func (r *SnapshotTaskReconciler) reportReady(ctx context.Context, task *runtimev1alpha1.SandboxSnapshotTask) (ctrl.Result, error) { + return ctrl.Result{}, r.patchTaskStatus(ctx, task, runtimev1alpha1.SnapshotArtifactPhaseReady, "") +} + +func (r *SnapshotTaskReconciler) reportFailed(ctx context.Context, task *runtimev1alpha1.SandboxSnapshotTask, msg string) (ctrl.Result, error) { + return ctrl.Result{}, r.patchTaskStatus(ctx, task, runtimev1alpha1.SnapshotArtifactPhaseFailed, msg) +} + +func (r *SnapshotTaskReconciler) patchTaskStatus(ctx context.Context, task *runtimev1alpha1.SandboxSnapshotTask, phase runtimev1alpha1.SnapshotArtifactPhase, msg string) error { + patch := client.MergeFrom(task.DeepCopy()) + now := metav1.Now() + task.Status.Phase = phase + task.Status.Message = msg + task.Status.ObservedAt = &now + return r.Status().Patch(ctx, task, patch) +} + +func containsSnapshotMode(modes []runtimev1alpha1.SandboxSnapshotMode, mode runtimev1alpha1.SandboxSnapshotMode) bool { + for _, m := range modes { + if m == mode { + return true + } + } + return false +} + +// SetupWithManager registers the controller and initializes mode handlers. +func (r *SnapshotTaskReconciler) SetupWithManager(mgr ctrl.Manager) error { + r.ModeHandlers = map[runtimev1alpha1.SandboxSnapshotMode]SnapshotModeTaskHandler{ + runtimev1alpha1.SandboxSnapshotModeFork: &ForkModeTaskHandler{Client: r.Client}, + } + + nodeFilter := predicate.NewPredicateFuncs(func(obj client.Object) bool { + task, ok := obj.(*runtimev1alpha1.SandboxSnapshotTask) + if !ok { + return false + } + return task.Spec.TargetNodeName == r.NodeName + }) + + return ctrl.NewControllerManagedBy(mgr). + For(&runtimev1alpha1.SandboxSnapshotTask{}, builder.WithPredicates(nodeFilter)). + Complete(r) +} diff --git a/pkg/apis/runtime/v1alpha1/snapshot_types.go b/pkg/apis/runtime/v1alpha1/snapshot_types.go new file mode 100644 index 00000000..8362b4de --- /dev/null +++ b/pkg/apis/runtime/v1alpha1/snapshot_types.go @@ -0,0 +1,283 @@ +/* +Copyright The Volcano Authors. + +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 ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +// SnapshotClass describes an infrastructure snapshot capability. +// It is cluster-scoped and managed by cluster administrators. +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Cluster +type SnapshotClass struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec SnapshotClassSpec `json:"spec"` +} + +// SnapshotClassSpec defines the snapshot provider and artifact placement strategy. +type SnapshotClassSpec struct { + // ProviderName is the stable identifier for the snapshot provider. + // It maps to a SnapshotDriver registered in the node agent. + // +kubebuilder:validation:Required + ProviderName string `json:"providerName"` + + // SupportedSnapshotModes lists the snapshot modes this class supports. + // +kubebuilder:validation:MinItems=1 + SupportedSnapshotModes []SandboxSnapshotMode `json:"supportedSnapshotModes"` + + // NodeSelector selects which nodes are eligible for snapshot builds. + // +optional + NodeSelector map[string]string `json:"nodeSelector,omitempty"` +} + +// SnapshotClassList contains a list of SnapshotClass. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +type SnapshotClassList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []SnapshotClass `json:"items"` +} + +// SandboxSnapshot declares a snapshot of an agent-sandbox execution environment. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced +// +kubebuilder:printcolumn:name="Mode",type="string",JSONPath=".spec.snapshotMode" +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase" +// +kubebuilder:printcolumn:name="Ready",type="integer",JSONPath=".status.readyNodeCount" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +type SandboxSnapshot struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec SandboxSnapshotSpec `json:"spec"` + Status SandboxSnapshotStatus `json:"status,omitempty"` +} + +// SandboxSnapshotSpec defines the desired state of a SandboxSnapshot. +type SandboxSnapshotSpec struct { + // SnapshotMode selects the usage mode. + // +kubebuilder:validation:Required + SnapshotMode SandboxSnapshotMode `json:"snapshotMode"` + + // SourceRef references the source SandboxTemplate for the snapshot. + // +kubebuilder:validation:Required + SourceRef corev1.TypedLocalObjectReference `json:"sourceRef"` + + // SnapshotClassName references the SnapshotClass that defines the provider and placement. + // +kubebuilder:validation:Required + SnapshotClassName string `json:"snapshotClassName"` + + // ForkPolicy controls when a Fork snapshot is rebuilt. + // +optional + ForkPolicy *SandboxSnapshotForkPolicy `json:"forkPolicy,omitempty"` +} + +// SandboxSnapshotMode selects the snapshot usage mode. +// +kubebuilder:validation:Enum=Fork +type SandboxSnapshotMode string + +const ( + // SandboxSnapshotModeFork creates a reusable baseline for 1:N forking. + SandboxSnapshotModeFork SandboxSnapshotMode = "Fork" +) + +// SandboxSnapshotForkPolicy describes when a Fork snapshot is rebuilt. +type SandboxSnapshotForkPolicy struct { + // RebuildOnSourceChange triggers a rebuild when the source SandboxTemplate changes. + // Defaults to true when nil. + // +optional + RebuildOnSourceChange *bool `json:"rebuildOnSourceChange,omitempty"` + + // RebuildAfter triggers a background replacement after the active artifact set + // has been Ready for this duration. The active set continues serving until the + // replacement is promoted. + // +optional + RebuildAfter *metav1.Duration `json:"rebuildAfter,omitempty"` +} + +// SandboxSnapshotStatus describes the observed state of a SandboxSnapshot. +type SandboxSnapshotStatus struct { + // Phase is the aggregate state of the snapshot's active artifact set. + Phase SandboxSnapshotPhase `json:"phase,omitempty"` + + // TargetNodeCount is the number of nodes the controller intends to cover. + TargetNodeCount int32 `json:"targetNodeCount,omitempty"` + + // CreatingNodeCount is the number of target nodes with an artifact build in progress. + CreatingNodeCount int32 `json:"creatingNodeCount,omitempty"` + + // ReadyNodeCount is the number of target nodes with a Ready artifact. + ReadyNodeCount int32 `json:"readyNodeCount,omitempty"` + + // FailedNodeCount is the number of target nodes where the build has reached a terminal failure. + FailedNodeCount int32 `json:"failedNodeCount,omitempty"` + + // UnavailableNodeCount is the number of target nodes whose artifact is not consumable. + UnavailableNodeCount int32 `json:"unavailableNodeCount,omitempty"` + + // ReadyAt is the time the snapshot first reached Phase=Ready. + // +optional + ReadyAt *metav1.Time `json:"readyAt,omitempty"` + + // Conditions holds machine-readable status signals. Reserved for future use. + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // Message is a human-readable summary of the current status. + // +optional + Message string `json:"message,omitempty"` +} + +// SandboxSnapshotPhase describes the aggregate lifecycle phase of a SandboxSnapshot. +type SandboxSnapshotPhase string + +const ( + SandboxSnapshotPhasePending SandboxSnapshotPhase = "Pending" + SandboxSnapshotPhaseCreating SandboxSnapshotPhase = "Creating" + SandboxSnapshotPhaseReady SandboxSnapshotPhase = "Ready" + SandboxSnapshotPhaseFailed SandboxSnapshotPhase = "Failed" +) + +// SandboxSnapshotList contains a list of SandboxSnapshot. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +type SandboxSnapshotList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []SandboxSnapshot `json:"items"` +} + +// SandboxSnapshotTask is the internal node-facing task object for snapshot builds. +// It is created by SandboxSnapshotController and watched by the node agent. +// Users do not create this resource. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced +// +kubebuilder:printcolumn:name="Node",type="string",JSONPath=".spec.targetNodeName" +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +type SandboxSnapshotTask struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec SandboxSnapshotTaskSpec `json:"spec"` + Status SandboxSnapshotTaskStatus `json:"status,omitempty"` +} + +// SandboxSnapshotTaskSpec describes the node-local snapshot build to perform. +type SandboxSnapshotTaskSpec struct { + // SnapshotRef is the owning SandboxSnapshot for this task. + SnapshotRef corev1.TypedLocalObjectReference `json:"snapshotRef"` + + // SnapshotUID is the UID of the owning SandboxSnapshot. + // Used to reject stale tasks after the snapshot is deleted and recreated. + SnapshotUID types.UID `json:"snapshotUID"` + + // SnapshotMode is the mode of this snapshot task. + SnapshotMode SandboxSnapshotMode `json:"snapshotMode"` + + // TargetSandboxRef is the temporary build Sandbox created by SandboxSnapshotController. + TargetSandboxRef corev1.TypedLocalObjectReference `json:"targetSandboxRef"` + + // TargetNodeName is the node where the snapshot build must run. + TargetNodeName string `json:"targetNodeName"` + + // ProviderName identifies the SnapshotDriver to use on the target node. + ProviderName string `json:"providerName"` + + // SnapshotKey is the logical restore reference generated by SandboxSnapshotController. + SnapshotKey string `json:"snapshotKey"` + + // SnapshotHash is the hash of the snapshot inputs that affect artifact contents. + SnapshotHash string `json:"snapshotHash"` +} + +// SandboxSnapshotTaskStatus is written by the node agent after the snapshot driver reports. +type SandboxSnapshotTaskStatus struct { + // Phase is the current artifact phase as reported by the node agent. + Phase SnapshotArtifactPhase `json:"phase,omitempty"` + + // Message is a human-readable description of the current phase. + // +optional + Message string `json:"message,omitempty"` + + // ObservedAt is the time the node agent last wrote this status. + // +optional + ObservedAt *metav1.Time `json:"observedAt,omitempty"` +} + +// SnapshotArtifactPhase describes the lifecycle phase of a single node-local artifact. +type SnapshotArtifactPhase string + +const ( + SnapshotArtifactPhaseCreating SnapshotArtifactPhase = "Creating" + SnapshotArtifactPhaseReady SnapshotArtifactPhase = "Ready" + SnapshotArtifactPhaseFailed SnapshotArtifactPhase = "Failed" + SnapshotArtifactPhaseUnavailable SnapshotArtifactPhase = "Unavailable" +) + +// SandboxSnapshotTaskList contains a list of SandboxSnapshotTask. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +type SandboxSnapshotTaskList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []SandboxSnapshotTask `json:"items"` +} + +// Annotation keys used for snapshot restore intent on session Sandboxes. +const ( + // SnapshotKeyAnnotation is set on a session Sandbox to request restore from the + // given snapshot key during Pod sandbox creation. + SnapshotKeyAnnotation = "agentcube.volcano.sh/snapshot-key" +) + +// Label keys used for snapshot build tracking. +const ( + // SnapshotNameLabelKey identifies the owning SandboxSnapshot for build Sandboxes and tasks. + SnapshotNameLabelKey = "agentcube.volcano.sh/snapshot-name" + // SnapshotKeyLabelKey identifies the snapshot key version for idempotent task lookup. + SnapshotKeyLabelKey = "agentcube.volcano.sh/snapshot-key" + // SnapshotNodeLabelKey identifies the target node for build Sandboxes and tasks. + SnapshotNodeLabelKey = "agentcube.volcano.sh/snapshot-node" + // SnapshotBuildLabelKey marks a Sandbox as a temporary fork-mode build sandbox. + SnapshotBuildLabelKey = "agentcube.volcano.sh/snapshot-build" +) + +// Node label key used to advertise snapshot provider capability. +const ( + // SnapshotProviderLabelPrefix is prepended by the provider name to form a node label. + // Example: agentcube.volcano.sh/snapshot-provider.snapstart.kuasar.io=true + SnapshotProviderLabelPrefix = "agentcube.volcano.sh/snapshot-provider." +) + +func init() { + SchemeBuilder.Register(&SandboxSnapshot{}, &SandboxSnapshotList{}) + SchemeBuilder.Register(&SandboxSnapshotTask{}, &SandboxSnapshotTaskList{}) + SchemeBuilder.Register(&SnapshotClass{}, &SnapshotClassList{}) +} diff --git a/pkg/apis/runtime/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/runtime/v1alpha1/zz_generated.deepcopy.go index a7dbb4d4..b204e666 100644 --- a/pkg/apis/runtime/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/runtime/v1alpha1/zz_generated.deepcopy.go @@ -320,6 +320,232 @@ func (in *CodeInterpreterStatus) DeepCopy() *CodeInterpreterStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxSnapshot) DeepCopyInto(out *SandboxSnapshot) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxSnapshot. +func (in *SandboxSnapshot) DeepCopy() *SandboxSnapshot { + if in == nil { + return nil + } + out := new(SandboxSnapshot) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SandboxSnapshot) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxSnapshotForkPolicy) DeepCopyInto(out *SandboxSnapshotForkPolicy) { + *out = *in + if in.RebuildOnSourceChange != nil { + in, out := &in.RebuildOnSourceChange, &out.RebuildOnSourceChange + *out = new(bool) + **out = **in + } + if in.RebuildAfter != nil { + in, out := &in.RebuildAfter, &out.RebuildAfter + *out = new(v1.Duration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxSnapshotForkPolicy. +func (in *SandboxSnapshotForkPolicy) DeepCopy() *SandboxSnapshotForkPolicy { + if in == nil { + return nil + } + out := new(SandboxSnapshotForkPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxSnapshotList) DeepCopyInto(out *SandboxSnapshotList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]SandboxSnapshot, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxSnapshotList. +func (in *SandboxSnapshotList) DeepCopy() *SandboxSnapshotList { + if in == nil { + return nil + } + out := new(SandboxSnapshotList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SandboxSnapshotList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxSnapshotSpec) DeepCopyInto(out *SandboxSnapshotSpec) { + *out = *in + in.SourceRef.DeepCopyInto(&out.SourceRef) + if in.ForkPolicy != nil { + in, out := &in.ForkPolicy, &out.ForkPolicy + *out = new(SandboxSnapshotForkPolicy) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxSnapshotSpec. +func (in *SandboxSnapshotSpec) DeepCopy() *SandboxSnapshotSpec { + if in == nil { + return nil + } + out := new(SandboxSnapshotSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxSnapshotStatus) DeepCopyInto(out *SandboxSnapshotStatus) { + *out = *in + if in.ReadyAt != nil { + in, out := &in.ReadyAt, &out.ReadyAt + *out = (*in).DeepCopy() + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxSnapshotStatus. +func (in *SandboxSnapshotStatus) DeepCopy() *SandboxSnapshotStatus { + if in == nil { + return nil + } + out := new(SandboxSnapshotStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxSnapshotTask) DeepCopyInto(out *SandboxSnapshotTask) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxSnapshotTask. +func (in *SandboxSnapshotTask) DeepCopy() *SandboxSnapshotTask { + if in == nil { + return nil + } + out := new(SandboxSnapshotTask) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SandboxSnapshotTask) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxSnapshotTaskList) DeepCopyInto(out *SandboxSnapshotTaskList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]SandboxSnapshotTask, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxSnapshotTaskList. +func (in *SandboxSnapshotTaskList) DeepCopy() *SandboxSnapshotTaskList { + if in == nil { + return nil + } + out := new(SandboxSnapshotTaskList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SandboxSnapshotTaskList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxSnapshotTaskSpec) DeepCopyInto(out *SandboxSnapshotTaskSpec) { + *out = *in + in.SnapshotRef.DeepCopyInto(&out.SnapshotRef) + in.TargetSandboxRef.DeepCopyInto(&out.TargetSandboxRef) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxSnapshotTaskSpec. +func (in *SandboxSnapshotTaskSpec) DeepCopy() *SandboxSnapshotTaskSpec { + if in == nil { + return nil + } + out := new(SandboxSnapshotTaskSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxSnapshotTaskStatus) DeepCopyInto(out *SandboxSnapshotTaskStatus) { + *out = *in + if in.ObservedAt != nil { + in, out := &in.ObservedAt, &out.ObservedAt + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxSnapshotTaskStatus. +func (in *SandboxSnapshotTaskStatus) DeepCopy() *SandboxSnapshotTaskStatus { + if in == nil { + return nil + } + out := new(SandboxSnapshotTaskStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SandboxTemplate) DeepCopyInto(out *SandboxTemplate) { *out = *in @@ -350,6 +576,91 @@ func (in *SandboxTemplate) DeepCopy() *SandboxTemplate { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SnapshotClass) DeepCopyInto(out *SnapshotClass) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotClass. +func (in *SnapshotClass) DeepCopy() *SnapshotClass { + if in == nil { + return nil + } + out := new(SnapshotClass) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SnapshotClass) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SnapshotClassList) DeepCopyInto(out *SnapshotClassList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]SnapshotClass, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotClassList. +func (in *SnapshotClassList) DeepCopy() *SnapshotClassList { + if in == nil { + return nil + } + out := new(SnapshotClassList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SnapshotClassList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SnapshotClassSpec) DeepCopyInto(out *SnapshotClassSpec) { + *out = *in + if in.SupportedSnapshotModes != nil { + in, out := &in.SupportedSnapshotModes, &out.SupportedSnapshotModes + *out = make([]SandboxSnapshotMode, len(*in)) + copy(*out, *in) + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotClassSpec. +func (in *SnapshotClassSpec) DeepCopy() *SnapshotClassSpec { + if in == nil { + return nil + } + out := new(SnapshotClassSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TargetPort) DeepCopyInto(out *TargetPort) { *out = *in diff --git a/pkg/store/artifact_store.go b/pkg/store/artifact_store.go new file mode 100644 index 00000000..ab39b189 --- /dev/null +++ b/pkg/store/artifact_store.go @@ -0,0 +1,141 @@ +/* +Copyright The Volcano Authors. + +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 store + +import ( + "context" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ArtifactStore persists SnapshotArtifactManifest records for SandboxSnapshotController. +// SandboxSnapshotController is the sole writer; Workload Manager reads active artifact sets +// to determine restore intent for session Sandbox creation. +type ArtifactStore interface { + // GetManifest retrieves the manifest for the given snapshot owner key. + // Returns nil, nil when no manifest exists. + GetManifest(ctx context.Context, ownerKey string) (*SnapshotArtifactManifest, error) + + // PutManifest atomically writes the manifest using a compare-and-set operation. + // version is the value returned by the previous GetManifest call (empty string for new records). + // Returns ErrArtifactStoreConflict when the compare-and-set fails. + PutManifest(ctx context.Context, ownerKey string, manifest *SnapshotArtifactManifest, version string) error + + // DeleteManifest removes the manifest for the given snapshot owner key. + DeleteManifest(ctx context.Context, ownerKey string) error + + // Close releases resources held by the store. + Close() error +} + +// ErrArtifactStoreConflict is returned by PutManifest when the compare-and-set fails. +var ErrArtifactStoreConflict = artifactStoreConflictError{} + +type artifactStoreConflictError struct{} + +func (artifactStoreConflictError) Error() string { return "artifact store conflict: version mismatch" } + +// SnapshotArtifactManifest is the owner-scoped artifact-store record for one SandboxSnapshot. +// ArtifactSets maps SnapshotKey to the corresponding artifact set; the map key must match +// SnapshotArtifactSet.SnapshotKey. +type SnapshotArtifactManifest struct { + // OwnerRef identifies the owning SandboxSnapshot. + OwnerRef metav1.OwnerReference `json:"ownerRef"` + + // ArtifactSets maps SnapshotKey to the corresponding artifact set. + ArtifactSets map[string]SnapshotArtifactSet `json:"artifactSets,omitempty"` + + // ActiveSetRef points to the active artifact set in ArtifactSets. + ActiveSetRef SnapshotArtifactSetRef `json:"activeSetRef,omitempty"` + + // PendingSetRef points to the pending replacement artifact set during background rebuild. + PendingSetRef SnapshotArtifactSetRef `json:"pendingSetRef,omitempty"` + + // RebuildSeq is a monotonically increasing rebuild sequence number, incremented by the + // controller each time a new artifact set is started. It is used to form the SnapshotKey. + RebuildSeq int32 `json:"rebuildSeq,omitempty"` +} + +// SnapshotArtifactSetRef points to an entry in SnapshotArtifactManifest.ArtifactSets by key. +type SnapshotArtifactSetRef struct { + // SnapshotKey is the map key in ArtifactSets. + SnapshotKey string `json:"snapshotKey,omitempty"` +} + +// SnapshotArtifactSet describes one logical snapshot version, shared across target nodes. +type SnapshotArtifactSet struct { + // SnapshotKey is the logical restore reference passed to the runtime. + SnapshotKey string `json:"snapshotKey"` + + // SnapshotHash is the hash of the snapshot inputs for this set. + SnapshotHash string `json:"snapshotHash"` + + // Artifacts contains the per-node artifacts in this set. + Artifacts []SnapshotArtifact `json:"artifacts,omitempty"` +} + +// SnapshotArtifact describes a single node-local snapshot artifact. +type SnapshotArtifact struct { + // ProviderName identifies the snapshot provider that created this artifact. + ProviderName string `json:"providerName"` + + // NodeName is the node where this artifact resides. + NodeName string `json:"nodeName,omitempty"` + + // Phase is the current lifecycle phase of this artifact. + Phase SnapshotArtifactPhase `json:"phase"` + + // SnapshotKey is the logical restore reference for this artifact (matches the parent set). + SnapshotKey string `json:"snapshotKey"` + + // SnapshotHash is the hash of the snapshot inputs for this artifact. + SnapshotHash string `json:"snapshotHash"` + + // CreatedAt is set when the artifact was successfully created. + CreatedAt *time.Time `json:"createdAt,omitempty"` + + // Retry records the build retry state for this artifact. + Retry *SnapshotBuildRetry `json:"retry,omitempty"` + + // Message is a human-readable description of the current phase. + Message string `json:"message,omitempty"` +} + +// SnapshotBuildRetry records retry state for a failed artifact build. +type SnapshotBuildRetry struct { + FailureCount int32 `json:"failureCount,omitempty"` + LastFailedAt *time.Time `json:"lastFailedAt,omitempty"` + NextRetryAt *time.Time `json:"nextRetryAt,omitempty"` +} + +// SnapshotArtifactPhase mirrors the CRD type for artifact-store records. +// Defined here to avoid an import cycle with the API package. +type SnapshotArtifactPhase string + +const ( + SnapshotArtifactPhaseCreating SnapshotArtifactPhase = "Creating" + SnapshotArtifactPhaseReady SnapshotArtifactPhase = "Ready" + SnapshotArtifactPhaseFailed SnapshotArtifactPhase = "Failed" + SnapshotArtifactPhaseUnavailable SnapshotArtifactPhase = "Unavailable" +) + +// ArtifactOwnerKey constructs the artifact store key for a SandboxSnapshot. +// Format: snapshot:{kind}:{namespace}:{name}:{uid} +func ArtifactOwnerKey(kind, namespace, name, uid string) string { + return "snapshot:" + kind + ":" + namespace + ":" + name + ":" + uid +} diff --git a/pkg/store/artifact_store_redis.go b/pkg/store/artifact_store_redis.go new file mode 100644 index 00000000..d612fd21 --- /dev/null +++ b/pkg/store/artifact_store_redis.go @@ -0,0 +1,98 @@ +/* +Copyright The Volcano Authors. + +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 store + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + redisv9 "github.com/redis/go-redis/v9" +) + +// redisArtifactStore implements ArtifactStore using Redis. +// The compare-and-set is implemented with WATCH + MULTI/EXEC. The version token +// is a JSON encoding of the raw string value, so the caller can pass it back +// unchanged. An empty string means "no previous value". +type redisArtifactStore struct { + cli *redisv9.Client +} + +// NewRedisArtifactStore creates an ArtifactStore backed by the given Redis client. +func NewRedisArtifactStore(cli *redisv9.Client) ArtifactStore { + return &redisArtifactStore{cli: cli} +} + +func (s *redisArtifactStore) GetManifest(ctx context.Context, ownerKey string) (*SnapshotArtifactManifest, error) { + raw, err := s.cli.Get(ctx, ownerKey).Result() + if errors.Is(err, redisv9.Nil) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("artifact store get %q: %w", ownerKey, err) + } + var m SnapshotArtifactManifest + if err := json.Unmarshal([]byte(raw), &m); err != nil { + return nil, fmt.Errorf("artifact store unmarshal %q: %w", ownerKey, err) + } + return &m, nil +} + +func (s *redisArtifactStore) PutManifest(ctx context.Context, ownerKey string, manifest *SnapshotArtifactManifest, version string) error { + encoded, err := json.Marshal(manifest) + if err != nil { + return fmt.Errorf("artifact store marshal: %w", err) + } + + // Use WATCH + MULTI/EXEC for optimistic compare-and-set. + txErr := s.cli.Watch(ctx, func(tx *redisv9.Tx) error { + current, err := tx.Get(ctx, ownerKey).Result() + if errors.Is(err, redisv9.Nil) { + current = "" + } else if err != nil { + return fmt.Errorf("artifact store watch-get %q: %w", ownerKey, err) + } + if current != version { + return ErrArtifactStoreConflict + } + _, err = tx.TxPipelined(ctx, func(pipe redisv9.Pipeliner) error { + pipe.Set(ctx, ownerKey, string(encoded), 0) + return nil + }) + return err + }, ownerKey) + + if txErr != nil { + if errors.Is(txErr, ErrArtifactStoreConflict) { + return ErrArtifactStoreConflict + } + return fmt.Errorf("artifact store put %q: %w", ownerKey, txErr) + } + return nil +} + +func (s *redisArtifactStore) DeleteManifest(ctx context.Context, ownerKey string) error { + if err := s.cli.Del(ctx, ownerKey).Err(); err != nil && !errors.Is(err, redisv9.Nil) { + return fmt.Errorf("artifact store delete %q: %w", ownerKey, err) + } + return nil +} + +func (s *redisArtifactStore) Close() error { + return s.cli.Close() +} diff --git a/pkg/workloadmanager/artifact_store_init.go b/pkg/workloadmanager/artifact_store_init.go new file mode 100644 index 00000000..727eb999 --- /dev/null +++ b/pkg/workloadmanager/artifact_store_init.go @@ -0,0 +1,83 @@ +/* +Copyright The Volcano Authors. + +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 workloadmanager + +import ( + "context" + "fmt" + "os" + "strings" + + redisv9 "github.com/redis/go-redis/v9" + "k8s.io/klog/v2" + + "github.com/volcano-sh/agentcube/pkg/store" +) + +// NewArtifactStoreFromEnv creates an ArtifactStore from the same environment variables +// used by the session store (REDIS_ADDR / VALKEY_ADDR, REDIS_PASSWORD). +// Falls back to a no-op store when configuration is absent so the binary can start +// in environments that have not yet configured snapshot infrastructure. +func NewArtifactStoreFromEnv() store.ArtifactStore { + // Prefer Valkey when VALKEY_ADDR is set. + if addr := os.Getenv("VALKEY_ADDR"); addr != "" { + password := os.Getenv("REDIS_PASSWORD") + klog.V(2).InfoS("artifact store: using Valkey", "addr", addr) + return store.NewRedisArtifactStore(newValkeyRedisCompat(addr, password)) + } + + if addr := os.Getenv("REDIS_ADDR"); addr != "" { + password := os.Getenv("REDIS_PASSWORD") + required := strings.ToLower(os.Getenv("REDIS_PASSWORD_REQUIRED")) != "false" + if required && password == "" { + klog.Warningf("REDIS_PASSWORD is required but not set; snapshot restore will use cold start") + return &noopArtifactStore{} + } + client := redisv9.NewClient(&redisv9.Options{ + Addr: addr, + Password: password, + }) + klog.V(2).InfoS("artifact store: using Redis", "addr", addr) + return store.NewRedisArtifactStore(client) + } + + klog.V(2).Info("artifact store: no address configured; snapshot restore will use cold start") + return &noopArtifactStore{} +} + +// newValkeyRedisCompat creates a go-redis client that connects to a Valkey instance. +// Valkey and Redis share the same RESP wire protocol, so go-redis works as the transport. +func newValkeyRedisCompat(addr, password string) *redisv9.Client { + return redisv9.NewClient(&redisv9.Options{ + Addr: addr, + Password: password, + }) +} + +// noopArtifactStore is returned when no store backend is configured. +// Reads and writes fail so snapshot controllers back off instead of creating +// untracked build resources. Delete remains a no-op to keep finalizer cleanup safe. +type noopArtifactStore struct{} + +func (n *noopArtifactStore) GetManifest(_ context.Context, _ string) (*store.SnapshotArtifactManifest, error) { + return nil, fmt.Errorf("artifact store is not configured") +} +func (n *noopArtifactStore) PutManifest(_ context.Context, _ string, _ *store.SnapshotArtifactManifest, _ string) error { + return fmt.Errorf("artifact store is not configured") +} +func (n *noopArtifactStore) DeleteManifest(_ context.Context, _ string) error { return nil } +func (n *noopArtifactStore) Close() error { return nil } diff --git a/pkg/workloadmanager/codeinterpreter_controller.go b/pkg/workloadmanager/codeinterpreter_controller.go index da010cbb..5b1a4a90 100644 --- a/pkg/workloadmanager/codeinterpreter_controller.go +++ b/pkg/workloadmanager/codeinterpreter_controller.go @@ -59,33 +59,27 @@ func (r *CodeInterpreterReconciler) Reconcile(ctx context.Context, req ctrl.Requ return ctrl.Result{}, err } - // Manage SandboxTemplate and SandboxWarmPool if configured + // Always reconcile SandboxTemplate so SandboxSnapshotController can use it as a Fork source. + result, err := r.ensureSandboxTemplate(ctx, codeInterpreter) + if err != nil { + logger.Error(err, "failed to ensure SandboxTemplate") + return ctrl.Result{}, err + } + if result.RequeueAfter > 0 { + return result, nil + } + + // Manage SandboxWarmPool only when WarmPoolSize is configured. if codeInterpreter.Spec.WarmPoolSize != nil && *codeInterpreter.Spec.WarmPoolSize > 0 { - // Ensure SandboxTemplate exists (required for SandboxWarmPool) - result, err := r.ensureSandboxTemplate(ctx, codeInterpreter) - if err != nil { - logger.Error(err, "failed to ensure SandboxTemplate") - return ctrl.Result{}, err - } - if result.RequeueAfter > 0 { - return result, nil - } - // Ensure SandboxWarmPool exists if err := r.ensureSandboxWarmPool(ctx, codeInterpreter); err != nil { logger.Error(err, "failed to ensure SandboxWarmPool") return ctrl.Result{}, err } } else { - // Delete SandboxWarmPool if WarmPoolSize is 0 or nil if err := r.deleteSandboxWarmPool(ctx, codeInterpreter); err != nil { logger.Error(err, "failed to delete SandboxWarmPool") return ctrl.Result{}, err } - // Delete SandboxTemplate if WarmPoolSize is 0 or nil - if err := r.deleteSandboxTemplate(ctx, codeInterpreter); err != nil { - logger.Error(err, "failed to delete SandboxTemplate") - return ctrl.Result{}, err - } } // Update status with ready condition @@ -266,26 +260,6 @@ func (r *CodeInterpreterReconciler) deleteSandboxWarmPool(ctx context.Context, c return nil } -// deleteSandboxTemplate deletes the SandboxTemplate if it exists -func (r *CodeInterpreterReconciler) deleteSandboxTemplate(ctx context.Context, ci *runtimev1alpha1.CodeInterpreter) error { - templateName := ci.Name - sandboxTemplate := &extensionsv1alpha1.SandboxTemplate{} - err := r.Get(ctx, types.NamespacedName{Name: templateName, Namespace: ci.Namespace}, sandboxTemplate) - if errors.IsNotFound(err) { - return nil - } else if err != nil { - return fmt.Errorf("failed to get SandboxTemplate: %w", err) - } - - if err := r.Delete(ctx, sandboxTemplate); err != nil { - if !errors.IsNotFound(err) { - return fmt.Errorf("failed to delete SandboxTemplate: %w", err) - } - } - - return nil -} - // convertToPodTemplate converts CodeInterpreterSandboxTemplate to sandboxv1alpha1.PodTemplate func (r *CodeInterpreterReconciler) convertToPodTemplate(template *runtimev1alpha1.CodeInterpreterSandboxTemplate, ci *runtimev1alpha1.CodeInterpreter) sandboxv1alpha1.PodTemplate { // Normalize RuntimeClassName: if it's an empty string, set it to nil diff --git a/pkg/workloadmanager/handlers.go b/pkg/workloadmanager/handlers.go index 7d417c6e..760077db 100644 --- a/pkg/workloadmanager/handlers.go +++ b/pkg/workloadmanager/handlers.go @@ -32,6 +32,7 @@ import ( extensionsv1alpha1 "sigs.k8s.io/agent-sandbox/extensions/api/v1alpha1" "github.com/volcano-sh/agentcube/pkg/api" + runtimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" "github.com/volcano-sh/agentcube/pkg/common/types" "github.com/volcano-sh/agentcube/pkg/store" ) @@ -99,17 +100,7 @@ func (s *Server) handleSandboxCreate(c *gin.Context, kind string) { return } - var sandbox *sandboxv1alpha1.Sandbox - var sandboxClaim *extensionsv1alpha1.SandboxClaim - var sandboxEntry *sandboxEntry - var err error - switch sandboxReq.Kind { - case types.AgentRuntimeKind: - sandbox, sandboxEntry, err = buildSandboxByAgentRuntime(sandboxReq.Namespace, sandboxReq.Name, s.informers) - case types.CodeInterpreterKind: - sandbox, sandboxClaim, sandboxEntry, err = buildSandboxByCodeInterpreter(sandboxReq.Namespace, sandboxReq.Name, s.informers) - } - + sandbox, sandboxClaim, sandboxEntry, err := s.buildSandboxForRequest(c.Request.Context(), sandboxReq) if err != nil { klog.Errorf("build sandbox failed %s/%s: %v", sandboxReq.Namespace, sandboxReq.Name, err) if errors.Is(err, api.ErrAgentRuntimeNotFound) || errors.Is(err, api.ErrCodeInterpreterNotFound) { @@ -175,6 +166,37 @@ func (s *Server) handleSandboxCreate(c *gin.Context, kind string) { respondJSON(c, http.StatusOK, response) } +func (s *Server) buildSandboxForRequest(ctx context.Context, sandboxReq *types.CreateSandboxRequest) (*sandboxv1alpha1.Sandbox, *extensionsv1alpha1.SandboxClaim, *sandboxEntry, error) { + switch sandboxReq.Kind { + case types.AgentRuntimeKind: + sandbox, sandboxEntry, err := buildSandboxByAgentRuntime(sandboxReq.Namespace, sandboxReq.Name, s.informers) + return sandbox, nil, sandboxEntry, err + case types.CodeInterpreterKind: + sandbox, sandboxClaim, sandboxEntry, err := buildSandboxByCodeInterpreter(sandboxReq.Namespace, sandboxReq.Name, s.informers) + if err == nil { + s.injectSnapshotRestoreIntent(ctx, sandboxReq, sandbox, sandboxClaim) + } + return sandbox, sandboxClaim, sandboxEntry, err + default: + return nil, nil, nil, fmt.Errorf("unsupported sandbox kind %q", sandboxReq.Kind) + } +} + +func (s *Server) injectSnapshotRestoreIntent(ctx context.Context, sandboxReq *types.CreateSandboxRequest, sandbox *sandboxv1alpha1.Sandbox, sandboxClaim *extensionsv1alpha1.SandboxClaim) { + if sandboxClaim != nil || s.snapshotClient == nil || s.artifactStore == nil { + return + } + snapshotKey := lookupActiveForkSnapshotKey(ctx, s.snapshotClient, s.artifactStore, sandboxReq.Namespace, sandboxReq.Name) + if snapshotKey == "" { + return + } + if sandbox.Spec.PodTemplate.ObjectMeta.Annotations == nil { + sandbox.Spec.PodTemplate.ObjectMeta.Annotations = make(map[string]string) + } + sandbox.Spec.PodTemplate.ObjectMeta.Annotations[runtimev1alpha1.SnapshotKeyAnnotation] = snapshotKey + klog.V(4).InfoS("injecting snapshot restore intent", "sandbox", sandbox.Name, "snapshotKey", snapshotKey) +} + // createK8sResources creates the K8s sandbox or sandbox claim resource. func (s *Server) createK8sResources(ctx context.Context, dynamicClient dynamic.Interface, sandbox *sandboxv1alpha1.Sandbox, sandboxClaim *extensionsv1alpha1.SandboxClaim) error { if sandboxClaim != nil { diff --git a/pkg/workloadmanager/server.go b/pkg/workloadmanager/server.go index f77c157e..b183ce39 100644 --- a/pkg/workloadmanager/server.go +++ b/pkg/workloadmanager/server.go @@ -27,6 +27,7 @@ import ( "github.com/gin-gonic/gin" "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/volcano-sh/agentcube/pkg/mtls" "github.com/volcano-sh/agentcube/pkg/store" @@ -44,8 +45,14 @@ type Server struct { tokenCache *TokenCache informers *Informers storeClient store.Store - wg sync.WaitGroup - certWatcher *mtls.CertWatcher // mTLS cert watcher for graceful cleanup + // snapshotClient is a controller-runtime client used for snapshot lookups. + // It may be nil when no snapshot controller is configured. + snapshotClient client.Client + // artifactStore holds SnapshotArtifactManifest records for restore intent lookup. + // It may be nil when no snapshot controller is configured. + artifactStore store.ArtifactStore + wg sync.WaitGroup + certWatcher *mtls.CertWatcher // mTLS cert watcher for graceful cleanup } type Config struct { @@ -72,8 +79,9 @@ type Config struct { MTLSConfig mtls.Config } -// NewServer creates a new API server instance -func NewServer(config *Config, sandboxController *SandboxReconciler) (*Server, error) { +// NewServer creates a new API server instance. +// snapshotClient and artifactStore are optional; pass nil to disable snapshot restore intent. +func NewServer(config *Config, sandboxController *SandboxReconciler, snapshotClient client.Client, artifactStore store.ArtifactStore) (*Server, error) { if config == nil { return nil, fmt.Errorf("config cannot be nil") } @@ -104,6 +112,8 @@ func NewServer(config *Config, sandboxController *SandboxReconciler) (*Server, e tokenCache: tokenCache, informers: NewInformers(k8sClient), storeClient: store.Storage(), + snapshotClient: snapshotClient, + artifactStore: artifactStore, } // Setup routes diff --git a/pkg/workloadmanager/snapshot_controller.go b/pkg/workloadmanager/snapshot_controller.go new file mode 100644 index 00000000..f113669b --- /dev/null +++ b/pkg/workloadmanager/snapshot_controller.go @@ -0,0 +1,493 @@ +/* +Copyright The Volcano Authors. + +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 workloadmanager + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + sandboxv1alpha1 "sigs.k8s.io/agent-sandbox/api/v1alpha1" + + runtimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" + "github.com/volcano-sh/agentcube/pkg/store" +) + +const ( + snapshotFinalizer = "agentcube.volcano.sh/snapshot-protection" + maxInt32Value = 1<<31 - 1 +) + +// SandboxSnapshotReconciler reconciles SandboxSnapshot objects. +// Mode-specific logic is fully delegated to the registered SnapshotModeHandler. +type SandboxSnapshotReconciler struct { + client.Client + ArtifactStore store.ArtifactStore + Recorder record.EventRecorder + Handlers map[runtimev1alpha1.SandboxSnapshotMode]SnapshotModeHandler +} + +func (r *SandboxSnapshotReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + ss := &runtimev1alpha1.SandboxSnapshot{} + if err := r.Get(ctx, req.NamespacedName, ss); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + if !ss.DeletionTimestamp.IsZero() { + return r.reconcileDelete(ctx, ss) + } + + if !controllerutil.ContainsFinalizer(ss, snapshotFinalizer) { + controllerutil.AddFinalizer(ss, snapshotFinalizer) + if err := r.Update(ctx, ss); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + sc := &runtimev1alpha1.SnapshotClass{} + if err := r.Get(ctx, types.NamespacedName{Name: ss.Spec.SnapshotClassName}, sc); err != nil { + if apierrors.IsNotFound(err) { + return r.setFailed(ctx, ss, "SnapshotClass not found: "+ss.Spec.SnapshotClassName) + } + return ctrl.Result{}, err + } + + handler, ok := r.Handlers[ss.Spec.SnapshotMode] + if !ok { + return r.setFailed(ctx, ss, "unsupported snapshotMode: "+string(ss.Spec.SnapshotMode)) + } + if !containsMode(sc.Spec.SupportedSnapshotModes, ss.Spec.SnapshotMode) { + return r.setFailed(ctx, ss, fmt.Sprintf("SnapshotClass %q does not support mode %q", sc.Name, ss.Spec.SnapshotMode)) + } + + return r.reconcileWithHandler(ctx, ss, sc, handler) +} + +func (r *SandboxSnapshotReconciler) reconcileWithHandler(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, sc *runtimev1alpha1.SnapshotClass, handler SnapshotModeHandler) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + currentHash, err := handler.ComputeHash(ctx, ss, sc) + if err != nil { + return ctrl.Result{}, fmt.Errorf("compute snapshot hash: %w", err) + } + + ownerKey := store.ArtifactOwnerKey("SandboxSnapshot", ss.Namespace, ss.Name, string(ss.UID)) + manifest, rawVersion, err := loadManifest(ctx, r.ArtifactStore, ownerKey) + if err != nil { + return ctrl.Result{}, err + } + + rawVersion, err = handler.PrepareArtifactSet(ctx, ss, sc, manifest, ownerKey, rawVersion, currentHash) + if err != nil { + return ctrl.Result{}, err + } + + rawVersion, err = r.reconcileTasksAndArtifacts(ctx, ss, sc, manifest, ownerKey, rawVersion, handler) + if err != nil { + return ctrl.Result{}, err + } + + if manifest.PendingSetRef.SnapshotKey != "" { + pending := manifest.ArtifactSets[manifest.PendingSetRef.SnapshotKey] + if handler.ReadyToPromote(pending) { + logger.Info("promoting pending artifact set to active", "snapshot", ss.Name, "snapshotKey", pending.SnapshotKey) + if manifest.ActiveSetRef.SnapshotKey != "" { + delete(manifest.ArtifactSets, manifest.ActiveSetRef.SnapshotKey) + } + manifest.ActiveSetRef = manifest.PendingSetRef + manifest.PendingSetRef = store.SnapshotArtifactSetRef{} + ss.Status.ReadyAt = nil + if _, err = saveManifest(ctx, r.ArtifactStore, ownerKey, manifest, rawVersion); err != nil { + return ctrl.Result{}, err + } + } + } + + return r.aggregateAndUpdateStatus(ctx, ss, manifest) +} + +func (r *SandboxSnapshotReconciler) reconcileDelete(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + ownerKey := store.ArtifactOwnerKey("SandboxSnapshot", ss.Namespace, ss.Name, string(ss.UID)) + if err := r.ArtifactStore.DeleteManifest(ctx, ownerKey); err != nil { + return ctrl.Result{}, fmt.Errorf("delete artifact manifest: %w", err) + } + logger.Info("deleted artifact manifest", "snapshot", ss.Name) + + if handler, ok := r.Handlers[ss.Spec.SnapshotMode]; ok { + if err := handler.CleanupAll(ctx, ss); err != nil { + return ctrl.Result{}, err + } + } + + controllerutil.RemoveFinalizer(ss, snapshotFinalizer) + return ctrl.Result{}, r.Update(ctx, ss) +} + +func (r *SandboxSnapshotReconciler) reconcileTasksAndArtifacts(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, sc *runtimev1alpha1.SnapshotClass, manifest *store.SnapshotArtifactManifest, ownerKey, rawVersion string, handler SnapshotModeHandler) (string, error) { + workingKey := manifest.PendingSetRef.SnapshotKey + if workingKey == "" { + workingKey = manifest.ActiveSetRef.SnapshotKey + } + if workingKey == "" { + return rawVersion, nil + } + artifactSet, exists := manifest.ArtifactSets[workingKey] + if !exists { + return rawVersion, nil + } + + taskList, err := r.listSnapshotTasks(ctx, ss, workingKey) + if err != nil { + return rawVersion, err + } + rawVersion, artifactSet, err = r.syncArtifactStatus(ctx, manifest, ownerKey, rawVersion, workingKey, artifactSet, taskList) + if err != nil { + return rawVersion, err + } + + rawVersion, err = handler.EnsureTasks(ctx, ss, sc, manifest, ownerKey, rawVersion, workingKey, artifactSet) + if err != nil { + return rawVersion, err + } + + if workingKey == manifest.ActiveSetRef.SnapshotKey { + r.cleanupCompletedTasks(ctx, ss, workingKey, handler) + } + + return rawVersion, nil +} + +func (r *SandboxSnapshotReconciler) cleanupCompletedTasks(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, snapshotKey string, handler SnapshotModeHandler) { + logger := log.FromContext(ctx) + + taskList := &runtimev1alpha1.SandboxSnapshotTaskList{} + if err := r.List(ctx, taskList, client.InNamespace(ss.Namespace), client.MatchingLabels{ + runtimev1alpha1.SnapshotNameLabelKey: ss.Name, + runtimev1alpha1.SnapshotKeyLabelKey: snapshotKey, + }); err != nil { + logger.Error(err, "list completed tasks for cleanup") + return + } + + for i := range taskList.Items { + task := &taskList.Items[i] + phase := task.Status.Phase + if phase != runtimev1alpha1.SnapshotArtifactPhaseReady && phase != runtimev1alpha1.SnapshotArtifactPhaseFailed { + continue + } + if err := handler.CleanupTask(ctx, ss, task); err != nil { + logger.Error(err, "mode-specific task cleanup failed", "task", task.Name) + } + if err := r.Delete(ctx, task); err != nil && !apierrors.IsNotFound(err) { + logger.Error(err, "delete completed task", "name", task.Name) + } + } +} + +func (r *SandboxSnapshotReconciler) listSnapshotTasks(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, snapshotKey string) (*runtimev1alpha1.SandboxSnapshotTaskList, error) { + taskList := &runtimev1alpha1.SandboxSnapshotTaskList{} + if err := r.List(ctx, taskList, client.InNamespace(ss.Namespace), client.MatchingLabels{ + runtimev1alpha1.SnapshotNameLabelKey: ss.Name, + runtimev1alpha1.SnapshotKeyLabelKey: snapshotKey, + }); err != nil { + return nil, fmt.Errorf("list snapshot tasks: %w", err) + } + return taskList, nil +} + +func (r *SandboxSnapshotReconciler) syncArtifactStatus(ctx context.Context, manifest *store.SnapshotArtifactManifest, ownerKey, rawVersion, workingKey string, artifactSet store.SnapshotArtifactSet, taskList *runtimev1alpha1.SandboxSnapshotTaskList) (string, store.SnapshotArtifactSet, error) { + tasksByNode := make(map[string]*runtimev1alpha1.SandboxSnapshotTask, len(taskList.Items)) + for i := range taskList.Items { + t := &taskList.Items[i] + tasksByNode[t.Spec.TargetNodeName] = t + } + + changed := false + for i := range artifactSet.Artifacts { + art := &artifactSet.Artifacts[i] + task, hasTask := tasksByNode[art.NodeName] + if !hasTask { + continue + } + if updateArtifactFromTask(art, task) { + changed = true + } + } + if !changed { + return rawVersion, artifactSet, nil + } + manifest.ArtifactSets[workingKey] = artifactSet + rawVersion, err := saveManifest(ctx, r.ArtifactStore, ownerKey, manifest, rawVersion) + return rawVersion, artifactSet, err +} + +func updateArtifactFromTask(art *store.SnapshotArtifact, task *runtimev1alpha1.SandboxSnapshotTask) bool { + newPhase := store.SnapshotArtifactPhase(task.Status.Phase) + if newPhase == "" || newPhase == art.Phase { + return false + } + art.Phase = newPhase + art.Message = task.Status.Message + if newPhase == store.SnapshotArtifactPhaseReady && art.CreatedAt == nil { + now := time.Now() + art.CreatedAt = &now + } + return true +} + +func (r *SandboxSnapshotReconciler) aggregateAndUpdateStatus(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, manifest *store.SnapshotArtifactManifest) (ctrl.Result, error) { + activeSet := activeArtifactSet(manifest) + pendingSet := pendingArtifactSet(manifest) + status := r.buildSnapshotStatus(ss, activeSet, pendingSet) + + if status.Phase == runtimev1alpha1.SandboxSnapshotPhaseReady && ss.Status.Phase != runtimev1alpha1.SandboxSnapshotPhaseReady { + r.Recorder.Event(ss, corev1.EventTypeNormal, "SandboxSnapshotReady", "at least one artifact is available") + } + if status.Phase == runtimev1alpha1.SandboxSnapshotPhaseCreating { + return r.patchSnapshotStatus(ctx, ss, status, ctrl.Result{RequeueAfter: 15 * time.Second}) + } + return r.patchSnapshotStatus(ctx, ss, status, ctrl.Result{}) +} + +func (r *SandboxSnapshotReconciler) buildSnapshotStatus(ss *runtimev1alpha1.SandboxSnapshot, activeSet, pendingSet *store.SnapshotArtifactSet) runtimev1alpha1.SandboxSnapshotStatus { + var workingArtifacts []store.SnapshotArtifact + if activeSet != nil { + workingArtifacts = activeSet.Artifacts + } else if pendingSet != nil { + workingArtifacts = pendingSet.Artifacts + } + + status := runtimev1alpha1.SandboxSnapshotStatus{ + TargetNodeCount: int32Len(workingArtifacts), + } + countArtifactPhases(workingArtifacts, &status) + + total := status.TargetNodeCount + switch { + case total == 0: + status.Phase = runtimev1alpha1.SandboxSnapshotPhasePending + case status.ReadyNodeCount > 0 && activeSet != nil: + status.Phase = runtimev1alpha1.SandboxSnapshotPhaseReady + if ss.Status.ReadyAt == nil { + now := metav1.Now() + status.ReadyAt = &now + } else { + status.ReadyAt = ss.Status.ReadyAt + } + if status.FailedNodeCount > 0 || status.UnavailableNodeCount > 0 { + r.Recorder.Event(ss, corev1.EventTypeWarning, "SandboxSnapshotDegraded", + fmt.Sprintf("%d/%d nodes have failed or unavailable artifacts", status.FailedNodeCount+status.UnavailableNodeCount, total)) + } + case status.FailedNodeCount == total && total > 0: + status.Phase = runtimev1alpha1.SandboxSnapshotPhaseFailed + r.Recorder.Event(ss, corev1.EventTypeWarning, "SandboxSnapshotFailed", "all artifact builds failed") + default: + status.Phase = runtimev1alpha1.SandboxSnapshotPhaseCreating + } + return status +} + +func (r *SandboxSnapshotReconciler) patchSnapshotStatus(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, status runtimev1alpha1.SandboxSnapshotStatus, result ctrl.Result) (ctrl.Result, error) { + if snapshotStatusEqual(ss.Status, status) { + return result, nil + } + patch := client.MergeFrom(ss.DeepCopy()) + ss.Status = status + if err := r.Status().Patch(ctx, ss, patch); err != nil { + return ctrl.Result{}, fmt.Errorf("patch snapshot status: %w", err) + } + return result, nil +} + +func (r *SandboxSnapshotReconciler) setFailed(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, msg string) (ctrl.Result, error) { + patch := client.MergeFrom(ss.DeepCopy()) + ss.Status.Phase = runtimev1alpha1.SandboxSnapshotPhaseFailed + ss.Status.Message = msg + if err := r.Status().Patch(ctx, ss, patch); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil +} + +// -- Shared manifest helpers -- + +func loadManifest(ctx context.Context, as store.ArtifactStore, ownerKey string) (*store.SnapshotArtifactManifest, string, error) { + manifest, err := as.GetManifest(ctx, ownerKey) + if err != nil { + return nil, "", fmt.Errorf("get artifact manifest: %w", err) + } + if manifest == nil { + return &store.SnapshotArtifactManifest{}, "", nil + } + raw, err := json.Marshal(manifest) + if err != nil { + return nil, "", fmt.Errorf("marshal manifest for version: %w", err) + } + return manifest, string(raw), nil +} + +func saveManifest(ctx context.Context, as store.ArtifactStore, ownerKey string, manifest *store.SnapshotArtifactManifest, version string) (string, error) { + if err := as.PutManifest(ctx, ownerKey, manifest, version); err != nil { + if errors.Is(err, store.ErrArtifactStoreConflict) { + return version, fmt.Errorf("artifact store conflict (will retry): %w", err) + } + return version, fmt.Errorf("put artifact manifest: %w", err) + } + raw, _ := json.Marshal(manifest) + return string(raw), nil +} + +func startNewArtifactSet(ss *runtimev1alpha1.SandboxSnapshot, manifest *store.SnapshotArtifactManifest, snapshotHash string) string { + manifest.RebuildSeq++ + snapshotKey := buildSnapshotKey(ss, manifest.RebuildSeq) + if manifest.ArtifactSets == nil { + manifest.ArtifactSets = make(map[string]store.SnapshotArtifactSet) + } + manifest.ArtifactSets[snapshotKey] = store.SnapshotArtifactSet{ + SnapshotKey: snapshotKey, + SnapshotHash: snapshotHash, + } + return snapshotKey +} + +// -- Shared key/label utilities -- + +func buildSnapshotKey(ss *runtimev1alpha1.SandboxSnapshot, rebuildSeq int32) string { + mode := strings.ToLower(string(ss.Spec.SnapshotMode)) + return fmt.Sprintf("%s-%s-g%d-r%d", normalizeLabel(ss.Name), mode, ss.Generation, rebuildSeq) +} + +func normalizeLabel(s string) string { + var b strings.Builder + for _, c := range strings.ToLower(s) { + if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' { + b.WriteRune(c) + } else { + b.WriteRune('-') + } + } + result := strings.Trim(b.String(), "-") + if len(result) > 63 { + result = result[:63] + } + return result +} + +// -- Shared manifest accessors -- + +func activeArtifactSet(manifest *store.SnapshotArtifactManifest) *store.SnapshotArtifactSet { + if manifest.ActiveSetRef.SnapshotKey == "" { + return nil + } + s, ok := manifest.ArtifactSets[manifest.ActiveSetRef.SnapshotKey] + if !ok { + return nil + } + return &s +} + +func pendingArtifactSet(manifest *store.SnapshotArtifactManifest) *store.SnapshotArtifactSet { + if manifest.PendingSetRef.SnapshotKey == "" { + return nil + } + s, ok := manifest.ArtifactSets[manifest.PendingSetRef.SnapshotKey] + if !ok { + return nil + } + return &s +} + +// -- Shared status helpers -- + +func snapshotStatusEqual(a, b runtimev1alpha1.SandboxSnapshotStatus) bool { + return a.Phase == b.Phase && + a.TargetNodeCount == b.TargetNodeCount && + a.CreatingNodeCount == b.CreatingNodeCount && + a.ReadyNodeCount == b.ReadyNodeCount && + a.FailedNodeCount == b.FailedNodeCount && + a.UnavailableNodeCount == b.UnavailableNodeCount && + a.Message == b.Message +} + +func countArtifactPhases(artifacts []store.SnapshotArtifact, status *runtimev1alpha1.SandboxSnapshotStatus) { + for _, art := range artifacts { + switch art.Phase { + case store.SnapshotArtifactPhaseCreating: + status.CreatingNodeCount++ + case store.SnapshotArtifactPhaseReady: + status.ReadyNodeCount++ + case store.SnapshotArtifactPhaseFailed: + status.FailedNodeCount++ + case store.SnapshotArtifactPhaseUnavailable: + status.UnavailableNodeCount++ + } + } +} + +func int32Len(artifacts []store.SnapshotArtifact) int32 { + count := int32(0) + for range artifacts { + if count == maxInt32Value { + return maxInt32Value + } + count++ + } + return count +} + +func containsMode(modes []runtimev1alpha1.SandboxSnapshotMode, mode runtimev1alpha1.SandboxSnapshotMode) bool { + for _, m := range modes { + if m == mode { + return true + } + } + return false +} + +// SetupWithManager registers the controller and initializes mode handlers. +func (r *SandboxSnapshotReconciler) SetupWithManager(mgr ctrl.Manager) error { + r.Recorder = mgr.GetEventRecorderFor("sandbox-snapshot-controller") + r.Handlers = map[runtimev1alpha1.SandboxSnapshotMode]SnapshotModeHandler{ + runtimev1alpha1.SandboxSnapshotModeFork: &ForkModeHandler{ + Client: r.Client, + ArtifactStore: r.ArtifactStore, + Recorder: r.Recorder, + Scheme: mgr.GetScheme(), + }, + } + return ctrl.NewControllerManagedBy(mgr). + For(&runtimev1alpha1.SandboxSnapshot{}). + Owns(&runtimev1alpha1.SandboxSnapshotTask{}). + Owns(&sandboxv1alpha1.Sandbox{}). + Complete(r) +} diff --git a/pkg/workloadmanager/snapshot_fork.go b/pkg/workloadmanager/snapshot_fork.go new file mode 100644 index 00000000..a441b4e4 --- /dev/null +++ b/pkg/workloadmanager/snapshot_fork.go @@ -0,0 +1,457 @@ +/* +Copyright The Volcano Authors. + +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 workloadmanager + +import ( + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "sort" + "time" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + k8sruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + sandboxv1alpha1 "sigs.k8s.io/agent-sandbox/api/v1alpha1" + extensionsv1alpha1 "sigs.k8s.io/agent-sandbox/extensions/api/v1alpha1" + + "k8s.io/klog/v2" + + runtimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" + "github.com/volcano-sh/agentcube/pkg/store" +) + +// ForkModeHandler implements SnapshotModeHandler for Fork-mode snapshots. +// It creates a per-node build Sandbox from a SandboxTemplate, snapshots it, +// then makes the artifact available for 1:N forking by new sessions. +type ForkModeHandler struct { + Client client.Client + ArtifactStore store.ArtifactStore + Recorder record.EventRecorder + Scheme *k8sruntime.Scheme +} + +func (h *ForkModeHandler) ComputeHash(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, sc *runtimev1alpha1.SnapshotClass) (string, error) { + tmpl := &extensionsv1alpha1.SandboxTemplate{} + if err := h.Client.Get(ctx, types.NamespacedName{Name: ss.Spec.SourceRef.Name, Namespace: ss.Namespace}, tmpl); err != nil { + if apierrors.IsNotFound(err) { + return "", fmt.Errorf("source SandboxTemplate %q not found", ss.Spec.SourceRef.Name) + } + return "", fmt.Errorf("get source SandboxTemplate %q: %w", ss.Spec.SourceRef.Name, err) + } + return computeSnapshotHash(ss, tmpl.Spec.PodTemplate.Spec, sc) +} + +func (h *ForkModeHandler) PrepareArtifactSet(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, _ *runtimev1alpha1.SnapshotClass, manifest *store.SnapshotArtifactManifest, ownerKey, rawVersion, currentHash string) (string, error) { + var err error + rawVersion, err = h.clearStaleActiveSet(ctx, ss, manifest, ownerKey, rawVersion, currentHash) + if err != nil { + return rawVersion, err + } + rawVersion, err = h.maybeStartBackgroundRebuild(ctx, ss, manifest, ownerKey, rawVersion, currentHash) + if err != nil { + return rawVersion, err + } + return h.ensurePendingSet(ctx, ss, manifest, ownerKey, rawVersion, currentHash) +} + +func (h *ForkModeHandler) EnsureTasks(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, sc *runtimev1alpha1.SnapshotClass, manifest *store.SnapshotArtifactManifest, ownerKey, rawVersion, workingKey string, artifactSet store.SnapshotArtifactSet) (string, error) { + targetNodes, err := h.selectTargetNodes(ctx, sc) + if err != nil { + return rawVersion, fmt.Errorf("select target nodes: %w", err) + } + coveredNodes := coveredArtifactNodes(artifactSet.Artifacts) + addedArtifacts := false + for _, nodeName := range targetNodes { + if _, covered := coveredNodes[nodeName]; covered { + continue + } + if err := h.ensureBuildSandboxAndTask(ctx, ss, sc, artifactSet.SnapshotKey, artifactSet.SnapshotHash, nodeName); err != nil { + log.FromContext(ctx).Error(err, "failed to ensure build sandbox and task", "node", nodeName) + continue + } + artifactSet.Artifacts = append(artifactSet.Artifacts, newCreatingArtifact(sc.Spec.ProviderName, nodeName, artifactSet)) + addedArtifacts = true + } + if !addedArtifacts { + return rawVersion, nil + } + manifest.ArtifactSets[workingKey] = artifactSet + return saveManifest(ctx, h.ArtifactStore, ownerKey, manifest, rawVersion) +} + +func (h *ForkModeHandler) ReadyToPromote(pending store.SnapshotArtifactSet) bool { + return allNodeArtifactsReady(pending.Artifacts) +} + +func (h *ForkModeHandler) CleanupTask(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, task *runtimev1alpha1.SandboxSnapshotTask) error { + sbName := buildSandboxName(ss.Name, task.Spec.TargetNodeName) + buildSandbox := &sandboxv1alpha1.Sandbox{} + if err := h.Client.Get(ctx, types.NamespacedName{Name: sbName, Namespace: ss.Namespace}, buildSandbox); err != nil { + if apierrors.IsNotFound(err) { + return nil + } + return fmt.Errorf("get build sandbox %s: %w", sbName, err) + } + if err := h.Client.Delete(ctx, buildSandbox); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("delete build sandbox %s: %w", sbName, err) + } + return nil +} + +func (h *ForkModeHandler) CleanupAll(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot) error { + sandboxList := &sandboxv1alpha1.SandboxList{} + if err := h.Client.List(ctx, sandboxList, client.InNamespace(ss.Namespace), client.MatchingLabels{ + runtimev1alpha1.SnapshotNameLabelKey: ss.Name, + runtimev1alpha1.SnapshotBuildLabelKey: "true", + }); err != nil { + return fmt.Errorf("list build sandboxes: %w", err) + } + for i := range sandboxList.Items { + sb := &sandboxList.Items[i] + if err := h.Client.Delete(ctx, sb); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("delete build sandbox %s: %w", sb.Name, err) + } + } + return nil +} + +func (h *ForkModeHandler) clearStaleActiveSet(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, manifest *store.SnapshotArtifactManifest, ownerKey, rawVersion, currentHash string) (string, error) { + if !forkRebuildsOnSourceChange(ss) { + return rawVersion, nil + } + activeSet := activeArtifactSet(manifest) + if activeSet == nil || activeSet.SnapshotHash == currentHash { + return rawVersion, nil + } + log.FromContext(ctx).Info("snapshot hash changed, clearing active artifact set", "snapshot", ss.Name) + h.Recorder.Event(ss, corev1.EventTypeNormal, "SandboxSnapshotRebuilding", "source change detected; clearing active artifact set") + manifest.ActiveSetRef = store.SnapshotArtifactSetRef{} + delete(manifest.ArtifactSets, activeSet.SnapshotKey) + return saveManifest(ctx, h.ArtifactStore, ownerKey, manifest, rawVersion) +} + +func (h *ForkModeHandler) maybeStartBackgroundRebuild(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, manifest *store.SnapshotArtifactManifest, ownerKey, rawVersion, currentHash string) (string, error) { + if ss.Spec.ForkPolicy == nil || ss.Spec.ForkPolicy.RebuildAfter == nil || ss.Status.ReadyAt == nil { + return rawVersion, nil + } + if activeArtifactSet(manifest) == nil || manifest.PendingSetRef.SnapshotKey != "" { + return rawVersion, nil + } + if time.Since(ss.Status.ReadyAt.Time) <= ss.Spec.ForkPolicy.RebuildAfter.Duration { + return rawVersion, nil + } + log.FromContext(ctx).Info("rebuildAfter elapsed, starting background replacement", "snapshot", ss.Name) + h.Recorder.Event(ss, corev1.EventTypeNormal, "SandboxSnapshotRebuilding", "rebuildAfter elapsed; starting background replacement") + pendingKey := startNewArtifactSet(ss, manifest, currentHash) + manifest.PendingSetRef = store.SnapshotArtifactSetRef{SnapshotKey: pendingKey} + return saveManifest(ctx, h.ArtifactStore, ownerKey, manifest, rawVersion) +} + +func (h *ForkModeHandler) ensurePendingSet(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, manifest *store.SnapshotArtifactManifest, ownerKey, rawVersion, currentHash string) (string, error) { + if activeArtifactSet(manifest) != nil || manifest.ActiveSetRef.SnapshotKey != "" || manifest.PendingSetRef.SnapshotKey != "" { + return rawVersion, nil + } + log.FromContext(ctx).Info("no active artifact set, starting initial build", "snapshot", ss.Name) + h.Recorder.Event(ss, corev1.EventTypeNormal, "SandboxSnapshotCreating", "starting initial snapshot build") + pendingKey := startNewArtifactSet(ss, manifest, currentHash) + manifest.PendingSetRef = store.SnapshotArtifactSetRef{SnapshotKey: pendingKey} + return saveManifest(ctx, h.ArtifactStore, ownerKey, manifest, rawVersion) +} + +func (h *ForkModeHandler) selectTargetNodes(ctx context.Context, sc *runtimev1alpha1.SnapshotClass) ([]string, error) { + nodeList := &corev1.NodeList{} + sel := labels.SelectorFromSet(sc.Spec.NodeSelector) + if err := h.Client.List(ctx, nodeList, &client.ListOptions{LabelSelector: sel}); err != nil { + return nil, fmt.Errorf("list nodes: %w", err) + } + var result []string + for _, node := range nodeList.Items { + if !nodeIsReady(&node) { + continue + } + capLabel := runtimev1alpha1.SnapshotProviderLabelPrefix + sc.Spec.ProviderName + if node.Labels[capLabel] != "true" { + continue + } + result = append(result, node.Name) + } + return result, nil +} + +func (h *ForkModeHandler) ensureBuildSandboxAndTask(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, sc *runtimev1alpha1.SnapshotClass, snapshotKey, snapshotHash, nodeName string) error { + sbName := buildSandboxName(ss.Name, nodeName) + taskName := buildTaskName(nodeName, snapshotKey) + + existingTask := &runtimev1alpha1.SandboxSnapshotTask{} + err := h.Client.Get(ctx, types.NamespacedName{Name: taskName, Namespace: ss.Namespace}, existingTask) + if err == nil { + _, err = h.ensureBuildSandbox(ctx, ss, snapshotKey, nodeName, sbName) + return err + } + if !apierrors.IsNotFound(err) { + return fmt.Errorf("get snapshot task %s: %w", taskName, err) + } + + buildSandbox, err := h.ensureBuildSandbox(ctx, ss, snapshotKey, nodeName, sbName) + if err != nil { + return err + } + + task := &runtimev1alpha1.SandboxSnapshotTask{ + ObjectMeta: metaWithLabels(taskName, ss.Namespace, map[string]string{ + runtimev1alpha1.SnapshotNameLabelKey: ss.Name, + runtimev1alpha1.SnapshotKeyLabelKey: snapshotKey, + runtimev1alpha1.SnapshotNodeLabelKey: nodeName, + }), + Spec: runtimev1alpha1.SandboxSnapshotTaskSpec{ + SnapshotRef: corev1.TypedLocalObjectReference{ + APIGroup: ptr.To(runtimev1alpha1.GroupVersion.Group), + Kind: "SandboxSnapshot", + Name: ss.Name, + }, + SnapshotUID: ss.UID, + SnapshotMode: ss.Spec.SnapshotMode, + TargetSandboxRef: corev1.TypedLocalObjectReference{ + APIGroup: ptr.To("agents.x-k8s.io"), + Kind: "Sandbox", + Name: buildSandbox.Name, + }, + TargetNodeName: nodeName, + ProviderName: sc.Spec.ProviderName, + SnapshotKey: snapshotKey, + SnapshotHash: snapshotHash, + }, + } + if err := controllerutil.SetControllerReference(ss, task, h.Scheme); err != nil { + return fmt.Errorf("set controller reference on task: %w", err) + } + if err := h.Client.Create(ctx, task); err != nil && !apierrors.IsAlreadyExists(err) { + return fmt.Errorf("create snapshot task %s: %w", taskName, err) + } + return nil +} + +func (h *ForkModeHandler) ensureBuildSandbox(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, snapshotKey, nodeName, sbName string) (*sandboxv1alpha1.Sandbox, error) { + buildSandbox := &sandboxv1alpha1.Sandbox{} + err := h.Client.Get(ctx, types.NamespacedName{Name: sbName, Namespace: ss.Namespace}, buildSandbox) + if apierrors.IsNotFound(err) { + tmpl := &extensionsv1alpha1.SandboxTemplate{} + if err := h.Client.Get(ctx, types.NamespacedName{Name: ss.Spec.SourceRef.Name, Namespace: ss.Namespace}, tmpl); err != nil { + return nil, fmt.Errorf("get source SandboxTemplate: %w", err) + } + podSpec := tmpl.Spec.PodTemplate.Spec.DeepCopy() + podSpec.NodeName = nodeName + + buildSandbox = &sandboxv1alpha1.Sandbox{ + ObjectMeta: metaWithLabels(sbName, ss.Namespace, map[string]string{ + runtimev1alpha1.SnapshotNameLabelKey: ss.Name, + runtimev1alpha1.SnapshotKeyLabelKey: snapshotKey, + runtimev1alpha1.SnapshotNodeLabelKey: nodeName, + runtimev1alpha1.SnapshotBuildLabelKey: "true", + }), + Spec: sandboxv1alpha1.SandboxSpec{ + PodTemplate: sandboxv1alpha1.PodTemplate{Spec: *podSpec}, + Replicas: ptr.To[int32](1), + }, + } + if err := controllerutil.SetControllerReference(ss, buildSandbox, h.Scheme); err != nil { + return nil, fmt.Errorf("set controller reference on build sandbox: %w", err) + } + if err := h.Client.Create(ctx, buildSandbox); err != nil && !apierrors.IsAlreadyExists(err) { + return nil, fmt.Errorf("create build sandbox %s: %w", sbName, err) + } + if err := h.Client.Get(ctx, types.NamespacedName{Name: sbName, Namespace: ss.Namespace}, buildSandbox); err != nil { + return nil, fmt.Errorf("re-fetch build sandbox: %w", err) + } + } else if err != nil { + return nil, fmt.Errorf("get build sandbox %s: %w", sbName, err) + } + return buildSandbox, nil +} + +// -- Fork-specific helper functions -- + +func forkRebuildsOnSourceChange(ss *runtimev1alpha1.SandboxSnapshot) bool { + if ss.Spec.ForkPolicy == nil || ss.Spec.ForkPolicy.RebuildOnSourceChange == nil { + return true + } + return *ss.Spec.ForkPolicy.RebuildOnSourceChange +} + +func allNodeArtifactsReady(artifacts []store.SnapshotArtifact) bool { + if len(artifacts) == 0 { + return false + } + for _, a := range artifacts { + if a.Phase != store.SnapshotArtifactPhaseReady { + return false + } + } + return true +} + +func coveredArtifactNodes(artifacts []store.SnapshotArtifact) map[string]struct{} { + covered := make(map[string]struct{}, len(artifacts)) + for _, art := range artifacts { + covered[art.NodeName] = struct{}{} + } + return covered +} + +func newCreatingArtifact(providerName, nodeName string, artifactSet store.SnapshotArtifactSet) store.SnapshotArtifact { + return store.SnapshotArtifact{ + ProviderName: providerName, + NodeName: nodeName, + Phase: store.SnapshotArtifactPhaseCreating, + SnapshotKey: artifactSet.SnapshotKey, + SnapshotHash: artifactSet.SnapshotHash, + } +} + +func buildSandboxName(snapshotName, nodeName string) string { + return fmt.Sprintf("%s-build-%s", normalizeLabel(snapshotName), normalizeLabel(nodeName)) +} + +func buildTaskName(nodeName, snapshotKey string) string { + return fmt.Sprintf("%s-%s", normalizeLabel(snapshotKey), normalizeLabel(nodeName)) +} + +func nodeIsReady(node *corev1.Node) bool { + for _, cond := range node.Status.Conditions { + if cond.Type == corev1.NodeReady { + return cond.Status == corev1.ConditionTrue + } + } + return false +} + +// snapshotHashInput is the stable serialization for hash computation. +type snapshotHashInput struct { + SnapshotMode string `json:"snapshotMode"` + SourceNamespace string `json:"sourceNamespace"` + SourceName string `json:"sourceName"` + SourceUID string `json:"sourceUID"` + PodTemplateSpec corev1.PodSpec `json:"podTemplateSpec"` + SnapshotClass snapshotClassHash `json:"snapshotClass"` +} + +type snapshotClassHash struct { + Name string `json:"name"` + ProviderName string `json:"providerName"` +} + +func computeSnapshotHash(ss *runtimev1alpha1.SandboxSnapshot, podSpec corev1.PodSpec, sc *runtimev1alpha1.SnapshotClass) (string, error) { + input := snapshotHashInput{ + SnapshotMode: string(ss.Spec.SnapshotMode), + SourceNamespace: ss.Namespace, + SourceName: ss.Spec.SourceRef.Name, + SourceUID: string(ss.UID), + PodTemplateSpec: normalizePodSpec(podSpec), + SnapshotClass: snapshotClassHash{ + Name: sc.Name, + ProviderName: sc.Spec.ProviderName, + }, + } + data, err := json.Marshal(input) + if err != nil { + return "", err + } + h := sha256.Sum256(data) + return fmt.Sprintf("sha256:%x", h), nil +} + +func normalizePodSpec(spec corev1.PodSpec) corev1.PodSpec { + s := spec.DeepCopy() + s.NodeName = "" + sort.Slice(s.Tolerations, func(i, j int) bool { + return s.Tolerations[i].Key < s.Tolerations[j].Key + }) + return *s +} + +func metaWithLabels(name, namespace string, lbls map[string]string) metav1.ObjectMeta { + return metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: lbls, + } +} + +// lookupActiveForkSnapshotKey returns the active snapshot key for a Fork-mode SandboxSnapshot +// whose sourceRef matches sandboxTemplateName, or an empty string when none is Ready. +// Errors from the artifact store are logged and treated as cache-miss so session creation +// falls back to cold start rather than failing. +func lookupActiveForkSnapshotKey( + ctx context.Context, + k8sClient client.Client, + artifactStore store.ArtifactStore, + namespace, sandboxTemplateName string, +) string { + snapshotList := &runtimev1alpha1.SandboxSnapshotList{} + if err := k8sClient.List(ctx, snapshotList, client.InNamespace(namespace)); err != nil { + klog.V(4).InfoS("snapshot lookup: failed to list snapshots, falling back to cold start", + "namespace", namespace, "error", err) + return "" + } + + for i := range snapshotList.Items { + ss := &snapshotList.Items[i] + if ss.Spec.SnapshotMode != runtimev1alpha1.SandboxSnapshotModeFork { + continue + } + if ss.Spec.SourceRef.Name != sandboxTemplateName { + continue + } + if ss.Status.Phase != runtimev1alpha1.SandboxSnapshotPhaseReady { + continue + } + + ownerKey := store.ArtifactOwnerKey("SandboxSnapshot", ss.Namespace, ss.Name, string(ss.UID)) + manifest, err := artifactStore.GetManifest(ctx, ownerKey) + if err != nil { + klog.V(4).InfoS("snapshot lookup: artifact store error, falling back to cold start", + "snapshot", ss.Name, "error", err) + continue + } + if manifest == nil || manifest.ActiveSetRef.SnapshotKey == "" { + continue + } + activeSet, ok := manifest.ArtifactSets[manifest.ActiveSetRef.SnapshotKey] + if !ok { + continue + } + for _, art := range activeSet.Artifacts { + if art.Phase == store.SnapshotArtifactPhaseReady { + klog.V(4).InfoS("snapshot lookup: found active fork snapshot key", + "snapshot", ss.Name, "snapshotKey", manifest.ActiveSetRef.SnapshotKey) + return manifest.ActiveSetRef.SnapshotKey + } + } + } + return "" +} diff --git a/pkg/workloadmanager/snapshot_mode.go b/pkg/workloadmanager/snapshot_mode.go new file mode 100644 index 00000000..67ccb07c --- /dev/null +++ b/pkg/workloadmanager/snapshot_mode.go @@ -0,0 +1,53 @@ +/* +Copyright The Volcano Authors. + +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 workloadmanager + +import ( + "context" + + runtimev1alpha1 "github.com/volcano-sh/agentcube/pkg/apis/runtime/v1alpha1" + "github.com/volcano-sh/agentcube/pkg/store" +) + +// SnapshotModeHandler encapsulates all mode-specific logic for reconciling a SandboxSnapshot. +// To add a new snapshot mode, implement this interface and register it in SetupWithManager. +type SnapshotModeHandler interface { + // ComputeHash returns the content hash for the snapshot's current source inputs. + // The hash is used to detect when the snapshot needs to be rebuilt. + ComputeHash(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, sc *runtimev1alpha1.SnapshotClass) (string, error) + + // PrepareArtifactSet ensures the manifest has an active or pending artifact set, + // applying mode-specific rebuild policy. May save the manifest multiple times. + // Returns the updated version token. + PrepareArtifactSet(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, sc *runtimev1alpha1.SnapshotClass, + manifest *store.SnapshotArtifactManifest, ownerKey, rawVersion, currentHash string) (string, error) + + // EnsureTasks creates snapshot tasks for uncovered target nodes in the working artifact set. + // Appends new artifacts and saves the manifest when tasks are added. + EnsureTasks(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, sc *runtimev1alpha1.SnapshotClass, + manifest *store.SnapshotArtifactManifest, ownerKey, rawVersion, workingKey string, + artifactSet store.SnapshotArtifactSet) (string, error) + + // ReadyToPromote returns true when the pending artifact set should be promoted to active. + ReadyToPromote(pending store.SnapshotArtifactSet) bool + + // CleanupTask performs mode-specific cleanup when a task reaches a terminal phase. + CleanupTask(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot, task *runtimev1alpha1.SandboxSnapshotTask) error + + // CleanupAll performs mode-specific cleanup when the SandboxSnapshot is deleted. + CleanupAll(ctx context.Context, ss *runtimev1alpha1.SandboxSnapshot) error +}