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:
|
cpuLimit:
|
||||||
description: CPU limit for the deployed container
|
description: CPU limit for the deployed container
|
||||||
type: string
|
type: string
|
||||||
|
volumeMounts:
|
||||||
|
description: Volume to mount into the container filesystem
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/VolumeMount'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: container was successfully added to the devfile
|
description: container was successfully added to the devfile
|
||||||
@@ -813,6 +818,101 @@ paths:
|
|||||||
example:
|
example:
|
||||||
message: "Error deleting the resource"
|
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:
|
/devstate/applyCommand:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
@@ -1315,6 +1415,7 @@ components:
|
|||||||
- containers
|
- containers
|
||||||
- images
|
- images
|
||||||
- resources
|
- resources
|
||||||
|
- volumes
|
||||||
- events
|
- events
|
||||||
- metadata
|
- metadata
|
||||||
properties:
|
properties:
|
||||||
@@ -1336,6 +1437,10 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Resource'
|
$ref: '#/components/schemas/Resource'
|
||||||
|
volumes:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Volume'
|
||||||
events:
|
events:
|
||||||
$ref: '#/components/schemas/Events'
|
$ref: '#/components/schemas/Events'
|
||||||
metadata:
|
metadata:
|
||||||
@@ -1416,6 +1521,7 @@ components:
|
|||||||
- memoryLimit
|
- memoryLimit
|
||||||
- cpuRequest
|
- cpuRequest
|
||||||
- cpuLimit
|
- cpuLimit
|
||||||
|
- volumeMounts
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
@@ -1437,6 +1543,20 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
cpuLimit:
|
cpuLimit:
|
||||||
type: string
|
type: string
|
||||||
|
volumeMounts:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/VolumeMount'
|
||||||
|
VolumeMount:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- path
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
Image:
|
Image:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -1472,6 +1592,17 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
uri:
|
uri:
|
||||||
type: string
|
type: string
|
||||||
|
Volume:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
ephemeral:
|
||||||
|
type: boolean
|
||||||
|
size:
|
||||||
|
type: string
|
||||||
Events:
|
Events:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
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_image_post_request.go
|
||||||
go/model__devstate_quantity_valid_post_request.go
|
go/model__devstate_quantity_valid_post_request.go
|
||||||
go/model__devstate_resource_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__instance_get_200_response.go
|
||||||
go/model_apply_command.go
|
go/model_apply_command.go
|
||||||
go/model_command.go
|
go/model_command.go
|
||||||
@@ -38,3 +39,5 @@ go/model_metadata.go
|
|||||||
go/model_metadata_request.go
|
go/model_metadata_request.go
|
||||||
go/model_resource.go
|
go/model_resource.go
|
||||||
go/model_telemetry_response.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)
|
DevstateQuantityValidPost(http.ResponseWriter, *http.Request)
|
||||||
DevstateResourcePost(http.ResponseWriter, *http.Request)
|
DevstateResourcePost(http.ResponseWriter, *http.Request)
|
||||||
DevstateResourceResourceNameDelete(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
|
// DefaultApiServicer defines the api actions for the DefaultApi service
|
||||||
@@ -92,4 +94,6 @@ type DevstateApiServicer interface {
|
|||||||
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)
|
||||||
|
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}",
|
"/api/v1/devstate/resource/{resourceName}",
|
||||||
c.DevstateResourceResourceNameDelete,
|
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)
|
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
|
// CPU limit for the deployed container
|
||||||
CpuLimit string `json:"cpuLimit,omitempty"`
|
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
|
// AssertDevstateContainerPostRequestRequired checks if the required fields are not zero-ed
|
||||||
func AssertDevstateContainerPostRequestRequired(obj DevstateContainerPostRequest) error {
|
func AssertDevstateContainerPostRequestRequired(obj DevstateContainerPostRequest) error {
|
||||||
|
for _, el := range obj.VolumeMounts {
|
||||||
|
if err := AssertVolumeMountRequired(el); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
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"`
|
CpuRequest string `json:"cpuRequest"`
|
||||||
|
|
||||||
CpuLimit string `json:"cpuLimit"`
|
CpuLimit string `json:"cpuLimit"`
|
||||||
|
|
||||||
|
VolumeMounts []VolumeMount `json:"volumeMounts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssertContainerRequired checks if the required fields are not zero-ed
|
// AssertContainerRequired checks if the required fields are not zero-ed
|
||||||
@@ -38,6 +40,7 @@ func AssertContainerRequired(obj Container) error {
|
|||||||
"memoryLimit": obj.MemoryLimit,
|
"memoryLimit": obj.MemoryLimit,
|
||||||
"cpuRequest": obj.CpuRequest,
|
"cpuRequest": obj.CpuRequest,
|
||||||
"cpuLimit": obj.CpuLimit,
|
"cpuLimit": obj.CpuLimit,
|
||||||
|
"volumeMounts": obj.VolumeMounts,
|
||||||
}
|
}
|
||||||
for name, el := range elements {
|
for name, el := range elements {
|
||||||
if isZero := IsZeroValue(el); isZero {
|
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
|
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"`
|
Resources []Resource `json:"resources"`
|
||||||
|
|
||||||
|
Volumes []Volume `json:"volumes"`
|
||||||
|
|
||||||
Events Events `json:"events"`
|
Events Events `json:"events"`
|
||||||
|
|
||||||
Metadata Metadata `json:"metadata"`
|
Metadata Metadata `json:"metadata"`
|
||||||
@@ -33,6 +35,7 @@ func AssertDevfileContentRequired(obj DevfileContent) error {
|
|||||||
"containers": obj.Containers,
|
"containers": obj.Containers,
|
||||||
"images": obj.Images,
|
"images": obj.Images,
|
||||||
"resources": obj.Resources,
|
"resources": obj.Resources,
|
||||||
|
"volumes": obj.Volumes,
|
||||||
"events": obj.Events,
|
"events": obj.Events,
|
||||||
"metadata": obj.Metadata,
|
"metadata": obj.Metadata,
|
||||||
}
|
}
|
||||||
@@ -62,6 +65,11 @@ func AssertDevfileContentRequired(obj DevfileContent) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, el := range obj.Volumes {
|
||||||
|
if err := AssertVolumeRequired(el); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := AssertEventsRequired(obj.Events); err != nil {
|
if err := AssertEventsRequired(obj.Events); err != nil {
|
||||||
return err
|
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.MemLimit,
|
||||||
container.CpuReq,
|
container.CpuReq,
|
||||||
container.CpuLimit,
|
container.CpuLimit,
|
||||||
|
container.VolumeMounts,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return openapi.Response(http.StatusInternalServerError, openapi.GeneralError{
|
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
|
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) {
|
func (s *DevstateApiService) DevstateApplyCommandPost(ctx context.Context, command openapi.DevstateApplyCommandPostRequest) (openapi.ImplResponse, error) {
|
||||||
newContent, err := s.devfileState.AddApplyCommand(
|
newContent, err := s.devfileState.AddApplyCommand(
|
||||||
command.Name,
|
command.Name,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
|
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
. "github.com/redhat-developer/odo/pkg/apiserver-gen/go"
|
. "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) {
|
func TestDevfileState_AddExecCommand(t *testing.T) {
|
||||||
@@ -36,6 +37,7 @@ func TestDevfileState_AddExecCommand(t *testing.T) {
|
|||||||
"2Gi",
|
"2Gi",
|
||||||
"100m",
|
"100m",
|
||||||
"200m",
|
"200m",
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -96,10 +98,12 @@ schemaVersion: 2.2.0
|
|||||||
MemoryLimit: "2Gi",
|
MemoryLimit: "2Gi",
|
||||||
CpuRequest: "100m",
|
CpuRequest: "100m",
|
||||||
CpuLimit: "200m",
|
CpuLimit: "200m",
|
||||||
|
VolumeMounts: []openapi.VolumeMount{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Images: []Image{},
|
Images: []Image{},
|
||||||
Resources: []Resource{},
|
Resources: []Resource{},
|
||||||
|
Volumes: []Volume{},
|
||||||
Events: Events{},
|
Events: Events{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -186,6 +190,7 @@ schemaVersion: 2.2.0
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Resources: []Resource{},
|
Resources: []Resource{},
|
||||||
|
Volumes: []Volume{},
|
||||||
Events: Events{},
|
Events: Events{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -235,6 +240,7 @@ func TestDevfileState_AddCompositeCommand(t *testing.T) {
|
|||||||
"2Gi",
|
"2Gi",
|
||||||
"100m",
|
"100m",
|
||||||
"200m",
|
"200m",
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -313,10 +319,12 @@ schemaVersion: 2.2.0
|
|||||||
MemoryLimit: "2Gi",
|
MemoryLimit: "2Gi",
|
||||||
CpuRequest: "100m",
|
CpuRequest: "100m",
|
||||||
CpuLimit: "200m",
|
CpuLimit: "200m",
|
||||||
|
VolumeMounts: []openapi.VolumeMount{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Images: []Image{},
|
Images: []Image{},
|
||||||
Resources: []Resource{},
|
Resources: []Resource{},
|
||||||
|
Volumes: []Volume{},
|
||||||
Events: Events{},
|
Events: Events{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -364,6 +372,7 @@ func TestDevfileState_DeleteCommand(t *testing.T) {
|
|||||||
"2Gi",
|
"2Gi",
|
||||||
"100m",
|
"100m",
|
||||||
"200m",
|
"200m",
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -412,10 +421,12 @@ schemaVersion: 2.2.0
|
|||||||
MemoryLimit: "2Gi",
|
MemoryLimit: "2Gi",
|
||||||
CpuRequest: "100m",
|
CpuRequest: "100m",
|
||||||
CpuLimit: "200m",
|
CpuLimit: "200m",
|
||||||
|
VolumeMounts: []openapi.VolumeMount{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Images: []Image{},
|
Images: []Image{},
|
||||||
Resources: []Resource{},
|
Resources: []Resource{},
|
||||||
|
Volumes: []Volume{},
|
||||||
Events: Events{},
|
Events: Events{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -627,6 +638,7 @@ schemaVersion: 2.2.0
|
|||||||
Containers: []Container{},
|
Containers: []Container{},
|
||||||
Images: []Image{},
|
Images: []Image{},
|
||||||
Resources: []Resource{},
|
Resources: []Resource{},
|
||||||
|
Volumes: []Volume{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// TODO: Add test cases.
|
// TODO: Add test cases.
|
||||||
@@ -713,6 +725,7 @@ schemaVersion: 2.2.0
|
|||||||
Containers: []Container{},
|
Containers: []Container{},
|
||||||
Images: []Image{},
|
Images: []Image{},
|
||||||
Resources: []Resource{},
|
Resources: []Resource{},
|
||||||
|
Volumes: []Volume{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// TODO: Add test cases.
|
// TODO: Add test cases.
|
||||||
@@ -801,6 +814,7 @@ schemaVersion: 2.2.0
|
|||||||
Containers: []Container{},
|
Containers: []Container{},
|
||||||
Images: []Image{},
|
Images: []Image{},
|
||||||
Resources: []Resource{},
|
Resources: []Resource{},
|
||||||
|
Volumes: []Volume{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// TODO: Add test cases.
|
// TODO: Add test cases.
|
||||||
|
|||||||
@@ -9,7 +9,24 @@ import (
|
|||||||
. "github.com/redhat-developer/odo/pkg/apiserver-gen/go"
|
. "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{
|
container := v1alpha2.Component{
|
||||||
Name: name,
|
Name: name,
|
||||||
ComponentUnion: v1alpha2.ComponentUnion{
|
ComponentUnion: v1alpha2.ComponentUnion{
|
||||||
@@ -22,6 +39,7 @@ func (o *DevfileState) AddContainer(name string, image string, command []string,
|
|||||||
MemoryLimit: memLimit,
|
MemoryLimit: memLimit,
|
||||||
CpuRequest: cpuRequest,
|
CpuRequest: cpuRequest,
|
||||||
CpuLimit: cpuLimit,
|
CpuLimit: cpuLimit,
|
||||||
|
VolumeMounts: v1alpha2VolumeMounts,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -184,3 +202,56 @@ func (o *DevfileState) checkResourceUsed(name string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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,18 +5,20 @@ import (
|
|||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
. "github.com/redhat-developer/odo/pkg/apiserver-gen/go"
|
. "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) {
|
func TestDevfileState_AddContainer(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
name string
|
name string
|
||||||
image string
|
image string
|
||||||
command []string
|
command []string
|
||||||
args []string
|
args []string
|
||||||
memRequest string
|
memRequest string
|
||||||
memLimit string
|
memLimit string
|
||||||
cpuRequest string
|
cpuRequest string
|
||||||
cpuLimit string
|
cpuLimit string
|
||||||
|
volumeMounts []openapi.VolumeMount
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -39,6 +41,12 @@ func TestDevfileState_AddContainer(t *testing.T) {
|
|||||||
memLimit: "2Gi",
|
memLimit: "2Gi",
|
||||||
cpuRequest: "100m",
|
cpuRequest: "100m",
|
||||||
cpuLimit: "200m",
|
cpuLimit: "200m",
|
||||||
|
volumeMounts: []openapi.VolumeMount{
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
Path: "/mnt/volume1",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
want: DevfileContent{
|
want: DevfileContent{
|
||||||
Content: `components:
|
Content: `components:
|
||||||
@@ -54,6 +62,9 @@ func TestDevfileState_AddContainer(t *testing.T) {
|
|||||||
image: an-image
|
image: an-image
|
||||||
memoryLimit: 2Gi
|
memoryLimit: 2Gi
|
||||||
memoryRequest: 1Gi
|
memoryRequest: 1Gi
|
||||||
|
volumeMounts:
|
||||||
|
- name: vol1
|
||||||
|
path: /mnt/volume1
|
||||||
name: a-name
|
name: a-name
|
||||||
metadata: {}
|
metadata: {}
|
||||||
schemaVersion: 2.2.0
|
schemaVersion: 2.2.0
|
||||||
@@ -69,10 +80,17 @@ schemaVersion: 2.2.0
|
|||||||
MemoryLimit: "2Gi",
|
MemoryLimit: "2Gi",
|
||||||
CpuRequest: "100m",
|
CpuRequest: "100m",
|
||||||
CpuLimit: "200m",
|
CpuLimit: "200m",
|
||||||
|
VolumeMounts: []openapi.VolumeMount{
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
Path: "/mnt/volume1",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Images: []Image{},
|
Images: []Image{},
|
||||||
Resources: []Resource{},
|
Resources: []Resource{},
|
||||||
|
Volumes: []Volume{},
|
||||||
Events: Events{},
|
Events: Events{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -81,7 +99,7 @@ schemaVersion: 2.2.0
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
o := tt.state()
|
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 {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("DevfileState.AddContainer() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("DevfileState.AddContainer() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
@@ -120,6 +138,7 @@ func TestDevfileState_DeleteContainer(t *testing.T) {
|
|||||||
"2Gi",
|
"2Gi",
|
||||||
"100m",
|
"100m",
|
||||||
"200m",
|
"200m",
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -137,6 +156,7 @@ schemaVersion: 2.2.0
|
|||||||
Containers: []Container{},
|
Containers: []Container{},
|
||||||
Images: []Image{},
|
Images: []Image{},
|
||||||
Resources: []Resource{},
|
Resources: []Resource{},
|
||||||
|
Volumes: []Volume{},
|
||||||
Events: Events{},
|
Events: Events{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -153,6 +173,7 @@ schemaVersion: 2.2.0
|
|||||||
"2Gi",
|
"2Gi",
|
||||||
"100m",
|
"100m",
|
||||||
"200m",
|
"200m",
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -242,6 +263,7 @@ schemaVersion: 2.2.0
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Resources: []Resource{},
|
Resources: []Resource{},
|
||||||
|
Volumes: []Volume{},
|
||||||
Events: Events{},
|
Events: Events{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -304,6 +326,7 @@ schemaVersion: 2.2.0
|
|||||||
Containers: []Container{},
|
Containers: []Container{},
|
||||||
Images: []Image{},
|
Images: []Image{},
|
||||||
Resources: []Resource{},
|
Resources: []Resource{},
|
||||||
|
Volumes: []Volume{},
|
||||||
Events: Events{},
|
Events: Events{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -389,7 +412,8 @@ schemaVersion: 2.2.0
|
|||||||
Uri: "an-uri",
|
Uri: "an-uri",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Events: Events{},
|
Volumes: []Volume{},
|
||||||
|
Events: Events{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -418,7 +442,8 @@ schemaVersion: 2.2.0
|
|||||||
Inlined: "inline resource...",
|
Inlined: "inline resource...",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Events: Events{},
|
Volumes: []Volume{},
|
||||||
|
Events: Events{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// TODO: Add test cases.
|
// TODO: Add test cases.
|
||||||
@@ -477,6 +502,7 @@ schemaVersion: 2.2.0
|
|||||||
Containers: []Container{},
|
Containers: []Container{},
|
||||||
Images: []Image{},
|
Images: []Image{},
|
||||||
Resources: []Resource{},
|
Resources: []Resource{},
|
||||||
|
Volumes: []Volume{},
|
||||||
Events: Events{},
|
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 = ","
|
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
|
// getContent returns the YAML content of the global devfile as string
|
||||||
func (o *DevfileState) GetContent() (DevfileContent, error) {
|
func (o *DevfileState) GetContent() (DevfileContent, error) {
|
||||||
err := o.Devfile.WriteYamlDevfile()
|
err := o.Devfile.WriteYamlDevfile()
|
||||||
@@ -137,12 +46,18 @@ func (o *DevfileState) GetContent() (DevfileContent, error) {
|
|||||||
return DevfileContent{}, errors.New("error getting Kubernetes resources")
|
return DevfileContent{}, errors.New("error getting Kubernetes resources")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
volumes, err := o.getVolumes()
|
||||||
|
if err != nil {
|
||||||
|
return DevfileContent{}, errors.New("error getting volumes")
|
||||||
|
}
|
||||||
|
|
||||||
return DevfileContent{
|
return DevfileContent{
|
||||||
Content: string(result),
|
Content: string(result),
|
||||||
Commands: commands,
|
Commands: commands,
|
||||||
Containers: containers,
|
Containers: containers,
|
||||||
Images: images,
|
Images: images,
|
||||||
Resources: resources,
|
Resources: resources,
|
||||||
|
Volumes: volumes,
|
||||||
Events: o.getEvents(),
|
Events: o.getEvents(),
|
||||||
Metadata: o.getMetadata(),
|
Metadata: o.getMetadata(),
|
||||||
}, nil
|
}, nil
|
||||||
@@ -255,11 +170,23 @@ func (o *DevfileState) getContainers() ([]Container, error) {
|
|||||||
MemoryLimit: container.ComponentUnion.Container.MemoryLimit,
|
MemoryLimit: container.ComponentUnion.Container.MemoryLimit,
|
||||||
CpuRequest: container.ComponentUnion.Container.CpuRequest,
|
CpuRequest: container.ComponentUnion.Container.CpuRequest,
|
||||||
CpuLimit: container.ComponentUnion.Container.CpuLimit,
|
CpuLimit: container.ComponentUnion.Container.CpuLimit,
|
||||||
|
VolumeMounts: o.getVolumeMounts(container.Container.Container),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result, nil
|
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) {
|
func (o *DevfileState) getImages() ([]Image, error) {
|
||||||
images, err := o.Devfile.Data.GetComponents(common.DevfileOptions{
|
images, err := o.Devfile.Data.GetComponents(common.DevfileOptions{
|
||||||
ComponentOptions: common.ComponentOptions{
|
ComponentOptions: common.ComponentOptions{
|
||||||
@@ -303,6 +230,26 @@ func (o *DevfileState) getResources() ([]Resource, error) {
|
|||||||
return result, nil
|
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 {
|
func (o *DevfileState) getEvents() Events {
|
||||||
events := o.Devfile.Data.GetEvents()
|
events := o.Devfile.Data.GetEvents()
|
||||||
return Events{
|
return Events{
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ func TestDevfileState_GetContent(t *testing.T) {
|
|||||||
Containers: []Container{},
|
Containers: []Container{},
|
||||||
Images: []Image{},
|
Images: []Image{},
|
||||||
Resources: []Resource{},
|
Resources: []Resource{},
|
||||||
|
Volumes: []Volume{},
|
||||||
Events: Events{},
|
Events: Events{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ schemaVersion: 2.2.0
|
|||||||
Containers: []Container{},
|
Containers: []Container{},
|
||||||
Images: []Image{},
|
Images: []Image{},
|
||||||
Resources: []Resource{},
|
Resources: []Resource{},
|
||||||
|
Volumes: []Volume{},
|
||||||
Events: Events{
|
Events: Events{
|
||||||
PreStart: []string{"command1"},
|
PreStart: []string{"command1"},
|
||||||
},
|
},
|
||||||
@@ -70,6 +71,7 @@ schemaVersion: 2.2.0
|
|||||||
Containers: []Container{},
|
Containers: []Container{},
|
||||||
Images: []Image{},
|
Images: []Image{},
|
||||||
Resources: []Resource{},
|
Resources: []Resource{},
|
||||||
|
Volumes: []Volume{},
|
||||||
Events: Events{
|
Events: Events{
|
||||||
PreStart: []string{"command1"},
|
PreStart: []string{"command1"},
|
||||||
PostStart: []string{"command2"},
|
PostStart: []string{"command2"},
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ schemaVersion: 2.2.0
|
|||||||
Containers: []Container{},
|
Containers: []Container{},
|
||||||
Images: []Image{},
|
Images: []Image{},
|
||||||
Resources: []Resource{},
|
Resources: []Resource{},
|
||||||
|
Volumes: []Volume{},
|
||||||
Metadata: Metadata{
|
Metadata: Metadata{
|
||||||
Name: "a-name",
|
Name: "a-name",
|
||||||
Version: "v1.1.1",
|
Version: "v1.1.1",
|
||||||
|
|||||||
@@ -543,6 +543,11 @@ paths:
|
|||||||
cpuLimit:
|
cpuLimit:
|
||||||
description: CPU limit for the deployed container
|
description: CPU limit for the deployed container
|
||||||
type: string
|
type: string
|
||||||
|
volumeMounts:
|
||||||
|
description: Volume to mount into the container filesystem
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/VolumeMount'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: container was successfully added to the devfile
|
description: container was successfully added to the devfile
|
||||||
@@ -813,6 +818,101 @@ paths:
|
|||||||
example:
|
example:
|
||||||
message: "Error deleting the resource"
|
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:
|
/devstate/applyCommand:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
@@ -1315,6 +1415,7 @@ components:
|
|||||||
- containers
|
- containers
|
||||||
- images
|
- images
|
||||||
- resources
|
- resources
|
||||||
|
- volumes
|
||||||
- events
|
- events
|
||||||
- metadata
|
- metadata
|
||||||
properties:
|
properties:
|
||||||
@@ -1336,6 +1437,10 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Resource'
|
$ref: '#/components/schemas/Resource'
|
||||||
|
volumes:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Volume'
|
||||||
events:
|
events:
|
||||||
$ref: '#/components/schemas/Events'
|
$ref: '#/components/schemas/Events'
|
||||||
metadata:
|
metadata:
|
||||||
@@ -1416,6 +1521,7 @@ components:
|
|||||||
- memoryLimit
|
- memoryLimit
|
||||||
- cpuRequest
|
- cpuRequest
|
||||||
- cpuLimit
|
- cpuLimit
|
||||||
|
- volumeMounts
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
@@ -1437,6 +1543,20 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
cpuLimit:
|
cpuLimit:
|
||||||
type: string
|
type: string
|
||||||
|
volumeMounts:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/VolumeMount'
|
||||||
|
VolumeMount:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- path
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
Image:
|
Image:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -1472,6 +1592,17 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
uri:
|
uri:
|
||||||
type: string
|
type: string
|
||||||
|
Volume:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
ephemeral:
|
||||||
|
type: boolean
|
||||||
|
size:
|
||||||
|
type: string
|
||||||
Events:
|
Events:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
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">
|
<body class="mat-typography">
|
||||||
<div id="loading">Loading, please wait...</div>
|
<div id="loading">Loading, please wait...</div>
|
||||||
<app-root></app-root>
|
<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>
|
</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', () => {
|
describe('devfile editor spec', () => {
|
||||||
|
|
||||||
@@ -42,14 +42,39 @@ describe('devfile editor spec', () => {
|
|||||||
it('displays a created container', () => {
|
it('displays a created container', () => {
|
||||||
cy.init();
|
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.selectTab(TAB_CONTAINERS);
|
||||||
cy.getByDataCy('container-name').type('created-container');
|
cy.getByDataCy('container-name').type('created-container');
|
||||||
cy.getByDataCy('container-image').type('an-image');
|
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-create').click();
|
||||||
|
|
||||||
cy.getByDataCy('container-info').first()
|
cy.getByDataCy('container-info').first()
|
||||||
.should('contain.text', 'created-container')
|
.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', () => {
|
it('displays a created image', () => {
|
||||||
@@ -97,9 +122,30 @@ describe('devfile editor spec', () => {
|
|||||||
.should('contain.text', '/my/manifest.yaml');
|
.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', () => {
|
it('creates an exec command with a new container', () => {
|
||||||
cy.init();
|
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.selectTab(TAB_COMMANDS);
|
||||||
cy.getByDataCy('add').click();
|
cy.getByDataCy('add').click();
|
||||||
cy.getByDataCy('new-command-exec').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('select-container').click().get('mat-option').contains('(New Container)').click();
|
||||||
cy.getByDataCy('container-name').type('a-created-container');
|
cy.getByDataCy('container-name').type('a-created-container');
|
||||||
cy.getByDataCy('container-image').type('an-image');
|
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-create').click();
|
||||||
|
|
||||||
cy.getByDataCy('select-container').should('contain', 'a-created-container');
|
cy.getByDataCy('select-container').should('contain', 'a-created-container');
|
||||||
@@ -124,7 +181,15 @@ describe('devfile editor spec', () => {
|
|||||||
cy.selectTab(TAB_CONTAINERS);
|
cy.selectTab(TAB_CONTAINERS);
|
||||||
cy.getByDataCy('container-info').first()
|
cy.getByDataCy('container-info').first()
|
||||||
.should('contain.text', 'a-created-container')
|
.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', () => {
|
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/devstateImagePostRequest.ts
|
||||||
model/devstateQuantityValidPostRequest.ts
|
model/devstateQuantityValidPostRequest.ts
|
||||||
model/devstateResourcePostRequest.ts
|
model/devstateResourcePostRequest.ts
|
||||||
|
model/devstateVolumePostRequest.ts
|
||||||
model/events.ts
|
model/events.ts
|
||||||
model/execCommand.ts
|
model/execCommand.ts
|
||||||
model/generalError.ts
|
model/generalError.ts
|
||||||
@@ -41,5 +42,7 @@ model/metadataRequest.ts
|
|||||||
model/models.ts
|
model/models.ts
|
||||||
model/resource.ts
|
model/resource.ts
|
||||||
model/telemetryResponse.ts
|
model/telemetryResponse.ts
|
||||||
|
model/volume.ts
|
||||||
|
model/volumeMount.ts
|
||||||
param.ts
|
param.ts
|
||||||
variables.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
|
// @ts-ignore
|
||||||
import { DevstateResourcePostRequest } from '../model/devstateResourcePostRequest';
|
import { DevstateResourcePostRequest } from '../model/devstateResourcePostRequest';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
import { DevstateVolumePostRequest } from '../model/devstateVolumePostRequest';
|
||||||
|
// @ts-ignore
|
||||||
import { GeneralError } from '../model/generalError';
|
import { GeneralError } from '../model/generalError';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { GeneralSuccess } from '../model/generalSuccess';
|
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
|
* https://openapi-generator.tech
|
||||||
* Do not edit the class manually.
|
* Do not edit the class manually.
|
||||||
*/
|
*/
|
||||||
|
import { VolumeMount } from './volumeMount';
|
||||||
|
|
||||||
|
|
||||||
export interface Container {
|
export interface Container {
|
||||||
@@ -20,5 +21,6 @@ export interface Container {
|
|||||||
memoryLimit: string;
|
memoryLimit: string;
|
||||||
cpuRequest: string;
|
cpuRequest: string;
|
||||||
cpuLimit: 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 { Container } from './container';
|
||||||
import { Command } from './command';
|
import { Command } from './command';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
|
import { Volume } from './volume';
|
||||||
import { Metadata } from './metadata';
|
import { Metadata } from './metadata';
|
||||||
import { Resource } from './resource';
|
import { Resource } from './resource';
|
||||||
import { Image } from './image';
|
import { Image } from './image';
|
||||||
@@ -23,6 +24,7 @@ export interface DevfileContent {
|
|||||||
containers: Array<Container>;
|
containers: Array<Container>;
|
||||||
images: Array<Image>;
|
images: Array<Image>;
|
||||||
resources: Array<Resource>;
|
resources: Array<Resource>;
|
||||||
|
volumes: Array<Volume>;
|
||||||
events: Events;
|
events: Events;
|
||||||
metadata: Metadata;
|
metadata: Metadata;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
* https://openapi-generator.tech
|
* https://openapi-generator.tech
|
||||||
* Do not edit the class manually.
|
* Do not edit the class manually.
|
||||||
*/
|
*/
|
||||||
|
import { VolumeMount } from './volumeMount';
|
||||||
|
|
||||||
|
|
||||||
export interface DevstateContainerPostRequest {
|
export interface DevstateContainerPostRequest {
|
||||||
@@ -44,5 +45,9 @@ export interface DevstateContainerPostRequest {
|
|||||||
* CPU limit for the deployed container
|
* CPU limit for the deployed container
|
||||||
*/
|
*/
|
||||||
cpuLimit?: string;
|
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 './devstateImagePostRequest';
|
||||||
export * from './devstateQuantityValidPostRequest';
|
export * from './devstateQuantityValidPostRequest';
|
||||||
export * from './devstateResourcePostRequest';
|
export * from './devstateResourcePostRequest';
|
||||||
|
export * from './devstateVolumePostRequest';
|
||||||
export * from './events';
|
export * from './events';
|
||||||
export * from './execCommand';
|
export * from './execCommand';
|
||||||
export * from './generalError';
|
export * from './generalError';
|
||||||
@@ -30,3 +31,5 @@ export * from './metadata';
|
|||||||
export * from './metadataRequest';
|
export * from './metadataRequest';
|
||||||
export * from './resource';
|
export * from './resource';
|
||||||
export * from './telemetryResponse';
|
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>
|
<mat-icon class="tab-icon material-icons-outlined">storage</mat-icon>
|
||||||
{{tabNames[8]}}
|
{{tabNames[8]}}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
<app-volumes></app-volumes>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
|
|
||||||
</mat-tab-group>
|
</mat-tab-group>
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ import { MultiCommandComponent } from './controls/multi-command/multi-command.co
|
|||||||
import { EventsComponent } from './tabs/events/events.component';
|
import { EventsComponent } from './tabs/events/events.component';
|
||||||
import { ChipsEventsComponent } from './controls/chips-events/chips-events.component';
|
import { ChipsEventsComponent } from './controls/chips-events/chips-events.component';
|
||||||
import { ConfirmComponent } from './components/confirm/confirm.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({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -68,6 +71,9 @@ import { ConfirmComponent } from './components/confirm/confirm.component';
|
|||||||
EventsComponent,
|
EventsComponent,
|
||||||
ChipsEventsComponent,
|
ChipsEventsComponent,
|
||||||
ConfirmComponent,
|
ConfirmComponent,
|
||||||
|
VolumesComponent,
|
||||||
|
VolumeComponent,
|
||||||
|
VolumeMountsComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<h3>{{title}}</h3>
|
<h3 *ngIf="title">{{title}}</h3>
|
||||||
<div class="group">
|
<div class="group">
|
||||||
<span *ngFor="let text of texts; let i=index">
|
<span *ngFor="let text of texts; let i=index">
|
||||||
<mat-form-field class="inline" appearance="outline">
|
<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
|
<app-container
|
||||||
*ngIf="showNewContainer"
|
*ngIf="showNewContainer"
|
||||||
|
[volumeNames]="volumeNames ?? []"
|
||||||
(created)="onNewContainerCreated($event)"
|
(created)="onNewContainerCreated($event)"
|
||||||
></app-container>
|
></app-container>
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
|
|||||||
import { StateService } from 'src/app/services/state.service';
|
import { StateService } from 'src/app/services/state.service';
|
||||||
import { DevstateService } from 'src/app/services/devstate.service';
|
import { DevstateService } from 'src/app/services/devstate.service';
|
||||||
import { PATTERN_COMMAND_ID } from '../patterns';
|
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 { TelemetryService } from 'src/app/services/telemetry.service';
|
||||||
|
import { ToCreate } from '../container/container.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-command-exec',
|
selector: 'app-command-exec',
|
||||||
@@ -18,6 +19,8 @@ export class CommandExecComponent {
|
|||||||
containerList: string[] = [];
|
containerList: string[] = [];
|
||||||
showNewContainer: boolean = false;
|
showNewContainer: boolean = false;
|
||||||
containerToCreate: Container | null = null;
|
containerToCreate: Container | null = null;
|
||||||
|
volumesToCreate: Volume[] = [];
|
||||||
|
volumeNames: string[] | undefined = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private devstate: DevstateService,
|
private devstate: DevstateService,
|
||||||
@@ -33,6 +36,7 @@ export class CommandExecComponent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.state.state.subscribe(async newContent => {
|
this.state.state.subscribe(async newContent => {
|
||||||
|
this.volumeNames = newContent?.volumes.map((v: Volume) => v.name);
|
||||||
const containers = newContent?.containers;
|
const containers = newContent?.containers;
|
||||||
if (containers == null) {
|
if (containers == null) {
|
||||||
return
|
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() {
|
create() {
|
||||||
this.telemetry.track("[ui] create exec command");
|
this.telemetry.track("[ui] create exec command");
|
||||||
@@ -56,7 +75,8 @@ export class CommandExecComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.containerToCreate != null &&
|
this.createVolumes(this.volumesToCreate, 0, () => {
|
||||||
|
if (this.containerToCreate != null &&
|
||||||
this.containerToCreate?.name == this.form.controls["component"].value) {
|
this.containerToCreate?.name == this.form.controls["component"].value) {
|
||||||
const res = this.devstate.addContainer(this.containerToCreate);
|
const res = this.devstate.addContainer(this.containerToCreate);
|
||||||
res.subscribe({
|
res.subscribe({
|
||||||
@@ -67,9 +87,10 @@ export class CommandExecComponent {
|
|||||||
alert(error.error.message);
|
alert(error.error.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
subcreate();
|
subcreate();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
@@ -84,10 +105,12 @@ export class CommandExecComponent {
|
|||||||
this.showNewContainer = v;
|
this.showNewContainer = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewContainerCreated(container: Container) {
|
onNewContainerCreated(toCreate: ToCreate) {
|
||||||
|
const container = toCreate.container;
|
||||||
this.containerList.push(container.name);
|
this.containerList.push(container.name);
|
||||||
this.form.controls["component"].setValue(container.name);
|
this.form.controls["component"].setValue(container.name);
|
||||||
this.showNewContainer = false;
|
this.showNewContainer = false;
|
||||||
this.containerToCreate = container;
|
this.containerToCreate = container;
|
||||||
|
this.volumesToCreate.push(...toCreate.volumes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,15 @@
|
|||||||
<mat-label><span>Image</span></mat-label>
|
<mat-label><span>Image</span></mat-label>
|
||||||
<input placeholder="Image to start the container" data-cy="container-image" matInput formControlName="image">
|
<input placeholder="Image to start the container" data-cy="container-image" matInput formControlName="image">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<app-multi-text formControlName="command" title="Command" label="Command" addLabel="Add command"></app-multi-text>
|
<h3>Command and Arguments</h3>
|
||||||
<app-multi-text formControlName="args" title="Arguments to command" label="Arg" addLabel="Add arg"></app-multi-text>
|
<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-form-field appearance="outline" class="mid-width">
|
||||||
<mat-label><span>Memory Request</span></mat-label>
|
<mat-label><span>Memory Request</span></mat-label>
|
||||||
<mat-error>{{quantityErrMsgMemory}}</mat-error>
|
<mat-error>{{quantityErrMsgMemory}}</mat-error>
|
||||||
|
|||||||
@@ -1,26 +1,33 @@
|
|||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
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 { PATTERN_COMPONENT_ID } from '../patterns';
|
||||||
import { DevstateService } from 'src/app/services/devstate.service';
|
import { DevstateService } from 'src/app/services/devstate.service';
|
||||||
import { Observable, of, map, catchError } from 'rxjs';
|
import { Container, Volume } from 'src/app/api-gen';
|
||||||
import { Container } from 'src/app/api-gen';
|
|
||||||
import { TelemetryService } from 'src/app/services/telemetry.service';
|
import { TelemetryService } from 'src/app/services/telemetry.service';
|
||||||
|
|
||||||
|
export interface ToCreate {
|
||||||
|
container: Container;
|
||||||
|
volumes: Volume[];
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-container',
|
selector: 'app-container',
|
||||||
templateUrl: './container.component.html',
|
templateUrl: './container.component.html',
|
||||||
styleUrls: ['./container.component.css']
|
styleUrls: ['./container.component.css']
|
||||||
})
|
})
|
||||||
export class ContainerComponent {
|
export class ContainerComponent {
|
||||||
|
@Input() volumeNames: string[] = [];
|
||||||
@Input() cancelable: boolean = false;
|
@Input() cancelable: boolean = false;
|
||||||
@Output() canceled = new EventEmitter<void>();
|
@Output() canceled = new EventEmitter<void>();
|
||||||
@Output() created = new EventEmitter<Container>();
|
@Output() created = new EventEmitter<ToCreate>();
|
||||||
|
|
||||||
form: FormGroup;
|
form: FormGroup;
|
||||||
|
|
||||||
quantityErrMsgMemory = 'Numeric value, with optional unit Ki, Mi, Gi, Ti, Pi, Ei';
|
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';
|
quantityErrMsgCPU = 'Numeric value, with optional unit m, k, M, G, T, P, E';
|
||||||
|
|
||||||
|
volumesToCreate: Volume[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private devstate: DevstateService,
|
private devstate: DevstateService,
|
||||||
private telemetry: TelemetryService
|
private telemetry: TelemetryService
|
||||||
@@ -30,33 +37,27 @@ export class ContainerComponent {
|
|||||||
image: new FormControl("", [Validators.required]),
|
image: new FormControl("", [Validators.required]),
|
||||||
command: new FormControl([]),
|
command: new FormControl([]),
|
||||||
args: new FormControl([]),
|
args: new FormControl([]),
|
||||||
memoryRequest: new FormControl("", null, [this.isQuantity()]),
|
memoryRequest: new FormControl("", null, [this.devstate.isQuantity()]),
|
||||||
memoryLimit: new FormControl("", null, [this.isQuantity()]),
|
memoryLimit: new FormControl("", null, [this.devstate.isQuantity()]),
|
||||||
cpuRequest: new FormControl("", null, [this.isQuantity()]),
|
cpuRequest: new FormControl("", null, [this.devstate.isQuantity()]),
|
||||||
cpuLimit: new FormControl("", null, [this.isQuantity()]),
|
cpuLimit: new FormControl("", null, [this.devstate.isQuantity()]),
|
||||||
|
volumeMounts: new FormControl([]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
this.telemetry.track("[ui] create container");
|
this.telemetry.track("[ui] create container");
|
||||||
this.created.emit(this.form.value);
|
this.created.emit({
|
||||||
|
container: this.form.value,
|
||||||
|
volumes: this.volumesToCreate,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.canceled.emit();
|
this.canceled.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
isQuantity(): AsyncValidatorFn {
|
onCreateNewVolume(v: Volume) {
|
||||||
return (control: AbstractControl): Observable<ValidationErrors | null> => {
|
this.volumesToCreate.push(v);
|
||||||
const val = control.value;
|
}
|
||||||
if (val == '') {
|
|
||||||
return of(null);
|
|
||||||
}
|
|
||||||
const valid = this.devstate.isQuantityValid(val);
|
|
||||||
return valid.pipe(
|
|
||||||
map(() => null),
|
|
||||||
catchError(() => of({"isQuantity": false}))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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 { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, catchError, map, of } from 'rxjs';
|
||||||
import { ApplyCommand, CompositeCommand, Container, DevfileContent, DevstateChartGet200Response, ExecCommand, Image, Metadata, Resource } from '../api-gen';
|
import { ApplyCommand, CompositeCommand, Container, DevfileContent, DevstateChartGet200Response, ExecCommand, Image, Metadata, Resource, Volume } from '../api-gen';
|
||||||
|
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -22,6 +23,7 @@ export class DevstateService {
|
|||||||
memLimit: container.memoryLimit,
|
memLimit: container.memoryLimit,
|
||||||
cpuReq: container.cpuRequest,
|
cpuReq: container.cpuRequest,
|
||||||
cpuLimit: container.cpuLimit,
|
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> {
|
addExecCommand(name: string, cmd: ExecCommand): Observable<DevfileContent> {
|
||||||
return this.http.post<DevfileContent>(this.base+"/execCommand", {
|
return this.http.post<DevfileContent>(this.base+"/execCommand", {
|
||||||
name: name,
|
name: name,
|
||||||
@@ -145,6 +155,10 @@ export class DevstateService {
|
|||||||
return this.http.delete<DevfileContent>(this.base+"/resource/"+resource);
|
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> {
|
updateEvents(event: "preStart"|"postStart"|"preStop"|"postStop", commands: string[]): Observable<DevfileContent> {
|
||||||
return this.http.put<DevfileContent>(this.base+"/events", {
|
return this.http.put<DevfileContent>(this.base+"/events", {
|
||||||
eventName: event,
|
eventName: event,
|
||||||
@@ -157,4 +171,18 @@ export class DevstateService {
|
|||||||
quantity: quantity
|
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; }
|
.main { padding: 16px; }
|
||||||
mat-card { margin-bottom: 16px; }
|
mat-card { margin-bottom: 16px; }
|
||||||
mat-card-content { padding: 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>Args:</td>
|
||||||
<td><code>{{container.args.join(" ")}}</code></td>
|
<td><code>{{container.args.join(" ")}}</code></td>
|
||||||
</tr>
|
</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">
|
<tr *ngIf="container.memoryRequest != null && container.memoryRequest.length > 0">
|
||||||
<td>Memory Request:</td>
|
<td>Memory Request:</td>
|
||||||
<td><code>{{container.memoryRequest}}</code></td>
|
<td><code>{{container.memoryRequest}}</code></td>
|
||||||
@@ -46,6 +56,7 @@
|
|||||||
|
|
||||||
<app-container
|
<app-container
|
||||||
*ngIf="forceDisplayAdd || containers == undefined || containers.length == 0"
|
*ngIf="forceDisplayAdd || containers == undefined || containers.length == 0"
|
||||||
|
[volumeNames]="volumeNames ?? []"
|
||||||
[cancelable]="forceDisplayAdd"
|
[cancelable]="forceDisplayAdd"
|
||||||
(canceled)="undisplayAddForm()"
|
(canceled)="undisplayAddForm()"
|
||||||
(created)="onCreated($event)"
|
(created)="onCreated($event)"
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { StateService } from 'src/app/services/state.service';
|
import { StateService } from 'src/app/services/state.service';
|
||||||
import { DevstateService } from 'src/app/services/devstate.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({
|
@Component({
|
||||||
selector: 'app-containers',
|
selector: 'app-containers',
|
||||||
@@ -12,6 +13,7 @@ export class ContainersComponent implements OnInit {
|
|||||||
|
|
||||||
forceDisplayAdd: boolean = false;
|
forceDisplayAdd: boolean = false;
|
||||||
containers: Container[] | undefined = [];
|
containers: Container[] | undefined = [];
|
||||||
|
volumeNames: string[] | undefined = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private state: StateService,
|
private state: StateService,
|
||||||
@@ -21,6 +23,7 @@ export class ContainersComponent implements OnInit {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
const that = this;
|
const that = this;
|
||||||
this.state.state.subscribe(async newContent => {
|
this.state.state.subscribe(async newContent => {
|
||||||
|
this.volumeNames = newContent?.volumes.map((v: Volume) => v.name);
|
||||||
that.containers = newContent?.containers;
|
that.containers = newContent?.containers;
|
||||||
if (this.containers == null) {
|
if (this.containers == null) {
|
||||||
return
|
return
|
||||||
@@ -54,16 +57,36 @@ export class ContainersComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onCreated(container: Container) {
|
createVolumes(volumes: Volume[], i: number, next: () => any) {
|
||||||
const result = this.devstate.addContainer(container);
|
if (volumes.length == i) {
|
||||||
result.subscribe({
|
next();
|
||||||
next: value => {
|
return;
|
||||||
this.state.changeDevfileYaml(value);
|
}
|
||||||
},
|
const res = this.devstate.addVolume(volumes[i]);
|
||||||
error: error => {
|
res.subscribe({
|
||||||
alert(error.error.message);
|
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 => {
|
||||||
|
this.state.changeDevfileYaml(value);
|
||||||
|
},
|
||||||
|
error: error => {
|
||||||
|
alert(error.error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToBottom() {
|
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