diff --git a/.gitattributes b/.gitattributes index b91e6c3315a..434491cd347 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,3 +12,4 @@ pkg/apiserver-gen/**/* linguist-generated=true pkg/**/mock.go linguist-generated=true pkg/**/mock_Backend.go linguist-generated=true pkg/**/mock_Client.go linguist-generated=true +ui/src/app/api-gen/**/* linguist-generated=true diff --git a/ododevapispec.yaml b/ododevapispec.yaml index ab378e8e2d5..0465e2c9315 100644 --- a/ododevapispec.yaml +++ b/ododevapispec.yaml @@ -223,6 +223,59 @@ paths: example: message: "a push operation is not possible at this time. Please retry later" + /devfile: + put: + description: Updates the Devfile used by the current dev session + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/DevfilePutRequest" + responses: + '200': + description: Devfile content was successfully updated + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralSuccess' + example: + message: "The Devfile content has been updated successfully" + + '500': + description: Error updating the Devfile content + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + example: + message: "Error updating the Devfile content" + + get: + description: Get the raw content of the Devfile used by the current dev session + responses: + '200': + description: Devfile content was successfully returned + content: + application/json: + schema: + type: object + properties: + content: + type: string + example: + { + "content": "schemaVersion: 2.2.0\n", + } + '500': + description: Error getting the Devfile content + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + example: + message: "Error getting the Devfile content" + + /devstate/devfile: put: description: Updates the complete Devfile content @@ -230,12 +283,7 @@ paths: content: application/json: schema: - type: object - required: - - content - properties: - content: - type: string + $ref: "#/components/schemas/DevstateDevfilePutRequest" responses: '200': description: Devfile content was successfully updated @@ -382,7 +430,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Metadata' + $ref: '#/components/schemas/MetadataRequest' responses: '200': description: metadata was successfully updated @@ -1176,6 +1224,20 @@ components: properties: message: type: string + DevfilePutRequest: + type: object + required: + - content + properties: + content: + type: string + DevstateDevfilePutRequest: + type: object + required: + - content + properties: + content: + type: string DevfileContent: type: object required: @@ -1361,6 +1423,49 @@ components: items: type: string Metadata: + type: object + required: + - name + - version + - displayName + - description + - tags + - architectures + - icon + - globalMemoryLimit + - projectType + - language + - website + - provider + - supportUrl + properties: + name: + type: string + version: + type: string + displayName: + type: string + description: + type: string + tags: + type: string + architectures: + type: string + icon: + type: string + globalMemoryLimit: + type: string + projectType: + type: string + language: + type: string + website: + type: string + provider: + type: string + supportUrl: + type: string + MetadataRequest: type: object properties: name: diff --git a/pkg/apiserver-gen/.openapi-generator/FILES b/pkg/apiserver-gen/.openapi-generator/FILES index 0b3c4c8a585..39feb17ffe1 100644 --- a/pkg/apiserver-gen/.openapi-generator/FILES +++ b/pkg/apiserver-gen/.openapi-generator/FILES @@ -8,13 +8,13 @@ go/impl.go go/logger.go go/model__component_command_post_request.go go/model__component_get_200_response.go +go/model__devfile_get_200_response.go go/model__devstate_apply_command_post_request.go go/model__devstate_chart_get_200_response.go 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_post_request.go go/model__devstate_container_post_request.go -go/model__devstate_devfile_put_request.go go/model__devstate_events_put_request.go go/model__devstate_exec_command_post_request.go go/model__devstate_image_post_request.go @@ -26,6 +26,8 @@ go/model_command.go go/model_composite_command.go go/model_container.go go/model_devfile_content.go +go/model_devfile_put_request.go +go/model_devstate_devfile_put_request.go go/model_events.go go/model_exec_command.go go/model_general_error.go @@ -33,5 +35,6 @@ go/model_general_success.go go/model_image.go go/model_image_command.go go/model_metadata.go +go/model_metadata_request.go go/model_resource.go go/routers.go diff --git a/pkg/apiserver-gen/go/api.go b/pkg/apiserver-gen/go/api.go index 5a920f366c7..6b3b4b03a7f 100644 --- a/pkg/apiserver-gen/go/api.go +++ b/pkg/apiserver-gen/go/api.go @@ -20,6 +20,8 @@ import ( type DefaultApiRouter interface { ComponentCommandPost(http.ResponseWriter, *http.Request) ComponentGet(http.ResponseWriter, *http.Request) + DevfileGet(http.ResponseWriter, *http.Request) + DevfilePut(http.ResponseWriter, *http.Request) DevstateApplyCommandPost(http.ResponseWriter, *http.Request) DevstateChartGet(http.ResponseWriter, *http.Request) DevstateCommandCommandNameDelete(http.ResponseWriter, *http.Request) @@ -51,6 +53,8 @@ type DefaultApiRouter interface { type DefaultApiServicer interface { ComponentCommandPost(context.Context, ComponentCommandPostRequest) (ImplResponse, error) ComponentGet(context.Context) (ImplResponse, error) + DevfileGet(context.Context) (ImplResponse, error) + DevfilePut(context.Context, DevfilePutRequest) (ImplResponse, error) DevstateApplyCommandPost(context.Context, DevstateApplyCommandPostRequest) (ImplResponse, error) DevstateChartGet(context.Context) (ImplResponse, error) DevstateCommandCommandNameDelete(context.Context, string) (ImplResponse, error) @@ -67,7 +71,7 @@ type DefaultApiServicer interface { DevstateExecCommandPost(context.Context, DevstateExecCommandPostRequest) (ImplResponse, error) DevstateImageImageNameDelete(context.Context, string) (ImplResponse, error) DevstateImagePost(context.Context, DevstateImagePostRequest) (ImplResponse, error) - DevstateMetadataPut(context.Context, Metadata) (ImplResponse, error) + DevstateMetadataPut(context.Context, MetadataRequest) (ImplResponse, error) DevstateQuantityValidPost(context.Context, DevstateQuantityValidPostRequest) (ImplResponse, error) DevstateResourcePost(context.Context, DevstateResourcePostRequest) (ImplResponse, error) DevstateResourceResourceNameDelete(context.Context, string) (ImplResponse, error) diff --git a/pkg/apiserver-gen/go/api_default.go b/pkg/apiserver-gen/go/api_default.go index 5eea644c344..ad2a05479a9 100644 --- a/pkg/apiserver-gen/go/api_default.go +++ b/pkg/apiserver-gen/go/api_default.go @@ -62,6 +62,18 @@ func (c *DefaultApiController) Routes() Routes { "/api/v1/component", c.ComponentGet, }, + { + "DevfileGet", + strings.ToUpper("Get"), + "/api/v1/devfile", + c.DevfileGet, + }, + { + "DevfilePut", + strings.ToUpper("Put"), + "/api/v1/devfile", + c.DevfilePut, + }, { "DevstateApplyCommandPost", strings.ToUpper("Post"), @@ -234,6 +246,43 @@ func (c *DefaultApiController) ComponentGet(w http.ResponseWriter, r *http.Reque } +// DevfileGet - +func (c *DefaultApiController) DevfileGet(w http.ResponseWriter, r *http.Request) { + result, err := c.service.DevfileGet(r.Context()) + // 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) + +} + +// DevfilePut - +func (c *DefaultApiController) DevfilePut(w http.ResponseWriter, r *http.Request) { + devfilePutRequestParam := DevfilePutRequest{} + d := json.NewDecoder(r.Body) + d.DisallowUnknownFields() + if err := d.Decode(&devfilePutRequestParam); err != nil { + c.errorHandler(w, r, &ParsingError{Err: err}, nil) + return + } + if err := AssertDevfilePutRequestRequired(devfilePutRequestParam); err != nil { + c.errorHandler(w, r, err, nil) + return + } + result, err := c.service.DevfilePut(r.Context(), devfilePutRequestParam) + // 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) + +} + // DevstateApplyCommandPost - func (c *DefaultApiController) DevstateApplyCommandPost(w http.ResponseWriter, r *http.Request) { devstateApplyCommandPostRequestParam := DevstateApplyCommandPostRequest{} @@ -555,18 +604,18 @@ func (c *DefaultApiController) DevstateImagePost(w http.ResponseWriter, r *http. // DevstateMetadataPut - func (c *DefaultApiController) DevstateMetadataPut(w http.ResponseWriter, r *http.Request) { - metadataParam := Metadata{} + metadataRequestParam := MetadataRequest{} d := json.NewDecoder(r.Body) d.DisallowUnknownFields() - if err := d.Decode(&metadataParam); err != nil { + if err := d.Decode(&metadataRequestParam); err != nil { c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } - if err := AssertMetadataRequired(metadataParam); err != nil { + if err := AssertMetadataRequestRequired(metadataRequestParam); err != nil { c.errorHandler(w, r, err, nil) return } - result, err := c.service.DevstateMetadataPut(r.Context(), metadataParam) + result, err := c.service.DevstateMetadataPut(r.Context(), metadataRequestParam) // If an error occurred, encode the error with the status code if err != nil { c.errorHandler(w, r, err, &result) diff --git a/pkg/apiserver-gen/go/model__devfile_get_200_response.go b/pkg/apiserver-gen/go/model__devfile_get_200_response.go new file mode 100644 index 00000000000..6ff877bc9dd --- /dev/null +++ b/pkg/apiserver-gen/go/model__devfile_get_200_response.go @@ -0,0 +1,31 @@ +/* + * odo dev + * + * API interface for 'odo dev' + * + * API version: 0.1 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +type DevfileGet200Response struct { + Content string `json:"content,omitempty"` +} + +// AssertDevfileGet200ResponseRequired checks if the required fields are not zero-ed +func AssertDevfileGet200ResponseRequired(obj DevfileGet200Response) error { + return nil +} + +// AssertRecurseDevfileGet200ResponseRequired recursively checks if required fields are not zero-ed in a nested slice. +// Accepts only nested slice of DevfileGet200Response (e.g. [][]DevfileGet200Response), otherwise ErrTypeAssertionError is thrown. +func AssertRecurseDevfileGet200ResponseRequired(objSlice interface{}) error { + return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { + aDevfileGet200Response, ok := obj.(DevfileGet200Response) + if !ok { + return ErrTypeAssertionError + } + return AssertDevfileGet200ResponseRequired(aDevfileGet200Response) + }) +} diff --git a/pkg/apiserver-gen/go/model__devstate_container_post_200_response.go b/pkg/apiserver-gen/go/model__devstate_container_post_200_response.go deleted file mode 100644 index 68941382966..00000000000 --- a/pkg/apiserver-gen/go/model__devstate_container_post_200_response.go +++ /dev/null @@ -1,33 +0,0 @@ -/* - * odo dev - * - * API interface for 'odo dev' - * - * API version: 0.1 - * Generated by: OpenAPI Generator (https://openapi-generator.tech) - */ - -package openapi - -type DevstateContainerPost200Response struct { - - // Content of the Devfile - Component map[string]interface{} `json:"component,omitempty"` -} - -// AssertDevstateContainerPost200ResponseRequired checks if the required fields are not zero-ed -func AssertDevstateContainerPost200ResponseRequired(obj DevstateContainerPost200Response) error { - return nil -} - -// AssertRecurseDevstateContainerPost200ResponseRequired recursively checks if required fields are not zero-ed in a nested slice. -// Accepts only nested slice of DevstateContainerPost200Response (e.g. [][]DevstateContainerPost200Response), otherwise ErrTypeAssertionError is thrown. -func AssertRecurseDevstateContainerPost200ResponseRequired(objSlice interface{}) error { - return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { - aDevstateContainerPost200Response, ok := obj.(DevstateContainerPost200Response) - if !ok { - return ErrTypeAssertionError - } - return AssertDevstateContainerPost200ResponseRequired(aDevstateContainerPost200Response) - }) -} diff --git a/pkg/apiserver-gen/go/model__devstate_metadata_put_200_response.go b/pkg/apiserver-gen/go/model__devstate_metadata_put_200_response.go deleted file mode 100644 index 64e9b8345e4..00000000000 --- a/pkg/apiserver-gen/go/model__devstate_metadata_put_200_response.go +++ /dev/null @@ -1,33 +0,0 @@ -/* - * odo dev - * - * API interface for 'odo dev' - * - * API version: 0.1 - * Generated by: OpenAPI Generator (https://openapi-generator.tech) - */ - -package openapi - -type DevstateMetadataPut200Response struct { - - // Content of the Devfile - Component map[string]interface{} `json:"component,omitempty"` -} - -// AssertDevstateMetadataPut200ResponseRequired checks if the required fields are not zero-ed -func AssertDevstateMetadataPut200ResponseRequired(obj DevstateMetadataPut200Response) error { - return nil -} - -// AssertRecurseDevstateMetadataPut200ResponseRequired recursively checks if required fields are not zero-ed in a nested slice. -// Accepts only nested slice of DevstateMetadataPut200Response (e.g. [][]DevstateMetadataPut200Response), otherwise ErrTypeAssertionError is thrown. -func AssertRecurseDevstateMetadataPut200ResponseRequired(objSlice interface{}) error { - return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { - aDevstateMetadataPut200Response, ok := obj.(DevstateMetadataPut200Response) - if !ok { - return ErrTypeAssertionError - } - return AssertDevstateMetadataPut200ResponseRequired(aDevstateMetadataPut200Response) - }) -} diff --git a/pkg/apiserver-gen/go/model_devfile_put_request.go b/pkg/apiserver-gen/go/model_devfile_put_request.go new file mode 100644 index 00000000000..8830a2dc0bd --- /dev/null +++ b/pkg/apiserver-gen/go/model_devfile_put_request.go @@ -0,0 +1,40 @@ +/* + * odo dev + * + * API interface for 'odo dev' + * + * API version: 0.1 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +type DevfilePutRequest struct { + Content string `json:"content"` +} + +// AssertDevfilePutRequestRequired checks if the required fields are not zero-ed +func AssertDevfilePutRequestRequired(obj DevfilePutRequest) error { + elements := map[string]interface{}{ + "content": obj.Content, + } + for name, el := range elements { + if isZero := IsZeroValue(el); isZero { + return &RequiredError{Field: name} + } + } + + return nil +} + +// AssertRecurseDevfilePutRequestRequired recursively checks if required fields are not zero-ed in a nested slice. +// Accepts only nested slice of DevfilePutRequest (e.g. [][]DevfilePutRequest), otherwise ErrTypeAssertionError is thrown. +func AssertRecurseDevfilePutRequestRequired(objSlice interface{}) error { + return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { + aDevfilePutRequest, ok := obj.(DevfilePutRequest) + if !ok { + return ErrTypeAssertionError + } + return AssertDevfilePutRequestRequired(aDevfilePutRequest) + }) +} diff --git a/pkg/apiserver-gen/go/model__devstate_devfile_put_request.go b/pkg/apiserver-gen/go/model_devstate_devfile_put_request.go similarity index 100% rename from pkg/apiserver-gen/go/model__devstate_devfile_put_request.go rename to pkg/apiserver-gen/go/model_devstate_devfile_put_request.go diff --git a/pkg/apiserver-gen/go/model_metadata.go b/pkg/apiserver-gen/go/model_metadata.go index 6ff59c02312..b5618a30e9d 100644 --- a/pkg/apiserver-gen/go/model_metadata.go +++ b/pkg/apiserver-gen/go/model_metadata.go @@ -10,35 +10,56 @@ package openapi type Metadata struct { - Name string `json:"name,omitempty"` + Name string `json:"name"` - Version string `json:"version,omitempty"` + Version string `json:"version"` - DisplayName string `json:"displayName,omitempty"` + DisplayName string `json:"displayName"` - Description string `json:"description,omitempty"` + Description string `json:"description"` - Tags string `json:"tags,omitempty"` + Tags string `json:"tags"` - Architectures string `json:"architectures,omitempty"` + Architectures string `json:"architectures"` - Icon string `json:"icon,omitempty"` + Icon string `json:"icon"` - GlobalMemoryLimit string `json:"globalMemoryLimit,omitempty"` + GlobalMemoryLimit string `json:"globalMemoryLimit"` - ProjectType string `json:"projectType,omitempty"` + ProjectType string `json:"projectType"` - Language string `json:"language,omitempty"` + Language string `json:"language"` - Website string `json:"website,omitempty"` + Website string `json:"website"` - Provider string `json:"provider,omitempty"` + Provider string `json:"provider"` - SupportUrl string `json:"supportUrl,omitempty"` + SupportUrl string `json:"supportUrl"` } // AssertMetadataRequired checks if the required fields are not zero-ed func AssertMetadataRequired(obj Metadata) error { + elements := map[string]interface{}{ + "name": obj.Name, + "version": obj.Version, + "displayName": obj.DisplayName, + "description": obj.Description, + "tags": obj.Tags, + "architectures": obj.Architectures, + "icon": obj.Icon, + "globalMemoryLimit": obj.GlobalMemoryLimit, + "projectType": obj.ProjectType, + "language": obj.Language, + "website": obj.Website, + "provider": obj.Provider, + "supportUrl": obj.SupportUrl, + } + for name, el := range elements { + if isZero := IsZeroValue(el); isZero { + return &RequiredError{Field: name} + } + } + return nil } diff --git a/pkg/apiserver-gen/go/model__devstate_metadata_put_request.go b/pkg/apiserver-gen/go/model_metadata_request.go similarity index 56% rename from pkg/apiserver-gen/go/model__devstate_metadata_put_request.go rename to pkg/apiserver-gen/go/model_metadata_request.go index ac879723023..4e0b88695e2 100644 --- a/pkg/apiserver-gen/go/model__devstate_metadata_put_request.go +++ b/pkg/apiserver-gen/go/model_metadata_request.go @@ -9,7 +9,7 @@ package openapi -type DevstateMetadataPutRequest struct { +type MetadataRequest struct { Name string `json:"name,omitempty"` Version string `json:"version,omitempty"` @@ -37,19 +37,19 @@ type DevstateMetadataPutRequest struct { SupportUrl string `json:"supportUrl,omitempty"` } -// AssertDevstateMetadataPutRequestRequired checks if the required fields are not zero-ed -func AssertDevstateMetadataPutRequestRequired(obj DevstateMetadataPutRequest) error { +// AssertMetadataRequestRequired checks if the required fields are not zero-ed +func AssertMetadataRequestRequired(obj MetadataRequest) error { return nil } -// AssertRecurseDevstateMetadataPutRequestRequired recursively checks if required fields are not zero-ed in a nested slice. -// Accepts only nested slice of DevstateMetadataPutRequest (e.g. [][]DevstateMetadataPutRequest), otherwise ErrTypeAssertionError is thrown. -func AssertRecurseDevstateMetadataPutRequestRequired(objSlice interface{}) error { +// AssertRecurseMetadataRequestRequired recursively checks if required fields are not zero-ed in a nested slice. +// Accepts only nested slice of MetadataRequest (e.g. [][]MetadataRequest), otherwise ErrTypeAssertionError is thrown. +func AssertRecurseMetadataRequestRequired(objSlice interface{}) error { return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { - aDevstateMetadataPutRequest, ok := obj.(DevstateMetadataPutRequest) + aMetadataRequest, ok := obj.(MetadataRequest) if !ok { return ErrTypeAssertionError } - return AssertDevstateMetadataPutRequestRequired(aDevstateMetadataPutRequest) + return AssertMetadataRequestRequired(aMetadataRequest) }) } diff --git a/pkg/apiserver-impl/api_default_service.go b/pkg/apiserver-impl/api_default_service.go index ac60935de39..52e78f97a84 100644 --- a/pkg/apiserver-impl/api_default_service.go +++ b/pkg/apiserver-impl/api_default_service.go @@ -4,25 +4,33 @@ import ( "context" "fmt" "net/http" + "os" + "path/filepath" openapi "github.com/redhat-developer/odo/pkg/apiserver-gen/go" "github.com/redhat-developer/odo/pkg/apiserver-impl/devstate" "github.com/redhat-developer/odo/pkg/component/describe" + "github.com/redhat-developer/odo/pkg/devfile" + "github.com/redhat-developer/odo/pkg/devfile/validate" "github.com/redhat-developer/odo/pkg/kclient" + fcontext "github.com/redhat-developer/odo/pkg/odo/commonflags/context" odocontext "github.com/redhat-developer/odo/pkg/odo/context" "github.com/redhat-developer/odo/pkg/podman" + "github.com/redhat-developer/odo/pkg/preference" "github.com/redhat-developer/odo/pkg/state" + "k8s.io/klog" ) // DefaultApiService is a service that implements the logic for the DefaultApiServicer // This service should implement the business logic for every endpoint for the DefaultApi API. // Include any external packages or services that will be required by this service. type DefaultApiService struct { - cancel context.CancelFunc - pushWatcher chan<- struct{} - kubeClient kclient.ClientInterface - podmanClient podman.Client - stateClient state.Client + cancel context.CancelFunc + pushWatcher chan<- struct{} + kubeClient kclient.ClientInterface + podmanClient podman.Client + stateClient state.Client + preferenceClient preference.Client devfileState devstate.DevfileState } @@ -34,13 +42,15 @@ func NewDefaultApiService( kubeClient kclient.ClientInterface, podmanClient podman.Client, stateClient state.Client, + preferenceClient preference.Client, ) openapi.DefaultApiServicer { return &DefaultApiService{ - cancel: cancel, - pushWatcher: pushWatcher, - kubeClient: kubeClient, - podmanClient: podmanClient, - stateClient: stateClient, + cancel: cancel, + pushWatcher: pushWatcher, + kubeClient: kubeClient, + podmanClient: podmanClient, + stateClient: stateClient, + preferenceClient: preferenceClient, devfileState: devstate.NewDevfileState(), } @@ -95,3 +105,73 @@ func (s *DefaultApiService) InstanceGet(ctx context.Context) (openapi.ImplRespon } return openapi.Response(http.StatusOK, response), nil } + +func (s *DefaultApiService) DevfileGet(ctx context.Context) (openapi.ImplResponse, error) { + devfilePath := odocontext.GetDevfilePath(ctx) + content, err := os.ReadFile(devfilePath) + if err != nil { + return openapi.Response(http.StatusInternalServerError, openapi.GeneralError{ + Message: fmt.Sprintf("error getting Devfile content: %s", err), + }), nil + } + return openapi.Response(http.StatusOK, openapi.DevfileGet200Response{ + Content: string(content), + }), nil + +} + +func (s *DefaultApiService) DevfilePut(ctx context.Context, params openapi.DevfilePutRequest) (openapi.ImplResponse, error) { + + tmpdir, err := func() (string, error) { + dir, err := os.MkdirTemp("", "odo") + if err != nil { + return "", err + } + return dir, os.WriteFile(filepath.Join(dir, "devfile.yaml"), []byte(params.Content), 0600) + }() + defer func() { + if tmpdir != "" { + err = os.RemoveAll(tmpdir) + if err != nil { + klog.V(1).Infof("Error deleting temp directory %q: %s", tmpdir, err) + } + } + }() + if err != nil { + return openapi.Response(http.StatusInternalServerError, openapi.GeneralError{ + Message: fmt.Sprintf("error saving temp Devfile: %s", err), + }), nil + } + + err = s.validateDevfile(ctx, tmpdir) + if err != nil { + return openapi.Response(http.StatusInternalServerError, openapi.GeneralError{ + Message: fmt.Sprintf("error validating Devfile: %s", err), + }), nil + } + + devfilePath := odocontext.GetDevfilePath(ctx) + err = os.WriteFile(devfilePath, []byte(params.Content), 0600) + if err != nil { + return openapi.Response(http.StatusInternalServerError, openapi.GeneralError{ + Message: fmt.Sprintf("error writing Devfile content to %q: %s", devfilePath, err), + }), nil + } + + return openapi.Response(http.StatusOK, openapi.GeneralSuccess{ + Message: "devfile has been successfully written to disk", + }), nil + +} + +func (s *DefaultApiService) validateDevfile(ctx context.Context, dir string) error { + var ( + variables = fcontext.GetVariables(ctx) + imageRegistry = s.preferenceClient.GetImageRegistry() + ) + devObj, err := devfile.ParseAndValidateFromFileWithVariables(dir, variables, imageRegistry, false) + if err != nil { + return fmt.Errorf("failed to parse the devfile: %w", err) + } + return validate.ValidateDevfileData(devObj.Data) +} diff --git a/pkg/apiserver-impl/devstate.go b/pkg/apiserver-impl/devstate.go index f437f954225..e9565ce9185 100644 --- a/pkg/apiserver-impl/devstate.go +++ b/pkg/apiserver-impl/devstate.go @@ -144,7 +144,7 @@ func (s *DefaultApiService) DevstateExecCommandPost(ctx context.Context, command return openapi.Response(http.StatusOK, newContent), nil } -func (s *DefaultApiService) DevstateMetadataPut(ctx context.Context, metadata openapi.Metadata) (openapi.ImplResponse, error) { +func (s *DefaultApiService) DevstateMetadataPut(ctx context.Context, metadata openapi.MetadataRequest) (openapi.ImplResponse, error) { newContent, err := s.devfileState.SetMetadata( metadata.Name, metadata.Version, diff --git a/pkg/apiserver-impl/starterserver.go b/pkg/apiserver-impl/starterserver.go index 7aa09173245..bd36e6d5a49 100644 --- a/pkg/apiserver-impl/starterserver.go +++ b/pkg/apiserver-impl/starterserver.go @@ -9,6 +9,7 @@ import ( openapi "github.com/redhat-developer/odo/pkg/apiserver-gen/go" "github.com/redhat-developer/odo/pkg/kclient" "github.com/redhat-developer/odo/pkg/podman" + "github.com/redhat-developer/odo/pkg/preference" "github.com/redhat-developer/odo/pkg/state" "github.com/redhat-developer/odo/pkg/util" "k8s.io/klog" @@ -25,6 +26,7 @@ func StartServer( kubernetesClient kclient.ClientInterface, podmanClient podman.Client, stateClient state.Client, + preferenceClient preference.Client, ) ApiServer { pushWatcher := make(chan struct{}) @@ -34,6 +36,7 @@ func StartServer( kubernetesClient, podmanClient, stateClient, + preferenceClient, ) defaultApiController := openapi.NewDefaultApiController(defaultApiService) diff --git a/pkg/odo/cli/apiserver/apiserver.go b/pkg/odo/cli/apiserver/apiserver.go index a7596dfc22e..22bd07923fe 100644 --- a/pkg/odo/cli/apiserver/apiserver.go +++ b/pkg/odo/cli/apiserver/apiserver.go @@ -56,6 +56,7 @@ func (o *ApiServerOptions) Run(ctx context.Context) (err error) { nil, nil, o.clientset.StateClient, + o.clientset.PreferenceClient, ) <-ctx.Done() return nil diff --git a/pkg/odo/cli/dev/dev.go b/pkg/odo/cli/dev/dev.go index 58e6d0c63ce..fdd94da5a9a 100644 --- a/pkg/odo/cli/dev/dev.go +++ b/pkg/odo/cli/dev/dev.go @@ -267,6 +267,7 @@ func (o *DevOptions) Run(ctx context.Context) (err error) { o.clientset.KubernetesClient, o.clientset.PodmanClient, o.clientset.StateClient, + o.clientset.PreferenceClient, ) } diff --git a/ui/src/app/api-gen/.openapi-generator/FILES b/ui/src/app/api-gen/.openapi-generator/FILES index df67b7c93bb..7727f049fed 100644 --- a/ui/src/app/api-gen/.openapi-generator/FILES +++ b/ui/src/app/api-gen/.openapi-generator/FILES @@ -14,6 +14,8 @@ model/componentGet200Response.ts model/compositeCommand.ts model/container.ts model/devfileContent.ts +model/devfileGet200Response.ts +model/devfilePutRequest.ts model/devstateApplyCommandPostRequest.ts model/devstateChartGet200Response.ts model/devstateCommandCommandNameMovePostRequest.ts @@ -34,6 +36,7 @@ model/image.ts model/imageCommand.ts model/instanceGet200Response.ts model/metadata.ts +model/metadataRequest.ts model/models.ts model/resource.ts param.ts diff --git a/ui/src/app/api-gen/api/default.service.ts b/ui/src/app/api-gen/api/default.service.ts index df518b77d54..50ad6befcb6 100644 --- a/ui/src/app/api-gen/api/default.service.ts +++ b/ui/src/app/api-gen/api/default.service.ts @@ -25,6 +25,10 @@ import { ComponentGet200Response } from '../model/componentGet200Response'; // @ts-ignore import { DevfileContent } from '../model/devfileContent'; // @ts-ignore +import { DevfileGet200Response } from '../model/devfileGet200Response'; +// @ts-ignore +import { DevfilePutRequest } from '../model/devfilePutRequest'; +// @ts-ignore import { DevstateApplyCommandPostRequest } from '../model/devstateApplyCommandPostRequest'; // @ts-ignore import { DevstateChartGet200Response } from '../model/devstateChartGet200Response'; @@ -55,7 +59,7 @@ import { GeneralSuccess } from '../model/generalSuccess'; // @ts-ignore import { InstanceGet200Response } from '../model/instanceGet200Response'; // @ts-ignore -import { Metadata } from '../model/metadata'; +import { MetadataRequest } from '../model/metadataRequest'; // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; @@ -246,6 +250,125 @@ export class DefaultService { ); } + /** + * Get the raw content of the Devfile used by the current dev session + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public devfileGet(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; + public devfileGet(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public devfileGet(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public devfileGet(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/devfile`; + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + + /** + * Updates the Devfile used by the current dev session + * @param devfilePutRequest + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public devfilePut(devfilePutRequest?: DevfilePutRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; + public devfilePut(devfilePutRequest?: DevfilePutRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public devfilePut(devfilePutRequest?: DevfilePutRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public devfilePut(devfilePutRequest?: DevfilePutRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/devfile`; + return this.httpClient.request('put', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: devfilePutRequest, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + /** * Add a new Apply Command to the Devfile * @param devstateApplyCommandPostRequest @@ -1235,14 +1358,14 @@ export class DefaultService { /** * Updates the metadata for the Devfile - * @param metadata + * @param metadataRequest * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public devstateMetadataPut(metadata?: Metadata, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; - public devstateMetadataPut(metadata?: Metadata, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; - public devstateMetadataPut(metadata?: Metadata, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; - public devstateMetadataPut(metadata?: Metadata, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { + public devstateMetadataPut(metadataRequest?: MetadataRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; + public devstateMetadataPut(metadataRequest?: MetadataRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public devstateMetadataPut(metadataRequest?: MetadataRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public devstateMetadataPut(metadataRequest?: MetadataRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { let localVarHeaders = this.defaultHeaders; @@ -1288,7 +1411,7 @@ export class DefaultService { return this.httpClient.request('put', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, - body: metadata, + body: metadataRequest, responseType: responseType_, withCredentials: this.configuration.withCredentials, headers: localVarHeaders, diff --git a/ui/src/app/api-gen/model/devfileGet200Response.ts b/ui/src/app/api-gen/model/devfileGet200Response.ts new file mode 100644 index 00000000000..f917f74f881 --- /dev/null +++ b/ui/src/app/api-gen/model/devfileGet200Response.ts @@ -0,0 +1,17 @@ +/** + * odo dev + * API interface for \'odo dev\' + * + * The version of the OpenAPI document: 0.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface DevfileGet200Response { + content?: string; +} + diff --git a/ui/src/app/api-gen/model/devfilePutRequest.ts b/ui/src/app/api-gen/model/devfilePutRequest.ts new file mode 100644 index 00000000000..7c128bf3d6a --- /dev/null +++ b/ui/src/app/api-gen/model/devfilePutRequest.ts @@ -0,0 +1,17 @@ +/** + * odo dev + * API interface for \'odo dev\' + * + * The version of the OpenAPI document: 0.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface DevfilePutRequest { + content: string; +} + diff --git a/ui/src/app/api-gen/model/metadata.ts b/ui/src/app/api-gen/model/metadata.ts index f14bddd8727..a9c19cea89e 100644 --- a/ui/src/app/api-gen/model/metadata.ts +++ b/ui/src/app/api-gen/model/metadata.ts @@ -12,18 +12,18 @@ export interface Metadata { - name?: string; - version?: string; - displayName?: string; - description?: string; - tags?: string; - architectures?: string; - icon?: string; - globalMemoryLimit?: string; - projectType?: string; - language?: string; - website?: string; - provider?: string; - supportUrl?: string; + name: string; + version: string; + displayName: string; + description: string; + tags: string; + architectures: string; + icon: string; + globalMemoryLimit: string; + projectType: string; + language: string; + website: string; + provider: string; + supportUrl: string; } diff --git a/ui/src/app/api-gen/model/devstateMetadataPutRequest.ts b/ui/src/app/api-gen/model/metadataRequest.ts similarity index 92% rename from ui/src/app/api-gen/model/devstateMetadataPutRequest.ts rename to ui/src/app/api-gen/model/metadataRequest.ts index 6f4f88f64a3..43be8a539eb 100644 --- a/ui/src/app/api-gen/model/devstateMetadataPutRequest.ts +++ b/ui/src/app/api-gen/model/metadataRequest.ts @@ -11,7 +11,7 @@ */ -export interface DevstateMetadataPutRequest { +export interface MetadataRequest { name?: string; version?: string; displayName?: string; diff --git a/ui/src/app/api-gen/model/models.ts b/ui/src/app/api-gen/model/models.ts index 3134e20e218..d1660c4511d 100644 --- a/ui/src/app/api-gen/model/models.ts +++ b/ui/src/app/api-gen/model/models.ts @@ -5,6 +5,8 @@ export * from './componentGet200Response'; export * from './compositeCommand'; export * from './container'; export * from './devfileContent'; +export * from './devfileGet200Response'; +export * from './devfilePutRequest'; export * from './devstateApplyCommandPostRequest'; export * from './devstateChartGet200Response'; export * from './devstateCommandCommandNameMovePostRequest'; @@ -25,4 +27,5 @@ export * from './image'; export * from './imageCommand'; export * from './instanceGet200Response'; export * from './metadata'; +export * from './metadataRequest'; export * from './resource'; diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 7448def87a4..82c1f670b08 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -18,8 +18,9 @@ Devfile YAML - - + + + diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index 18801fa8120..716f21b7a8a 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -4,6 +4,7 @@ import { DomSanitizer } from '@angular/platform-browser'; import { MermaidService } from './services/mermaid.service'; import { StateService } from './services/state.service'; import { MatIconRegistry } from "@angular/material/icon"; +import { OdoapiService } from './services/odoapi.service'; @Component({ selector: 'app-root', @@ -20,6 +21,7 @@ export class AppComponent implements OnInit { protected sanitizer: DomSanitizer, private matIconRegistry: MatIconRegistry, private wasmGo: DevstateService, + private odoApi: OdoapiService, private mermaid: MermaidService, private state: StateService, ) { @@ -35,10 +37,12 @@ export class AppComponent implements OnInit { loading.style.visibility = "hidden"; } - const devfile = this.wasmGo.getDevfileContent(); + const devfile = this.odoApi.getDevfile(); devfile.subscribe({ next: (devfile) => { - this.onButtonClick(devfile.content); + if (devfile.content != undefined) { + this.onButtonClick(devfile.content, false); + } } }); @@ -62,12 +66,20 @@ export class AppComponent implements OnInit { }); } - onButtonClick(content: string){ + onButtonClick(content: string, save: boolean){ const result = this.wasmGo.setDevfileContent(content); result.subscribe({ next: (value) => { this.errorMessage = ''; - this.state.changeDevfileYaml(value); + this.state.changeDevfileYaml(value); + if (save) { + this.odoApi.saveDevfile(value.content).subscribe({ + next: () => {}, + error: (error) => { + this.errorMessage = error.error.message; + } + }); + } }, error: (error) => { this.errorMessage = error.error.message; @@ -79,7 +91,7 @@ export class AppComponent implements OnInit { if (confirm('You will delete the content of the Devfile. Continue?')) { this.wasmGo.clearDevfileContent().subscribe({ next: (value) => { - this.onButtonClick(value.content); + this.onButtonClick(value.content, false); } }); } diff --git a/ui/src/app/forms/metadata/metadata.component.html b/ui/src/app/forms/metadata/metadata.component.html index 041f64c10cd..f5875562b52 100644 --- a/ui/src/app/forms/metadata/metadata.component.html +++ b/ui/src/app/forms/metadata/metadata.component.html @@ -64,5 +64,5 @@ - + diff --git a/ui/src/app/services/odoapi.service.spec.ts b/ui/src/app/services/odoapi.service.spec.ts new file mode 100644 index 00000000000..9bec09b84fb --- /dev/null +++ b/ui/src/app/services/odoapi.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { OdoapiService } from './odoapi.service'; + +describe('OdoapiService', () => { + let service: OdoapiService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(OdoapiService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/ui/src/app/services/odoapi.service.ts b/ui/src/app/services/odoapi.service.ts new file mode 100644 index 00000000000..2666b5b9a25 --- /dev/null +++ b/ui/src/app/services/odoapi.service.ts @@ -0,0 +1,24 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { DevfileGet200Response, GeneralSuccess } from '../api-gen'; + +@Injectable({ + providedIn: 'root' +}) +export class OdoapiService { + + private base = "/api/v1"; + + constructor(private http: HttpClient) { } + + getDevfile(): Observable { + return this.http.get(this.base+"/devfile"); + } + + saveDevfile(content: string): Observable { + return this.http.put(this.base+"/devfile", { + content: content + }); + } +}