diff --git a/main.go b/main.go index f7e7c86f..f2ac2461 100644 --- a/main.go +++ b/main.go @@ -4,11 +4,12 @@ import ( "context" "crypto/tls" "flag" + "os" + "github.com/hobbyfarm/gargantua/pkg/preinstall" "github.com/hobbyfarm/gargantua/pkg/settingclient" "github.com/hobbyfarm/gargantua/pkg/settingserver" "github.com/hobbyfarm/gargantua/pkg/webhook/validation" - "os" "github.com/ebauman/crder" "github.com/hobbyfarm/gargantua/pkg/crd" @@ -17,6 +18,7 @@ import ( "github.com/hobbyfarm/gargantua/pkg/rbacserver" tls2 "github.com/hobbyfarm/gargantua/pkg/tls" "github.com/hobbyfarm/gargantua/pkg/webhook/conversion" + "github.com/hobbyfarm/gargantua/pkg/webhook/conversion/scenario" "github.com/hobbyfarm/gargantua/pkg/webhook/conversion/user" "golang.org/x/sync/errgroup" apiextensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" @@ -47,7 +49,7 @@ import ( "github.com/hobbyfarm/gargantua/pkg/courseclient" "github.com/hobbyfarm/gargantua/pkg/courseserver" "github.com/hobbyfarm/gargantua/pkg/environmentserver" - "github.com/hobbyfarm/gargantua/pkg/predefinedserviceserver" + predefinedservicesserver "github.com/hobbyfarm/gargantua/pkg/predefinedserviceserver" "github.com/hobbyfarm/gargantua/pkg/progressserver" "github.com/hobbyfarm/gargantua/pkg/scenarioclient" "github.com/hobbyfarm/gargantua/pkg/scenarioserver" @@ -324,6 +326,8 @@ func main() { // shell server does not serve webhook endpoint, so don't start it if !shellServer { user.Init() + scenario.Init() + webhookRouter := mux.NewRouter() conversion.New(webhookRouter, apiExtensionsClient, string(ca)) diff --git a/pkg/apis/hobbyfarm.io/v2/register.go b/pkg/apis/hobbyfarm.io/v2/register.go index 66d5b0f6..2ea10c15 100644 --- a/pkg/apis/hobbyfarm.io/v2/register.go +++ b/pkg/apis/hobbyfarm.io/v2/register.go @@ -25,6 +25,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &User{}, &UserList{}, + &Scenario{}, + &ScenarioList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) diff --git a/pkg/apis/hobbyfarm.io/v2/types.go b/pkg/apis/hobbyfarm.io/v2/types.go index 74885854..346721cf 100644 --- a/pkg/apis/hobbyfarm.io/v2/types.go +++ b/pkg/apis/hobbyfarm.io/v2/types.go @@ -26,3 +26,51 @@ type UserSpec struct { AccessCodes []string `json:"access_codes"` Settings map[string]string `json:"settings"` } + +// +genclient +// +genclient:noStatus +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +type Scenario struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec ScenarioSpec `json:"spec"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ScenarioList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []Scenario `json:"items"` +} + +type ScenarioSpec struct { + Name string `json:"name"` + Description string `json:"description"` + Steps []ScenarioStep `json:"steps"` + Categories []string `json:"categories"` + Tags []string `json:"tags"` + VirtualMachines []map[string]string `json:"virtualmachines"` + KeepAliveDuration string `json:"keepalive_duration"` + PauseDuration string `json:"pause_duration"` + Pauseable bool `json:"pauseable"` + Tasks []VirtualMachineTasks `json:"vm_tasks"` + +} + +type VirtualMachineTasks struct{ + VMName string `json:"vm_name"` + Tasks []Task `json:"task_command"` +} +type Task struct{ + Name string `json:"name"` + Description string `json:"description"` + Command string `json:"command"` + ExpectedOutputValue string `json:"expected_output_value"` + ExpectedReturnCode int `json:"expected_return_code"` +} + +type ScenarioStep struct { + Title string `json:"title"` + Content string `json:"content"` +} \ No newline at end of file diff --git a/pkg/apis/hobbyfarm.io/v2/zz_generated.deepcopy.go b/pkg/apis/hobbyfarm.io/v2/zz_generated.deepcopy.go index 1a22c5cc..377d844d 100644 --- a/pkg/apis/hobbyfarm.io/v2/zz_generated.deepcopy.go +++ b/pkg/apis/hobbyfarm.io/v2/zz_generated.deepcopy.go @@ -25,6 +25,149 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Scenario) DeepCopyInto(out *Scenario) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Scenario. +func (in *Scenario) DeepCopy() *Scenario { + if in == nil { + return nil + } + out := new(Scenario) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Scenario) 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 *ScenarioList) DeepCopyInto(out *ScenarioList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Scenario, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScenarioList. +func (in *ScenarioList) DeepCopy() *ScenarioList { + if in == nil { + return nil + } + out := new(ScenarioList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ScenarioList) 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 *ScenarioSpec) DeepCopyInto(out *ScenarioSpec) { + *out = *in + if in.Steps != nil { + in, out := &in.Steps, &out.Steps + *out = make([]ScenarioStep, len(*in)) + copy(*out, *in) + } + if in.Categories != nil { + in, out := &in.Categories, &out.Categories + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.VirtualMachines != nil { + in, out := &in.VirtualMachines, &out.VirtualMachines + *out = make([]map[string]string, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + } + } + if in.Tasks != nil { + in, out := &in.Tasks, &out.Tasks + *out = make([]VirtualMachineTasks, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScenarioSpec. +func (in *ScenarioSpec) DeepCopy() *ScenarioSpec { + if in == nil { + return nil + } + out := new(ScenarioSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ScenarioStep) DeepCopyInto(out *ScenarioStep) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScenarioStep. +func (in *ScenarioStep) DeepCopy() *ScenarioStep { + if in == nil { + return nil + } + out := new(ScenarioStep) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Task) DeepCopyInto(out *Task) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Task. +func (in *Task) DeepCopy() *Task { + if in == nil { + return nil + } + out := new(Task) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *User) DeepCopyInto(out *User) { *out = *in @@ -112,3 +255,24 @@ func (in *UserSpec) DeepCopy() *UserSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineTasks) DeepCopyInto(out *VirtualMachineTasks) { + *out = *in + if in.Tasks != nil { + in, out := &in.Tasks, &out.Tasks + *out = make([]Task, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineTasks. +func (in *VirtualMachineTasks) DeepCopy() *VirtualMachineTasks { + if in == nil { + return nil + } + out := new(VirtualMachineTasks) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/client/clientset/versioned/typed/hobbyfarm.io/v2/fake/fake_hobbyfarm.io_client.go b/pkg/client/clientset/versioned/typed/hobbyfarm.io/v2/fake/fake_hobbyfarm.io_client.go index 2ef5630e..344796b1 100644 --- a/pkg/client/clientset/versioned/typed/hobbyfarm.io/v2/fake/fake_hobbyfarm.io_client.go +++ b/pkg/client/clientset/versioned/typed/hobbyfarm.io/v2/fake/fake_hobbyfarm.io_client.go @@ -28,6 +28,10 @@ type FakeHobbyfarmV2 struct { *testing.Fake } +func (c *FakeHobbyfarmV2) Scenarios(namespace string) v2.ScenarioInterface { + return &FakeScenarios{c, namespace} +} + func (c *FakeHobbyfarmV2) Users(namespace string) v2.UserInterface { return &FakeUsers{c, namespace} } diff --git a/pkg/client/clientset/versioned/typed/hobbyfarm.io/v2/fake/fake_scenario.go b/pkg/client/clientset/versioned/typed/hobbyfarm.io/v2/fake/fake_scenario.go new file mode 100644 index 00000000..d167d4a9 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/hobbyfarm.io/v2/fake/fake_scenario.go @@ -0,0 +1,130 @@ +/* +Copyright The Kubernetes 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 ( + "context" + + v2 "github.com/hobbyfarm/gargantua/pkg/apis/hobbyfarm.io/v2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeScenarios implements ScenarioInterface +type FakeScenarios struct { + Fake *FakeHobbyfarmV2 + ns string +} + +var scenariosResource = schema.GroupVersionResource{Group: "hobbyfarm.io", Version: "v2", Resource: "scenarios"} + +var scenariosKind = schema.GroupVersionKind{Group: "hobbyfarm.io", Version: "v2", Kind: "Scenario"} + +// Get takes name of the scenario, and returns the corresponding scenario object, and an error if there is any. +func (c *FakeScenarios) Get(ctx context.Context, name string, options v1.GetOptions) (result *v2.Scenario, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(scenariosResource, c.ns, name), &v2.Scenario{}) + + if obj == nil { + return nil, err + } + return obj.(*v2.Scenario), err +} + +// List takes label and field selectors, and returns the list of Scenarios that match those selectors. +func (c *FakeScenarios) List(ctx context.Context, opts v1.ListOptions) (result *v2.ScenarioList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(scenariosResource, scenariosKind, c.ns, opts), &v2.ScenarioList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v2.ScenarioList{ListMeta: obj.(*v2.ScenarioList).ListMeta} + for _, item := range obj.(*v2.ScenarioList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested scenarios. +func (c *FakeScenarios) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(scenariosResource, c.ns, opts)) + +} + +// Create takes the representation of a scenario and creates it. Returns the server's representation of the scenario, and an error, if there is any. +func (c *FakeScenarios) Create(ctx context.Context, scenario *v2.Scenario, opts v1.CreateOptions) (result *v2.Scenario, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(scenariosResource, c.ns, scenario), &v2.Scenario{}) + + if obj == nil { + return nil, err + } + return obj.(*v2.Scenario), err +} + +// Update takes the representation of a scenario and updates it. Returns the server's representation of the scenario, and an error, if there is any. +func (c *FakeScenarios) Update(ctx context.Context, scenario *v2.Scenario, opts v1.UpdateOptions) (result *v2.Scenario, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(scenariosResource, c.ns, scenario), &v2.Scenario{}) + + if obj == nil { + return nil, err + } + return obj.(*v2.Scenario), err +} + +// Delete takes name of the scenario and deletes it. Returns an error if one occurs. +func (c *FakeScenarios) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(scenariosResource, c.ns, name, opts), &v2.Scenario{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeScenarios) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(scenariosResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v2.ScenarioList{}) + return err +} + +// Patch applies the patch and returns the patched scenario. +func (c *FakeScenarios) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v2.Scenario, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(scenariosResource, c.ns, name, pt, data, subresources...), &v2.Scenario{}) + + if obj == nil { + return nil, err + } + return obj.(*v2.Scenario), err +} diff --git a/pkg/client/clientset/versioned/typed/hobbyfarm.io/v2/generated_expansion.go b/pkg/client/clientset/versioned/typed/hobbyfarm.io/v2/generated_expansion.go index e5ccd4ee..92ee2ef6 100644 --- a/pkg/client/clientset/versioned/typed/hobbyfarm.io/v2/generated_expansion.go +++ b/pkg/client/clientset/versioned/typed/hobbyfarm.io/v2/generated_expansion.go @@ -18,4 +18,6 @@ limitations under the License. package v2 +type ScenarioExpansion interface{} + type UserExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/hobbyfarm.io/v2/hobbyfarm.io_client.go b/pkg/client/clientset/versioned/typed/hobbyfarm.io/v2/hobbyfarm.io_client.go index e3dd3b28..e308b04b 100644 --- a/pkg/client/clientset/versioned/typed/hobbyfarm.io/v2/hobbyfarm.io_client.go +++ b/pkg/client/clientset/versioned/typed/hobbyfarm.io/v2/hobbyfarm.io_client.go @@ -28,6 +28,7 @@ import ( type HobbyfarmV2Interface interface { RESTClient() rest.Interface + ScenariosGetter UsersGetter } @@ -36,6 +37,10 @@ type HobbyfarmV2Client struct { restClient rest.Interface } +func (c *HobbyfarmV2Client) Scenarios(namespace string) ScenarioInterface { + return newScenarios(c, namespace) +} + func (c *HobbyfarmV2Client) Users(namespace string) UserInterface { return newUsers(c, namespace) } diff --git a/pkg/client/clientset/versioned/typed/hobbyfarm.io/v2/scenario.go b/pkg/client/clientset/versioned/typed/hobbyfarm.io/v2/scenario.go new file mode 100644 index 00000000..77030558 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/hobbyfarm.io/v2/scenario.go @@ -0,0 +1,178 @@ +/* +Copyright The Kubernetes 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 v2 + +import ( + "context" + "time" + + v2 "github.com/hobbyfarm/gargantua/pkg/apis/hobbyfarm.io/v2" + scheme "github.com/hobbyfarm/gargantua/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// ScenariosGetter has a method to return a ScenarioInterface. +// A group's client should implement this interface. +type ScenariosGetter interface { + Scenarios(namespace string) ScenarioInterface +} + +// ScenarioInterface has methods to work with Scenario resources. +type ScenarioInterface interface { + Create(ctx context.Context, scenario *v2.Scenario, opts v1.CreateOptions) (*v2.Scenario, error) + Update(ctx context.Context, scenario *v2.Scenario, opts v1.UpdateOptions) (*v2.Scenario, 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) (*v2.Scenario, error) + List(ctx context.Context, opts v1.ListOptions) (*v2.ScenarioList, 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 *v2.Scenario, err error) + ScenarioExpansion +} + +// scenarios implements ScenarioInterface +type scenarios struct { + client rest.Interface + ns string +} + +// newScenarios returns a Scenarios +func newScenarios(c *HobbyfarmV2Client, namespace string) *scenarios { + return &scenarios{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the scenario, and returns the corresponding scenario object, and an error if there is any. +func (c *scenarios) Get(ctx context.Context, name string, options v1.GetOptions) (result *v2.Scenario, err error) { + result = &v2.Scenario{} + err = c.client.Get(). + Namespace(c.ns). + Resource("scenarios"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of Scenarios that match those selectors. +func (c *scenarios) List(ctx context.Context, opts v1.ListOptions) (result *v2.ScenarioList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v2.ScenarioList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("scenarios"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested scenarios. +func (c *scenarios) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("scenarios"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a scenario and creates it. Returns the server's representation of the scenario, and an error, if there is any. +func (c *scenarios) Create(ctx context.Context, scenario *v2.Scenario, opts v1.CreateOptions) (result *v2.Scenario, err error) { + result = &v2.Scenario{} + err = c.client.Post(). + Namespace(c.ns). + Resource("scenarios"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(scenario). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a scenario and updates it. Returns the server's representation of the scenario, and an error, if there is any. +func (c *scenarios) Update(ctx context.Context, scenario *v2.Scenario, opts v1.UpdateOptions) (result *v2.Scenario, err error) { + result = &v2.Scenario{} + err = c.client.Put(). + Namespace(c.ns). + Resource("scenarios"). + Name(scenario.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(scenario). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the scenario and deletes it. Returns an error if one occurs. +func (c *scenarios) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("scenarios"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *scenarios) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("scenarios"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched scenario. +func (c *scenarios) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v2.Scenario, err error) { + result = &v2.Scenario{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("scenarios"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index 697da996..20480617 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -91,6 +91,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Hobbyfarm().V1().VirtualMachineTemplates().Informer()}, nil // Group=hobbyfarm.io, Version=v2 + case v2.SchemeGroupVersion.WithResource("scenarios"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Hobbyfarm().V2().Scenarios().Informer()}, nil case v2.SchemeGroupVersion.WithResource("users"): return &genericInformer{resource: resource.GroupResource(), informer: f.Hobbyfarm().V2().Users().Informer()}, nil diff --git a/pkg/client/informers/externalversions/hobbyfarm.io/v2/interface.go b/pkg/client/informers/externalversions/hobbyfarm.io/v2/interface.go index c80fcdeb..5060d381 100644 --- a/pkg/client/informers/externalversions/hobbyfarm.io/v2/interface.go +++ b/pkg/client/informers/externalversions/hobbyfarm.io/v2/interface.go @@ -24,6 +24,8 @@ import ( // Interface provides access to all the informers in this group version. type Interface interface { + // Scenarios returns a ScenarioInformer. + Scenarios() ScenarioInformer // Users returns a UserInformer. Users() UserInformer } @@ -39,6 +41,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// Scenarios returns a ScenarioInformer. +func (v *version) Scenarios() ScenarioInformer { + return &scenarioInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // Users returns a UserInformer. func (v *version) Users() UserInformer { return &userInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/pkg/client/informers/externalversions/hobbyfarm.io/v2/scenario.go b/pkg/client/informers/externalversions/hobbyfarm.io/v2/scenario.go new file mode 100644 index 00000000..047ab078 --- /dev/null +++ b/pkg/client/informers/externalversions/hobbyfarm.io/v2/scenario.go @@ -0,0 +1,90 @@ +/* +Copyright The Kubernetes 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 v2 + +import ( + "context" + time "time" + + hobbyfarmiov2 "github.com/hobbyfarm/gargantua/pkg/apis/hobbyfarm.io/v2" + versioned "github.com/hobbyfarm/gargantua/pkg/client/clientset/versioned" + internalinterfaces "github.com/hobbyfarm/gargantua/pkg/client/informers/externalversions/internalinterfaces" + v2 "github.com/hobbyfarm/gargantua/pkg/client/listers/hobbyfarm.io/v2" + 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" +) + +// ScenarioInformer provides access to a shared informer and lister for +// Scenarios. +type ScenarioInformer interface { + Informer() cache.SharedIndexInformer + Lister() v2.ScenarioLister +} + +type scenarioInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewScenarioInformer constructs a new informer for Scenario 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 NewScenarioInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredScenarioInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredScenarioInformer constructs a new informer for Scenario 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 NewFilteredScenarioInformer(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.HobbyfarmV2().Scenarios(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HobbyfarmV2().Scenarios(namespace).Watch(context.TODO(), options) + }, + }, + &hobbyfarmiov2.Scenario{}, + resyncPeriod, + indexers, + ) +} + +func (f *scenarioInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredScenarioInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *scenarioInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&hobbyfarmiov2.Scenario{}, f.defaultInformer) +} + +func (f *scenarioInformer) Lister() v2.ScenarioLister { + return v2.NewScenarioLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/listers/hobbyfarm.io/v2/expansion_generated.go b/pkg/client/listers/hobbyfarm.io/v2/expansion_generated.go index a3f3c1b1..2c701d4c 100644 --- a/pkg/client/listers/hobbyfarm.io/v2/expansion_generated.go +++ b/pkg/client/listers/hobbyfarm.io/v2/expansion_generated.go @@ -18,6 +18,14 @@ limitations under the License. package v2 +// ScenarioListerExpansion allows custom methods to be added to +// ScenarioLister. +type ScenarioListerExpansion interface{} + +// ScenarioNamespaceListerExpansion allows custom methods to be added to +// ScenarioNamespaceLister. +type ScenarioNamespaceListerExpansion interface{} + // UserListerExpansion allows custom methods to be added to // UserLister. type UserListerExpansion interface{} diff --git a/pkg/client/listers/hobbyfarm.io/v2/scenario.go b/pkg/client/listers/hobbyfarm.io/v2/scenario.go new file mode 100644 index 00000000..057b1d89 --- /dev/null +++ b/pkg/client/listers/hobbyfarm.io/v2/scenario.go @@ -0,0 +1,99 @@ +/* +Copyright The Kubernetes 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 v2 + +import ( + v2 "github.com/hobbyfarm/gargantua/pkg/apis/hobbyfarm.io/v2" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// ScenarioLister helps list Scenarios. +// All objects returned here must be treated as read-only. +type ScenarioLister interface { + // List lists all Scenarios in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v2.Scenario, err error) + // Scenarios returns an object that can list and get Scenarios. + Scenarios(namespace string) ScenarioNamespaceLister + ScenarioListerExpansion +} + +// scenarioLister implements the ScenarioLister interface. +type scenarioLister struct { + indexer cache.Indexer +} + +// NewScenarioLister returns a new ScenarioLister. +func NewScenarioLister(indexer cache.Indexer) ScenarioLister { + return &scenarioLister{indexer: indexer} +} + +// List lists all Scenarios in the indexer. +func (s *scenarioLister) List(selector labels.Selector) (ret []*v2.Scenario, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v2.Scenario)) + }) + return ret, err +} + +// Scenarios returns an object that can list and get Scenarios. +func (s *scenarioLister) Scenarios(namespace string) ScenarioNamespaceLister { + return scenarioNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// ScenarioNamespaceLister helps list and get Scenarios. +// All objects returned here must be treated as read-only. +type ScenarioNamespaceLister interface { + // List lists all Scenarios in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v2.Scenario, err error) + // Get retrieves the Scenario from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v2.Scenario, error) + ScenarioNamespaceListerExpansion +} + +// scenarioNamespaceLister implements the ScenarioNamespaceLister +// interface. +type scenarioNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all Scenarios in the indexer for a given namespace. +func (s scenarioNamespaceLister) List(selector labels.Selector) (ret []*v2.Scenario, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v2.Scenario)) + }) + return ret, err +} + +// Get retrieves the Scenario from the indexer for a given namespace and name. +func (s scenarioNamespaceLister) Get(name string) (*v2.Scenario, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v2.Resource("scenario"), name) + } + return obj.(*v2.Scenario), nil +} diff --git a/pkg/courseserver/courseserver.go b/pkg/courseserver/courseserver.go index 47ef6a74..5d087517 100644 --- a/pkg/courseserver/courseserver.go +++ b/pkg/courseserver/courseserver.go @@ -542,7 +542,7 @@ func (c CourseServer) AppendDynamicScenariosByCategories(scenariosList []string, categorySelector = metav1.ListOptions{ LabelSelector: fmt.Sprintf("%s", categorySelectorString), } - scenarios, err := c.hfClientSet.HobbyfarmV1().Scenarios(util.GetReleaseNamespace()).List(c.ctx, categorySelector) + scenarios, err := c.hfClientSet.HobbyfarmV2().Scenarios(util.GetReleaseNamespace()).List(c.ctx, categorySelector) if err != nil { glog.Errorf("error while retrieving scenarios %v", err) diff --git a/pkg/crd/crd.go b/pkg/crd/crd.go index 5a5977ad..d8f4fe8b 100644 --- a/pkg/crd/crd.go +++ b/pkg/crd/crd.go @@ -2,6 +2,7 @@ package crd import ( "fmt" + "github.com/ebauman/crder" v1 "github.com/hobbyfarm/gargantua/pkg/apis/hobbyfarm.io/v1" v2 "github.com/hobbyfarm/gargantua/pkg/apis/hobbyfarm.io/v2" @@ -69,7 +70,21 @@ func GenerateCRDs(caBundle string, reference ServiceReference) []crder.CRD { hobbyfarmCRD(&v1.Scenario{}, func(c *crder.CRD) { c. IsNamespaced(true). - AddVersion("v1", &v1.Scenario{}, nil) + AddVersion("v1", &v1.Scenario{}, func(cv *crder.Version) { + cv.IsServed(true) + cv.IsStored(false) + }). + AddVersion("v2", &v2.Scenario{}, func(cv *crder.Version) { + cv.IsServed(true) + cv.IsStored(true) + }). + WithConversion(func(cc *crder.Conversion) { + cc. + StrategyWebhook(). + WithCABundle(caBundle). + WithService(reference.Toapiextv1WithPath("/conversion/scenarios.hobbyfarm.io")). + WithVersions("v2", "v1") + }) }), hobbyfarmCRD(&v1.Session{}, func(c *crder.CRD) { c. diff --git a/pkg/scenarioclient/scenarioclient.go b/pkg/scenarioclient/scenarioclient.go index 2df0cec8..b9525376 100644 --- a/pkg/scenarioclient/scenarioclient.go +++ b/pkg/scenarioclient/scenarioclient.go @@ -1,7 +1,7 @@ package scenarioclient import ( - hfv1 "github.com/hobbyfarm/gargantua/pkg/apis/hobbyfarm.io/v1" + hfv2 "github.com/hobbyfarm/gargantua/pkg/apis/hobbyfarm.io/v2" "github.com/hobbyfarm/gargantua/pkg/scenarioserver" ) @@ -16,12 +16,12 @@ func NewScenarioClient(sServer *scenarioserver.ScenarioServer) (*ScenarioClient, return &a, nil } -func (sc ScenarioClient) GetScenarioById(id string) (hfv1.Scenario, error) { +func (sc ScenarioClient) GetScenarioById(id string) (hfv2.Scenario, error) { sResult, err := sc.sServer.GetScenarioById(id) if err != nil { - return hfv1.Scenario{}, err + return hfv2.Scenario{}, err } return sResult, nil diff --git a/pkg/scenarioserver/scenarioserver.go b/pkg/scenarioserver/scenarioserver.go index 6d7a2f2f..b871d0c6 100644 --- a/pkg/scenarioserver/scenarioserver.go +++ b/pkg/scenarioserver/scenarioserver.go @@ -7,28 +7,31 @@ import ( "encoding/base64" "encoding/json" "fmt" - "github.com/hobbyfarm/gargantua/pkg/rbacclient" "net/http" "sort" "strconv" "strings" + "github.com/hobbyfarm/gargantua/pkg/rbacclient" + "github.com/golang/glog" "github.com/gorilla/mux" "github.com/hobbyfarm/gargantua/pkg/accesscode" hfv1 "github.com/hobbyfarm/gargantua/pkg/apis/hobbyfarm.io/v1" + hfv2 "github.com/hobbyfarm/gargantua/pkg/apis/hobbyfarm.io/v2" "github.com/hobbyfarm/gargantua/pkg/authclient" hfClientset "github.com/hobbyfarm/gargantua/pkg/client/clientset/versioned" hfInformers "github.com/hobbyfarm/gargantua/pkg/client/informers/externalversions" "github.com/hobbyfarm/gargantua/pkg/courseclient" "github.com/hobbyfarm/gargantua/pkg/util" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/retry" ) const ( - idIndex = "scenarioserver.hobbyfarm.io/id-index" + idIndex = "scenarioserver.hobbyfarm.io/id-index" resourcePlural = "scenarios" ) @@ -47,18 +50,19 @@ type PreparedScenarioStep struct { } type PreparedScenario struct { - Id string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - StepCount int `json:"stepcount"` - VirtualMachines []map[string]string `json:"virtualmachines"` - Pauseable bool `json:"pauseable"` - Printable bool `json:"printable"` + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + StepCount int `json:"stepcount"` + VirtualMachines []map[string]string `json:"virtualmachines"` + Pauseable bool `json:"pauseable"` + Printable bool `json:"printable"` + Tasks []hfv2.VirtualMachineTasks `json:"vm_tasks"` } type AdminPreparedScenario struct { ID string `json:"id"` - hfv1.ScenarioSpec + hfv2.ScenarioSpec } func NewScenarioServer(authClient *authclient.AuthClient, acClient *accesscode.AccessCodeClient, hfClientset hfClientset.Interface, hfInformerFactory hfInformers.SharedInformerFactory, ctx context.Context, courseClient *courseclient.CourseClient) (*ScenarioServer, error) { @@ -68,7 +72,7 @@ func NewScenarioServer(authClient *authclient.AuthClient, acClient *accesscode.A scenario.acClient = acClient scenario.courseClient = courseClient scenario.auth = authClient - inf := hfInformerFactory.Hobbyfarm().V1().Scenarios().Informer() + inf := hfInformerFactory.Hobbyfarm().V2().Scenarios().Informer() indexers := map[string]cache.IndexFunc{idIndex: idIndexer} err := inf.AddIndexers(indexers) if err != nil { @@ -96,7 +100,7 @@ func (s ScenarioServer) SetupRoutes(r *mux.Router) { glog.V(2).Infof("set up route") } -func (s ScenarioServer) prepareScenario(scenario hfv1.Scenario, printable bool) (PreparedScenario, error) { +func (s ScenarioServer) prepareScenario(scenario hfv2.Scenario, printable bool) (PreparedScenario, error) { ps := PreparedScenario{} ps.Id = scenario.Name @@ -106,7 +110,7 @@ func (s ScenarioServer) prepareScenario(scenario hfv1.Scenario, printable bool) ps.Pauseable = scenario.Spec.Pauseable ps.Printable = printable ps.StepCount = len(scenario.Spec.Steps) - + ps.Tasks = scenario.Spec.Tasks return ps, nil } @@ -265,13 +269,11 @@ func (s ScenarioServer) AdminDeleteFunc(w http.ResponseWriter, r *http.Request) return } - // when can we safely a scenario? // 1. when there are no active scheduled events using the scenario // 2. when there are no sessions using the scenario // 3. when there is no course using the scenario - seList, err := s.hfClientSet.HobbyfarmV1().ScheduledEvents(util.GetReleaseNamespace()).List(s.ctx, metav1.ListOptions{}) if err != nil { glog.Errorf("error retrieving scheduledevent list: %v", err) @@ -387,9 +389,9 @@ func (s ScenarioServer) ListScenarioForAccessCode(w http.ResponseWriter, r *http contains := false for _, acc := range user.Spec.AccessCodes { - if(acc == accessCode){ + if acc == accessCode { contains = true - break; + break } } @@ -470,7 +472,7 @@ func (s ScenarioServer) ListFunc(w http.ResponseWriter, r *http.Request, categor } } - scenarios, err := s.hfClientSet.HobbyfarmV1().Scenarios(util.GetReleaseNamespace()).List(s.ctx, categorySelector) + scenarios, err := s.hfClientSet.HobbyfarmV2().Scenarios(util.GetReleaseNamespace()).List(s.ctx, categorySelector) if err != nil { glog.Errorf("error while retrieving scenarios %v", err) @@ -501,7 +503,7 @@ func (s ScenarioServer) ListCategories(w http.ResponseWriter, r *http.Request) { return } - scenarios, err := s.hfClientSet.HobbyfarmV1().Scenarios(util.GetReleaseNamespace()).List(s.ctx, metav1.ListOptions{}) + scenarios, err := s.hfClientSet.HobbyfarmV2().Scenarios(util.GetReleaseNamespace()).List(s.ctx, metav1.ListOptions{}) if err != nil { glog.Errorf("error while retrieving scenarios %v", err) @@ -675,7 +677,7 @@ func (s ScenarioServer) CopyFunc(w http.ResponseWriter, r *http.Request) { } retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { - scenario, err := s.hfClientSet.HobbyfarmV1().Scenarios(util.GetReleaseNamespace()).Get(s.ctx, id, metav1.GetOptions{}) + scenario, err := s.hfClientSet.HobbyfarmV2().Scenarios(util.GetReleaseNamespace()).Get(s.ctx, id, metav1.GetOptions{}) if err != nil { glog.Error(err) util.ReturnHTTPMessage(w, r, http.StatusNotFound, "badrequest", "no scenario found with given ID") @@ -695,7 +697,7 @@ func (s ScenarioServer) CopyFunc(w http.ResponseWriter, r *http.Request) { scenario.Spec.Name = copyName scenario.Name = "s-" + strings.ToLower(sha) - _, updateErr := s.hfClientSet.HobbyfarmV1().Scenarios(util.GetReleaseNamespace()).Create(s.ctx, scenario, metav1.CreateOptions{}) + _, updateErr := s.hfClientSet.HobbyfarmV2().Scenarios(util.GetReleaseNamespace()).Create(s.ctx, scenario, metav1.CreateOptions{}) return updateErr }) @@ -729,7 +731,7 @@ func (s ScenarioServer) CreateFunc(w http.ResponseWriter, r *http.Request) { keepaliveDuration := r.PostFormValue("keepalive_duration") // we won't error if no keep alive duration is passed in or if it's blank because we'll default elsewhere - steps := []hfv1.ScenarioStep{} + steps := []hfv2.ScenarioStep{} virtualmachines := []map[string]string{} categories := []string{} tags := []string{} @@ -777,7 +779,7 @@ func (s ScenarioServer) CreateFunc(w http.ResponseWriter, r *http.Request) { pauseable := r.PostFormValue("pauseable") pauseDuration := r.PostFormValue("pause_duration") - scenario := &hfv1.Scenario{} + scenario := &hfv2.Scenario{} hasher := sha256.New() hasher.Write([]byte(name)) @@ -792,6 +794,23 @@ func (s ScenarioServer) CreateFunc(w http.ResponseWriter, r *http.Request) { scenario.Spec.Tags = tags scenario.Spec.KeepAliveDuration = keepaliveDuration + rawVMTasks := r.PostFormValue("vm_tasks") + if rawVMTasks != "" { + vm_tasks := []hfv2.VirtualMachineTasks{} + + err = json.Unmarshal([]byte(rawVMTasks), &vm_tasks) + if err != nil { + glog.Errorf("error while unmarshaling tasks %v", err) + return + } + err = VerifyTaskContent(vm_tasks) + if err != nil { + glog.Errorf("error tasks content %v", err) + return + } + scenario.Spec.Tasks = vm_tasks + } + scenario.Spec.Pauseable = false if pauseable != "" { if strings.ToLower(pauseable) == "true" { @@ -803,7 +822,7 @@ func (s ScenarioServer) CreateFunc(w http.ResponseWriter, r *http.Request) { scenario.Spec.PauseDuration = pauseDuration } - scenario, err = s.hfClientSet.HobbyfarmV1().Scenarios(util.GetReleaseNamespace()).Create(s.ctx, scenario, metav1.CreateOptions{}) + scenario, err = s.hfClientSet.HobbyfarmV2().Scenarios(util.GetReleaseNamespace()).Create(s.ctx, scenario, metav1.CreateOptions{}) if err != nil { glog.Errorf("error creating scenario %v", err) util.ReturnHTTPMessage(w, r, 500, "internalerror", "error creating scenario") @@ -830,7 +849,7 @@ func (s ScenarioServer) UpdateFunc(w http.ResponseWriter, r *http.Request) { } retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { - scenario, err := s.hfClientSet.HobbyfarmV1().Scenarios(util.GetReleaseNamespace()).Get(s.ctx, id, metav1.GetOptions{}) + scenario, err := s.hfClientSet.HobbyfarmV2().Scenarios(util.GetReleaseNamespace()).Get(s.ctx, id, metav1.GetOptions{}) if err != nil { glog.Error(err) util.ReturnHTTPMessage(w, r, http.StatusNotFound, "badrequest", "no scenario found with given ID") @@ -846,6 +865,7 @@ func (s ScenarioServer) UpdateFunc(w http.ResponseWriter, r *http.Request) { rawVirtualMachines := r.PostFormValue("virtualmachines") rawCategories := r.PostFormValue("categories") rawTags := r.PostFormValue("tags") + rawVMTasks := r.PostFormValue("vm_tasks") if name != "" { scenario.Spec.Name = name @@ -870,7 +890,7 @@ func (s ScenarioServer) UpdateFunc(w http.ResponseWriter, r *http.Request) { } if rawSteps != "" { - steps := []hfv1.ScenarioStep{} + steps := []hfv2.ScenarioStep{} err = json.Unmarshal([]byte(rawSteps), &steps) if err != nil { @@ -927,11 +947,29 @@ func (s ScenarioServer) UpdateFunc(w http.ResponseWriter, r *http.Request) { scenario.Spec.Tags = tagsSlice } - _, updateErr := s.hfClientSet.HobbyfarmV1().Scenarios(util.GetReleaseNamespace()).Update(s.ctx, scenario, metav1.UpdateOptions{}) + if rawVMTasks != "" { + vm_tasks := []hfv2.VirtualMachineTasks{} + + err = json.Unmarshal([]byte(rawVMTasks), &vm_tasks) + if err != nil { + glog.Errorf("error while unmarshaling tasks %v", err) + return fmt.Errorf("bad") + } + + err = VerifyTaskContent(vm_tasks) + if err != nil { + glog.Errorf("error tasks content %v", err) + return err + } + scenario.Spec.Tasks = vm_tasks + } + + _, updateErr := s.hfClientSet.HobbyfarmV2().Scenarios(util.GetReleaseNamespace()).Update(s.ctx, scenario, metav1.UpdateOptions{}) return updateErr }) if retryErr != nil { + glog.Errorf("error while updating scenario: %v", retryErr) util.ReturnHTTPMessage(w, r, 500, "error", "error attempting to update") return } @@ -940,24 +978,49 @@ func (s ScenarioServer) UpdateFunc(w http.ResponseWriter, r *http.Request) { return } -func (s ScenarioServer) GetScenarioById(id string) (hfv1.Scenario, error) { +func VerifyTaskContent(vm_tasks []hfv2.VirtualMachineTasks) error { + //Verify that name, description, command must not empty + for _, vm_task := range vm_tasks { + if vm_task.VMName == "" { + glog.Errorf("error while vm_name empty") + return fmt.Errorf("bad") + } + for _, task := range vm_task.Tasks { + if task.Name == "" { + glog.Errorf("error while Name of task empty") + return fmt.Errorf("bad") + } + if task.Description == "" { + glog.Errorf("error while Description of task empty") + return fmt.Errorf("bad") + } + if task.Command == "" || task.Command == "[]" { + glog.Errorf("error while Command of task empty") + return fmt.Errorf("bad") + } + } + } + return nil +} + +func (s ScenarioServer) GetScenarioById(id string) (hfv2.Scenario, error) { if len(id) == 0 { - return hfv1.Scenario{}, fmt.Errorf("scenario id passed in was blank") + return hfv2.Scenario{}, fmt.Errorf("scenario id passed in was blank") } obj, err := s.scenarioIndexer.ByIndex(idIndex, id) if err != nil { - return hfv1.Scenario{}, fmt.Errorf("error while retrieving scenario by ID %s %v", id, err) + return hfv2.Scenario{}, fmt.Errorf("error while retrieving scenario by ID %s %v", id, err) } if len(obj) < 1 { - return hfv1.Scenario{}, fmt.Errorf("error while retrieving scenario by ID %s", id) + return hfv2.Scenario{}, fmt.Errorf("error while retrieving scenario by ID %s", id) } - scenario, ok := obj[0].(*hfv1.Scenario) + scenario, ok := obj[0].(*hfv2.Scenario) if !ok { - return hfv1.Scenario{}, fmt.Errorf("error while retrieving scenario by ID %s %v", id, ok) + return hfv2.Scenario{}, fmt.Errorf("error while retrieving scenario by ID %s %v", id, ok) } return *scenario, nil @@ -975,7 +1038,7 @@ func filterScheduledEvents(scenario string, seList *hfv1.ScheduledEventList) *[] for _, s := range se.Spec.Scenarios { if s == scenario { outList = append(outList, se) - break; + break } } } @@ -1009,7 +1072,7 @@ func filterCourses(scenario string, list *hfv1.CourseList) *[]hfv1.Course { } func idIndexer(obj interface{}) ([]string, error) { - scenario, ok := obj.(*hfv1.Scenario) + scenario, ok := obj.(*hfv2.Scenario) if !ok { return []string{}, nil } diff --git a/pkg/sessionserver/sessionserver.go b/pkg/sessionserver/sessionserver.go index cf368101..48212dfa 100644 --- a/pkg/sessionserver/sessionserver.go +++ b/pkg/sessionserver/sessionserver.go @@ -4,15 +4,17 @@ import ( "context" "encoding/json" "fmt" - "github.com/hobbyfarm/gargantua/pkg/rbacclient" "net/http" "os" "time" + "github.com/hobbyfarm/gargantua/pkg/rbacclient" + "github.com/golang/glog" "github.com/gorilla/mux" "github.com/hobbyfarm/gargantua/pkg/accesscode" hfv1 "github.com/hobbyfarm/gargantua/pkg/apis/hobbyfarm.io/v1" + hfv2 "github.com/hobbyfarm/gargantua/pkg/apis/hobbyfarm.io/v2" "github.com/hobbyfarm/gargantua/pkg/authclient" hfClientset "github.com/hobbyfarm/gargantua/pkg/client/clientset/versioned" hfInformers "github.com/hobbyfarm/gargantua/pkg/client/informers/externalversions" @@ -25,11 +27,11 @@ import ( ) const ( - ssIndex = "sss.hobbyfarm.io/session-id-index" - newSSTimeout = "5m" - keepaliveSSTimeout = "5m" - pauseSSTimeout = "2h" - resourcePlural = "sessions" + ssIndex = "sss.hobbyfarm.io/session-id-index" + newSSTimeout = "5m" + keepaliveSSTimeout = "5m" + pauseSSTimeout = "2h" + resourcePlural = "sessions" ) type SessionServer struct { @@ -122,7 +124,7 @@ func (sss SessionServer) NewSessionFunc(w http.ResponseWriter, r *http.Request) } random := util.RandStringRunes(10) var course hfv1.Course - var scenario hfv1.Scenario + var scenario hfv2.Scenario // get the course and/or scenario objects if courseid != "" { @@ -263,8 +265,8 @@ func (sss SessionServer) NewSessionFunc(w http.ResponseWriter, r *http.Request) virtualMachineClaim := hfv1.VirtualMachineClaim{} vmcId := util.GenerateResourceName(baseName, util.RandStringRunes(10), 10) labels := make(map[string]string) - labels[util.SessionLabel] = session.Name // map vmc to session - labels[util.UserLabel] = user.Name // map session to user in a way that is searchable + labels[util.SessionLabel] = session.Name // map vmc to session + labels[util.UserLabel] = user.Name // map session to user in a way that is searchable labels[util.AccessCodeLabel] = session.Labels[util.AccessCodeLabel] labels[util.ScheduledEventLabel] = schedEvent.Name virtualMachineClaim.Labels = labels @@ -380,8 +382,8 @@ func (sss SessionServer) CreateProgress(sessionId string, scheduledEventId strin labels := make(map[string]string) labels[util.SessionLabel] = sessionId // map to session labels[util.ScheduledEventLabel] = scheduledEventId // map to scheduledevent - labels[util.UserLabel] = userId // map to scheduledevent - labels["finished"] = "false" // default is in progress, finished = false + labels[util.UserLabel] = userId // map to scheduledevent + labels["finished"] = "false" // default is in progress, finished = false progress.Labels = labels createdProgress, err := sss.hfClientSet.HobbyfarmV1().Progresses(util.GetReleaseNamespace()).Create(sss.ctx, &progress, metav1.CreateOptions{}) @@ -518,7 +520,7 @@ func (sss SessionServer) KeepAliveSessionFunc(w http.ResponseWriter, r *http.Req return } - var scenario hfv1.Scenario + var scenario hfv2.Scenario var course hfv1.Course if ss.Spec.ScenarioId != "" { @@ -598,7 +600,7 @@ func (sss SessionServer) PauseSessionFunc(w http.ResponseWriter, r *http.Request } var course hfv1.Course - var scenario hfv1.Scenario + var scenario hfv2.Scenario if ss.Spec.CourseId != "" { course, err = sss.courseClient.GetCourseById(ss.Spec.CourseId) @@ -684,7 +686,7 @@ func (sss SessionServer) ResumeSessionFunc(w http.ResponseWriter, r *http.Reques } var course hfv1.Course - var scenario hfv1.Scenario + var scenario hfv2.Scenario if ss.Spec.CourseId != "" { course, err = sss.courseClient.GetCourseById(ss.Spec.CourseId) diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go index 012aa380..04aef494 100644 --- a/pkg/shell/shell.go +++ b/pkg/shell/shell.go @@ -12,6 +12,7 @@ import ( "regexp" "strconv" "strings" + "sync" "time" "github.com/golang/glog" @@ -24,6 +25,7 @@ import ( "github.com/hobbyfarm/gargantua/pkg/util" "github.com/hobbyfarm/gargantua/pkg/vmclient" "golang.org/x/crypto/ssh" + "golang.org/x/sync/semaphore" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" ) @@ -38,14 +40,14 @@ type ShellProxy struct { } type Service struct { - Name string `json:"name"` - HasWebinterface bool `json:"hasWebinterface"` - Port int `json:"port"` - Path string `json:"path"` - HasOwnTab bool `json:"hasOwnTab"` - NoRewriteRootPath bool `json:"noRewriteRootPath"` - RewriteHostHeader bool `json:"rewriteHostHeader"` - RewriteOriginHeader bool `json:"rewriteOriginHeader"` + Name string `json:"name"` + HasWebinterface bool `json:"hasWebinterface"` + Port int `json:"port"` + Path string `json:"path"` + HasOwnTab bool `json:"hasOwnTab"` + NoRewriteRootPath bool `json:"noRewriteRootPath"` + RewriteHostHeader bool `json:"rewriteHostHeader"` + RewriteOriginHeader bool `json:"rewriteOriginHeader"` } var sshDev = "" @@ -87,6 +89,7 @@ func NewShellProxy(authClient *authclient.AuthClient, vmClient *vmclient.Virtual func (sp ShellProxy) SetupRoutes(r *mux.Router) { r.HandleFunc("/shell/{vm_id}/connect", sp.ConnectSSHFunc) + r.HandleFunc("/shell/verify", sp.VerifyTasksFuncByVMIdGroupWithSemaphore) r.HandleFunc("/guacShell/{vm_id}/connect", sp.ConnectGuacFunc) r.HandleFunc("/p/{vm_id}/{port}/{rest:.*}", sp.checkCookieAndProxy) r.HandleFunc("/pa/{token}/{vm_id}/{port}/{rest:.*}", sp.authAndProxyFunc) @@ -187,14 +190,13 @@ func (sp ShellProxy) proxy(w http.ResponseWriter, r *http.Request, user v2.User) util.ReturnHTTPMessage(w, r, 404, "error", "no vm template found") return } - + // Get the target Port variable, default to 80 targetPort := vars["port"] if targetPort == "" { targetPort = "80" } - // find the corresponding service service := Service{} hasService := false @@ -202,7 +204,7 @@ func (sp ShellProxy) proxy(w http.ResponseWriter, r *http.Request, user v2.User) servicesUnmarshaled := []Service{} err = json.Unmarshal([]byte(servicesMarhaled), &servicesUnmarshaled) - if(err != nil){ + if err != nil { glog.Infof("Error umarshaling: %v", err) } else { for _, s := range servicesUnmarshaled { @@ -278,16 +280,16 @@ func (sp ShellProxy) proxy(w http.ResponseWriter, r *http.Request, user v2.User) proxy := &httputil.ReverseProxy{ Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(remote) - if( hasService && service.RewriteHostHeader){ + if hasService && service.RewriteHostHeader { // Rewrite Host header to proxy server host (this is needed for some applications like code-server) r.Out.Host = r.In.Host } - if( hasService && service.RewriteOriginHeader){ + if hasService && service.RewriteOriginHeader { // Rewrite Origin header to remote host (this is needed for some applications like jupyter) r.Out.Header.Set("Origin", target) } - + }, } proxy.Transport = &http.Transport{ @@ -484,6 +486,266 @@ func copyResponse(rw http.ResponseWriter, resp *http.Response) error { return err } +type VirtualMachineInputTask struct { + VMId string `json:"vm_id"` + VMName string `json:"vm_name"` + TaskInputCommands []TaskInputCommand `json:"task_command"` +} + +type TaskInputCommand struct { + Name string `json:"name"` + Description string `json:"description"` + Command string `json:"command"` + ExpectedOutputValue string `json:"expected_output_value"` + ExpectedReturnCode int `json:"expected_return_code"` +} + +type VirtualMachineOutputTask struct { + VMId string `json:"vm_id"` + VMName string `json:"vm_name"` + TaskOutputCommands []TaskOutputCommand `json:"task_command_output"` +} + +type TaskOutputCommand struct { + Name string `json:"name"` + Description string `json:"description"` + Command string `json:"command"` + ExpectedOutputValue string `json:"expected_output_value"` + ExpectedReturnCode int `json:"expected_return_code"` + ActualOutputValue string `json:"actual_output_value"` + ActualReturnCode int `json:"actual_return_code"` + Success bool `json:"success"` +} + +func VMTaskCommandRun(task_cmd *TaskInputCommand, sess *ssh.Session) (*TaskOutputCommand, error) { + out, err := sess.CombinedOutput(task_cmd.Command) + actual_output_value := strings.TrimRight(string(out), "\r\n") + actual_return_code := 0 + if err != nil { + switch err.(type){ + case *ssh.ExitError : + actual_return_code = err.(*ssh.ExitError).ExitStatus() + glog.Infof("%v", actual_return_code) + default: + return nil, err + } + + } + task_cmd_res := &TaskOutputCommand{ + Name: task_cmd.Name, + Description: task_cmd.Description, + Command: task_cmd.Command, + ExpectedOutputValue: task_cmd.ExpectedOutputValue, + ExpectedReturnCode: task_cmd.ExpectedReturnCode, + ActualOutputValue: actual_output_value, + ActualReturnCode: actual_return_code, + Success: task_cmd.ExpectedOutputValue == actual_output_value && task_cmd.ExpectedReturnCode == actual_return_code, + } + return task_cmd_res, nil +} + +func (sp ShellProxy) VerifyTasksFuncByVMIdGroupWithSemaphore(w http.ResponseWriter, r *http.Request) { + // TODO: settings for define max command go routine run in same time in VM + const MAX_COMMANDS_GO = 3 + // TODO: settings for define max try command run in VM if return code 141 + const MAX_TRY_COMMAND_RUN = 5 + + user, err := sp.auth.AuthN(w, r) + if err != nil { + util.ReturnHTTPMessage(w, r, 403, "forbidden", "no access to get vm") + return + } + + var vm_input_tasks []VirtualMachineInputTask + + err = json.NewDecoder(r.Body).Decode(&vm_input_tasks) + if err != nil { + glog.Infof("%s", err) + } + glog.Infof("vm_input_tasks: %+v", vm_input_tasks) + + errorChan := make(chan error, 1) + + vm_output_tasks := make([]*VirtualMachineOutputTask, 0) + var vm_mutex = &sync.Mutex{} + var vm_wg sync.WaitGroup + + for _, vm_input_task := range vm_input_tasks { + vm_wg.Add(1) + + go func(closure_vm_input_task VirtualMachineInputTask, errChan chan<- error, max_commands_go int) { + defer vm_wg.Done() + + vmId := closure_vm_input_task.VMId + vm, err := sp.vmClient.GetVirtualMachineById(vmId) + + if err != nil { + glog.Errorf("did not find the right virtual machine ID") + if len(errorChan) < cap(errorChan) { + errChan <- err + } + return + } + + if vm.Spec.UserId != user.Name { + // check if the user has access to access user sessions + // TODO: add permission like 'virtualmachine/shell' similar to 'pod/exec' + _, err := sp.auth.AuthGrantWS( + rbacclient.RbacRequest(). + HobbyfarmPermission("users", rbacclient.VerbGet). + HobbyfarmPermission("sessions", rbacclient.VerbGet). + HobbyfarmPermission("virtualmachines", rbacclient.VerbGet), + w, r) + if err != nil { + glog.Infof("Error doing authGrantWS %s", err) + if len(errorChan) < cap(errorChan) { + errChan <- err + } + return + } + } + + // ok first get the secret for the vm + secret, err := sp.kubeClient.CoreV1().Secrets(util.GetReleaseNamespace()).Get(sp.ctx, vm.Spec.SecretName, v1.GetOptions{}) // idk? + if err != nil { + glog.Errorf("did not find secret for virtual machine") + if len(errorChan) < cap(errorChan) { + errChan <- err + } + return + } + + // parse the private key + signer, err := ssh.ParsePrivateKey(secret.Data["private_key"]) + if err != nil { + glog.Errorf("did not correctly parse private key") + if len(errorChan) < cap(errorChan) { + errChan <- err + } + return + } + + sshUsername := vm.Spec.SshUsername + if len(sshUsername) < 1 { + sshUsername = defaultSshUsername + } + + // now use the secret and ssh off to something + config := &ssh.ClientConfig{ + User: sshUsername, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(signer), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + + // get the host and port + host, ok := vm.Annotations["sshEndpoint"] + if !ok { + host = vm.Status.PublicIP + } + port := "22" + if sshDev == "true" { + if sshDevHost != "" { + host = sshDevHost + } + if sshDevPort != "" { + port = sshDevPort + } + } + + // dial the instance + sshConn, err := ssh.Dial("tcp", host+":"+port, config) + if err != nil { + glog.Errorf("did not connect ssh successfully: %s", err) + if len(errorChan) < cap(errorChan) { + errChan <- err + } + return + } + + commands_resp := make([]TaskOutputCommand, 0) + var commands_mutex = &sync.Mutex{} + var commands_wg sync.WaitGroup + // Semaphore for count go routine run in same time in VM + // a context is required for the weighted semaphore pkg. + ctx := context.Background() + var commands_semaphore = semaphore.NewWeighted(int64(max_commands_go)) + + for _, task_command := range closure_vm_input_task.TaskInputCommands { + commands_wg.Add(1) + if err := commands_semaphore.Acquire(ctx, 1); err != nil { + glog.Errorf("did not acquire vm_semafore") + } + glog.Infof("before go vm: %v, sem: %v", vmId, commands_semaphore) + go func(closure_task_command TaskInputCommand, errChan chan<- error, max_try_command_run int) { + defer commands_wg.Done() + defer commands_semaphore.Release(1) + glog.Infof("vm: %v, sem: %v", vmId, commands_semaphore) + // try command run again when exit code == 141 + count_try_command_run := max_try_command_run + for count_try_command_run > 0 { + sess, err = sshConn.NewSession() + if err != nil { + glog.Errorf("did not setup ssh session properly") + if len(errorChan) < cap(errorChan) { + errChan <- err + } + return + } + if err != nil { + glog.Infof("%s", err) + } + vm_task_output, err := VMTaskCommandRun(&closure_task_command, sess) + + if err != nil { + glog.Infof("error sending command: %v", err) + if len(errorChan) < cap(errorChan) { + errChan <- err + } + return + } + sess.Close() + count_try_command_run -= 1 + if vm_task_output.ActualReturnCode != 141 || count_try_command_run == 0 { + commands_mutex.Lock() + commands_resp = append(commands_resp,*vm_task_output) + commands_mutex.Unlock() + break + } + } + }(task_command, errorChan, MAX_TRY_COMMAND_RUN) + } + commands_wg.Wait() + vm_output_task := VirtualMachineOutputTask{ + VMId: closure_vm_input_task.VMId, + VMName: closure_vm_input_task.VMName, + TaskOutputCommands: commands_resp, + } + + vm_mutex.Lock() + vm_output_tasks = append(vm_output_tasks, &vm_output_task) + vm_mutex.Unlock() + }(vm_input_task, errorChan, MAX_COMMANDS_GO) + } + vm_wg.Wait() + + // Check for errors in the errorChan + select { + case err = <-errorChan: + // Handle the error (log, return HTTP error response) + close(errorChan) + glog.Infof("Error in goroutine: %v", err) + util.ReturnHTTPMessage(w, r, 500, "error", "could send command to vm") + return + default: + // No error in the errorChan + glog.Infof("No Error in goroutine: %v", vm_output_tasks) + jsonStr, _ := json.Marshal(vm_output_tasks) + util.ReturnHTTPContent(w, r, 200, "success", jsonStr) + } +} + /* * This is mainly used for SSH Connections to VMs */ @@ -656,15 +918,15 @@ func ResizePty(h int, w int) { } func retry[T any](attempts int, sleep int, f func() (T, error)) (result T, err error) { - for i := 0; i < attempts; i++ { - if i > 0 { - time.Sleep(time.Duration(sleep) * time.Millisecond) - sleep *= 2 - } - result, err = f() - if err == nil { - return result, nil - } - } - return result, fmt.Errorf("after %d attempts, last error: %s", attempts, err) -} \ No newline at end of file + for i := 0; i < attempts; i++ { + if i > 0 { + time.Sleep(time.Duration(sleep) * time.Millisecond) + sleep *= 2 + } + result, err = f() + if err == nil { + return result, nil + } + } + return result, fmt.Errorf("after %d attempts, last error: %s", attempts, err) +} diff --git a/pkg/webhook/conversion/scenario/scenario.go b/pkg/webhook/conversion/scenario/scenario.go new file mode 100644 index 00000000..28194a20 --- /dev/null +++ b/pkg/webhook/conversion/scenario/scenario.go @@ -0,0 +1,48 @@ +package scenario + +import ( + v2 "github.com/hobbyfarm/gargantua/pkg/apis/hobbyfarm.io/v2" + "github.com/hobbyfarm/gargantua/pkg/webhook/conversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func Init() { + conversion.RegisterConverter(schema.GroupKind{ + Group: "hobbyfarm.io", + Kind: "scenarios", + }, convert) +} + +func convert(Object *unstructured.Unstructured, toVersion string) (*unstructured.Unstructured, metav1.Status) { + convertedObject := Object.DeepCopy() + fromVersion := Object.GetAPIVersion() + + if toVersion == fromVersion { + return nil, conversion.StatusFailureWithMessage("cannot convert from/to same version") + } + + switch Object.GetAPIVersion() { + case "hobbyfarm.io/v2": + switch toVersion { + case "hobbyfarm.io/v1": + if _, ok := convertedObject.Object["vm_tasks"]; ok { + delete(convertedObject.Object, "vm_tasks") + } + default: + return nil, conversion.StatusFailureWithMessage("unexpected version %v for conversion", toVersion) + } + case "hobbyfarm.io/v1": + switch toVersion { + case "hobbyfarm.io/v2": + var vmTasks []v2.VirtualMachineTasks + vmTasks = make([]v2.VirtualMachineTasks, 0) + convertedObject.Object["vm_tasks"] = vmTasks + default: + return nil, conversion.StatusFailureWithMessage("unexpected version %v for conversion", toVersion) + } + } + + return convertedObject, metav1.Status{Status: metav1.StatusSuccess} +}