diff --git a/ododevapispec.yaml b/ododevapispec.yaml index 006b84ddf..72bc351e4 100644 --- a/ododevapispec.yaml +++ b/ododevapispec.yaml @@ -543,6 +543,11 @@ paths: cpuLimit: description: CPU limit for the deployed container type: string + volumeMounts: + description: Volume to mount into the container filesystem + type: array + items: + $ref: '#/components/schemas/VolumeMount' responses: '200': description: container was successfully added to the devfile @@ -813,6 +818,101 @@ paths: example: message: "Error deleting the resource" + /devstate/volume: + post: + tags: + - devstate + description: Add a new Volume to the Devfile + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + description: Name of the volume + type: string + size: + description: Minimal size of the volume + type: string + ephemeral: + description: True if the Volume is Ephemeral + type: boolean + responses: + '200': + description: volume was successfully added to the devfile + content: + application/json: + schema: + $ref: '#/components/schemas/DevfileContent' + example: + { + "content": "schemaVersion: 2.2.0\n", + "commands": [], + "containers": [], + "images": [], + "resources": [], + "events": { + "preStart": null, + "postStart": null, + "preStop": null, + "postStop": null + }, + "metadata": { + "name": "", + "version": "", + "displayName": "", + description": "", + "tags": "", + "architectures": "", + "icon": "", + "globalMemoryLimit": "", + "projectType": "", + "language": "", + "website": "", + "provider": "", + "supportUrl": "" + } + } + '500': + description: Error adding the volume + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + example: + message: "Error adding the volume" + + /devstate/volume/{volumeName}: + delete: + tags: + - devstate + description: "Delete a volume from the Devfile" + parameters: + - name: volumeName + in: path + description: Volume name to delete + required: true + schema: + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralSuccess' + example: + message: "Volume has been deleted" + description: "Volume has been deleted" + '500': + description: Error deleting the volume + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + example: + message: "Error deleting the volume" + /devstate/applyCommand: post: tags: @@ -1315,6 +1415,7 @@ components: - containers - images - resources + - volumes - events - metadata properties: @@ -1336,6 +1437,10 @@ components: type: array items: $ref: '#/components/schemas/Resource' + volumes: + type: array + items: + $ref: '#/components/schemas/Volume' events: $ref: '#/components/schemas/Events' metadata: @@ -1416,6 +1521,7 @@ components: - memoryLimit - cpuRequest - cpuLimit + - volumeMounts properties: name: type: string @@ -1437,6 +1543,20 @@ components: type: string cpuLimit: type: string + volumeMounts: + type: array + items: + $ref: '#/components/schemas/VolumeMount' + VolumeMount: + type: object + required: + - name + - path + properties: + name: + type: string + path: + type: string Image: type: object required: @@ -1472,6 +1592,17 @@ components: type: string uri: type: string + Volume: + type: object + required: + - name + properties: + name: + type: string + ephemeral: + type: boolean + size: + type: string Events: type: object properties: diff --git a/pkg/apiserver-gen/.openapi-generator/FILES b/pkg/apiserver-gen/.openapi-generator/FILES index 81077ed00..e50a41eaf 100644 --- a/pkg/apiserver-gen/.openapi-generator/FILES +++ b/pkg/apiserver-gen/.openapi-generator/FILES @@ -20,6 +20,7 @@ go/model__devstate_exec_command_post_request.go go/model__devstate_image_post_request.go go/model__devstate_quantity_valid_post_request.go go/model__devstate_resource_post_request.go +go/model__devstate_volume_post_request.go go/model__instance_get_200_response.go go/model_apply_command.go go/model_command.go @@ -38,3 +39,5 @@ go/model_metadata.go go/model_metadata_request.go go/model_resource.go go/model_telemetry_response.go +go/model_volume.go +go/model_volume_mount.go diff --git a/pkg/apiserver-gen/go/api.go b/pkg/apiserver-gen/go/api.go index 222258018..d7361a6f8 100644 --- a/pkg/apiserver-gen/go/api.go +++ b/pkg/apiserver-gen/go/api.go @@ -51,6 +51,8 @@ type DevstateApiRouter interface { DevstateQuantityValidPost(http.ResponseWriter, *http.Request) DevstateResourcePost(http.ResponseWriter, *http.Request) DevstateResourceResourceNameDelete(http.ResponseWriter, *http.Request) + DevstateVolumePost(http.ResponseWriter, *http.Request) + DevstateVolumeVolumeNameDelete(http.ResponseWriter, *http.Request) } // DefaultApiServicer defines the api actions for the DefaultApi service @@ -92,4 +94,6 @@ type DevstateApiServicer interface { DevstateQuantityValidPost(context.Context, DevstateQuantityValidPostRequest) (ImplResponse, error) DevstateResourcePost(context.Context, DevstateResourcePostRequest) (ImplResponse, error) DevstateResourceResourceNameDelete(context.Context, string) (ImplResponse, error) + DevstateVolumePost(context.Context, DevstateVolumePostRequest) (ImplResponse, error) + DevstateVolumeVolumeNameDelete(context.Context, string) (ImplResponse, error) } diff --git a/pkg/apiserver-gen/go/api_devstate.go b/pkg/apiserver-gen/go/api_devstate.go index f7386d105..4693b12b3 100644 --- a/pkg/apiserver-gen/go/api_devstate.go +++ b/pkg/apiserver-gen/go/api_devstate.go @@ -170,6 +170,18 @@ func (c *DevstateApiController) Routes() Routes { "/api/v1/devstate/resource/{resourceName}", c.DevstateResourceResourceNameDelete, }, + { + "DevstateVolumePost", + strings.ToUpper("Post"), + "/api/v1/devstate/volume", + c.DevstateVolumePost, + }, + { + "DevstateVolumeVolumeNameDelete", + strings.ToUpper("Delete"), + "/api/v1/devstate/volume/{volumeName}", + c.DevstateVolumeVolumeNameDelete, + }, } } @@ -578,3 +590,42 @@ func (c *DevstateApiController) DevstateResourceResourceNameDelete(w http.Respon EncodeJSONResponse(result.Body, &result.Code, w) } + +// DevstateVolumePost - +func (c *DevstateApiController) DevstateVolumePost(w http.ResponseWriter, r *http.Request) { + devstateVolumePostRequestParam := DevstateVolumePostRequest{} + d := json.NewDecoder(r.Body) + d.DisallowUnknownFields() + if err := d.Decode(&devstateVolumePostRequestParam); err != nil { + c.errorHandler(w, r, &ParsingError{Err: err}, nil) + return + } + if err := AssertDevstateVolumePostRequestRequired(devstateVolumePostRequestParam); err != nil { + c.errorHandler(w, r, err, nil) + return + } + result, err := c.service.DevstateVolumePost(r.Context(), devstateVolumePostRequestParam) + // If an error occurred, encode the error with the status code + if err != nil { + c.errorHandler(w, r, err, &result) + return + } + // If no error, encode the body and the result code + EncodeJSONResponse(result.Body, &result.Code, w) + +} + +// DevstateVolumeVolumeNameDelete - +func (c *DevstateApiController) DevstateVolumeVolumeNameDelete(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + volumeNameParam := params["volumeName"] + result, err := c.service.DevstateVolumeVolumeNameDelete(r.Context(), volumeNameParam) + // If an error occurred, encode the error with the status code + if err != nil { + c.errorHandler(w, r, err, &result) + return + } + // If no error, encode the body and the result code + EncodeJSONResponse(result.Body, &result.Code, w) + +} diff --git a/pkg/apiserver-gen/go/model__devstate_container_post_request.go b/pkg/apiserver-gen/go/model__devstate_container_post_request.go index e6ea6f75c..c2b17ba3f 100644 --- a/pkg/apiserver-gen/go/model__devstate_container_post_request.go +++ b/pkg/apiserver-gen/go/model__devstate_container_post_request.go @@ -34,10 +34,18 @@ type DevstateContainerPostRequest struct { // CPU limit for the deployed container CpuLimit string `json:"cpuLimit,omitempty"` + + // Volume to mount into the container filesystem + VolumeMounts []VolumeMount `json:"volumeMounts,omitempty"` } // AssertDevstateContainerPostRequestRequired checks if the required fields are not zero-ed func AssertDevstateContainerPostRequestRequired(obj DevstateContainerPostRequest) error { + for _, el := range obj.VolumeMounts { + if err := AssertVolumeMountRequired(el); err != nil { + return err + } + } return nil } diff --git a/pkg/apiserver-gen/go/model__devstate_volume_post_request.go b/pkg/apiserver-gen/go/model__devstate_volume_post_request.go new file mode 100644 index 000000000..67821e956 --- /dev/null +++ b/pkg/apiserver-gen/go/model__devstate_volume_post_request.go @@ -0,0 +1,39 @@ +/* + * odo dev + * + * API interface for 'odo dev' + * + * API version: 0.1 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +type DevstateVolumePostRequest struct { + + // Name of the volume + Name string `json:"name,omitempty"` + + // Minimal size of the volume + Size string `json:"size,omitempty"` + + // True if the Volume is Ephemeral + Ephemeral bool `json:"ephemeral,omitempty"` +} + +// AssertDevstateVolumePostRequestRequired checks if the required fields are not zero-ed +func AssertDevstateVolumePostRequestRequired(obj DevstateVolumePostRequest) error { + return nil +} + +// AssertRecurseDevstateVolumePostRequestRequired recursively checks if required fields are not zero-ed in a nested slice. +// Accepts only nested slice of DevstateVolumePostRequest (e.g. [][]DevstateVolumePostRequest), otherwise ErrTypeAssertionError is thrown. +func AssertRecurseDevstateVolumePostRequestRequired(objSlice interface{}) error { + return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { + aDevstateVolumePostRequest, ok := obj.(DevstateVolumePostRequest) + if !ok { + return ErrTypeAssertionError + } + return AssertDevstateVolumePostRequestRequired(aDevstateVolumePostRequest) + }) +} diff --git a/pkg/apiserver-gen/go/model_container.go b/pkg/apiserver-gen/go/model_container.go index cb725a238..cad9aa2d4 100644 --- a/pkg/apiserver-gen/go/model_container.go +++ b/pkg/apiserver-gen/go/model_container.go @@ -25,6 +25,8 @@ type Container struct { CpuRequest string `json:"cpuRequest"` CpuLimit string `json:"cpuLimit"` + + VolumeMounts []VolumeMount `json:"volumeMounts"` } // AssertContainerRequired checks if the required fields are not zero-ed @@ -38,6 +40,7 @@ func AssertContainerRequired(obj Container) error { "memoryLimit": obj.MemoryLimit, "cpuRequest": obj.CpuRequest, "cpuLimit": obj.CpuLimit, + "volumeMounts": obj.VolumeMounts, } for name, el := range elements { if isZero := IsZeroValue(el); isZero { @@ -45,6 +48,11 @@ func AssertContainerRequired(obj Container) error { } } + for _, el := range obj.VolumeMounts { + if err := AssertVolumeMountRequired(el); err != nil { + return err + } + } return nil } diff --git a/pkg/apiserver-gen/go/model_devfile_content.go b/pkg/apiserver-gen/go/model_devfile_content.go index 360468cd7..1f0cb3623 100644 --- a/pkg/apiserver-gen/go/model_devfile_content.go +++ b/pkg/apiserver-gen/go/model_devfile_content.go @@ -20,6 +20,8 @@ type DevfileContent struct { Resources []Resource `json:"resources"` + Volumes []Volume `json:"volumes"` + Events Events `json:"events"` Metadata Metadata `json:"metadata"` @@ -33,6 +35,7 @@ func AssertDevfileContentRequired(obj DevfileContent) error { "containers": obj.Containers, "images": obj.Images, "resources": obj.Resources, + "volumes": obj.Volumes, "events": obj.Events, "metadata": obj.Metadata, } @@ -62,6 +65,11 @@ func AssertDevfileContentRequired(obj DevfileContent) error { return err } } + for _, el := range obj.Volumes { + if err := AssertVolumeRequired(el); err != nil { + return err + } + } if err := AssertEventsRequired(obj.Events); err != nil { return err } diff --git a/pkg/apiserver-gen/go/model_volume.go b/pkg/apiserver-gen/go/model_volume.go new file mode 100644 index 000000000..0f37127f6 --- /dev/null +++ b/pkg/apiserver-gen/go/model_volume.go @@ -0,0 +1,44 @@ +/* + * odo dev + * + * API interface for 'odo dev' + * + * API version: 0.1 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +type Volume struct { + Name string `json:"name"` + + Ephemeral bool `json:"ephemeral,omitempty"` + + Size string `json:"size,omitempty"` +} + +// AssertVolumeRequired checks if the required fields are not zero-ed +func AssertVolumeRequired(obj Volume) error { + elements := map[string]interface{}{ + "name": obj.Name, + } + for name, el := range elements { + if isZero := IsZeroValue(el); isZero { + return &RequiredError{Field: name} + } + } + + return nil +} + +// AssertRecurseVolumeRequired recursively checks if required fields are not zero-ed in a nested slice. +// Accepts only nested slice of Volume (e.g. [][]Volume), otherwise ErrTypeAssertionError is thrown. +func AssertRecurseVolumeRequired(objSlice interface{}) error { + return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { + aVolume, ok := obj.(Volume) + if !ok { + return ErrTypeAssertionError + } + return AssertVolumeRequired(aVolume) + }) +} diff --git a/pkg/apiserver-gen/go/model_volume_mount.go b/pkg/apiserver-gen/go/model_volume_mount.go new file mode 100644 index 000000000..bbc5d1c79 --- /dev/null +++ b/pkg/apiserver-gen/go/model_volume_mount.go @@ -0,0 +1,43 @@ +/* + * odo dev + * + * API interface for 'odo dev' + * + * API version: 0.1 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +type VolumeMount struct { + Name string `json:"name"` + + Path string `json:"path"` +} + +// AssertVolumeMountRequired checks if the required fields are not zero-ed +func AssertVolumeMountRequired(obj VolumeMount) error { + elements := map[string]interface{}{ + "name": obj.Name, + "path": obj.Path, + } + for name, el := range elements { + if isZero := IsZeroValue(el); isZero { + return &RequiredError{Field: name} + } + } + + return nil +} + +// AssertRecurseVolumeMountRequired recursively checks if required fields are not zero-ed in a nested slice. +// Accepts only nested slice of VolumeMount (e.g. [][]VolumeMount), otherwise ErrTypeAssertionError is thrown. +func AssertRecurseVolumeMountRequired(objSlice interface{}) error { + return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { + aVolumeMount, ok := obj.(VolumeMount) + if !ok { + return ErrTypeAssertionError + } + return AssertVolumeMountRequired(aVolumeMount) + }) +} diff --git a/pkg/apiserver-impl/devstate.go b/pkg/apiserver-impl/devstate.go index 25f5d2a55..e4e7c7ccd 100644 --- a/pkg/apiserver-impl/devstate.go +++ b/pkg/apiserver-impl/devstate.go @@ -19,6 +19,7 @@ func (s *DevstateApiService) DevstateContainerPost(ctx context.Context, containe container.MemLimit, container.CpuReq, container.CpuLimit, + container.VolumeMounts, ) if err != nil { return openapi.Response(http.StatusInternalServerError, openapi.GeneralError{ @@ -90,6 +91,30 @@ func (s *DevstateApiService) DevstateResourceResourceNameDelete(ctx context.Cont return openapi.Response(http.StatusOK, newContent), nil } +func (s *DevstateApiService) DevstateVolumePost(ctx context.Context, volume openapi.DevstateVolumePostRequest) (openapi.ImplResponse, error) { + newContent, err := s.devfileState.AddVolume( + volume.Name, + volume.Ephemeral, + volume.Size, + ) + if err != nil { + return openapi.Response(http.StatusInternalServerError, openapi.GeneralError{ + Message: fmt.Sprintf("Error adding the volume: %s", err), + }), nil + } + return openapi.Response(http.StatusOK, newContent), nil +} + +func (s *DevstateApiService) DevstateVolumeVolumeNameDelete(ctx context.Context, volumeName string) (openapi.ImplResponse, error) { + newContent, err := s.devfileState.DeleteVolume(volumeName) + if err != nil { + return openapi.Response(http.StatusInternalServerError, openapi.GeneralError{ + Message: fmt.Sprintf("Error deleting the volume: %s", err), + }), nil + } + return openapi.Response(http.StatusOK, newContent), nil +} + func (s *DevstateApiService) DevstateApplyCommandPost(ctx context.Context, command openapi.DevstateApplyCommandPostRequest) (openapi.ImplResponse, error) { newContent, err := s.devfileState.AddApplyCommand( command.Name, diff --git a/pkg/apiserver-impl/devstate/commands_test.go b/pkg/apiserver-impl/devstate/commands_test.go index 66db027e2..ca958e153 100644 --- a/pkg/apiserver-impl/devstate/commands_test.go +++ b/pkg/apiserver-impl/devstate/commands_test.go @@ -6,6 +6,7 @@ import ( "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/google/go-cmp/cmp" . "github.com/redhat-developer/odo/pkg/apiserver-gen/go" + openapi "github.com/redhat-developer/odo/pkg/apiserver-gen/go" ) func TestDevfileState_AddExecCommand(t *testing.T) { @@ -36,6 +37,7 @@ func TestDevfileState_AddExecCommand(t *testing.T) { "2Gi", "100m", "200m", + nil, ) if err != nil { t.Fatal(err) @@ -96,10 +98,12 @@ schemaVersion: 2.2.0 MemoryLimit: "2Gi", CpuRequest: "100m", CpuLimit: "200m", + VolumeMounts: []openapi.VolumeMount{}, }, }, Images: []Image{}, Resources: []Resource{}, + Volumes: []Volume{}, Events: Events{}, }, }, @@ -186,6 +190,7 @@ schemaVersion: 2.2.0 }, }, Resources: []Resource{}, + Volumes: []Volume{}, Events: Events{}, }, }, @@ -235,6 +240,7 @@ func TestDevfileState_AddCompositeCommand(t *testing.T) { "2Gi", "100m", "200m", + nil, ) if err != nil { t.Fatal(err) @@ -313,10 +319,12 @@ schemaVersion: 2.2.0 MemoryLimit: "2Gi", CpuRequest: "100m", CpuLimit: "200m", + VolumeMounts: []openapi.VolumeMount{}, }, }, Images: []Image{}, Resources: []Resource{}, + Volumes: []Volume{}, Events: Events{}, }, }, @@ -364,6 +372,7 @@ func TestDevfileState_DeleteCommand(t *testing.T) { "2Gi", "100m", "200m", + nil, ) if err != nil { t.Fatal(err) @@ -412,10 +421,12 @@ schemaVersion: 2.2.0 MemoryLimit: "2Gi", CpuRequest: "100m", CpuLimit: "200m", + VolumeMounts: []openapi.VolumeMount{}, }, }, Images: []Image{}, Resources: []Resource{}, + Volumes: []Volume{}, Events: Events{}, }, }, @@ -627,6 +638,7 @@ schemaVersion: 2.2.0 Containers: []Container{}, Images: []Image{}, Resources: []Resource{}, + Volumes: []Volume{}, }, }, // TODO: Add test cases. @@ -713,6 +725,7 @@ schemaVersion: 2.2.0 Containers: []Container{}, Images: []Image{}, Resources: []Resource{}, + Volumes: []Volume{}, }, }, // TODO: Add test cases. @@ -801,6 +814,7 @@ schemaVersion: 2.2.0 Containers: []Container{}, Images: []Image{}, Resources: []Resource{}, + Volumes: []Volume{}, }, }, // TODO: Add test cases. diff --git a/pkg/apiserver-impl/devstate/components.go b/pkg/apiserver-impl/devstate/components.go index 3d1b4790a..079119721 100644 --- a/pkg/apiserver-impl/devstate/components.go +++ b/pkg/apiserver-impl/devstate/components.go @@ -9,7 +9,24 @@ import ( . "github.com/redhat-developer/odo/pkg/apiserver-gen/go" ) -func (o *DevfileState) AddContainer(name string, image string, command []string, args []string, memRequest string, memLimit string, cpuRequest string, cpuLimit string) (DevfileContent, error) { +func (o *DevfileState) AddContainer( + name string, + image string, + command []string, + args []string, + memRequest string, + memLimit string, + cpuRequest string, + cpuLimit string, + volumeMounts []VolumeMount, +) (DevfileContent, error) { + v1alpha2VolumeMounts := make([]v1alpha2.VolumeMount, 0, len(volumeMounts)) + for _, vm := range volumeMounts { + v1alpha2VolumeMounts = append(v1alpha2VolumeMounts, v1alpha2.VolumeMount{ + Name: vm.Name, + Path: vm.Path, + }) + } container := v1alpha2.Component{ Name: name, ComponentUnion: v1alpha2.ComponentUnion{ @@ -22,6 +39,7 @@ func (o *DevfileState) AddContainer(name string, image string, command []string, MemoryLimit: memLimit, CpuRequest: cpuRequest, CpuLimit: cpuLimit, + VolumeMounts: v1alpha2VolumeMounts, }, }, }, @@ -184,3 +202,56 @@ func (o *DevfileState) checkResourceUsed(name string) error { } return nil } + +func (o *DevfileState) AddVolume(name string, ephemeral bool, size string) (DevfileContent, error) { + volume := v1alpha2.Component{ + Name: name, + ComponentUnion: v1alpha2.ComponentUnion{ + Volume: &v1alpha2.VolumeComponent{ + Volume: v1alpha2.Volume{ + Ephemeral: &ephemeral, + Size: size, + }, + }, + }, + } + err := o.Devfile.Data.AddComponents([]v1alpha2.Component{volume}) + if err != nil { + return DevfileContent{}, err + } + return o.GetContent() +} + +func (o *DevfileState) DeleteVolume(name string) (DevfileContent, error) { + + err := o.checkVolumeUsed(name) + if err != nil { + return DevfileContent{}, fmt.Errorf("error deleting volume %q: %w", name, err) + } + // TODO check if it is a Volume, not another component + + err = o.Devfile.Data.DeleteComponent(name) + if err != nil { + return DevfileContent{}, err + } + return o.GetContent() +} + +func (o *DevfileState) checkVolumeUsed(name string) error { + containers, err := o.Devfile.Data.GetComponents(common.DevfileOptions{ + ComponentOptions: common.ComponentOptions{ + ComponentType: v1alpha2.ContainerComponentType, + }, + }) + if err != nil { + return err + } + for _, container := range containers { + for _, mount := range container.Container.VolumeMounts { + if mount.Name == name { + return fmt.Errorf("volume %q is mounted by Container %q", name, container.Name) + } + } + } + return nil +} diff --git a/pkg/apiserver-impl/devstate/components_test.go b/pkg/apiserver-impl/devstate/components_test.go index 9cf45c0a0..3373f905b 100644 --- a/pkg/apiserver-impl/devstate/components_test.go +++ b/pkg/apiserver-impl/devstate/components_test.go @@ -5,18 +5,20 @@ import ( "github.com/google/go-cmp/cmp" . "github.com/redhat-developer/odo/pkg/apiserver-gen/go" + openapi "github.com/redhat-developer/odo/pkg/apiserver-gen/go" ) func TestDevfileState_AddContainer(t *testing.T) { type args struct { - name string - image string - command []string - args []string - memRequest string - memLimit string - cpuRequest string - cpuLimit string + name string + image string + command []string + args []string + memRequest string + memLimit string + cpuRequest string + cpuLimit string + volumeMounts []openapi.VolumeMount } tests := []struct { name string @@ -39,6 +41,12 @@ func TestDevfileState_AddContainer(t *testing.T) { memLimit: "2Gi", cpuRequest: "100m", cpuLimit: "200m", + volumeMounts: []openapi.VolumeMount{ + { + Name: "vol1", + Path: "/mnt/volume1", + }, + }, }, want: DevfileContent{ Content: `components: @@ -54,6 +62,9 @@ func TestDevfileState_AddContainer(t *testing.T) { image: an-image memoryLimit: 2Gi memoryRequest: 1Gi + volumeMounts: + - name: vol1 + path: /mnt/volume1 name: a-name metadata: {} schemaVersion: 2.2.0 @@ -69,10 +80,17 @@ schemaVersion: 2.2.0 MemoryLimit: "2Gi", CpuRequest: "100m", CpuLimit: "200m", + VolumeMounts: []openapi.VolumeMount{ + { + Name: "vol1", + Path: "/mnt/volume1", + }, + }, }, }, Images: []Image{}, Resources: []Resource{}, + Volumes: []Volume{}, Events: Events{}, }, }, @@ -81,7 +99,7 @@ schemaVersion: 2.2.0 for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { o := tt.state() - got, err := o.AddContainer(tt.args.name, tt.args.image, tt.args.command, tt.args.args, tt.args.memRequest, tt.args.memLimit, tt.args.cpuRequest, tt.args.cpuLimit) + got, err := o.AddContainer(tt.args.name, tt.args.image, tt.args.command, tt.args.args, tt.args.memRequest, tt.args.memLimit, tt.args.cpuRequest, tt.args.cpuLimit, tt.args.volumeMounts) if (err != nil) != tt.wantErr { t.Errorf("DevfileState.AddContainer() error = %v, wantErr %v", err, tt.wantErr) return @@ -120,6 +138,7 @@ func TestDevfileState_DeleteContainer(t *testing.T) { "2Gi", "100m", "200m", + nil, ) if err != nil { t.Fatal(err) @@ -137,6 +156,7 @@ schemaVersion: 2.2.0 Containers: []Container{}, Images: []Image{}, Resources: []Resource{}, + Volumes: []Volume{}, Events: Events{}, }, }, @@ -153,6 +173,7 @@ schemaVersion: 2.2.0 "2Gi", "100m", "200m", + nil, ) if err != nil { t.Fatal(err) @@ -242,6 +263,7 @@ schemaVersion: 2.2.0 }, }, Resources: []Resource{}, + Volumes: []Volume{}, Events: Events{}, }, }, @@ -304,6 +326,7 @@ schemaVersion: 2.2.0 Containers: []Container{}, Images: []Image{}, Resources: []Resource{}, + Volumes: []Volume{}, Events: Events{}, }, }, @@ -389,7 +412,8 @@ schemaVersion: 2.2.0 Uri: "an-uri", }, }, - Events: Events{}, + Volumes: []Volume{}, + Events: Events{}, }, }, { @@ -418,7 +442,8 @@ schemaVersion: 2.2.0 Inlined: "inline resource...", }, }, - Events: Events{}, + Volumes: []Volume{}, + Events: Events{}, }, }, // TODO: Add test cases. @@ -477,6 +502,7 @@ schemaVersion: 2.2.0 Containers: []Container{}, Images: []Image{}, Resources: []Resource{}, + Volumes: []Volume{}, Events: Events{}, }, }, @@ -519,3 +545,148 @@ schemaVersion: 2.2.0 }) } } + +func TestDevfileState_AddVolume(t *testing.T) { + type args struct { + name string + size string + ephemeral bool + } + tests := []struct { + name string + state func() DevfileState + args args + want DevfileContent + wantErr bool + }{ + { + name: "Add a volume", + state: func() DevfileState { + return NewDevfileState() + }, + args: args{ + name: "a-name", + size: "1Gi", + ephemeral: true, + }, + want: DevfileContent{ + Content: `components: +- name: a-name + volume: + ephemeral: true + size: 1Gi +metadata: {} +schemaVersion: 2.2.0 +`, + Commands: []Command{}, + Containers: []Container{}, + Images: []Image{}, + Resources: []Resource{}, + Volumes: []Volume{ + { + Name: "a-name", + Size: "1Gi", + Ephemeral: true, + }, + }, + Events: Events{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := tt.state() + got, err := o.AddVolume(tt.args.name, tt.args.ephemeral, tt.args.size) + if (err != nil) != tt.wantErr { + t.Errorf("DevfileState.AddVolume() error = %v, wantErr %v", err, tt.wantErr) + return + } + if diff := cmp.Diff(tt.want.Content, got.Content); diff != "" { + t.Errorf("DevfileState.AddVolume() mismatch (-want +got):\n%s", diff) + } + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("DevfileState.AddVolume() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestDevfileState_DeleteVolume(t *testing.T) { + type args struct { + name string + } + tests := []struct { + name string + state func(t *testing.T) DevfileState + args args + want DevfileContent + wantErr bool + }{ + { + name: "Delete an existing volume", + state: func(t *testing.T) DevfileState { + state := NewDevfileState() + _, err := state.AddVolume( + "a-name", + true, + "1Gi", + ) + if err != nil { + t.Fatal(err) + } + return state + }, + args: args{ + name: "a-name", + }, + want: DevfileContent{ + Content: `metadata: {} +schemaVersion: 2.2.0 +`, + Commands: []Command{}, + Containers: []Container{}, + Images: []Image{}, + Resources: []Resource{}, + Volumes: []Volume{}, + Events: Events{}, + }, + }, + { + name: "Delete a non existing resource", + state: func(t *testing.T) DevfileState { + state := NewDevfileState() + _, err := state.AddVolume( + "a-name", + true, + "1Gi", + ) + if err != nil { + t.Fatal(err) + } + return state + }, + args: args{ + name: "another-name", + }, + want: DevfileContent{}, + wantErr: true, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := tt.state(t) + got, err := o.DeleteVolume(tt.args.name) + if (err != nil) != tt.wantErr { + t.Errorf("DevfileState.DeleteVolume() error = %v, wantErr %v", err, tt.wantErr) + return + } + if diff := cmp.Diff(tt.want.Content, got.Content); diff != "" { + t.Errorf("DevfileState.DeleteVolume() mismatch (-want +got):\n%s", diff) + } + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("DevfileState.DeleteVolume() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/apiserver-impl/devstate/content.go b/pkg/apiserver-impl/devstate/content.go index 2b64e89a9..ad278b35f 100644 --- a/pkg/apiserver-impl/devstate/content.go +++ b/pkg/apiserver-impl/devstate/content.go @@ -16,97 +16,6 @@ const ( SEPARATOR = "," ) -/* -type DevfileContent struct { - Content string `json:"content"` - Commands []Command `json:"commands"` - Containers []Container `json:"containers"` - Images []Image `json:"images"` - Resources []Resource `json:"resources"` - Events Events `json:"events"` - Metadata Metadata `json:"metadata"` -} - -type Metadata struct { - Name string `json:"name"` - Version string `json:"version"` - DisplayName string `json:"displayName"` - Description string `json:"description"` - Tags string `json:"tags"` - Architectures string `json:"architectures"` - Icon string `json:"icon"` - GlobalMemoryLimit string `json:"globalMemoryLimit"` - ProjectType string `json:"projectType"` - Language string `json:"language"` - Website string `json:"website"` - Provider string `json:"provider"` - SupportUrl string `json:"supportUrl"` -} - -type Command struct { - Name string `json:"name"` - Group string `json:"group"` - Default bool `json:"default"` - Type string `json:"type"` - Exec *ExecCommand `json:"exec"` - Apply *ApplyCommand `json:"apply"` - Image *ImageCommand `json:"image"` - Composite *CompositeCommand `json:"composite"` -} - -type ExecCommand struct { - Component string `json:"component"` - CommandLine string `json:"commandLine"` - WorkingDir string `json:"workingDir"` - HotReloadCapable bool `json:"hotReloadCapable"` -} - -type ApplyCommand struct { - Component string `json:"component"` -} - -type ImageCommand struct { - Component string `json:"component"` -} - -type CompositeCommand struct { - Commands []string `json:"commands"` - Parallel bool `json:"parallel"` -} - -type Container struct { - Name string `json:"name"` - Image string `json:"image"` - Command []string `json:"command"` - Args []string `json:"args"` - MemoryRequest string `json:"memoryRequest"` - MemoryLimit string `json:"memoryLimit"` - CpuRequest string `json:"cpuRequest"` - CpuLimit string `json:"cpuLimit"` -} - -type Image struct { - Name string `json:"name"` - ImageName string `json:"imageName"` - Args []string `json:"args"` - BuildContext string `json:"buildContext"` - RootRequired bool `json:"rootRequired"` - URI string `json:"uri"` -} - -type Resource struct { - Name string `json:"name"` - Inlined string `json:"inlined"` - URI string `json:"uri"` -} - -type Events struct { - PreStart []string `json:"preStart"` - PostStart []string `json:"postStart"` - PreStop []string `json:"preStop"` - PostStop []string `json:"postStop"` -} -*/ // getContent returns the YAML content of the global devfile as string func (o *DevfileState) GetContent() (DevfileContent, error) { err := o.Devfile.WriteYamlDevfile() @@ -137,12 +46,18 @@ func (o *DevfileState) GetContent() (DevfileContent, error) { return DevfileContent{}, errors.New("error getting Kubernetes resources") } + volumes, err := o.getVolumes() + if err != nil { + return DevfileContent{}, errors.New("error getting volumes") + } + return DevfileContent{ Content: string(result), Commands: commands, Containers: containers, Images: images, Resources: resources, + Volumes: volumes, Events: o.getEvents(), Metadata: o.getMetadata(), }, nil @@ -255,11 +170,23 @@ func (o *DevfileState) getContainers() ([]Container, error) { MemoryLimit: container.ComponentUnion.Container.MemoryLimit, CpuRequest: container.ComponentUnion.Container.CpuRequest, CpuLimit: container.ComponentUnion.Container.CpuLimit, + VolumeMounts: o.getVolumeMounts(container.Container.Container), }) } return result, nil } +func (o *DevfileState) getVolumeMounts(container v1alpha2.Container) []VolumeMount { + result := make([]VolumeMount, 0, len(container.VolumeMounts)) + for _, vm := range container.VolumeMounts { + result = append(result, VolumeMount{ + Name: vm.Name, + Path: vm.Path, + }) + } + return result +} + func (o *DevfileState) getImages() ([]Image, error) { images, err := o.Devfile.Data.GetComponents(common.DevfileOptions{ ComponentOptions: common.ComponentOptions{ @@ -303,6 +230,26 @@ func (o *DevfileState) getResources() ([]Resource, error) { return result, nil } +func (o *DevfileState) getVolumes() ([]Volume, error) { + volumes, err := o.Devfile.Data.GetComponents(common.DevfileOptions{ + ComponentOptions: common.ComponentOptions{ + ComponentType: v1alpha2.VolumeComponentType, + }, + }) + if err != nil { + return nil, err + } + result := make([]Volume, 0, len(volumes)) + for _, volume := range volumes { + result = append(result, Volume{ + Name: volume.Name, + Ephemeral: *volume.Volume.Ephemeral, + Size: volume.Volume.Size, + }) + } + return result, nil +} + func (o *DevfileState) getEvents() Events { events := o.Devfile.Data.GetEvents() return Events{ diff --git a/pkg/apiserver-impl/devstate/content_test.go b/pkg/apiserver-impl/devstate/content_test.go index 608d19c92..817b3e260 100644 --- a/pkg/apiserver-impl/devstate/content_test.go +++ b/pkg/apiserver-impl/devstate/content_test.go @@ -24,6 +24,7 @@ func TestDevfileState_GetContent(t *testing.T) { Containers: []Container{}, Images: []Image{}, Resources: []Resource{}, + Volumes: []Volume{}, Events: Events{}, }, }, diff --git a/pkg/apiserver-impl/devstate/events_test.go b/pkg/apiserver-impl/devstate/events_test.go index cb74761ac..a53e0a1cb 100644 --- a/pkg/apiserver-impl/devstate/events_test.go +++ b/pkg/apiserver-impl/devstate/events_test.go @@ -39,6 +39,7 @@ schemaVersion: 2.2.0 Containers: []Container{}, Images: []Image{}, Resources: []Resource{}, + Volumes: []Volume{}, Events: Events{ PreStart: []string{"command1"}, }, @@ -70,6 +71,7 @@ schemaVersion: 2.2.0 Containers: []Container{}, Images: []Image{}, Resources: []Resource{}, + Volumes: []Volume{}, Events: Events{ PreStart: []string{"command1"}, PostStart: []string{"command2"}, diff --git a/pkg/apiserver-impl/devstate/state_test.go b/pkg/apiserver-impl/devstate/state_test.go index ba201cbd2..8e62381c6 100644 --- a/pkg/apiserver-impl/devstate/state_test.go +++ b/pkg/apiserver-impl/devstate/state_test.go @@ -75,6 +75,7 @@ schemaVersion: 2.2.0 Containers: []Container{}, Images: []Image{}, Resources: []Resource{}, + Volumes: []Volume{}, Metadata: Metadata{ Name: "a-name", Version: "v1.1.1", diff --git a/pkg/apiserver-impl/swagger-ui/swagger.yaml b/pkg/apiserver-impl/swagger-ui/swagger.yaml index 006b84ddf..72bc351e4 100644 --- a/pkg/apiserver-impl/swagger-ui/swagger.yaml +++ b/pkg/apiserver-impl/swagger-ui/swagger.yaml @@ -543,6 +543,11 @@ paths: cpuLimit: description: CPU limit for the deployed container type: string + volumeMounts: + description: Volume to mount into the container filesystem + type: array + items: + $ref: '#/components/schemas/VolumeMount' responses: '200': description: container was successfully added to the devfile @@ -813,6 +818,101 @@ paths: example: message: "Error deleting the resource" + /devstate/volume: + post: + tags: + - devstate + description: Add a new Volume to the Devfile + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + description: Name of the volume + type: string + size: + description: Minimal size of the volume + type: string + ephemeral: + description: True if the Volume is Ephemeral + type: boolean + responses: + '200': + description: volume was successfully added to the devfile + content: + application/json: + schema: + $ref: '#/components/schemas/DevfileContent' + example: + { + "content": "schemaVersion: 2.2.0\n", + "commands": [], + "containers": [], + "images": [], + "resources": [], + "events": { + "preStart": null, + "postStart": null, + "preStop": null, + "postStop": null + }, + "metadata": { + "name": "", + "version": "", + "displayName": "", + description": "", + "tags": "", + "architectures": "", + "icon": "", + "globalMemoryLimit": "", + "projectType": "", + "language": "", + "website": "", + "provider": "", + "supportUrl": "" + } + } + '500': + description: Error adding the volume + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + example: + message: "Error adding the volume" + + /devstate/volume/{volumeName}: + delete: + tags: + - devstate + description: "Delete a volume from the Devfile" + parameters: + - name: volumeName + in: path + description: Volume name to delete + required: true + schema: + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralSuccess' + example: + message: "Volume has been deleted" + description: "Volume has been deleted" + '500': + description: Error deleting the volume + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + example: + message: "Error deleting the volume" + /devstate/applyCommand: post: tags: @@ -1315,6 +1415,7 @@ components: - containers - images - resources + - volumes - events - metadata properties: @@ -1336,6 +1437,10 @@ components: type: array items: $ref: '#/components/schemas/Resource' + volumes: + type: array + items: + $ref: '#/components/schemas/Volume' events: $ref: '#/components/schemas/Events' metadata: @@ -1416,6 +1521,7 @@ components: - memoryLimit - cpuRequest - cpuLimit + - volumeMounts properties: name: type: string @@ -1437,6 +1543,20 @@ components: type: string cpuLimit: type: string + volumeMounts: + type: array + items: + $ref: '#/components/schemas/VolumeMount' + VolumeMount: + type: object + required: + - name + - path + properties: + name: + type: string + path: + type: string Image: type: object required: @@ -1472,6 +1592,17 @@ components: type: string uri: type: string + Volume: + type: object + required: + - name + properties: + name: + type: string + ephemeral: + type: boolean + size: + type: string Events: type: object properties: diff --git a/pkg/apiserver-impl/ui/index.html b/pkg/apiserver-impl/ui/index.html index b2a28c324..a8ca1609a 100644 --- a/pkg/apiserver-impl/ui/index.html +++ b/pkg/apiserver-impl/ui/index.html @@ -11,6 +11,6 @@