mirror of
https://github.com/redhat-developer/odo.git
synced 2025-10-19 03:06:19 +03:00
[ui] Create/Delete volumes (#7029)
* [api/devstate] Add volumes to Devfile content * Add Volume related endpoints to API * Create/Delete volumes from the Volumes Tab * Update UI static files * API Devstate returns VolumeMounts * Display volume mounts in containers * [api] Add VolumeMounts to containers * [ui] Define container's volume mounts * [ui] e2e tests * Update UI static files * [ui] create volumes from container / exec command creation * Update UI static files * Update container display * Update UI static files * Regenerate UI static files
This commit is contained in:
@@ -543,6 +543,11 @@ paths:
|
||||
cpuLimit:
|
||||
description: CPU limit for the deployed container
|
||||
type: string
|
||||
volumeMounts:
|
||||
description: Volume to mount into the container filesystem
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/VolumeMount'
|
||||
responses:
|
||||
'200':
|
||||
description: container was successfully added to the devfile
|
||||
@@ -813,6 +818,101 @@ paths:
|
||||
example:
|
||||
message: "Error deleting the resource"
|
||||
|
||||
/devstate/volume:
|
||||
post:
|
||||
tags:
|
||||
- devstate
|
||||
description: Add a new Volume to the Devfile
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
description: Name of the volume
|
||||
type: string
|
||||
size:
|
||||
description: Minimal size of the volume
|
||||
type: string
|
||||
ephemeral:
|
||||
description: True if the Volume is Ephemeral
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
description: volume was successfully added to the devfile
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DevfileContent'
|
||||
example:
|
||||
{
|
||||
"content": "schemaVersion: 2.2.0\n",
|
||||
"commands": [],
|
||||
"containers": [],
|
||||
"images": [],
|
||||
"resources": [],
|
||||
"events": {
|
||||
"preStart": null,
|
||||
"postStart": null,
|
||||
"preStop": null,
|
||||
"postStop": null
|
||||
},
|
||||
"metadata": {
|
||||
"name": "",
|
||||
"version": "",
|
||||
"displayName": "",
|
||||
description": "",
|
||||
"tags": "",
|
||||
"architectures": "",
|
||||
"icon": "",
|
||||
"globalMemoryLimit": "",
|
||||
"projectType": "",
|
||||
"language": "",
|
||||
"website": "",
|
||||
"provider": "",
|
||||
"supportUrl": ""
|
||||
}
|
||||
}
|
||||
'500':
|
||||
description: Error adding the volume
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
example:
|
||||
message: "Error adding the volume"
|
||||
|
||||
/devstate/volume/{volumeName}:
|
||||
delete:
|
||||
tags:
|
||||
- devstate
|
||||
description: "Delete a volume from the Devfile"
|
||||
parameters:
|
||||
- name: volumeName
|
||||
in: path
|
||||
description: Volume name to delete
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralSuccess'
|
||||
example:
|
||||
message: "Volume has been deleted"
|
||||
description: "Volume has been deleted"
|
||||
'500':
|
||||
description: Error deleting the volume
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
example:
|
||||
message: "Error deleting the volume"
|
||||
|
||||
/devstate/applyCommand:
|
||||
post:
|
||||
tags:
|
||||
@@ -1315,6 +1415,7 @@ components:
|
||||
- containers
|
||||
- images
|
||||
- resources
|
||||
- volumes
|
||||
- events
|
||||
- metadata
|
||||
properties:
|
||||
@@ -1336,6 +1437,10 @@ components:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Resource'
|
||||
volumes:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Volume'
|
||||
events:
|
||||
$ref: '#/components/schemas/Events'
|
||||
metadata:
|
||||
@@ -1416,6 +1521,7 @@ components:
|
||||
- memoryLimit
|
||||
- cpuRequest
|
||||
- cpuLimit
|
||||
- volumeMounts
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
@@ -1437,6 +1543,20 @@ components:
|
||||
type: string
|
||||
cpuLimit:
|
||||
type: string
|
||||
volumeMounts:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/VolumeMount'
|
||||
VolumeMount:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- path
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
Image:
|
||||
type: object
|
||||
required:
|
||||
@@ -1472,6 +1592,17 @@ components:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
Volume:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
ephemeral:
|
||||
type: boolean
|
||||
size:
|
||||
type: string
|
||||
Events:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
3
pkg/apiserver-gen/.openapi-generator/FILES
generated
3
pkg/apiserver-gen/.openapi-generator/FILES
generated
@@ -20,6 +20,7 @@ go/model__devstate_exec_command_post_request.go
|
||||
go/model__devstate_image_post_request.go
|
||||
go/model__devstate_quantity_valid_post_request.go
|
||||
go/model__devstate_resource_post_request.go
|
||||
go/model__devstate_volume_post_request.go
|
||||
go/model__instance_get_200_response.go
|
||||
go/model_apply_command.go
|
||||
go/model_command.go
|
||||
@@ -38,3 +39,5 @@ go/model_metadata.go
|
||||
go/model_metadata_request.go
|
||||
go/model_resource.go
|
||||
go/model_telemetry_response.go
|
||||
go/model_volume.go
|
||||
go/model_volume_mount.go
|
||||
|
||||
4
pkg/apiserver-gen/go/api.go
generated
4
pkg/apiserver-gen/go/api.go
generated
@@ -51,6 +51,8 @@ type DevstateApiRouter interface {
|
||||
DevstateQuantityValidPost(http.ResponseWriter, *http.Request)
|
||||
DevstateResourcePost(http.ResponseWriter, *http.Request)
|
||||
DevstateResourceResourceNameDelete(http.ResponseWriter, *http.Request)
|
||||
DevstateVolumePost(http.ResponseWriter, *http.Request)
|
||||
DevstateVolumeVolumeNameDelete(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
// DefaultApiServicer defines the api actions for the DefaultApi service
|
||||
@@ -92,4 +94,6 @@ type DevstateApiServicer interface {
|
||||
DevstateQuantityValidPost(context.Context, DevstateQuantityValidPostRequest) (ImplResponse, error)
|
||||
DevstateResourcePost(context.Context, DevstateResourcePostRequest) (ImplResponse, error)
|
||||
DevstateResourceResourceNameDelete(context.Context, string) (ImplResponse, error)
|
||||
DevstateVolumePost(context.Context, DevstateVolumePostRequest) (ImplResponse, error)
|
||||
DevstateVolumeVolumeNameDelete(context.Context, string) (ImplResponse, error)
|
||||
}
|
||||
|
||||
51
pkg/apiserver-gen/go/api_devstate.go
generated
51
pkg/apiserver-gen/go/api_devstate.go
generated
@@ -170,6 +170,18 @@ func (c *DevstateApiController) Routes() Routes {
|
||||
"/api/v1/devstate/resource/{resourceName}",
|
||||
c.DevstateResourceResourceNameDelete,
|
||||
},
|
||||
{
|
||||
"DevstateVolumePost",
|
||||
strings.ToUpper("Post"),
|
||||
"/api/v1/devstate/volume",
|
||||
c.DevstateVolumePost,
|
||||
},
|
||||
{
|
||||
"DevstateVolumeVolumeNameDelete",
|
||||
strings.ToUpper("Delete"),
|
||||
"/api/v1/devstate/volume/{volumeName}",
|
||||
c.DevstateVolumeVolumeNameDelete,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,3 +590,42 @@ func (c *DevstateApiController) DevstateResourceResourceNameDelete(w http.Respon
|
||||
EncodeJSONResponse(result.Body, &result.Code, w)
|
||||
|
||||
}
|
||||
|
||||
// DevstateVolumePost -
|
||||
func (c *DevstateApiController) DevstateVolumePost(w http.ResponseWriter, r *http.Request) {
|
||||
devstateVolumePostRequestParam := DevstateVolumePostRequest{}
|
||||
d := json.NewDecoder(r.Body)
|
||||
d.DisallowUnknownFields()
|
||||
if err := d.Decode(&devstateVolumePostRequestParam); err != nil {
|
||||
c.errorHandler(w, r, &ParsingError{Err: err}, nil)
|
||||
return
|
||||
}
|
||||
if err := AssertDevstateVolumePostRequestRequired(devstateVolumePostRequestParam); err != nil {
|
||||
c.errorHandler(w, r, err, nil)
|
||||
return
|
||||
}
|
||||
result, err := c.service.DevstateVolumePost(r.Context(), devstateVolumePostRequestParam)
|
||||
// If an error occurred, encode the error with the status code
|
||||
if err != nil {
|
||||
c.errorHandler(w, r, err, &result)
|
||||
return
|
||||
}
|
||||
// If no error, encode the body and the result code
|
||||
EncodeJSONResponse(result.Body, &result.Code, w)
|
||||
|
||||
}
|
||||
|
||||
// DevstateVolumeVolumeNameDelete -
|
||||
func (c *DevstateApiController) DevstateVolumeVolumeNameDelete(w http.ResponseWriter, r *http.Request) {
|
||||
params := mux.Vars(r)
|
||||
volumeNameParam := params["volumeName"]
|
||||
result, err := c.service.DevstateVolumeVolumeNameDelete(r.Context(), volumeNameParam)
|
||||
// If an error occurred, encode the error with the status code
|
||||
if err != nil {
|
||||
c.errorHandler(w, r, err, &result)
|
||||
return
|
||||
}
|
||||
// If no error, encode the body and the result code
|
||||
EncodeJSONResponse(result.Body, &result.Code, w)
|
||||
|
||||
}
|
||||
|
||||
@@ -34,10 +34,18 @@ type DevstateContainerPostRequest struct {
|
||||
|
||||
// CPU limit for the deployed container
|
||||
CpuLimit string `json:"cpuLimit,omitempty"`
|
||||
|
||||
// Volume to mount into the container filesystem
|
||||
VolumeMounts []VolumeMount `json:"volumeMounts,omitempty"`
|
||||
}
|
||||
|
||||
// AssertDevstateContainerPostRequestRequired checks if the required fields are not zero-ed
|
||||
func AssertDevstateContainerPostRequestRequired(obj DevstateContainerPostRequest) error {
|
||||
for _, el := range obj.VolumeMounts {
|
||||
if err := AssertVolumeMountRequired(el); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
39
pkg/apiserver-gen/go/model__devstate_volume_post_request.go
generated
Normal file
39
pkg/apiserver-gen/go/model__devstate_volume_post_request.go
generated
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* odo dev
|
||||
*
|
||||
* API interface for 'odo dev'
|
||||
*
|
||||
* API version: 0.1
|
||||
* Generated by: OpenAPI Generator (https://openapi-generator.tech)
|
||||
*/
|
||||
|
||||
package openapi
|
||||
|
||||
type DevstateVolumePostRequest struct {
|
||||
|
||||
// Name of the volume
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
// Minimal size of the volume
|
||||
Size string `json:"size,omitempty"`
|
||||
|
||||
// True if the Volume is Ephemeral
|
||||
Ephemeral bool `json:"ephemeral,omitempty"`
|
||||
}
|
||||
|
||||
// AssertDevstateVolumePostRequestRequired checks if the required fields are not zero-ed
|
||||
func AssertDevstateVolumePostRequestRequired(obj DevstateVolumePostRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AssertRecurseDevstateVolumePostRequestRequired recursively checks if required fields are not zero-ed in a nested slice.
|
||||
// Accepts only nested slice of DevstateVolumePostRequest (e.g. [][]DevstateVolumePostRequest), otherwise ErrTypeAssertionError is thrown.
|
||||
func AssertRecurseDevstateVolumePostRequestRequired(objSlice interface{}) error {
|
||||
return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error {
|
||||
aDevstateVolumePostRequest, ok := obj.(DevstateVolumePostRequest)
|
||||
if !ok {
|
||||
return ErrTypeAssertionError
|
||||
}
|
||||
return AssertDevstateVolumePostRequestRequired(aDevstateVolumePostRequest)
|
||||
})
|
||||
}
|
||||
8
pkg/apiserver-gen/go/model_container.go
generated
8
pkg/apiserver-gen/go/model_container.go
generated
@@ -25,6 +25,8 @@ type Container struct {
|
||||
CpuRequest string `json:"cpuRequest"`
|
||||
|
||||
CpuLimit string `json:"cpuLimit"`
|
||||
|
||||
VolumeMounts []VolumeMount `json:"volumeMounts"`
|
||||
}
|
||||
|
||||
// AssertContainerRequired checks if the required fields are not zero-ed
|
||||
@@ -38,6 +40,7 @@ func AssertContainerRequired(obj Container) error {
|
||||
"memoryLimit": obj.MemoryLimit,
|
||||
"cpuRequest": obj.CpuRequest,
|
||||
"cpuLimit": obj.CpuLimit,
|
||||
"volumeMounts": obj.VolumeMounts,
|
||||
}
|
||||
for name, el := range elements {
|
||||
if isZero := IsZeroValue(el); isZero {
|
||||
@@ -45,6 +48,11 @@ func AssertContainerRequired(obj Container) error {
|
||||
}
|
||||
}
|
||||
|
||||
for _, el := range obj.VolumeMounts {
|
||||
if err := AssertVolumeMountRequired(el); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
8
pkg/apiserver-gen/go/model_devfile_content.go
generated
8
pkg/apiserver-gen/go/model_devfile_content.go
generated
@@ -20,6 +20,8 @@ type DevfileContent struct {
|
||||
|
||||
Resources []Resource `json:"resources"`
|
||||
|
||||
Volumes []Volume `json:"volumes"`
|
||||
|
||||
Events Events `json:"events"`
|
||||
|
||||
Metadata Metadata `json:"metadata"`
|
||||
@@ -33,6 +35,7 @@ func AssertDevfileContentRequired(obj DevfileContent) error {
|
||||
"containers": obj.Containers,
|
||||
"images": obj.Images,
|
||||
"resources": obj.Resources,
|
||||
"volumes": obj.Volumes,
|
||||
"events": obj.Events,
|
||||
"metadata": obj.Metadata,
|
||||
}
|
||||
@@ -62,6 +65,11 @@ func AssertDevfileContentRequired(obj DevfileContent) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, el := range obj.Volumes {
|
||||
if err := AssertVolumeRequired(el); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := AssertEventsRequired(obj.Events); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
44
pkg/apiserver-gen/go/model_volume.go
generated
Normal file
44
pkg/apiserver-gen/go/model_volume.go
generated
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* odo dev
|
||||
*
|
||||
* API interface for 'odo dev'
|
||||
*
|
||||
* API version: 0.1
|
||||
* Generated by: OpenAPI Generator (https://openapi-generator.tech)
|
||||
*/
|
||||
|
||||
package openapi
|
||||
|
||||
type Volume struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
Ephemeral bool `json:"ephemeral,omitempty"`
|
||||
|
||||
Size string `json:"size,omitempty"`
|
||||
}
|
||||
|
||||
// AssertVolumeRequired checks if the required fields are not zero-ed
|
||||
func AssertVolumeRequired(obj Volume) error {
|
||||
elements := map[string]interface{}{
|
||||
"name": obj.Name,
|
||||
}
|
||||
for name, el := range elements {
|
||||
if isZero := IsZeroValue(el); isZero {
|
||||
return &RequiredError{Field: name}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AssertRecurseVolumeRequired recursively checks if required fields are not zero-ed in a nested slice.
|
||||
// Accepts only nested slice of Volume (e.g. [][]Volume), otherwise ErrTypeAssertionError is thrown.
|
||||
func AssertRecurseVolumeRequired(objSlice interface{}) error {
|
||||
return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error {
|
||||
aVolume, ok := obj.(Volume)
|
||||
if !ok {
|
||||
return ErrTypeAssertionError
|
||||
}
|
||||
return AssertVolumeRequired(aVolume)
|
||||
})
|
||||
}
|
||||
43
pkg/apiserver-gen/go/model_volume_mount.go
generated
Normal file
43
pkg/apiserver-gen/go/model_volume_mount.go
generated
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* odo dev
|
||||
*
|
||||
* API interface for 'odo dev'
|
||||
*
|
||||
* API version: 0.1
|
||||
* Generated by: OpenAPI Generator (https://openapi-generator.tech)
|
||||
*/
|
||||
|
||||
package openapi
|
||||
|
||||
type VolumeMount struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// AssertVolumeMountRequired checks if the required fields are not zero-ed
|
||||
func AssertVolumeMountRequired(obj VolumeMount) error {
|
||||
elements := map[string]interface{}{
|
||||
"name": obj.Name,
|
||||
"path": obj.Path,
|
||||
}
|
||||
for name, el := range elements {
|
||||
if isZero := IsZeroValue(el); isZero {
|
||||
return &RequiredError{Field: name}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AssertRecurseVolumeMountRequired recursively checks if required fields are not zero-ed in a nested slice.
|
||||
// Accepts only nested slice of VolumeMount (e.g. [][]VolumeMount), otherwise ErrTypeAssertionError is thrown.
|
||||
func AssertRecurseVolumeMountRequired(objSlice interface{}) error {
|
||||
return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error {
|
||||
aVolumeMount, ok := obj.(VolumeMount)
|
||||
if !ok {
|
||||
return ErrTypeAssertionError
|
||||
}
|
||||
return AssertVolumeMountRequired(aVolumeMount)
|
||||
})
|
||||
}
|
||||
@@ -19,6 +19,7 @@ func (s *DevstateApiService) DevstateContainerPost(ctx context.Context, containe
|
||||
container.MemLimit,
|
||||
container.CpuReq,
|
||||
container.CpuLimit,
|
||||
container.VolumeMounts,
|
||||
)
|
||||
if err != nil {
|
||||
return openapi.Response(http.StatusInternalServerError, openapi.GeneralError{
|
||||
@@ -90,6 +91,30 @@ func (s *DevstateApiService) DevstateResourceResourceNameDelete(ctx context.Cont
|
||||
return openapi.Response(http.StatusOK, newContent), nil
|
||||
}
|
||||
|
||||
func (s *DevstateApiService) DevstateVolumePost(ctx context.Context, volume openapi.DevstateVolumePostRequest) (openapi.ImplResponse, error) {
|
||||
newContent, err := s.devfileState.AddVolume(
|
||||
volume.Name,
|
||||
volume.Ephemeral,
|
||||
volume.Size,
|
||||
)
|
||||
if err != nil {
|
||||
return openapi.Response(http.StatusInternalServerError, openapi.GeneralError{
|
||||
Message: fmt.Sprintf("Error adding the volume: %s", err),
|
||||
}), nil
|
||||
}
|
||||
return openapi.Response(http.StatusOK, newContent), nil
|
||||
}
|
||||
|
||||
func (s *DevstateApiService) DevstateVolumeVolumeNameDelete(ctx context.Context, volumeName string) (openapi.ImplResponse, error) {
|
||||
newContent, err := s.devfileState.DeleteVolume(volumeName)
|
||||
if err != nil {
|
||||
return openapi.Response(http.StatusInternalServerError, openapi.GeneralError{
|
||||
Message: fmt.Sprintf("Error deleting the volume: %s", err),
|
||||
}), nil
|
||||
}
|
||||
return openapi.Response(http.StatusOK, newContent), nil
|
||||
}
|
||||
|
||||
func (s *DevstateApiService) DevstateApplyCommandPost(ctx context.Context, command openapi.DevstateApplyCommandPostRequest) (openapi.ImplResponse, error) {
|
||||
newContent, err := s.devfileState.AddApplyCommand(
|
||||
command.Name,
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
. "github.com/redhat-developer/odo/pkg/apiserver-gen/go"
|
||||
openapi "github.com/redhat-developer/odo/pkg/apiserver-gen/go"
|
||||
)
|
||||
|
||||
func TestDevfileState_AddExecCommand(t *testing.T) {
|
||||
@@ -36,6 +37,7 @@ func TestDevfileState_AddExecCommand(t *testing.T) {
|
||||
"2Gi",
|
||||
"100m",
|
||||
"200m",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -96,10 +98,12 @@ schemaVersion: 2.2.0
|
||||
MemoryLimit: "2Gi",
|
||||
CpuRequest: "100m",
|
||||
CpuLimit: "200m",
|
||||
VolumeMounts: []openapi.VolumeMount{},
|
||||
},
|
||||
},
|
||||
Images: []Image{},
|
||||
Resources: []Resource{},
|
||||
Volumes: []Volume{},
|
||||
Events: Events{},
|
||||
},
|
||||
},
|
||||
@@ -186,6 +190,7 @@ schemaVersion: 2.2.0
|
||||
},
|
||||
},
|
||||
Resources: []Resource{},
|
||||
Volumes: []Volume{},
|
||||
Events: Events{},
|
||||
},
|
||||
},
|
||||
@@ -235,6 +240,7 @@ func TestDevfileState_AddCompositeCommand(t *testing.T) {
|
||||
"2Gi",
|
||||
"100m",
|
||||
"200m",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -313,10 +319,12 @@ schemaVersion: 2.2.0
|
||||
MemoryLimit: "2Gi",
|
||||
CpuRequest: "100m",
|
||||
CpuLimit: "200m",
|
||||
VolumeMounts: []openapi.VolumeMount{},
|
||||
},
|
||||
},
|
||||
Images: []Image{},
|
||||
Resources: []Resource{},
|
||||
Volumes: []Volume{},
|
||||
Events: Events{},
|
||||
},
|
||||
},
|
||||
@@ -364,6 +372,7 @@ func TestDevfileState_DeleteCommand(t *testing.T) {
|
||||
"2Gi",
|
||||
"100m",
|
||||
"200m",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -412,10 +421,12 @@ schemaVersion: 2.2.0
|
||||
MemoryLimit: "2Gi",
|
||||
CpuRequest: "100m",
|
||||
CpuLimit: "200m",
|
||||
VolumeMounts: []openapi.VolumeMount{},
|
||||
},
|
||||
},
|
||||
Images: []Image{},
|
||||
Resources: []Resource{},
|
||||
Volumes: []Volume{},
|
||||
Events: Events{},
|
||||
},
|
||||
},
|
||||
@@ -627,6 +638,7 @@ schemaVersion: 2.2.0
|
||||
Containers: []Container{},
|
||||
Images: []Image{},
|
||||
Resources: []Resource{},
|
||||
Volumes: []Volume{},
|
||||
},
|
||||
},
|
||||
// TODO: Add test cases.
|
||||
@@ -713,6 +725,7 @@ schemaVersion: 2.2.0
|
||||
Containers: []Container{},
|
||||
Images: []Image{},
|
||||
Resources: []Resource{},
|
||||
Volumes: []Volume{},
|
||||
},
|
||||
},
|
||||
// TODO: Add test cases.
|
||||
@@ -801,6 +814,7 @@ schemaVersion: 2.2.0
|
||||
Containers: []Container{},
|
||||
Images: []Image{},
|
||||
Resources: []Resource{},
|
||||
Volumes: []Volume{},
|
||||
},
|
||||
},
|
||||
// TODO: Add test cases.
|
||||
|
||||
@@ -9,7 +9,24 @@ import (
|
||||
. "github.com/redhat-developer/odo/pkg/apiserver-gen/go"
|
||||
)
|
||||
|
||||
func (o *DevfileState) AddContainer(name string, image string, command []string, args []string, memRequest string, memLimit string, cpuRequest string, cpuLimit string) (DevfileContent, error) {
|
||||
func (o *DevfileState) AddContainer(
|
||||
name string,
|
||||
image string,
|
||||
command []string,
|
||||
args []string,
|
||||
memRequest string,
|
||||
memLimit string,
|
||||
cpuRequest string,
|
||||
cpuLimit string,
|
||||
volumeMounts []VolumeMount,
|
||||
) (DevfileContent, error) {
|
||||
v1alpha2VolumeMounts := make([]v1alpha2.VolumeMount, 0, len(volumeMounts))
|
||||
for _, vm := range volumeMounts {
|
||||
v1alpha2VolumeMounts = append(v1alpha2VolumeMounts, v1alpha2.VolumeMount{
|
||||
Name: vm.Name,
|
||||
Path: vm.Path,
|
||||
})
|
||||
}
|
||||
container := v1alpha2.Component{
|
||||
Name: name,
|
||||
ComponentUnion: v1alpha2.ComponentUnion{
|
||||
@@ -22,6 +39,7 @@ func (o *DevfileState) AddContainer(name string, image string, command []string,
|
||||
MemoryLimit: memLimit,
|
||||
CpuRequest: cpuRequest,
|
||||
CpuLimit: cpuLimit,
|
||||
VolumeMounts: v1alpha2VolumeMounts,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -184,3 +202,56 @@ func (o *DevfileState) checkResourceUsed(name string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *DevfileState) AddVolume(name string, ephemeral bool, size string) (DevfileContent, error) {
|
||||
volume := v1alpha2.Component{
|
||||
Name: name,
|
||||
ComponentUnion: v1alpha2.ComponentUnion{
|
||||
Volume: &v1alpha2.VolumeComponent{
|
||||
Volume: v1alpha2.Volume{
|
||||
Ephemeral: &ephemeral,
|
||||
Size: size,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := o.Devfile.Data.AddComponents([]v1alpha2.Component{volume})
|
||||
if err != nil {
|
||||
return DevfileContent{}, err
|
||||
}
|
||||
return o.GetContent()
|
||||
}
|
||||
|
||||
func (o *DevfileState) DeleteVolume(name string) (DevfileContent, error) {
|
||||
|
||||
err := o.checkVolumeUsed(name)
|
||||
if err != nil {
|
||||
return DevfileContent{}, fmt.Errorf("error deleting volume %q: %w", name, err)
|
||||
}
|
||||
// TODO check if it is a Volume, not another component
|
||||
|
||||
err = o.Devfile.Data.DeleteComponent(name)
|
||||
if err != nil {
|
||||
return DevfileContent{}, err
|
||||
}
|
||||
return o.GetContent()
|
||||
}
|
||||
|
||||
func (o *DevfileState) checkVolumeUsed(name string) error {
|
||||
containers, err := o.Devfile.Data.GetComponents(common.DevfileOptions{
|
||||
ComponentOptions: common.ComponentOptions{
|
||||
ComponentType: v1alpha2.ContainerComponentType,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, container := range containers {
|
||||
for _, mount := range container.Container.VolumeMounts {
|
||||
if mount.Name == name {
|
||||
return fmt.Errorf("volume %q is mounted by Container %q", name, container.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
. "github.com/redhat-developer/odo/pkg/apiserver-gen/go"
|
||||
openapi "github.com/redhat-developer/odo/pkg/apiserver-gen/go"
|
||||
)
|
||||
|
||||
func TestDevfileState_AddContainer(t *testing.T) {
|
||||
@@ -17,6 +18,7 @@ func TestDevfileState_AddContainer(t *testing.T) {
|
||||
memLimit string
|
||||
cpuRequest string
|
||||
cpuLimit string
|
||||
volumeMounts []openapi.VolumeMount
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -39,6 +41,12 @@ func TestDevfileState_AddContainer(t *testing.T) {
|
||||
memLimit: "2Gi",
|
||||
cpuRequest: "100m",
|
||||
cpuLimit: "200m",
|
||||
volumeMounts: []openapi.VolumeMount{
|
||||
{
|
||||
Name: "vol1",
|
||||
Path: "/mnt/volume1",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: DevfileContent{
|
||||
Content: `components:
|
||||
@@ -54,6 +62,9 @@ func TestDevfileState_AddContainer(t *testing.T) {
|
||||
image: an-image
|
||||
memoryLimit: 2Gi
|
||||
memoryRequest: 1Gi
|
||||
volumeMounts:
|
||||
- name: vol1
|
||||
path: /mnt/volume1
|
||||
name: a-name
|
||||
metadata: {}
|
||||
schemaVersion: 2.2.0
|
||||
@@ -69,10 +80,17 @@ schemaVersion: 2.2.0
|
||||
MemoryLimit: "2Gi",
|
||||
CpuRequest: "100m",
|
||||
CpuLimit: "200m",
|
||||
VolumeMounts: []openapi.VolumeMount{
|
||||
{
|
||||
Name: "vol1",
|
||||
Path: "/mnt/volume1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Images: []Image{},
|
||||
Resources: []Resource{},
|
||||
Volumes: []Volume{},
|
||||
Events: Events{},
|
||||
},
|
||||
},
|
||||
@@ -81,7 +99,7 @@ schemaVersion: 2.2.0
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := tt.state()
|
||||
got, err := o.AddContainer(tt.args.name, tt.args.image, tt.args.command, tt.args.args, tt.args.memRequest, tt.args.memLimit, tt.args.cpuRequest, tt.args.cpuLimit)
|
||||
got, err := o.AddContainer(tt.args.name, tt.args.image, tt.args.command, tt.args.args, tt.args.memRequest, tt.args.memLimit, tt.args.cpuRequest, tt.args.cpuLimit, tt.args.volumeMounts)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("DevfileState.AddContainer() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
@@ -120,6 +138,7 @@ func TestDevfileState_DeleteContainer(t *testing.T) {
|
||||
"2Gi",
|
||||
"100m",
|
||||
"200m",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -137,6 +156,7 @@ schemaVersion: 2.2.0
|
||||
Containers: []Container{},
|
||||
Images: []Image{},
|
||||
Resources: []Resource{},
|
||||
Volumes: []Volume{},
|
||||
Events: Events{},
|
||||
},
|
||||
},
|
||||
@@ -153,6 +173,7 @@ schemaVersion: 2.2.0
|
||||
"2Gi",
|
||||
"100m",
|
||||
"200m",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -242,6 +263,7 @@ schemaVersion: 2.2.0
|
||||
},
|
||||
},
|
||||
Resources: []Resource{},
|
||||
Volumes: []Volume{},
|
||||
Events: Events{},
|
||||
},
|
||||
},
|
||||
@@ -304,6 +326,7 @@ schemaVersion: 2.2.0
|
||||
Containers: []Container{},
|
||||
Images: []Image{},
|
||||
Resources: []Resource{},
|
||||
Volumes: []Volume{},
|
||||
Events: Events{},
|
||||
},
|
||||
},
|
||||
@@ -389,6 +412,7 @@ schemaVersion: 2.2.0
|
||||
Uri: "an-uri",
|
||||
},
|
||||
},
|
||||
Volumes: []Volume{},
|
||||
Events: Events{},
|
||||
},
|
||||
},
|
||||
@@ -418,6 +442,7 @@ schemaVersion: 2.2.0
|
||||
Inlined: "inline resource...",
|
||||
},
|
||||
},
|
||||
Volumes: []Volume{},
|
||||
Events: Events{},
|
||||
},
|
||||
},
|
||||
@@ -477,6 +502,7 @@ schemaVersion: 2.2.0
|
||||
Containers: []Container{},
|
||||
Images: []Image{},
|
||||
Resources: []Resource{},
|
||||
Volumes: []Volume{},
|
||||
Events: Events{},
|
||||
},
|
||||
},
|
||||
@@ -519,3 +545,148 @@ schemaVersion: 2.2.0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevfileState_AddVolume(t *testing.T) {
|
||||
type args struct {
|
||||
name string
|
||||
size string
|
||||
ephemeral bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
state func() DevfileState
|
||||
args args
|
||||
want DevfileContent
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Add a volume",
|
||||
state: func() DevfileState {
|
||||
return NewDevfileState()
|
||||
},
|
||||
args: args{
|
||||
name: "a-name",
|
||||
size: "1Gi",
|
||||
ephemeral: true,
|
||||
},
|
||||
want: DevfileContent{
|
||||
Content: `components:
|
||||
- name: a-name
|
||||
volume:
|
||||
ephemeral: true
|
||||
size: 1Gi
|
||||
metadata: {}
|
||||
schemaVersion: 2.2.0
|
||||
`,
|
||||
Commands: []Command{},
|
||||
Containers: []Container{},
|
||||
Images: []Image{},
|
||||
Resources: []Resource{},
|
||||
Volumes: []Volume{
|
||||
{
|
||||
Name: "a-name",
|
||||
Size: "1Gi",
|
||||
Ephemeral: true,
|
||||
},
|
||||
},
|
||||
Events: Events{},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := tt.state()
|
||||
got, err := o.AddVolume(tt.args.name, tt.args.ephemeral, tt.args.size)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("DevfileState.AddVolume() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(tt.want.Content, got.Content); diff != "" {
|
||||
t.Errorf("DevfileState.AddVolume() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
t.Errorf("DevfileState.AddVolume() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevfileState_DeleteVolume(t *testing.T) {
|
||||
type args struct {
|
||||
name string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
state func(t *testing.T) DevfileState
|
||||
args args
|
||||
want DevfileContent
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Delete an existing volume",
|
||||
state: func(t *testing.T) DevfileState {
|
||||
state := NewDevfileState()
|
||||
_, err := state.AddVolume(
|
||||
"a-name",
|
||||
true,
|
||||
"1Gi",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return state
|
||||
},
|
||||
args: args{
|
||||
name: "a-name",
|
||||
},
|
||||
want: DevfileContent{
|
||||
Content: `metadata: {}
|
||||
schemaVersion: 2.2.0
|
||||
`,
|
||||
Commands: []Command{},
|
||||
Containers: []Container{},
|
||||
Images: []Image{},
|
||||
Resources: []Resource{},
|
||||
Volumes: []Volume{},
|
||||
Events: Events{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Delete a non existing resource",
|
||||
state: func(t *testing.T) DevfileState {
|
||||
state := NewDevfileState()
|
||||
_, err := state.AddVolume(
|
||||
"a-name",
|
||||
true,
|
||||
"1Gi",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return state
|
||||
},
|
||||
args: args{
|
||||
name: "another-name",
|
||||
},
|
||||
want: DevfileContent{},
|
||||
wantErr: true,
|
||||
},
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := tt.state(t)
|
||||
got, err := o.DeleteVolume(tt.args.name)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("DevfileState.DeleteVolume() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(tt.want.Content, got.Content); diff != "" {
|
||||
t.Errorf("DevfileState.DeleteVolume() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
t.Errorf("DevfileState.DeleteVolume() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,97 +16,6 @@ const (
|
||||
SEPARATOR = ","
|
||||
)
|
||||
|
||||
/*
|
||||
type DevfileContent struct {
|
||||
Content string `json:"content"`
|
||||
Commands []Command `json:"commands"`
|
||||
Containers []Container `json:"containers"`
|
||||
Images []Image `json:"images"`
|
||||
Resources []Resource `json:"resources"`
|
||||
Events Events `json:"events"`
|
||||
Metadata Metadata `json:"metadata"`
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Description string `json:"description"`
|
||||
Tags string `json:"tags"`
|
||||
Architectures string `json:"architectures"`
|
||||
Icon string `json:"icon"`
|
||||
GlobalMemoryLimit string `json:"globalMemoryLimit"`
|
||||
ProjectType string `json:"projectType"`
|
||||
Language string `json:"language"`
|
||||
Website string `json:"website"`
|
||||
Provider string `json:"provider"`
|
||||
SupportUrl string `json:"supportUrl"`
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
Name string `json:"name"`
|
||||
Group string `json:"group"`
|
||||
Default bool `json:"default"`
|
||||
Type string `json:"type"`
|
||||
Exec *ExecCommand `json:"exec"`
|
||||
Apply *ApplyCommand `json:"apply"`
|
||||
Image *ImageCommand `json:"image"`
|
||||
Composite *CompositeCommand `json:"composite"`
|
||||
}
|
||||
|
||||
type ExecCommand struct {
|
||||
Component string `json:"component"`
|
||||
CommandLine string `json:"commandLine"`
|
||||
WorkingDir string `json:"workingDir"`
|
||||
HotReloadCapable bool `json:"hotReloadCapable"`
|
||||
}
|
||||
|
||||
type ApplyCommand struct {
|
||||
Component string `json:"component"`
|
||||
}
|
||||
|
||||
type ImageCommand struct {
|
||||
Component string `json:"component"`
|
||||
}
|
||||
|
||||
type CompositeCommand struct {
|
||||
Commands []string `json:"commands"`
|
||||
Parallel bool `json:"parallel"`
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
Command []string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
MemoryRequest string `json:"memoryRequest"`
|
||||
MemoryLimit string `json:"memoryLimit"`
|
||||
CpuRequest string `json:"cpuRequest"`
|
||||
CpuLimit string `json:"cpuLimit"`
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
Name string `json:"name"`
|
||||
ImageName string `json:"imageName"`
|
||||
Args []string `json:"args"`
|
||||
BuildContext string `json:"buildContext"`
|
||||
RootRequired bool `json:"rootRequired"`
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
|
||||
type Resource struct {
|
||||
Name string `json:"name"`
|
||||
Inlined string `json:"inlined"`
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
|
||||
type Events struct {
|
||||
PreStart []string `json:"preStart"`
|
||||
PostStart []string `json:"postStart"`
|
||||
PreStop []string `json:"preStop"`
|
||||
PostStop []string `json:"postStop"`
|
||||
}
|
||||
*/
|
||||
// getContent returns the YAML content of the global devfile as string
|
||||
func (o *DevfileState) GetContent() (DevfileContent, error) {
|
||||
err := o.Devfile.WriteYamlDevfile()
|
||||
@@ -137,12 +46,18 @@ func (o *DevfileState) GetContent() (DevfileContent, error) {
|
||||
return DevfileContent{}, errors.New("error getting Kubernetes resources")
|
||||
}
|
||||
|
||||
volumes, err := o.getVolumes()
|
||||
if err != nil {
|
||||
return DevfileContent{}, errors.New("error getting volumes")
|
||||
}
|
||||
|
||||
return DevfileContent{
|
||||
Content: string(result),
|
||||
Commands: commands,
|
||||
Containers: containers,
|
||||
Images: images,
|
||||
Resources: resources,
|
||||
Volumes: volumes,
|
||||
Events: o.getEvents(),
|
||||
Metadata: o.getMetadata(),
|
||||
}, nil
|
||||
@@ -255,11 +170,23 @@ func (o *DevfileState) getContainers() ([]Container, error) {
|
||||
MemoryLimit: container.ComponentUnion.Container.MemoryLimit,
|
||||
CpuRequest: container.ComponentUnion.Container.CpuRequest,
|
||||
CpuLimit: container.ComponentUnion.Container.CpuLimit,
|
||||
VolumeMounts: o.getVolumeMounts(container.Container.Container),
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (o *DevfileState) getVolumeMounts(container v1alpha2.Container) []VolumeMount {
|
||||
result := make([]VolumeMount, 0, len(container.VolumeMounts))
|
||||
for _, vm := range container.VolumeMounts {
|
||||
result = append(result, VolumeMount{
|
||||
Name: vm.Name,
|
||||
Path: vm.Path,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (o *DevfileState) getImages() ([]Image, error) {
|
||||
images, err := o.Devfile.Data.GetComponents(common.DevfileOptions{
|
||||
ComponentOptions: common.ComponentOptions{
|
||||
@@ -303,6 +230,26 @@ func (o *DevfileState) getResources() ([]Resource, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (o *DevfileState) getVolumes() ([]Volume, error) {
|
||||
volumes, err := o.Devfile.Data.GetComponents(common.DevfileOptions{
|
||||
ComponentOptions: common.ComponentOptions{
|
||||
ComponentType: v1alpha2.VolumeComponentType,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]Volume, 0, len(volumes))
|
||||
for _, volume := range volumes {
|
||||
result = append(result, Volume{
|
||||
Name: volume.Name,
|
||||
Ephemeral: *volume.Volume.Ephemeral,
|
||||
Size: volume.Volume.Size,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (o *DevfileState) getEvents() Events {
|
||||
events := o.Devfile.Data.GetEvents()
|
||||
return Events{
|
||||
|
||||
@@ -24,6 +24,7 @@ func TestDevfileState_GetContent(t *testing.T) {
|
||||
Containers: []Container{},
|
||||
Images: []Image{},
|
||||
Resources: []Resource{},
|
||||
Volumes: []Volume{},
|
||||
Events: Events{},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -39,6 +39,7 @@ schemaVersion: 2.2.0
|
||||
Containers: []Container{},
|
||||
Images: []Image{},
|
||||
Resources: []Resource{},
|
||||
Volumes: []Volume{},
|
||||
Events: Events{
|
||||
PreStart: []string{"command1"},
|
||||
},
|
||||
@@ -70,6 +71,7 @@ schemaVersion: 2.2.0
|
||||
Containers: []Container{},
|
||||
Images: []Image{},
|
||||
Resources: []Resource{},
|
||||
Volumes: []Volume{},
|
||||
Events: Events{
|
||||
PreStart: []string{"command1"},
|
||||
PostStart: []string{"command2"},
|
||||
|
||||
@@ -75,6 +75,7 @@ schemaVersion: 2.2.0
|
||||
Containers: []Container{},
|
||||
Images: []Image{},
|
||||
Resources: []Resource{},
|
||||
Volumes: []Volume{},
|
||||
Metadata: Metadata{
|
||||
Name: "a-name",
|
||||
Version: "v1.1.1",
|
||||
|
||||
@@ -543,6 +543,11 @@ paths:
|
||||
cpuLimit:
|
||||
description: CPU limit for the deployed container
|
||||
type: string
|
||||
volumeMounts:
|
||||
description: Volume to mount into the container filesystem
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/VolumeMount'
|
||||
responses:
|
||||
'200':
|
||||
description: container was successfully added to the devfile
|
||||
@@ -813,6 +818,101 @@ paths:
|
||||
example:
|
||||
message: "Error deleting the resource"
|
||||
|
||||
/devstate/volume:
|
||||
post:
|
||||
tags:
|
||||
- devstate
|
||||
description: Add a new Volume to the Devfile
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
description: Name of the volume
|
||||
type: string
|
||||
size:
|
||||
description: Minimal size of the volume
|
||||
type: string
|
||||
ephemeral:
|
||||
description: True if the Volume is Ephemeral
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
description: volume was successfully added to the devfile
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DevfileContent'
|
||||
example:
|
||||
{
|
||||
"content": "schemaVersion: 2.2.0\n",
|
||||
"commands": [],
|
||||
"containers": [],
|
||||
"images": [],
|
||||
"resources": [],
|
||||
"events": {
|
||||
"preStart": null,
|
||||
"postStart": null,
|
||||
"preStop": null,
|
||||
"postStop": null
|
||||
},
|
||||
"metadata": {
|
||||
"name": "",
|
||||
"version": "",
|
||||
"displayName": "",
|
||||
description": "",
|
||||
"tags": "",
|
||||
"architectures": "",
|
||||
"icon": "",
|
||||
"globalMemoryLimit": "",
|
||||
"projectType": "",
|
||||
"language": "",
|
||||
"website": "",
|
||||
"provider": "",
|
||||
"supportUrl": ""
|
||||
}
|
||||
}
|
||||
'500':
|
||||
description: Error adding the volume
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
example:
|
||||
message: "Error adding the volume"
|
||||
|
||||
/devstate/volume/{volumeName}:
|
||||
delete:
|
||||
tags:
|
||||
- devstate
|
||||
description: "Delete a volume from the Devfile"
|
||||
parameters:
|
||||
- name: volumeName
|
||||
in: path
|
||||
description: Volume name to delete
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralSuccess'
|
||||
example:
|
||||
message: "Volume has been deleted"
|
||||
description: "Volume has been deleted"
|
||||
'500':
|
||||
description: Error deleting the volume
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
example:
|
||||
message: "Error deleting the volume"
|
||||
|
||||
/devstate/applyCommand:
|
||||
post:
|
||||
tags:
|
||||
@@ -1315,6 +1415,7 @@ components:
|
||||
- containers
|
||||
- images
|
||||
- resources
|
||||
- volumes
|
||||
- events
|
||||
- metadata
|
||||
properties:
|
||||
@@ -1336,6 +1437,10 @@ components:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Resource'
|
||||
volumes:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Volume'
|
||||
events:
|
||||
$ref: '#/components/schemas/Events'
|
||||
metadata:
|
||||
@@ -1416,6 +1521,7 @@ components:
|
||||
- memoryLimit
|
||||
- cpuRequest
|
||||
- cpuLimit
|
||||
- volumeMounts
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
@@ -1437,6 +1543,20 @@ components:
|
||||
type: string
|
||||
cpuLimit:
|
||||
type: string
|
||||
volumeMounts:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/VolumeMount'
|
||||
VolumeMount:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- path
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
Image:
|
||||
type: object
|
||||
required:
|
||||
@@ -1472,6 +1592,17 @@ components:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
Volume:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
ephemeral:
|
||||
type: boolean
|
||||
size:
|
||||
type: string
|
||||
Events:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
2
pkg/apiserver-impl/ui/index.html
generated
2
pkg/apiserver-impl/ui/index.html
generated
@@ -11,6 +11,6 @@
|
||||
<body class="mat-typography">
|
||||
<div id="loading">Loading, please wait...</div>
|
||||
<app-root></app-root>
|
||||
<script src="runtime.1289ea0acffcdc5e.js" type="module"></script><script src="polyfills.8b3b37cedaf377c3.js" type="module"></script><script src="main.ae49ed4fe0fa0670.js" type="module"></script>
|
||||
<script src="runtime.1289ea0acffcdc5e.js" type="module"></script><script src="polyfills.8b3b37cedaf377c3.js" type="module"></script><script src="main.1046d99cec4375b1.js" type="module"></script>
|
||||
|
||||
</body></html>
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
import {TAB_YAML, TAB_COMMANDS, TAB_CONTAINERS, TAB_IMAGES, TAB_METADATA, TAB_RESOURCES, TAB_EVENTS} from './consts';
|
||||
import {TAB_YAML, TAB_COMMANDS, TAB_CONTAINERS, TAB_IMAGES, TAB_METADATA, TAB_RESOURCES, TAB_EVENTS, TAB_VOLUMES} from './consts';
|
||||
|
||||
describe('devfile editor spec', () => {
|
||||
|
||||
@@ -42,14 +42,39 @@ describe('devfile editor spec', () => {
|
||||
it('displays a created container', () => {
|
||||
cy.init();
|
||||
|
||||
cy.selectTab(TAB_VOLUMES);
|
||||
cy.getByDataCy('volume-name').type('volume1');
|
||||
cy.getByDataCy('volume-size').type('512Mi');
|
||||
cy.getByDataCy('volume-ephemeral').click();
|
||||
cy.getByDataCy('volume-create').click();
|
||||
|
||||
cy.selectTab(TAB_CONTAINERS);
|
||||
cy.getByDataCy('container-name').type('created-container');
|
||||
cy.getByDataCy('container-image').type('an-image');
|
||||
|
||||
cy.getByDataCy('volume-mount-add').click();
|
||||
cy.getByDataCy('volume-mount-path-0').type("/mnt/vol1");
|
||||
cy.getByDataCy('volume-mount-name-0').click().get('mat-option').contains('volume1').click();
|
||||
|
||||
cy.getByDataCy('volume-mount-add').click();
|
||||
cy.getByDataCy('volume-mount-path-1').type("/mnt/vol2");
|
||||
cy.getByDataCy('volume-mount-name-1').click().get('mat-option').contains('(New Volume)').click();
|
||||
cy.getByDataCy('volume-name').type('volume2');
|
||||
cy.getByDataCy('volume-create').click();
|
||||
|
||||
cy.getByDataCy('container-create').click();
|
||||
|
||||
cy.getByDataCy('container-info').first()
|
||||
.should('contain.text', 'created-container')
|
||||
.should('contain.text', 'an-image');
|
||||
.should('contain.text', 'an-image')
|
||||
.should('contain.text', 'volume1')
|
||||
.should('contain.text', '/mnt/vol1')
|
||||
.should('contain.text', 'volume2')
|
||||
.should('contain.text', '/mnt/vol2');
|
||||
|
||||
cy.selectTab(TAB_VOLUMES);
|
||||
cy.getByDataCy('volume-info').eq(1)
|
||||
.should('contain.text', 'volume2');
|
||||
});
|
||||
|
||||
it('displays a created image', () => {
|
||||
@@ -97,9 +122,30 @@ describe('devfile editor spec', () => {
|
||||
.should('contain.text', '/my/manifest.yaml');
|
||||
});
|
||||
|
||||
it('displays a created volume', () => {
|
||||
cy.init();
|
||||
|
||||
cy.selectTab(TAB_VOLUMES);
|
||||
cy.getByDataCy('volume-name').type('created-volume');
|
||||
cy.getByDataCy('volume-size').type('512Mi');
|
||||
cy.getByDataCy('volume-ephemeral').click();
|
||||
cy.getByDataCy('volume-create').click();
|
||||
|
||||
cy.getByDataCy('volume-info').first()
|
||||
.should('contain.text', 'created-volume')
|
||||
.should('contain.text', '512Mi')
|
||||
.should('contain.text', 'Yes')
|
||||
});
|
||||
|
||||
it('creates an exec command with a new container', () => {
|
||||
cy.init();
|
||||
|
||||
cy.selectTab(TAB_VOLUMES);
|
||||
cy.getByDataCy('volume-name').type('volume1');
|
||||
cy.getByDataCy('volume-size').type('512Mi');
|
||||
cy.getByDataCy('volume-ephemeral').click();
|
||||
cy.getByDataCy('volume-create').click();
|
||||
|
||||
cy.selectTab(TAB_COMMANDS);
|
||||
cy.getByDataCy('add').click();
|
||||
cy.getByDataCy('new-command-exec').click();
|
||||
@@ -110,6 +156,17 @@ describe('devfile editor spec', () => {
|
||||
cy.getByDataCy('select-container').click().get('mat-option').contains('(New Container)').click();
|
||||
cy.getByDataCy('container-name').type('a-created-container');
|
||||
cy.getByDataCy('container-image').type('an-image');
|
||||
|
||||
cy.getByDataCy('volume-mount-add').click();
|
||||
cy.getByDataCy('volume-mount-path-0').type("/mnt/vol1");
|
||||
cy.getByDataCy('volume-mount-name-0').click().get('mat-option').contains('volume1').click();
|
||||
|
||||
cy.getByDataCy('volume-mount-add').click();
|
||||
cy.getByDataCy('volume-mount-path-1').type("/mnt/vol2");
|
||||
cy.getByDataCy('volume-mount-name-1').click().get('mat-option').contains('(New Volume)').click();
|
||||
cy.getByDataCy('volume-name').type('volume2');
|
||||
cy.getByDataCy('volume-create').click();
|
||||
|
||||
cy.getByDataCy('container-create').click();
|
||||
|
||||
cy.getByDataCy('select-container').should('contain', 'a-created-container');
|
||||
@@ -124,7 +181,15 @@ describe('devfile editor spec', () => {
|
||||
cy.selectTab(TAB_CONTAINERS);
|
||||
cy.getByDataCy('container-info').first()
|
||||
.should('contain.text', 'a-created-container')
|
||||
.should('contain.text', 'an-image');
|
||||
.should('contain.text', 'an-image')
|
||||
.should('contain.text', 'volume1')
|
||||
.should('contain.text', '/mnt/vol1')
|
||||
.should('contain.text', 'volume2')
|
||||
.should('contain.text', '/mnt/vol2');
|
||||
|
||||
cy.selectTab(TAB_VOLUMES);
|
||||
cy.getByDataCy('volume-info').eq(1)
|
||||
.should('contain.text', 'volume2');
|
||||
});
|
||||
|
||||
it('creates an apply image command with a new image', () => {
|
||||
|
||||
3
ui/src/app/api-gen/.openapi-generator/FILES
generated
3
ui/src/app/api-gen/.openapi-generator/FILES
generated
@@ -29,6 +29,7 @@ model/devstateExecCommandPostRequest.ts
|
||||
model/devstateImagePostRequest.ts
|
||||
model/devstateQuantityValidPostRequest.ts
|
||||
model/devstateResourcePostRequest.ts
|
||||
model/devstateVolumePostRequest.ts
|
||||
model/events.ts
|
||||
model/execCommand.ts
|
||||
model/generalError.ts
|
||||
@@ -41,5 +42,7 @@ model/metadataRequest.ts
|
||||
model/models.ts
|
||||
model/resource.ts
|
||||
model/telemetryResponse.ts
|
||||
model/volume.ts
|
||||
model/volumeMount.ts
|
||||
param.ts
|
||||
variables.ts
|
||||
|
||||
125
ui/src/app/api-gen/api/devstate.service.ts
generated
125
ui/src/app/api-gen/api/devstate.service.ts
generated
@@ -45,6 +45,8 @@ import { DevstateQuantityValidPostRequest } from '../model/devstateQuantityValid
|
||||
// @ts-ignore
|
||||
import { DevstateResourcePostRequest } from '../model/devstateResourcePostRequest';
|
||||
// @ts-ignore
|
||||
import { DevstateVolumePostRequest } from '../model/devstateVolumePostRequest';
|
||||
// @ts-ignore
|
||||
import { GeneralError } from '../model/generalError';
|
||||
// @ts-ignore
|
||||
import { GeneralSuccess } from '../model/generalSuccess';
|
||||
@@ -1361,4 +1363,127 @@ export class DevstateService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new Volume to the Devfile
|
||||
* @param devstateVolumePostRequest
|
||||
* @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 devstateVolumePost(devstateVolumePostRequest?: DevstateVolumePostRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<DevfileContent>;
|
||||
public devstateVolumePost(devstateVolumePostRequest?: DevstateVolumePostRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpResponse<DevfileContent>>;
|
||||
public devstateVolumePost(devstateVolumePostRequest?: DevstateVolumePostRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpEvent<DevfileContent>>;
|
||||
public devstateVolumePost(devstateVolumePostRequest?: DevstateVolumePostRequest, 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 = `/devstate/volume`;
|
||||
return this.httpClient.request<DevfileContent>('post', `${this.configuration.basePath}${localVarPath}`,
|
||||
{
|
||||
context: localVarHttpContext,
|
||||
body: devstateVolumePostRequest,
|
||||
responseType: <any>responseType_,
|
||||
withCredentials: this.configuration.withCredentials,
|
||||
headers: localVarHeaders,
|
||||
observe: observe,
|
||||
reportProgress: reportProgress
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a volume from the Devfile
|
||||
* @param volumeName Volume name to delete
|
||||
* @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 devstateVolumeVolumeNameDelete(volumeName: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<GeneralSuccess>;
|
||||
public devstateVolumeVolumeNameDelete(volumeName: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpResponse<GeneralSuccess>>;
|
||||
public devstateVolumeVolumeNameDelete(volumeName: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpEvent<GeneralSuccess>>;
|
||||
public devstateVolumeVolumeNameDelete(volumeName: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<any> {
|
||||
if (volumeName === null || volumeName === undefined) {
|
||||
throw new Error('Required parameter volumeName was null or undefined when calling devstateVolumeVolumeNameDelete.');
|
||||
}
|
||||
|
||||
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 = `/devstate/volume/${this.configuration.encodeParam({name: "volumeName", value: volumeName, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}`;
|
||||
return this.httpClient.request<GeneralSuccess>('delete', `${this.configuration.basePath}${localVarPath}`,
|
||||
{
|
||||
context: localVarHttpContext,
|
||||
responseType: <any>responseType_,
|
||||
withCredentials: this.configuration.withCredentials,
|
||||
headers: localVarHeaders,
|
||||
observe: observe,
|
||||
reportProgress: reportProgress
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
2
ui/src/app/api-gen/model/container.ts
generated
2
ui/src/app/api-gen/model/container.ts
generated
@@ -9,6 +9,7 @@
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
import { VolumeMount } from './volumeMount';
|
||||
|
||||
|
||||
export interface Container {
|
||||
@@ -20,5 +21,6 @@ export interface Container {
|
||||
memoryLimit: string;
|
||||
cpuRequest: string;
|
||||
cpuLimit: string;
|
||||
volumeMounts: Array<VolumeMount>;
|
||||
}
|
||||
|
||||
|
||||
2
ui/src/app/api-gen/model/devfileContent.ts
generated
2
ui/src/app/api-gen/model/devfileContent.ts
generated
@@ -12,6 +12,7 @@
|
||||
import { Container } from './container';
|
||||
import { Command } from './command';
|
||||
import { Events } from './events';
|
||||
import { Volume } from './volume';
|
||||
import { Metadata } from './metadata';
|
||||
import { Resource } from './resource';
|
||||
import { Image } from './image';
|
||||
@@ -23,6 +24,7 @@ export interface DevfileContent {
|
||||
containers: Array<Container>;
|
||||
images: Array<Image>;
|
||||
resources: Array<Resource>;
|
||||
volumes: Array<Volume>;
|
||||
events: Events;
|
||||
metadata: Metadata;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
import { VolumeMount } from './volumeMount';
|
||||
|
||||
|
||||
export interface DevstateContainerPostRequest {
|
||||
@@ -44,5 +45,9 @@ export interface DevstateContainerPostRequest {
|
||||
* CPU limit for the deployed container
|
||||
*/
|
||||
cpuLimit?: string;
|
||||
/**
|
||||
* Volume to mount into the container filesystem
|
||||
*/
|
||||
volumeMounts?: Array<VolumeMount>;
|
||||
}
|
||||
|
||||
|
||||
28
ui/src/app/api-gen/model/devstateVolumePostRequest.ts
generated
Normal file
28
ui/src/app/api-gen/model/devstateVolumePostRequest.ts
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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 DevstateVolumePostRequest {
|
||||
/**
|
||||
* Name of the volume
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* Minimal size of the volume
|
||||
*/
|
||||
size?: string;
|
||||
/**
|
||||
* True if the Volume is Ephemeral
|
||||
*/
|
||||
ephemeral?: boolean;
|
||||
}
|
||||
|
||||
3
ui/src/app/api-gen/model/models.ts
generated
3
ui/src/app/api-gen/model/models.ts
generated
@@ -19,6 +19,7 @@ export * from './devstateExecCommandPostRequest';
|
||||
export * from './devstateImagePostRequest';
|
||||
export * from './devstateQuantityValidPostRequest';
|
||||
export * from './devstateResourcePostRequest';
|
||||
export * from './devstateVolumePostRequest';
|
||||
export * from './events';
|
||||
export * from './execCommand';
|
||||
export * from './generalError';
|
||||
@@ -30,3 +31,5 @@ export * from './metadata';
|
||||
export * from './metadataRequest';
|
||||
export * from './resource';
|
||||
export * from './telemetryResponse';
|
||||
export * from './volume';
|
||||
export * from './volumeMount';
|
||||
|
||||
19
ui/src/app/api-gen/model/volume.ts
generated
Normal file
19
ui/src/app/api-gen/model/volume.ts
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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 Volume {
|
||||
name: string;
|
||||
ephemeral?: boolean;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
18
ui/src/app/api-gen/model/volumeMount.ts
generated
Normal file
18
ui/src/app/api-gen/model/volumeMount.ts
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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 VolumeMount {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
<mat-icon class="tab-icon material-icons-outlined">storage</mat-icon>
|
||||
{{tabNames[8]}}
|
||||
</ng-template>
|
||||
<app-volumes></app-volumes>
|
||||
</mat-tab>
|
||||
|
||||
</mat-tab-group>
|
||||
|
||||
@@ -45,6 +45,9 @@ import { MultiCommandComponent } from './controls/multi-command/multi-command.co
|
||||
import { EventsComponent } from './tabs/events/events.component';
|
||||
import { ChipsEventsComponent } from './controls/chips-events/chips-events.component';
|
||||
import { ConfirmComponent } from './components/confirm/confirm.component';
|
||||
import { VolumesComponent } from './tabs/volumes/volumes.component';
|
||||
import { VolumeComponent } from './forms/volume/volume.component';
|
||||
import { VolumeMountsComponent } from './controls/volume-mounts/volume-mounts.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -68,6 +71,9 @@ import { ConfirmComponent } from './components/confirm/confirm.component';
|
||||
EventsComponent,
|
||||
ChipsEventsComponent,
|
||||
ConfirmComponent,
|
||||
VolumesComponent,
|
||||
VolumeComponent,
|
||||
VolumeMountsComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<h3>{{title}}</h3>
|
||||
<h3 *ngIf="title">{{title}}</h3>
|
||||
<div class="group">
|
||||
<span *ngFor="let text of texts; let i=index">
|
||||
<mat-form-field class="inline" appearance="outline">
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
h3 { margin-bottom: 0; }
|
||||
div.group { margin-bottom: 16px; }
|
||||
@@ -0,0 +1,26 @@
|
||||
<h3>Volume Mounts</h3>
|
||||
<div class="group">
|
||||
<div *ngFor="let vm of volumeMounts; let i=index">
|
||||
<mat-form-field class="inline" appearance="outline">
|
||||
<mat-label><span>Volume</span></mat-label>
|
||||
<mat-select [attr.data-cy]="'volume-mount-name-'+i" [value]="vm.name" (selectionChange)="onNameChange(i, $event.value)">
|
||||
<mat-option *ngFor="let volume of volumes" [value]="volume">{{volume}}</mat-option>
|
||||
<mat-option value="!">(New Volume)</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="inline" appearance="outline">
|
||||
<mat-label><span>Mount Path</span></mat-label>
|
||||
<input (input)="onPathChange(i, $event)" [attr.data-cy]="'volume-mount-path-'+i" matInput [value]="vm.path" (change)="onPathChange(i, $event)">
|
||||
</mat-form-field>
|
||||
|
||||
<app-volume
|
||||
*ngIf="showNewVolume[i]"
|
||||
(created)="onNewVolumeCreated(i, $event)"
|
||||
></app-volume>
|
||||
</div>
|
||||
<button data-cy="volume-mount-add" *ngIf="volumeMounts.length > 0" mat-icon-button (click)="add()">
|
||||
<mat-icon class="tab-icon material-icons-outlined">add</mat-icon>
|
||||
</button>
|
||||
<button data-cy="volume-mount-add" *ngIf="volumeMounts.length == 0" mat-flat-button (click)="add()">Add Volume Mount</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { VolumeMountsComponent } from './volume-mounts.component';
|
||||
|
||||
describe('VolumeMountsComponent', () => {
|
||||
let component: VolumeMountsComponent;
|
||||
let fixture: ComponentFixture<VolumeMountsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ VolumeMountsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(VolumeMountsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
86
ui/src/app/controls/volume-mounts/volume-mounts.component.ts
Normal file
86
ui/src/app/controls/volume-mounts/volume-mounts.component.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Component, EventEmitter, Input, Output, forwardRef } from '@angular/core';
|
||||
import { AbstractControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms';
|
||||
import { Volume, VolumeMount } from 'src/app/api-gen';
|
||||
|
||||
@Component({
|
||||
selector: 'app-volume-mounts',
|
||||
templateUrl: './volume-mounts.component.html',
|
||||
styleUrls: ['./volume-mounts.component.css'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
multi: true,
|
||||
useExisting: VolumeMountsComponent
|
||||
},
|
||||
{
|
||||
provide: NG_VALIDATORS,
|
||||
useExisting: forwardRef(() => VolumeMountsComponent),
|
||||
multi: true,
|
||||
},
|
||||
]
|
||||
})
|
||||
export class VolumeMountsComponent implements Validator {
|
||||
|
||||
@Input() volumes: string[] = [];
|
||||
|
||||
@Output() createNewVolume = new EventEmitter<Volume>();
|
||||
|
||||
volumeMounts: VolumeMount[] = [];
|
||||
showNewVolume: boolean[] = [];
|
||||
|
||||
onChange = (_: VolumeMount[]) => {};
|
||||
onValidatorChange = () => {};
|
||||
|
||||
writeValue(value: any) {
|
||||
this.volumeMounts = value;
|
||||
}
|
||||
|
||||
registerOnChange(onChange: any) {
|
||||
this.onChange = onChange;
|
||||
}
|
||||
|
||||
registerOnTouched(_: any) {}
|
||||
|
||||
add() {
|
||||
this.volumeMounts.push({name: "", path: ""});
|
||||
this.onChange(this.volumeMounts);
|
||||
}
|
||||
|
||||
onPathChange(i: number, e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
this.volumeMounts[i].path = target.value;
|
||||
this.onChange(this.volumeMounts);
|
||||
}
|
||||
|
||||
onNameChange(i: number, name: string) {
|
||||
if (name != "!") {
|
||||
this.volumeMounts[i].name = name;
|
||||
this.onChange(this.volumeMounts);
|
||||
}
|
||||
|
||||
this.showNewVolume[i] = name == "!";
|
||||
}
|
||||
|
||||
onNewVolumeCreated(i: number, v: Volume) {
|
||||
this.volumes.push(v.name);
|
||||
this.volumeMounts[i].name = v.name;
|
||||
this.createNewVolume.next(v);
|
||||
this.showNewVolume[i] = false;
|
||||
this.onValidatorChange();
|
||||
}
|
||||
|
||||
/* Validator implementation */
|
||||
validate(control: AbstractControl): ValidationErrors | null {
|
||||
for (let i=0; i<this.volumeMounts.length; i++) {
|
||||
const vm = this.volumeMounts[i];
|
||||
if (vm.name == "" || vm.path == "") {
|
||||
return {'internal': true};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
registerOnValidatorChange?(onValidatorChange: () => void): void {
|
||||
this.onValidatorChange = onValidatorChange;
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
<app-container
|
||||
*ngIf="showNewContainer"
|
||||
[volumeNames]="volumeNames ?? []"
|
||||
(created)="onNewContainerCreated($event)"
|
||||
></app-container>
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { DevstateService } from 'src/app/services/devstate.service';
|
||||
import { PATTERN_COMMAND_ID } from '../patterns';
|
||||
import { Container } from 'src/app/api-gen';
|
||||
import { Container, Volume } from 'src/app/api-gen';
|
||||
import { TelemetryService } from 'src/app/services/telemetry.service';
|
||||
import { ToCreate } from '../container/container.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-command-exec',
|
||||
@@ -18,6 +19,8 @@ export class CommandExecComponent {
|
||||
containerList: string[] = [];
|
||||
showNewContainer: boolean = false;
|
||||
containerToCreate: Container | null = null;
|
||||
volumesToCreate: Volume[] = [];
|
||||
volumeNames: string[] | undefined = [];
|
||||
|
||||
constructor(
|
||||
private devstate: DevstateService,
|
||||
@@ -33,6 +36,7 @@ export class CommandExecComponent {
|
||||
});
|
||||
|
||||
this.state.state.subscribe(async newContent => {
|
||||
this.volumeNames = newContent?.volumes.map((v: Volume) => v.name);
|
||||
const containers = newContent?.containers;
|
||||
if (containers == null) {
|
||||
return
|
||||
@@ -41,6 +45,21 @@ export class CommandExecComponent {
|
||||
});
|
||||
}
|
||||
|
||||
createVolumes(volumes: Volume[], i: number, next: () => any) {
|
||||
if (volumes.length == i) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const res = this.devstate.addVolume(volumes[i]);
|
||||
res.subscribe({
|
||||
next: value => {
|
||||
this.createVolumes(volumes, i+1, next);
|
||||
},
|
||||
error: error => {
|
||||
alert(error.error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
create() {
|
||||
this.telemetry.track("[ui] create exec command");
|
||||
@@ -56,6 +75,7 @@ export class CommandExecComponent {
|
||||
});
|
||||
}
|
||||
|
||||
this.createVolumes(this.volumesToCreate, 0, () => {
|
||||
if (this.containerToCreate != null &&
|
||||
this.containerToCreate?.name == this.form.controls["component"].value) {
|
||||
const res = this.devstate.addContainer(this.containerToCreate);
|
||||
@@ -70,6 +90,7 @@ export class CommandExecComponent {
|
||||
} else {
|
||||
subcreate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cancel() {
|
||||
@@ -84,10 +105,12 @@ export class CommandExecComponent {
|
||||
this.showNewContainer = v;
|
||||
}
|
||||
|
||||
onNewContainerCreated(container: Container) {
|
||||
onNewContainerCreated(toCreate: ToCreate) {
|
||||
const container = toCreate.container;
|
||||
this.containerList.push(container.name);
|
||||
this.form.controls["component"].setValue(container.name);
|
||||
this.showNewContainer = false;
|
||||
this.containerToCreate = container;
|
||||
this.volumesToCreate.push(...toCreate.volumes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,15 @@
|
||||
<mat-label><span>Image</span></mat-label>
|
||||
<input placeholder="Image to start the container" data-cy="container-image" matInput formControlName="image">
|
||||
</mat-form-field>
|
||||
<app-multi-text formControlName="command" title="Command" label="Command" addLabel="Add command"></app-multi-text>
|
||||
<app-multi-text formControlName="args" title="Arguments to command" label="Arg" addLabel="Add arg"></app-multi-text>
|
||||
<h3>Command and Arguments</h3>
|
||||
<app-multi-text formControlName="command" label="Command" addLabel="Add command"></app-multi-text>
|
||||
<app-multi-text formControlName="args" label="Arg" addLabel="Add arg"></app-multi-text>
|
||||
<app-volume-mounts
|
||||
[volumes]="volumeNames"
|
||||
formControlName="volumeMounts"
|
||||
(createNewVolume)="onCreateNewVolume($event)"></app-volume-mounts>
|
||||
|
||||
<h3>Resource Usage</h3>
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>Memory Request</span></mat-label>
|
||||
<mat-error>{{quantityErrMsgMemory}}</mat-error>
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { AbstractControl, AsyncValidatorFn, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { PATTERN_COMPONENT_ID } from '../patterns';
|
||||
import { DevstateService } from 'src/app/services/devstate.service';
|
||||
import { Observable, of, map, catchError } from 'rxjs';
|
||||
import { Container } from 'src/app/api-gen';
|
||||
import { Container, Volume } from 'src/app/api-gen';
|
||||
import { TelemetryService } from 'src/app/services/telemetry.service';
|
||||
|
||||
export interface ToCreate {
|
||||
container: Container;
|
||||
volumes: Volume[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-container',
|
||||
templateUrl: './container.component.html',
|
||||
styleUrls: ['./container.component.css']
|
||||
})
|
||||
export class ContainerComponent {
|
||||
@Input() volumeNames: string[] = [];
|
||||
@Input() cancelable: boolean = false;
|
||||
@Output() canceled = new EventEmitter<void>();
|
||||
@Output() created = new EventEmitter<Container>();
|
||||
@Output() created = new EventEmitter<ToCreate>();
|
||||
|
||||
form: FormGroup;
|
||||
|
||||
quantityErrMsgMemory = 'Numeric value, with optional unit Ki, Mi, Gi, Ti, Pi, Ei';
|
||||
quantityErrMsgCPU = 'Numeric value, with optional unit m, k, M, G, T, P, E';
|
||||
|
||||
volumesToCreate: Volume[] = [];
|
||||
|
||||
constructor(
|
||||
private devstate: DevstateService,
|
||||
private telemetry: TelemetryService
|
||||
@@ -30,33 +37,27 @@ export class ContainerComponent {
|
||||
image: new FormControl("", [Validators.required]),
|
||||
command: new FormControl([]),
|
||||
args: new FormControl([]),
|
||||
memoryRequest: new FormControl("", null, [this.isQuantity()]),
|
||||
memoryLimit: new FormControl("", null, [this.isQuantity()]),
|
||||
cpuRequest: new FormControl("", null, [this.isQuantity()]),
|
||||
cpuLimit: new FormControl("", null, [this.isQuantity()]),
|
||||
memoryRequest: new FormControl("", null, [this.devstate.isQuantity()]),
|
||||
memoryLimit: new FormControl("", null, [this.devstate.isQuantity()]),
|
||||
cpuRequest: new FormControl("", null, [this.devstate.isQuantity()]),
|
||||
cpuLimit: new FormControl("", null, [this.devstate.isQuantity()]),
|
||||
volumeMounts: new FormControl([]),
|
||||
})
|
||||
}
|
||||
|
||||
create() {
|
||||
this.telemetry.track("[ui] create container");
|
||||
this.created.emit(this.form.value);
|
||||
this.created.emit({
|
||||
container: this.form.value,
|
||||
volumes: this.volumesToCreate,
|
||||
});
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.canceled.emit();
|
||||
}
|
||||
|
||||
isQuantity(): AsyncValidatorFn {
|
||||
return (control: AbstractControl): Observable<ValidationErrors | null> => {
|
||||
const val = control.value;
|
||||
if (val == '') {
|
||||
return of(null);
|
||||
}
|
||||
const valid = this.devstate.isQuantityValid(val);
|
||||
return valid.pipe(
|
||||
map(() => null),
|
||||
catchError(() => of({"isQuantity": false}))
|
||||
);
|
||||
};
|
||||
onCreateNewVolume(v: Volume) {
|
||||
this.volumesToCreate.push(v);
|
||||
}
|
||||
}
|
||||
|
||||
3
ui/src/app/forms/volume/volume.component.css
Normal file
3
ui/src/app/forms/volume/volume.component.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.main { padding: 16px; }
|
||||
mat-form-field.full-width { width: 100%; }
|
||||
mat-form-field.mid-width { width: 50%; }
|
||||
20
ui/src/app/forms/volume/volume.component.html
Normal file
20
ui/src/app/forms/volume/volume.component.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<div class="main">
|
||||
<h2>Add a new volume</h2>
|
||||
<div class="description">A volume can be mounted and shared by several containers.</div>
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>Name</span></mat-label>
|
||||
<mat-error>Lowercase words separated by dashes. Ex: my-volume</mat-error>
|
||||
<input placeholder="unique name to identify the volume" data-cy="volume-name" matInput formControlName="name">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>Size</span></mat-label>
|
||||
<input placeholder="Minimal size of the volume" data-cy="volume-size" matInput formControlName="size">
|
||||
</mat-form-field>
|
||||
<mat-checkbox data-cy="volume-ephemeral" formControlName="ephemeral">Volume is Ephemeral</mat-checkbox>
|
||||
|
||||
</form>
|
||||
|
||||
<button data-cy="volume-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new volume" (click)="create()">Create</button>
|
||||
<button *ngIf="cancelable" mat-flat-button (click)="cancel()">Cancel</button>
|
||||
</div>
|
||||
23
ui/src/app/forms/volume/volume.component.spec.ts
Normal file
23
ui/src/app/forms/volume/volume.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { VolumeComponent } from './volume.component';
|
||||
|
||||
describe('VolumeComponent', () => {
|
||||
let component: VolumeComponent;
|
||||
let fixture: ComponentFixture<VolumeComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ VolumeComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(VolumeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
39
ui/src/app/forms/volume/volume.component.ts
Normal file
39
ui/src/app/forms/volume/volume.component.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { Volume } from 'src/app/api-gen';
|
||||
import { TelemetryService } from 'src/app/services/telemetry.service';
|
||||
import { PATTERN_COMPONENT_ID } from '../patterns';
|
||||
import { DevstateService } from 'src/app/services/devstate.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-volume',
|
||||
templateUrl: './volume.component.html',
|
||||
styleUrls: ['./volume.component.css']
|
||||
})
|
||||
export class VolumeComponent {
|
||||
@Input() cancelable: boolean = false;
|
||||
@Output() canceled = new EventEmitter<void>();
|
||||
@Output() created = new EventEmitter<Volume>();
|
||||
|
||||
form: FormGroup;
|
||||
|
||||
constructor(
|
||||
private devstate: DevstateService,
|
||||
private telemetry: TelemetryService
|
||||
) {
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl("", [Validators.required, Validators.pattern(PATTERN_COMPONENT_ID)]),
|
||||
size: new FormControl("", null, [this.devstate.isQuantity()]),
|
||||
ephemeral: new FormControl(false),
|
||||
})
|
||||
}
|
||||
|
||||
create() {
|
||||
this.telemetry.track("[ui] create volume");
|
||||
this.created.emit(this.form.value);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.canceled.emit();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ApplyCommand, CompositeCommand, Container, DevfileContent, DevstateChartGet200Response, ExecCommand, Image, Metadata, Resource } from '../api-gen';
|
||||
import { Observable, catchError, map, of } from 'rxjs';
|
||||
import { ApplyCommand, CompositeCommand, Container, DevfileContent, DevstateChartGet200Response, ExecCommand, Image, Metadata, Resource, Volume } from '../api-gen';
|
||||
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -22,6 +23,7 @@ export class DevstateService {
|
||||
memLimit: container.memoryLimit,
|
||||
cpuReq: container.cpuRequest,
|
||||
cpuLimit: container.cpuLimit,
|
||||
volumeMounts: container.volumeMounts,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,6 +46,14 @@ export class DevstateService {
|
||||
});
|
||||
}
|
||||
|
||||
addVolume(volume: Volume): Observable<DevfileContent> {
|
||||
return this.http.post<DevfileContent>(this.base+"/volume", {
|
||||
name: volume.name,
|
||||
ephemeral: volume.ephemeral,
|
||||
size: volume.size,
|
||||
});
|
||||
}
|
||||
|
||||
addExecCommand(name: string, cmd: ExecCommand): Observable<DevfileContent> {
|
||||
return this.http.post<DevfileContent>(this.base+"/execCommand", {
|
||||
name: name,
|
||||
@@ -145,6 +155,10 @@ export class DevstateService {
|
||||
return this.http.delete<DevfileContent>(this.base+"/resource/"+resource);
|
||||
}
|
||||
|
||||
deleteVolume(volume: string): Observable<DevfileContent> {
|
||||
return this.http.delete<DevfileContent>(this.base+"/volume/"+volume);
|
||||
}
|
||||
|
||||
updateEvents(event: "preStart"|"postStart"|"preStop"|"postStop", commands: string[]): Observable<DevfileContent> {
|
||||
return this.http.put<DevfileContent>(this.base+"/events", {
|
||||
eventName: event,
|
||||
@@ -157,4 +171,18 @@ export class DevstateService {
|
||||
quantity: quantity
|
||||
});
|
||||
}
|
||||
|
||||
isQuantity(): AsyncValidatorFn {
|
||||
return (control: AbstractControl): Observable<ValidationErrors | null> => {
|
||||
const val = control.value;
|
||||
if (val == '') {
|
||||
return of(null);
|
||||
}
|
||||
const valid = this.isQuantityValid(val);
|
||||
return valid.pipe(
|
||||
map(() => null),
|
||||
catchError(() => of({"isQuantity": false}))
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
.main { padding: 16px; }
|
||||
mat-card { margin-bottom: 16px; }
|
||||
mat-card-content { padding: 16px; }
|
||||
.volume-mount {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.volume-mount > mat-chip {
|
||||
top: -11px;
|
||||
}
|
||||
.volume-mount > span.path {
|
||||
position: relative;
|
||||
top: -14px;
|
||||
}
|
||||
table.aligned > tr > td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,16 @@
|
||||
<td>Args:</td>
|
||||
<td><code>{{container.args.join(" ")}}</code></td>
|
||||
</tr>
|
||||
<tr *ngIf="container.volumeMounts.length > 0">
|
||||
<td>Volume Mounts:</td>
|
||||
<td>
|
||||
<div class="volume-mount" *ngFor="let vm of container.volumeMounts">
|
||||
<mat-chip disableRipple>
|
||||
<mat-icon matChipAvatar class="material-icons-outlined">storage</mat-icon>
|
||||
{{vm.name}}
|
||||
</mat-chip><span class="path"> in <code>{{vm.path}}</code></span></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="container.memoryRequest != null && container.memoryRequest.length > 0">
|
||||
<td>Memory Request:</td>
|
||||
<td><code>{{container.memoryRequest}}</code></td>
|
||||
@@ -46,6 +56,7 @@
|
||||
|
||||
<app-container
|
||||
*ngIf="forceDisplayAdd || containers == undefined || containers.length == 0"
|
||||
[volumeNames]="volumeNames ?? []"
|
||||
[cancelable]="forceDisplayAdd"
|
||||
(canceled)="undisplayAddForm()"
|
||||
(created)="onCreated($event)"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { DevstateService } from 'src/app/services/devstate.service';
|
||||
import { Container } from 'src/app/api-gen';
|
||||
import { Container, Volume } from 'src/app/api-gen';
|
||||
import { ToCreate } from 'src/app/forms/container/container.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-containers',
|
||||
@@ -12,6 +13,7 @@ export class ContainersComponent implements OnInit {
|
||||
|
||||
forceDisplayAdd: boolean = false;
|
||||
containers: Container[] | undefined = [];
|
||||
volumeNames: string[] | undefined = [];
|
||||
|
||||
constructor(
|
||||
private state: StateService,
|
||||
@@ -21,6 +23,7 @@ export class ContainersComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
const that = this;
|
||||
this.state.state.subscribe(async newContent => {
|
||||
this.volumeNames = newContent?.volumes.map((v: Volume) => v.name);
|
||||
that.containers = newContent?.containers;
|
||||
if (this.containers == null) {
|
||||
return
|
||||
@@ -54,7 +57,25 @@ export class ContainersComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
onCreated(container: Container) {
|
||||
createVolumes(volumes: Volume[], i: number, next: () => any) {
|
||||
if (volumes.length == i) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const res = this.devstate.addVolume(volumes[i]);
|
||||
res.subscribe({
|
||||
next: value => {
|
||||
this.createVolumes(volumes, i+1, next);
|
||||
},
|
||||
error: error => {
|
||||
alert(error.error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCreated(toCreate: ToCreate) {
|
||||
const container = toCreate.container;
|
||||
this.createVolumes(toCreate.volumes, 0, () => {
|
||||
const result = this.devstate.addContainer(container);
|
||||
result.subscribe({
|
||||
next: value => {
|
||||
@@ -64,6 +85,8 @@ export class ContainersComponent implements OnInit {
|
||||
alert(error.error.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
|
||||
3
ui/src/app/tabs/volumes/volumes.component.css
Normal file
3
ui/src/app/tabs/volumes/volumes.component.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.main { padding: 16px; }
|
||||
mat-card { margin-bottom: 16px; }
|
||||
mat-card-content { padding: 16px; }
|
||||
38
ui/src/app/tabs/volumes/volumes.component.html
Normal file
38
ui/src/app/tabs/volumes/volumes.component.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<div class="main">
|
||||
<mat-card data-cy="volume-info" *ngFor="let volume of volumes">
|
||||
<mat-card-header class="colored-title">
|
||||
<mat-card-title>{{volume.name}}</mat-card-title>
|
||||
<mat-card-subtitle>Volume</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<table class="aligned">
|
||||
<tr *ngIf="volume.size">
|
||||
<td>Size:</td>
|
||||
<td><code>{{volume.size}}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Volume is Ephemeral:</td>
|
||||
<td><code>{{volume.ephemeral ? "Yes" : "No"}}</code></td>
|
||||
</tr>
|
||||
</table>
|
||||
</mat-card-content>
|
||||
|
||||
<mat-card-actions>
|
||||
<button mat-button color="warn" (click)="delete(volume.name)">Delete</button>
|
||||
</mat-card-actions>
|
||||
|
||||
</mat-card>
|
||||
|
||||
<app-volume
|
||||
*ngIf="forceDisplayAdd || volumes == undefined || volumes.length == 0"
|
||||
[cancelable]="forceDisplayAdd"
|
||||
(canceled)="undisplayAddForm()"
|
||||
(created)="onCreated($event)"
|
||||
></app-volume>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="!forceDisplayAdd && volumes != undefined && volumes.length > 0">
|
||||
<button class="fab" mat-fab color="primary" (click)="displayAddForm()">
|
||||
<mat-icon class="material-icons-outlined">add</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
23
ui/src/app/tabs/volumes/volumes.component.spec.ts
Normal file
23
ui/src/app/tabs/volumes/volumes.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { VolumesComponent } from './volumes.component';
|
||||
|
||||
describe('VolumesComponent', () => {
|
||||
let component: VolumesComponent;
|
||||
let fixture: ComponentFixture<VolumesComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ VolumesComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(VolumesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
72
ui/src/app/tabs/volumes/volumes.component.ts
Normal file
72
ui/src/app/tabs/volumes/volumes.component.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Volume } from 'src/app/api-gen';
|
||||
import { DevstateService } from 'src/app/services/devstate.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-volumes',
|
||||
templateUrl: './volumes.component.html',
|
||||
styleUrls: ['./volumes.component.css']
|
||||
})
|
||||
export class VolumesComponent {
|
||||
|
||||
forceDisplayAdd: boolean = false;
|
||||
volumes: Volume[] | undefined = [];
|
||||
|
||||
constructor(
|
||||
private state: StateService,
|
||||
private devstate: DevstateService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
const that = this;
|
||||
this.state.state.subscribe(async newContent => {
|
||||
that.volumes = newContent?.volumes;
|
||||
if (this.volumes == null) {
|
||||
return
|
||||
}
|
||||
that.forceDisplayAdd = false;
|
||||
});
|
||||
}
|
||||
|
||||
displayAddForm() {
|
||||
this.forceDisplayAdd = true;
|
||||
setTimeout(() => {
|
||||
this.scrollToBottom();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
undisplayAddForm() {
|
||||
this.forceDisplayAdd = false;
|
||||
}
|
||||
|
||||
delete(name: string) {
|
||||
if(confirm('You will delete the volume "'+name+'". Continue?')) {
|
||||
const result = this.devstate.deleteVolume(name);
|
||||
result.subscribe({
|
||||
next: (value) => {
|
||||
this.state.changeDevfileYaml(value);
|
||||
},
|
||||
error: (error) => {
|
||||
alert(error.error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onCreated(volume: Volume) {
|
||||
const result = this.devstate.addVolume(volume);
|
||||
result.subscribe({
|
||||
next: value => {
|
||||
this.state.changeDevfileYaml(value);
|
||||
},
|
||||
error: error => {
|
||||
alert(error.error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
window.scrollTo(0,document.body.scrollHeight);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user