diff --git a/ododevapispec.yaml b/ododevapispec.yaml index ba0fbdf302..2c2ad2c516 100644 --- a/ododevapispec.yaml +++ b/ododevapispec.yaml @@ -649,6 +649,126 @@ paths: example: message: "Error deleting the container" + patch: + tags: + - devstate + description: Update a container + parameters: + - name: containerName + in: path + description: Container name to update + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + required: + - name + - image + properties: + image: + description: Container image + type: string + command: + description: Entrypoint of the container + type: array + items: { + type: string + } + args: + description: Args passed to the Container entrypoint + type: array + items: { + type: string + } + env: + description: Environment variables to define + type: array + items: + $ref: '#/components/schemas/Env' + memReq: + description: Requested memory for the deployed container + type: string + memLimit: + description: Memory limit for the deployed container + type: string + cpuReq: + description: Requested CPU for the deployed container + type: string + 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' + configureSources: + description: If false, mountSources and sourceMapping values are not considered + type: boolean + mountSources: + description: If true, sources are mounted into container's filesystem + type: boolean + sourceMapping: + description: Specific directory on which to mount sources + type: string + annotation: + description: Annotations added to the resources created for this container + $ref: '#/components/schemas/Annotation' + endpoints: + description: Endpoints exposed by the container + type: array + items: + $ref: '#/components/schemas/Endpoint' + + responses: + '200': + description: container was successfully updated + 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 updating the container + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + example: + message: "Error updating the container" + /devstate/image: post: tags: diff --git a/pkg/apiserver-gen/.openapi-generator/FILES b/pkg/apiserver-gen/.openapi-generator/FILES index cebe475495..f53bcd1e74 100644 --- a/pkg/apiserver-gen/.openapi-generator/FILES +++ b/pkg/apiserver-gen/.openapi-generator/FILES @@ -16,6 +16,7 @@ go/model__devstate_command__command_name__move_post_request.go go/model__devstate_command__command_name__set_default_post_request.go go/model__devstate_composite_command__command_name__patch_request.go go/model__devstate_composite_command_post_request.go +go/model__devstate_container__container_name__patch_request.go go/model__devstate_container_post_request.go go/model__devstate_events_put_request.go go/model__devstate_exec_command__command_name__patch_request.go diff --git a/pkg/apiserver-gen/go/api.go b/pkg/apiserver-gen/go/api.go index 4323c48741..aa74917b85 100644 --- a/pkg/apiserver-gen/go/api.go +++ b/pkg/apiserver-gen/go/api.go @@ -41,6 +41,7 @@ type DevstateApiRouter interface { DevstateCompositeCommandCommandNamePatch(http.ResponseWriter, *http.Request) DevstateCompositeCommandPost(http.ResponseWriter, *http.Request) DevstateContainerContainerNameDelete(http.ResponseWriter, *http.Request) + DevstateContainerContainerNamePatch(http.ResponseWriter, *http.Request) DevstateContainerPost(http.ResponseWriter, *http.Request) DevstateDevfileDelete(http.ResponseWriter, *http.Request) DevstateDevfileGet(http.ResponseWriter, *http.Request) @@ -90,6 +91,7 @@ type DevstateApiServicer interface { DevstateCompositeCommandCommandNamePatch(context.Context, string, DevstateCompositeCommandCommandNamePatchRequest) (ImplResponse, error) DevstateCompositeCommandPost(context.Context, DevstateCompositeCommandPostRequest) (ImplResponse, error) DevstateContainerContainerNameDelete(context.Context, string) (ImplResponse, error) + DevstateContainerContainerNamePatch(context.Context, string, DevstateContainerContainerNamePatchRequest) (ImplResponse, error) DevstateContainerPost(context.Context, DevstateContainerPostRequest) (ImplResponse, error) DevstateDevfileDelete(context.Context) (ImplResponse, error) DevstateDevfileGet(context.Context) (ImplResponse, error) diff --git a/pkg/apiserver-gen/go/api_devstate.go b/pkg/apiserver-gen/go/api_devstate.go index d54e4b5371..9f81e93b47 100644 --- a/pkg/apiserver-gen/go/api_devstate.go +++ b/pkg/apiserver-gen/go/api_devstate.go @@ -110,6 +110,12 @@ func (c *DevstateApiController) Routes() Routes { "/api/v1/devstate/container/{containerName}", c.DevstateContainerContainerNameDelete, }, + { + "DevstateContainerContainerNamePatch", + strings.ToUpper("Patch"), + "/api/v1/devstate/container/{containerName}", + c.DevstateContainerContainerNamePatch, + }, { "DevstateContainerPost", strings.ToUpper("Post"), @@ -431,6 +437,32 @@ func (c *DevstateApiController) DevstateContainerContainerNameDelete(w http.Resp } +// DevstateContainerContainerNamePatch - +func (c *DevstateApiController) DevstateContainerContainerNamePatch(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + containerNameParam := params["containerName"] + devstateContainerContainerNamePatchRequestParam := DevstateContainerContainerNamePatchRequest{} + d := json.NewDecoder(r.Body) + d.DisallowUnknownFields() + if err := d.Decode(&devstateContainerContainerNamePatchRequestParam); err != nil { + c.errorHandler(w, r, &ParsingError{Err: err}, nil) + return + } + if err := AssertDevstateContainerContainerNamePatchRequestRequired(devstateContainerContainerNamePatchRequestParam); err != nil { + c.errorHandler(w, r, err, nil) + return + } + result, err := c.service.DevstateContainerContainerNamePatch(r.Context(), containerNameParam, devstateContainerContainerNamePatchRequestParam) + // 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) + +} + // DevstateContainerPost - func (c *DevstateApiController) DevstateContainerPost(w http.ResponseWriter, r *http.Request) { devstateContainerPostRequestParam := DevstateContainerPostRequest{} diff --git a/pkg/apiserver-gen/go/model__devstate_container__container_name__patch_request.go b/pkg/apiserver-gen/go/model__devstate_container__container_name__patch_request.go new file mode 100644 index 0000000000..027e90648d --- /dev/null +++ b/pkg/apiserver-gen/go/model__devstate_container__container_name__patch_request.go @@ -0,0 +1,98 @@ +/* + * odo dev + * + * API interface for 'odo dev' + * + * API version: 0.1 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +type DevstateContainerContainerNamePatchRequest struct { + + // Container image + Image string `json:"image"` + + // Entrypoint of the container + Command []string `json:"command,omitempty"` + + // Args passed to the Container entrypoint + Args []string `json:"args,omitempty"` + + // Environment variables to define + Env []Env `json:"env,omitempty"` + + // Requested memory for the deployed container + MemReq string `json:"memReq,omitempty"` + + // Memory limit for the deployed container + MemLimit string `json:"memLimit,omitempty"` + + // Requested CPU for the deployed container + CpuReq string `json:"cpuReq,omitempty"` + + // CPU limit for the deployed container + CpuLimit string `json:"cpuLimit,omitempty"` + + // Volume to mount into the container filesystem + VolumeMounts []VolumeMount `json:"volumeMounts,omitempty"` + + // If false, mountSources and sourceMapping values are not considered + ConfigureSources bool `json:"configureSources,omitempty"` + + // If true, sources are mounted into container's filesystem + MountSources bool `json:"mountSources,omitempty"` + + // Specific directory on which to mount sources + SourceMapping string `json:"sourceMapping,omitempty"` + + Annotation Annotation `json:"annotation,omitempty"` + + // Endpoints exposed by the container + Endpoints []Endpoint `json:"endpoints,omitempty"` +} + +// AssertDevstateContainerContainerNamePatchRequestRequired checks if the required fields are not zero-ed +func AssertDevstateContainerContainerNamePatchRequestRequired(obj DevstateContainerContainerNamePatchRequest) error { + elements := map[string]interface{}{ + "image": obj.Image, + } + for name, el := range elements { + if isZero := IsZeroValue(el); isZero { + return &RequiredError{Field: name} + } + } + + for _, el := range obj.Env { + if err := AssertEnvRequired(el); err != nil { + return err + } + } + for _, el := range obj.VolumeMounts { + if err := AssertVolumeMountRequired(el); err != nil { + return err + } + } + if err := AssertAnnotationRequired(obj.Annotation); err != nil { + return err + } + for _, el := range obj.Endpoints { + if err := AssertEndpointRequired(el); err != nil { + return err + } + } + return nil +} + +// AssertRecurseDevstateContainerContainerNamePatchRequestRequired recursively checks if required fields are not zero-ed in a nested slice. +// Accepts only nested slice of DevstateContainerContainerNamePatchRequest (e.g. [][]DevstateContainerContainerNamePatchRequest), otherwise ErrTypeAssertionError is thrown. +func AssertRecurseDevstateContainerContainerNamePatchRequestRequired(objSlice interface{}) error { + return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { + aDevstateContainerContainerNamePatchRequest, ok := obj.(DevstateContainerContainerNamePatchRequest) + if !ok { + return ErrTypeAssertionError + } + return AssertDevstateContainerContainerNamePatchRequestRequired(aDevstateContainerContainerNamePatchRequest) + }) +} diff --git a/pkg/apiserver-impl/devstate.go b/pkg/apiserver-impl/devstate.go index 0a0b229574..51f2aa60ef 100644 --- a/pkg/apiserver-impl/devstate.go +++ b/pkg/apiserver-impl/devstate.go @@ -384,7 +384,33 @@ func (s *DevstateApiService) DevstateCompositeCommandCommandNamePatch(ctx contex ) if err != nil { return openapi.Response(http.StatusInternalServerError, openapi.GeneralError{ - Message: fmt.Sprintf("Error updating the Image Command: %s", err), + Message: fmt.Sprintf("Error updating the Composite Command: %s", err), + }), nil + } + return openapi.Response(http.StatusOK, newContent), nil +} + +func (s *DevstateApiService) DevstateContainerContainerNamePatch(ctx context.Context, name string, patch openapi.DevstateContainerContainerNamePatchRequest) (openapi.ImplResponse, error) { + newContent, err := s.devfileState.PatchContainer( + name, + patch.Image, + patch.Command, + patch.Args, + patch.Env, + patch.MemReq, + patch.MemLimit, + patch.CpuReq, + patch.CpuLimit, + patch.VolumeMounts, + patch.ConfigureSources, + patch.MountSources, + patch.SourceMapping, + patch.Annotation, + patch.Endpoints, + ) + if err != nil { + return openapi.Response(http.StatusInternalServerError, openapi.GeneralError{ + Message: fmt.Sprintf("Error updating the container: %s", err), }), nil } return openapi.Response(http.StatusOK, newContent), nil diff --git a/pkg/apiserver-impl/devstate/components.go b/pkg/apiserver-impl/devstate/components.go index 47baefbfc9..7dc86479d9 100644 --- a/pkg/apiserver-impl/devstate/components.go +++ b/pkg/apiserver-impl/devstate/components.go @@ -27,44 +27,6 @@ func (o *DevfileState) AddContainer( annotation Annotation, endpoints []Endpoint, ) (DevfileContent, error) { - v1alpha2VolumeMounts := make([]v1alpha2.VolumeMount, 0, len(volumeMounts)) - for _, vm := range volumeMounts { - v1alpha2VolumeMounts = append(v1alpha2VolumeMounts, v1alpha2.VolumeMount{ - Name: vm.Name, - Path: vm.Path, - }) - } - - v1alpha2Envs := make([]v1alpha2.EnvVar, 0, len(envs)) - for _, env := range envs { - v1alpha2Envs = append(v1alpha2Envs, v1alpha2.EnvVar{ - Name: env.Name, - Value: env.Value, - }) - } - var annotations *v1alpha2.Annotation - if len(annotation.Deployment) > 0 || len(annotation.Service) > 0 { - annotations = &v1alpha2.Annotation{} - if len(annotation.Deployment) > 0 { - annotations.Deployment = annotation.Deployment - } - if len(annotation.Service) > 0 { - annotations.Service = annotation.Service - } - } - - v1alpha2Endpoints := make([]v1alpha2.Endpoint, 0, len(endpoints)) - for _, endpoint := range endpoints { - endpoint := endpoint - v1alpha2Endpoints = append(v1alpha2Endpoints, v1alpha2.Endpoint{ - Name: endpoint.Name, - TargetPort: int(endpoint.TargetPort), - Exposure: v1alpha2.EndpointExposure(endpoint.Exposure), - Protocol: v1alpha2.EndpointProtocol(endpoint.Protocol), - Secure: &endpoint.Secure, - Path: endpoint.Path, - }) - } container := v1alpha2.Component{ Name: name, @@ -74,15 +36,15 @@ func (o *DevfileState) AddContainer( Image: image, Command: command, Args: args, - Env: v1alpha2Envs, + Env: tov1alpha2EnvVars(envs), MemoryRequest: memRequest, MemoryLimit: memLimit, CpuRequest: cpuRequest, CpuLimit: cpuLimit, - VolumeMounts: v1alpha2VolumeMounts, - Annotation: annotations, + VolumeMounts: tov1alpha2VolumeMounts(volumeMounts), + Annotation: tov1alpha2Annotation(annotation), }, - Endpoints: v1alpha2Endpoints, + Endpoints: tov1alpha2Endpoints(endpoints), }, }, } @@ -97,6 +59,115 @@ func (o *DevfileState) AddContainer( return o.GetContent() } +func (o *DevfileState) PatchContainer( + name string, + image string, + command []string, + args []string, + envs []Env, + memRequest string, + memLimit string, + cpuRequest string, + cpuLimit string, + volumeMounts []VolumeMount, + configureSources bool, + mountSources bool, + sourceMapping string, + annotation Annotation, + endpoints []Endpoint, +) (DevfileContent, error) { + found, err := o.Devfile.Data.GetComponents(common.DevfileOptions{ + ComponentOptions: common.ComponentOptions{ + ComponentType: v1alpha2.ContainerComponentType, + }, + FilterByName: name, + }) + if err != nil { + return DevfileContent{}, err + } + if len(found) != 1 { + return DevfileContent{}, fmt.Errorf("%d Container found with name %q", len(found), name) + } + + container := found[0] + container.Container.Image = image + container.Container.Command = command + container.Container.Args = args + container.Container.Env = tov1alpha2EnvVars(envs) + container.Container.MemoryRequest = memRequest + container.Container.MemoryLimit = memLimit + container.Container.CpuRequest = cpuRequest + container.Container.CpuLimit = cpuLimit + container.Container.VolumeMounts = tov1alpha2VolumeMounts(volumeMounts) + + container.Container.MountSources = nil + container.Container.SourceMapping = "" + if configureSources { + container.Container.MountSources = &mountSources + container.Container.SourceMapping = sourceMapping + } + container.Container.Annotation = tov1alpha2Annotation(annotation) + container.Container.Endpoints = tov1alpha2Endpoints(endpoints) + + err = o.Devfile.Data.UpdateComponent(container) + if err != nil { + return DevfileContent{}, err + } + return o.GetContent() +} + +func tov1alpha2EnvVars(envs []Env) []v1alpha2.EnvVar { + result := make([]v1alpha2.EnvVar, 0, len(envs)) + for _, env := range envs { + result = append(result, v1alpha2.EnvVar{ + Name: env.Name, + Value: env.Value, + }) + } + return result +} + +func tov1alpha2VolumeMounts(volumeMounts []VolumeMount) []v1alpha2.VolumeMount { + result := make([]v1alpha2.VolumeMount, 0, len(volumeMounts)) + for _, vm := range volumeMounts { + result = append(result, v1alpha2.VolumeMount{ + Name: vm.Name, + Path: vm.Path, + }) + } + return result +} + +func tov1alpha2Annotation(annotation Annotation) *v1alpha2.Annotation { + var result *v1alpha2.Annotation + if len(annotation.Deployment) > 0 || len(annotation.Service) > 0 { + result = &v1alpha2.Annotation{} + if len(annotation.Deployment) > 0 { + result.Deployment = annotation.Deployment + } + if len(annotation.Service) > 0 { + result.Service = annotation.Service + } + } + return result +} + +func tov1alpha2Endpoints(endpoints []Endpoint) []v1alpha2.Endpoint { + result := make([]v1alpha2.Endpoint, 0, len(endpoints)) + for _, endpoint := range endpoints { + endpoint := endpoint + result = append(result, v1alpha2.Endpoint{ + Name: endpoint.Name, + TargetPort: int(endpoint.TargetPort), + Exposure: v1alpha2.EndpointExposure(endpoint.Exposure), + Protocol: v1alpha2.EndpointProtocol(endpoint.Protocol), + Secure: &endpoint.Secure, + Path: endpoint.Path, + }) + } + return result +} + func (o *DevfileState) DeleteContainer(name string) (DevfileContent, error) { err := o.checkContainerUsed(name) diff --git a/pkg/apiserver-impl/swagger-ui/swagger.yaml b/pkg/apiserver-impl/swagger-ui/swagger.yaml index ba0fbdf302..2c2ad2c516 100644 --- a/pkg/apiserver-impl/swagger-ui/swagger.yaml +++ b/pkg/apiserver-impl/swagger-ui/swagger.yaml @@ -649,6 +649,126 @@ paths: example: message: "Error deleting the container" + patch: + tags: + - devstate + description: Update a container + parameters: + - name: containerName + in: path + description: Container name to update + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + required: + - name + - image + properties: + image: + description: Container image + type: string + command: + description: Entrypoint of the container + type: array + items: { + type: string + } + args: + description: Args passed to the Container entrypoint + type: array + items: { + type: string + } + env: + description: Environment variables to define + type: array + items: + $ref: '#/components/schemas/Env' + memReq: + description: Requested memory for the deployed container + type: string + memLimit: + description: Memory limit for the deployed container + type: string + cpuReq: + description: Requested CPU for the deployed container + type: string + 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' + configureSources: + description: If false, mountSources and sourceMapping values are not considered + type: boolean + mountSources: + description: If true, sources are mounted into container's filesystem + type: boolean + sourceMapping: + description: Specific directory on which to mount sources + type: string + annotation: + description: Annotations added to the resources created for this container + $ref: '#/components/schemas/Annotation' + endpoints: + description: Endpoints exposed by the container + type: array + items: + $ref: '#/components/schemas/Endpoint' + + responses: + '200': + description: container was successfully updated + 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 updating the container + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + example: + message: "Error updating the container" + /devstate/image: post: tags: diff --git a/pkg/apiserver-impl/ui/index.html b/pkg/apiserver-impl/ui/index.html index 254ed66218..aca7f8586e 100644 --- a/pkg/apiserver-impl/ui/index.html +++ b/pkg/apiserver-impl/ui/index.html @@ -11,6 +11,6 @@