Add /devfile PUT and GET endpoints (#6950)

* Serve /devfile

* Implement /devfile endpoints

* Load/Save devfile from UI

* Required metadata fields in the response

* Add an Apply button on 1st tab

* Fix: validate new devfile, not previous one

* Add generated UI files to gitattributes file

* Fix rebase
This commit is contained in:
Philippe Martin
2023-07-06 18:30:48 +02:00
committed by GitHub
parent c4b103d9c4
commit 2c3d2ea0b1
29 changed files with 629 additions and 140 deletions

1
.gitattributes vendored
View File

@@ -12,3 +12,4 @@ pkg/apiserver-gen/**/* linguist-generated=true
pkg/**/mock.go linguist-generated=true pkg/**/mock.go linguist-generated=true
pkg/**/mock_Backend.go linguist-generated=true pkg/**/mock_Backend.go linguist-generated=true
pkg/**/mock_Client.go linguist-generated=true pkg/**/mock_Client.go linguist-generated=true
ui/src/app/api-gen/**/* linguist-generated=true

View File

@@ -223,6 +223,59 @@ paths:
example: example:
message: "a push operation is not possible at this time. Please retry later" 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: /devstate/devfile:
put: put:
description: Updates the complete Devfile content description: Updates the complete Devfile content
@@ -230,12 +283,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
type: object $ref: "#/components/schemas/DevstateDevfilePutRequest"
required:
- content
properties:
content:
type: string
responses: responses:
'200': '200':
description: Devfile content was successfully updated description: Devfile content was successfully updated
@@ -382,7 +430,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/Metadata' $ref: '#/components/schemas/MetadataRequest'
responses: responses:
'200': '200':
description: metadata was successfully updated description: metadata was successfully updated
@@ -1176,6 +1224,20 @@ components:
properties: properties:
message: message:
type: string type: string
DevfilePutRequest:
type: object
required:
- content
properties:
content:
type: string
DevstateDevfilePutRequest:
type: object
required:
- content
properties:
content:
type: string
DevfileContent: DevfileContent:
type: object type: object
required: required:
@@ -1361,6 +1423,49 @@ components:
items: items:
type: string type: string
Metadata: 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 type: object
properties: properties:
name: name:

View File

@@ -8,13 +8,13 @@ go/impl.go
go/logger.go go/logger.go
go/model__component_command_post_request.go go/model__component_command_post_request.go
go/model__component_get_200_response.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_apply_command_post_request.go
go/model__devstate_chart_get_200_response.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__move_post_request.go
go/model__devstate_command__command_name__set_default_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_composite_command_post_request.go
go/model__devstate_container_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_events_put_request.go
go/model__devstate_exec_command_post_request.go go/model__devstate_exec_command_post_request.go
go/model__devstate_image_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_composite_command.go
go/model_container.go go/model_container.go
go/model_devfile_content.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_events.go
go/model_exec_command.go go/model_exec_command.go
go/model_general_error.go go/model_general_error.go
@@ -33,5 +35,6 @@ go/model_general_success.go
go/model_image.go go/model_image.go
go/model_image_command.go go/model_image_command.go
go/model_metadata.go go/model_metadata.go
go/model_metadata_request.go
go/model_resource.go go/model_resource.go
go/routers.go go/routers.go

View File

@@ -20,6 +20,8 @@ import (
type DefaultApiRouter interface { type DefaultApiRouter interface {
ComponentCommandPost(http.ResponseWriter, *http.Request) ComponentCommandPost(http.ResponseWriter, *http.Request)
ComponentGet(http.ResponseWriter, *http.Request) ComponentGet(http.ResponseWriter, *http.Request)
DevfileGet(http.ResponseWriter, *http.Request)
DevfilePut(http.ResponseWriter, *http.Request)
DevstateApplyCommandPost(http.ResponseWriter, *http.Request) DevstateApplyCommandPost(http.ResponseWriter, *http.Request)
DevstateChartGet(http.ResponseWriter, *http.Request) DevstateChartGet(http.ResponseWriter, *http.Request)
DevstateCommandCommandNameDelete(http.ResponseWriter, *http.Request) DevstateCommandCommandNameDelete(http.ResponseWriter, *http.Request)
@@ -51,6 +53,8 @@ type DefaultApiRouter interface {
type DefaultApiServicer interface { type DefaultApiServicer interface {
ComponentCommandPost(context.Context, ComponentCommandPostRequest) (ImplResponse, error) ComponentCommandPost(context.Context, ComponentCommandPostRequest) (ImplResponse, error)
ComponentGet(context.Context) (ImplResponse, error) ComponentGet(context.Context) (ImplResponse, error)
DevfileGet(context.Context) (ImplResponse, error)
DevfilePut(context.Context, DevfilePutRequest) (ImplResponse, error)
DevstateApplyCommandPost(context.Context, DevstateApplyCommandPostRequest) (ImplResponse, error) DevstateApplyCommandPost(context.Context, DevstateApplyCommandPostRequest) (ImplResponse, error)
DevstateChartGet(context.Context) (ImplResponse, error) DevstateChartGet(context.Context) (ImplResponse, error)
DevstateCommandCommandNameDelete(context.Context, string) (ImplResponse, error) DevstateCommandCommandNameDelete(context.Context, string) (ImplResponse, error)
@@ -67,7 +71,7 @@ type DefaultApiServicer interface {
DevstateExecCommandPost(context.Context, DevstateExecCommandPostRequest) (ImplResponse, error) DevstateExecCommandPost(context.Context, DevstateExecCommandPostRequest) (ImplResponse, error)
DevstateImageImageNameDelete(context.Context, string) (ImplResponse, error) DevstateImageImageNameDelete(context.Context, string) (ImplResponse, error)
DevstateImagePost(context.Context, DevstateImagePostRequest) (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) DevstateQuantityValidPost(context.Context, DevstateQuantityValidPostRequest) (ImplResponse, error)
DevstateResourcePost(context.Context, DevstateResourcePostRequest) (ImplResponse, error) DevstateResourcePost(context.Context, DevstateResourcePostRequest) (ImplResponse, error)
DevstateResourceResourceNameDelete(context.Context, string) (ImplResponse, error) DevstateResourceResourceNameDelete(context.Context, string) (ImplResponse, error)

View File

@@ -62,6 +62,18 @@ func (c *DefaultApiController) Routes() Routes {
"/api/v1/component", "/api/v1/component",
c.ComponentGet, c.ComponentGet,
}, },
{
"DevfileGet",
strings.ToUpper("Get"),
"/api/v1/devfile",
c.DevfileGet,
},
{
"DevfilePut",
strings.ToUpper("Put"),
"/api/v1/devfile",
c.DevfilePut,
},
{ {
"DevstateApplyCommandPost", "DevstateApplyCommandPost",
strings.ToUpper("Post"), 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 - // DevstateApplyCommandPost -
func (c *DefaultApiController) DevstateApplyCommandPost(w http.ResponseWriter, r *http.Request) { func (c *DefaultApiController) DevstateApplyCommandPost(w http.ResponseWriter, r *http.Request) {
devstateApplyCommandPostRequestParam := DevstateApplyCommandPostRequest{} devstateApplyCommandPostRequestParam := DevstateApplyCommandPostRequest{}
@@ -555,18 +604,18 @@ func (c *DefaultApiController) DevstateImagePost(w http.ResponseWriter, r *http.
// DevstateMetadataPut - // DevstateMetadataPut -
func (c *DefaultApiController) DevstateMetadataPut(w http.ResponseWriter, r *http.Request) { func (c *DefaultApiController) DevstateMetadataPut(w http.ResponseWriter, r *http.Request) {
metadataParam := Metadata{} metadataRequestParam := MetadataRequest{}
d := json.NewDecoder(r.Body) d := json.NewDecoder(r.Body)
d.DisallowUnknownFields() d.DisallowUnknownFields()
if err := d.Decode(&metadataParam); err != nil { if err := d.Decode(&metadataRequestParam); err != nil {
c.errorHandler(w, r, &ParsingError{Err: err}, nil) c.errorHandler(w, r, &ParsingError{Err: err}, nil)
return return
} }
if err := AssertMetadataRequired(metadataParam); err != nil { if err := AssertMetadataRequestRequired(metadataRequestParam); err != nil {
c.errorHandler(w, r, err, nil) c.errorHandler(w, r, err, nil)
return 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 an error occurred, encode the error with the status code
if err != nil { if err != nil {
c.errorHandler(w, r, err, &result) c.errorHandler(w, r, err, &result)

View File

@@ -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)
})
}

View File

@@ -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)
})
}

View File

@@ -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)
})
}

View File

@@ -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)
})
}

View File

@@ -10,35 +10,56 @@
package openapi package openapi
type Metadata struct { 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 // AssertMetadataRequired checks if the required fields are not zero-ed
func AssertMetadataRequired(obj Metadata) error { 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 return nil
} }

View File

@@ -9,7 +9,7 @@
package openapi package openapi
type DevstateMetadataPutRequest struct { type MetadataRequest struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
@@ -37,19 +37,19 @@ type DevstateMetadataPutRequest struct {
SupportUrl string `json:"supportUrl,omitempty"` SupportUrl string `json:"supportUrl,omitempty"`
} }
// AssertDevstateMetadataPutRequestRequired checks if the required fields are not zero-ed // AssertMetadataRequestRequired checks if the required fields are not zero-ed
func AssertDevstateMetadataPutRequestRequired(obj DevstateMetadataPutRequest) error { func AssertMetadataRequestRequired(obj MetadataRequest) error {
return nil return nil
} }
// AssertRecurseDevstateMetadataPutRequestRequired recursively checks if required fields are not zero-ed in a nested slice. // AssertRecurseMetadataRequestRequired 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. // Accepts only nested slice of MetadataRequest (e.g. [][]MetadataRequest), otherwise ErrTypeAssertionError is thrown.
func AssertRecurseDevstateMetadataPutRequestRequired(objSlice interface{}) error { func AssertRecurseMetadataRequestRequired(objSlice interface{}) error {
return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error {
aDevstateMetadataPutRequest, ok := obj.(DevstateMetadataPutRequest) aMetadataRequest, ok := obj.(MetadataRequest)
if !ok { if !ok {
return ErrTypeAssertionError return ErrTypeAssertionError
} }
return AssertDevstateMetadataPutRequestRequired(aDevstateMetadataPutRequest) return AssertMetadataRequestRequired(aMetadataRequest)
}) })
} }

View File

@@ -4,25 +4,33 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"os"
"path/filepath"
openapi "github.com/redhat-developer/odo/pkg/apiserver-gen/go" 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/apiserver-impl/devstate"
"github.com/redhat-developer/odo/pkg/component/describe" "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" "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" odocontext "github.com/redhat-developer/odo/pkg/odo/context"
"github.com/redhat-developer/odo/pkg/podman" "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/state"
"k8s.io/klog"
) )
// DefaultApiService is a service that implements the logic for the DefaultApiServicer // 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. // 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. // Include any external packages or services that will be required by this service.
type DefaultApiService struct { type DefaultApiService struct {
cancel context.CancelFunc cancel context.CancelFunc
pushWatcher chan<- struct{} pushWatcher chan<- struct{}
kubeClient kclient.ClientInterface kubeClient kclient.ClientInterface
podmanClient podman.Client podmanClient podman.Client
stateClient state.Client stateClient state.Client
preferenceClient preference.Client
devfileState devstate.DevfileState devfileState devstate.DevfileState
} }
@@ -34,13 +42,15 @@ func NewDefaultApiService(
kubeClient kclient.ClientInterface, kubeClient kclient.ClientInterface,
podmanClient podman.Client, podmanClient podman.Client,
stateClient state.Client, stateClient state.Client,
preferenceClient preference.Client,
) openapi.DefaultApiServicer { ) openapi.DefaultApiServicer {
return &DefaultApiService{ return &DefaultApiService{
cancel: cancel, cancel: cancel,
pushWatcher: pushWatcher, pushWatcher: pushWatcher,
kubeClient: kubeClient, kubeClient: kubeClient,
podmanClient: podmanClient, podmanClient: podmanClient,
stateClient: stateClient, stateClient: stateClient,
preferenceClient: preferenceClient,
devfileState: devstate.NewDevfileState(), devfileState: devstate.NewDevfileState(),
} }
@@ -95,3 +105,73 @@ func (s *DefaultApiService) InstanceGet(ctx context.Context) (openapi.ImplRespon
} }
return openapi.Response(http.StatusOK, response), nil 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)
}

View File

@@ -144,7 +144,7 @@ func (s *DefaultApiService) DevstateExecCommandPost(ctx context.Context, command
return openapi.Response(http.StatusOK, newContent), nil 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( newContent, err := s.devfileState.SetMetadata(
metadata.Name, metadata.Name,
metadata.Version, metadata.Version,

View File

@@ -9,6 +9,7 @@ import (
openapi "github.com/redhat-developer/odo/pkg/apiserver-gen/go" openapi "github.com/redhat-developer/odo/pkg/apiserver-gen/go"
"github.com/redhat-developer/odo/pkg/kclient" "github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/podman" "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/state"
"github.com/redhat-developer/odo/pkg/util" "github.com/redhat-developer/odo/pkg/util"
"k8s.io/klog" "k8s.io/klog"
@@ -25,6 +26,7 @@ func StartServer(
kubernetesClient kclient.ClientInterface, kubernetesClient kclient.ClientInterface,
podmanClient podman.Client, podmanClient podman.Client,
stateClient state.Client, stateClient state.Client,
preferenceClient preference.Client,
) ApiServer { ) ApiServer {
pushWatcher := make(chan struct{}) pushWatcher := make(chan struct{})
@@ -34,6 +36,7 @@ func StartServer(
kubernetesClient, kubernetesClient,
podmanClient, podmanClient,
stateClient, stateClient,
preferenceClient,
) )
defaultApiController := openapi.NewDefaultApiController(defaultApiService) defaultApiController := openapi.NewDefaultApiController(defaultApiService)

View File

@@ -56,6 +56,7 @@ func (o *ApiServerOptions) Run(ctx context.Context) (err error) {
nil, nil,
nil, nil,
o.clientset.StateClient, o.clientset.StateClient,
o.clientset.PreferenceClient,
) )
<-ctx.Done() <-ctx.Done()
return nil return nil

View File

@@ -267,6 +267,7 @@ func (o *DevOptions) Run(ctx context.Context) (err error) {
o.clientset.KubernetesClient, o.clientset.KubernetesClient,
o.clientset.PodmanClient, o.clientset.PodmanClient,
o.clientset.StateClient, o.clientset.StateClient,
o.clientset.PreferenceClient,
) )
} }

View File

@@ -14,6 +14,8 @@ model/componentGet200Response.ts
model/compositeCommand.ts model/compositeCommand.ts
model/container.ts model/container.ts
model/devfileContent.ts model/devfileContent.ts
model/devfileGet200Response.ts
model/devfilePutRequest.ts
model/devstateApplyCommandPostRequest.ts model/devstateApplyCommandPostRequest.ts
model/devstateChartGet200Response.ts model/devstateChartGet200Response.ts
model/devstateCommandCommandNameMovePostRequest.ts model/devstateCommandCommandNameMovePostRequest.ts
@@ -34,6 +36,7 @@ model/image.ts
model/imageCommand.ts model/imageCommand.ts
model/instanceGet200Response.ts model/instanceGet200Response.ts
model/metadata.ts model/metadata.ts
model/metadataRequest.ts
model/models.ts model/models.ts
model/resource.ts model/resource.ts
param.ts param.ts

View File

@@ -25,6 +25,10 @@ import { ComponentGet200Response } from '../model/componentGet200Response';
// @ts-ignore // @ts-ignore
import { DevfileContent } from '../model/devfileContent'; import { DevfileContent } from '../model/devfileContent';
// @ts-ignore // @ts-ignore
import { DevfileGet200Response } from '../model/devfileGet200Response';
// @ts-ignore
import { DevfilePutRequest } from '../model/devfilePutRequest';
// @ts-ignore
import { DevstateApplyCommandPostRequest } from '../model/devstateApplyCommandPostRequest'; import { DevstateApplyCommandPostRequest } from '../model/devstateApplyCommandPostRequest';
// @ts-ignore // @ts-ignore
import { DevstateChartGet200Response } from '../model/devstateChartGet200Response'; import { DevstateChartGet200Response } from '../model/devstateChartGet200Response';
@@ -55,7 +59,7 @@ import { GeneralSuccess } from '../model/generalSuccess';
// @ts-ignore // @ts-ignore
import { InstanceGet200Response } from '../model/instanceGet200Response'; import { InstanceGet200Response } from '../model/instanceGet200Response';
// @ts-ignore // @ts-ignore
import { Metadata } from '../model/metadata'; import { MetadataRequest } from '../model/metadataRequest';
// @ts-ignore // @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; 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<DevfileGet200Response>;
public devfileGet(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpResponse<DevfileGet200Response>>;
public devfileGet(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpEvent<DevfileGet200Response>>;
public devfileGet(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<any> {
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<DevfileGet200Response>('get', `${this.configuration.basePath}${localVarPath}`,
{
context: localVarHttpContext,
responseType: <any>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<GeneralSuccess>;
public devfilePut(devfilePutRequest?: DevfilePutRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpResponse<GeneralSuccess>>;
public devfilePut(devfilePutRequest?: DevfilePutRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpEvent<GeneralSuccess>>;
public devfilePut(devfilePutRequest?: DevfilePutRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<any> {
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<GeneralSuccess>('put', `${this.configuration.basePath}${localVarPath}`,
{
context: localVarHttpContext,
body: devfilePutRequest,
responseType: <any>responseType_,
withCredentials: this.configuration.withCredentials,
headers: localVarHeaders,
observe: observe,
reportProgress: reportProgress
}
);
}
/** /**
* Add a new Apply Command to the Devfile * Add a new Apply Command to the Devfile
* @param devstateApplyCommandPostRequest * @param devstateApplyCommandPostRequest
@@ -1235,14 +1358,14 @@ export class DefaultService {
/** /**
* Updates the metadata for the Devfile * 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 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. * @param reportProgress flag to report request and response progress.
*/ */
public devstateMetadataPut(metadata?: Metadata, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<DevfileContent>; public devstateMetadataPut(metadataRequest?: MetadataRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<DevfileContent>;
public devstateMetadataPut(metadata?: Metadata, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpResponse<DevfileContent>>; public devstateMetadataPut(metadataRequest?: MetadataRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpResponse<DevfileContent>>;
public devstateMetadataPut(metadata?: Metadata, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpEvent<DevfileContent>>; public devstateMetadataPut(metadataRequest?: MetadataRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpEvent<DevfileContent>>;
public devstateMetadataPut(metadata?: Metadata, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<any> { public devstateMetadataPut(metadataRequest?: MetadataRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<any> {
let localVarHeaders = this.defaultHeaders; let localVarHeaders = this.defaultHeaders;
@@ -1288,7 +1411,7 @@ export class DefaultService {
return this.httpClient.request<DevfileContent>('put', `${this.configuration.basePath}${localVarPath}`, return this.httpClient.request<DevfileContent>('put', `${this.configuration.basePath}${localVarPath}`,
{ {
context: localVarHttpContext, context: localVarHttpContext,
body: metadata, body: metadataRequest,
responseType: <any>responseType_, responseType: <any>responseType_,
withCredentials: this.configuration.withCredentials, withCredentials: this.configuration.withCredentials,
headers: localVarHeaders, headers: localVarHeaders,

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -12,18 +12,18 @@
export interface Metadata { export interface Metadata {
name?: string; name: string;
version?: string; version: string;
displayName?: string; displayName: string;
description?: string; description: string;
tags?: string; tags: string;
architectures?: string; architectures: string;
icon?: string; icon: string;
globalMemoryLimit?: string; globalMemoryLimit: string;
projectType?: string; projectType: string;
language?: string; language: string;
website?: string; website: string;
provider?: string; provider: string;
supportUrl?: string; supportUrl: string;
} }

View File

@@ -11,7 +11,7 @@
*/ */
export interface DevstateMetadataPutRequest { export interface MetadataRequest {
name?: string; name?: string;
version?: string; version?: string;
displayName?: string; displayName?: string;

View File

@@ -5,6 +5,8 @@ export * from './componentGet200Response';
export * from './compositeCommand'; export * from './compositeCommand';
export * from './container'; export * from './container';
export * from './devfileContent'; export * from './devfileContent';
export * from './devfileGet200Response';
export * from './devfilePutRequest';
export * from './devstateApplyCommandPostRequest'; export * from './devstateApplyCommandPostRequest';
export * from './devstateChartGet200Response'; export * from './devstateChartGet200Response';
export * from './devstateCommandCommandNameMovePostRequest'; export * from './devstateCommandCommandNameMovePostRequest';
@@ -25,4 +27,5 @@ export * from './image';
export * from './imageCommand'; export * from './imageCommand';
export * from './instanceGet200Response'; export * from './instanceGet200Response';
export * from './metadata'; export * from './metadata';
export * from './metadataRequest';
export * from './resource'; export * from './resource';

View File

@@ -18,8 +18,9 @@
<mat-label>Devfile YAML</mat-label> <mat-label>Devfile YAML</mat-label>
<textarea data-cy="yaml-input" matInput #input id="input" rows="20" [value]="devfileYaml"></textarea> <textarea data-cy="yaml-input" matInput #input id="input" rows="20" [value]="devfileYaml"></textarea>
</mat-form-field> </mat-form-field>
<button data-cy="yaml-save" mat-flat-button color="primary" (click)="onButtonClick(input.value)">Save</button> <button data-cy="yaml-send" matTooltip="Save Devfile to disk" mat-flat-button color="primary" (click)="onButtonClick(input.value, true)">Save</button>
<button data-cy="yaml-clear" mat-flat-button color="warn" (click)="clear()">Clear</button> <button data-cy="yaml-save" matTooltip="Apply changes to other tabs" mat-flat-button color="normal" (click)="onButtonClick(input.value, false)">Apply</button>
<button data-cy="yaml-clear" matTooltip="Clear Devfile content" mat-flat-button color="normal" (click)="clear()">Clear</button>
</div> </div>
</mat-tab> </mat-tab>

View File

@@ -4,6 +4,7 @@ import { DomSanitizer } from '@angular/platform-browser';
import { MermaidService } from './services/mermaid.service'; import { MermaidService } from './services/mermaid.service';
import { StateService } from './services/state.service'; import { StateService } from './services/state.service';
import { MatIconRegistry } from "@angular/material/icon"; import { MatIconRegistry } from "@angular/material/icon";
import { OdoapiService } from './services/odoapi.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -20,6 +21,7 @@ export class AppComponent implements OnInit {
protected sanitizer: DomSanitizer, protected sanitizer: DomSanitizer,
private matIconRegistry: MatIconRegistry, private matIconRegistry: MatIconRegistry,
private wasmGo: DevstateService, private wasmGo: DevstateService,
private odoApi: OdoapiService,
private mermaid: MermaidService, private mermaid: MermaidService,
private state: StateService, private state: StateService,
) { ) {
@@ -35,10 +37,12 @@ export class AppComponent implements OnInit {
loading.style.visibility = "hidden"; loading.style.visibility = "hidden";
} }
const devfile = this.wasmGo.getDevfileContent(); const devfile = this.odoApi.getDevfile();
devfile.subscribe({ devfile.subscribe({
next: (devfile) => { 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); const result = this.wasmGo.setDevfileContent(content);
result.subscribe({ result.subscribe({
next: (value) => { next: (value) => {
this.errorMessage = ''; 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) => { error: (error) => {
this.errorMessage = error.error.message; 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?')) { if (confirm('You will delete the content of the Devfile. Continue?')) {
this.wasmGo.clearDevfileContent().subscribe({ this.wasmGo.clearDevfileContent().subscribe({
next: (value) => { next: (value) => {
this.onButtonClick(value.content); this.onButtonClick(value.content, false);
} }
}); });
} }

View File

@@ -64,5 +64,5 @@
</mat-form-field> </mat-form-field>
</form> </form>
<button [disabled]="form.invalid" mat-flat-button color="primary" (click)="onSave()">Save</button> <button [disabled]="form.invalid" mat-flat-button color="primary" (click)="onSave()">Apply</button>
</div> </div>

View File

@@ -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();
});
});

View File

@@ -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<DevfileGet200Response> {
return this.http.get<DevfileGet200Response>(this.base+"/devfile");
}
saveDevfile(content: string): Observable<GeneralSuccess> {
return this.http.put<GeneralSuccess>(this.base+"/devfile", {
content: content
});
}
}