[ui] Complete container creation (#7035)

* API returns more info about container

* Display more info about containers

* Update UI static files

* Fix unit tests

* Get/Set sources configuration

* [ui] create container with sources mount configuration

* e2e tests + ui static files

* Set containers's envvars

* Regenerate UI static files

* Add Annotation to POST /container

* [api] Create Container with Annotations

* [ui] Annotations when creating container

* Regenerate UI static files

* [api] Endpoints when adding container

* [ui] Endpoints when adding container

* Regenerate UI static files
This commit is contained in:
Philippe Martin
2023-08-29 09:28:03 +02:00
committed by GitHub
parent a9492307d7
commit e59cfa8852
44 changed files with 1367 additions and 95 deletions

View File

@@ -512,6 +512,9 @@ paths:
application/json:
schema:
type: object
required:
- name
- image
properties:
name:
description: Name of the container
@@ -531,6 +534,11 @@ paths:
items: {
type: string
}
env:
description: Environment variables to define
type: array
items:
$ref: '#/components/schemas/Env'
memReq:
description: Requested memory for the deployed container
type: string
@@ -548,6 +556,24 @@ paths:
type: array
items:
$ref: '#/components/schemas/VolumeMount'
configureSources:
description: If false, mountSources and sourceMapping values are not considered
type: boolean
mountSources:
description: If true, sources are mounted into container's filesystem
type: boolean
sourceMapping:
description: Specific directory on which to mount sources
type: string
annotation:
description: Annotations added to the resources created for this container
$ref: '#/components/schemas/Annotation'
endpoints:
description: Endpoints exposed by the container
type: array
items:
$ref: '#/components/schemas/Endpoint'
responses:
'200':
description: container was successfully added to the devfile
@@ -1522,6 +1548,12 @@ components:
- cpuRequest
- cpuLimit
- volumeMounts
- annotation
- endpoints
- env
- configureSources
- mountSources
- sourceMapping
properties:
name:
type: string
@@ -1547,6 +1579,22 @@ components:
type: array
items:
$ref: '#/components/schemas/VolumeMount'
annotation:
$ref: '#/components/schemas/Annotation'
endpoints:
type: array
items:
$ref: '#/components/schemas/Endpoint'
env:
type: array
items:
$ref: '#/components/schemas/Env'
configureSources:
type: boolean
mountSources:
type: boolean
sourceMapping:
type: string
VolumeMount:
type: object
required:
@@ -1557,6 +1605,50 @@ components:
type: string
path:
type: string
Annotation:
type: object
required:
- deployment
- service
properties:
deployment:
type: object
additionalProperties:
type: string
service:
type: object
additionalProperties:
type: string
Endpoint:
type: object
required:
- name
- targetPort
properties:
name:
type: string
exposure:
type: string
enum: [public,internal,none]
path:
type: string
protocol:
type: string
enum: [http,https,ws,wss,tcp,udp]
secure:
type: boolean
targetPort:
type: integer
Env:
type: object
required:
- name
- value
properties:
name:
type: string
value:
type: string
Image:
type: object
required:

View File

@@ -22,6 +22,7 @@ go/model__devstate_quantity_valid_post_request.go
go/model__devstate_resource_post_request.go
go/model__devstate_volume_post_request.go
go/model__instance_get_200_response.go
go/model_annotation.go
go/model_apply_command.go
go/model_command.go
go/model_composite_command.go
@@ -29,6 +30,8 @@ go/model_container.go
go/model_devfile_content.go
go/model_devfile_put_request.go
go/model_devstate_devfile_put_request.go
go/model_endpoint.go
go/model_env.go
go/model_events.go
go/model_exec_command.go
go/model_general_error.go

View File

@@ -12,10 +12,10 @@ package openapi
type DevstateContainerPostRequest struct {
// Name of the container
Name string `json:"name,omitempty"`
Name string `json:"name"`
// Container image
Image string `json:"image,omitempty"`
Image string `json:"image"`
// Entrypoint of the container
Command []string `json:"command,omitempty"`
@@ -23,6 +23,9 @@ type DevstateContainerPostRequest struct {
// Args passed to the Container entrypoint
Args []string `json:"args,omitempty"`
// Environment variables to define
Env []Env `json:"env,omitempty"`
// Requested memory for the deployed container
MemReq string `json:"memReq,omitempty"`
@@ -37,15 +40,52 @@ type DevstateContainerPostRequest struct {
// Volume to mount into the container filesystem
VolumeMounts []VolumeMount `json:"volumeMounts,omitempty"`
// If false, mountSources and sourceMapping values are not considered
ConfigureSources bool `json:"configureSources,omitempty"`
// If true, sources are mounted into container's filesystem
MountSources bool `json:"mountSources,omitempty"`
// Specific directory on which to mount sources
SourceMapping string `json:"sourceMapping,omitempty"`
Annotation Annotation `json:"annotation,omitempty"`
// Endpoints exposed by the container
Endpoints []Endpoint `json:"endpoints,omitempty"`
}
// AssertDevstateContainerPostRequestRequired checks if the required fields are not zero-ed
func AssertDevstateContainerPostRequestRequired(obj DevstateContainerPostRequest) error {
elements := map[string]interface{}{
"name": obj.Name,
"image": obj.Image,
}
for name, el := range elements {
if isZero := IsZeroValue(el); isZero {
return &RequiredError{Field: name}
}
}
for _, el := range obj.Env {
if err := AssertEnvRequired(el); err != nil {
return err
}
}
for _, el := range obj.VolumeMounts {
if err := AssertVolumeMountRequired(el); err != nil {
return err
}
}
if err := AssertAnnotationRequired(obj.Annotation); err != nil {
return err
}
for _, el := range obj.Endpoints {
if err := AssertEndpointRequired(el); err != nil {
return err
}
}
return nil
}

43
pkg/apiserver-gen/go/model_annotation.go generated Normal file
View 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 Annotation struct {
Deployment map[string]string `json:"deployment"`
Service map[string]string `json:"service"`
}
// AssertAnnotationRequired checks if the required fields are not zero-ed
func AssertAnnotationRequired(obj Annotation) error {
elements := map[string]interface{}{
"deployment": obj.Deployment,
"service": obj.Service,
}
for name, el := range elements {
if isZero := IsZeroValue(el); isZero {
return &RequiredError{Field: name}
}
}
return nil
}
// AssertRecurseAnnotationRequired recursively checks if required fields are not zero-ed in a nested slice.
// Accepts only nested slice of Annotation (e.g. [][]Annotation), otherwise ErrTypeAssertionError is thrown.
func AssertRecurseAnnotationRequired(objSlice interface{}) error {
return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error {
aAnnotation, ok := obj.(Annotation)
if !ok {
return ErrTypeAssertionError
}
return AssertAnnotationRequired(aAnnotation)
})
}

View File

@@ -27,20 +27,38 @@ type Container struct {
CpuLimit string `json:"cpuLimit"`
VolumeMounts []VolumeMount `json:"volumeMounts"`
Annotation Annotation `json:"annotation"`
Endpoints []Endpoint `json:"endpoints"`
Env []Env `json:"env"`
ConfigureSources bool `json:"configureSources"`
MountSources bool `json:"mountSources"`
SourceMapping string `json:"sourceMapping"`
}
// AssertContainerRequired checks if the required fields are not zero-ed
func AssertContainerRequired(obj Container) error {
elements := map[string]interface{}{
"name": obj.Name,
"image": obj.Image,
"command": obj.Command,
"args": obj.Args,
"memoryRequest": obj.MemoryRequest,
"memoryLimit": obj.MemoryLimit,
"cpuRequest": obj.CpuRequest,
"cpuLimit": obj.CpuLimit,
"volumeMounts": obj.VolumeMounts,
"name": obj.Name,
"image": obj.Image,
"command": obj.Command,
"args": obj.Args,
"memoryRequest": obj.MemoryRequest,
"memoryLimit": obj.MemoryLimit,
"cpuRequest": obj.CpuRequest,
"cpuLimit": obj.CpuLimit,
"volumeMounts": obj.VolumeMounts,
"annotation": obj.Annotation,
"endpoints": obj.Endpoints,
"env": obj.Env,
"configureSources": obj.ConfigureSources,
"mountSources": obj.MountSources,
"sourceMapping": obj.SourceMapping,
}
for name, el := range elements {
if isZero := IsZeroValue(el); isZero {
@@ -53,6 +71,19 @@ func AssertContainerRequired(obj Container) error {
return err
}
}
if err := AssertAnnotationRequired(obj.Annotation); err != nil {
return err
}
for _, el := range obj.Endpoints {
if err := AssertEndpointRequired(el); err != nil {
return err
}
}
for _, el := range obj.Env {
if err := AssertEnvRequired(el); err != nil {
return err
}
}
return nil
}

51
pkg/apiserver-gen/go/model_endpoint.go generated Normal file
View File

@@ -0,0 +1,51 @@
/*
* odo dev
*
* API interface for 'odo dev'
*
* API version: 0.1
* Generated by: OpenAPI Generator (https://openapi-generator.tech)
*/
package openapi
type Endpoint struct {
Name string `json:"name"`
Exposure string `json:"exposure,omitempty"`
Path string `json:"path,omitempty"`
Protocol string `json:"protocol,omitempty"`
Secure bool `json:"secure,omitempty"`
TargetPort int32 `json:"targetPort"`
}
// AssertEndpointRequired checks if the required fields are not zero-ed
func AssertEndpointRequired(obj Endpoint) error {
elements := map[string]interface{}{
"name": obj.Name,
"targetPort": obj.TargetPort,
}
for name, el := range elements {
if isZero := IsZeroValue(el); isZero {
return &RequiredError{Field: name}
}
}
return nil
}
// AssertRecurseEndpointRequired recursively checks if required fields are not zero-ed in a nested slice.
// Accepts only nested slice of Endpoint (e.g. [][]Endpoint), otherwise ErrTypeAssertionError is thrown.
func AssertRecurseEndpointRequired(objSlice interface{}) error {
return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error {
aEndpoint, ok := obj.(Endpoint)
if !ok {
return ErrTypeAssertionError
}
return AssertEndpointRequired(aEndpoint)
})
}

43
pkg/apiserver-gen/go/model_env.go generated Normal file
View 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 Env struct {
Name string `json:"name"`
Value string `json:"value"`
}
// AssertEnvRequired checks if the required fields are not zero-ed
func AssertEnvRequired(obj Env) error {
elements := map[string]interface{}{
"name": obj.Name,
"value": obj.Value,
}
for name, el := range elements {
if isZero := IsZeroValue(el); isZero {
return &RequiredError{Field: name}
}
}
return nil
}
// AssertRecurseEnvRequired recursively checks if required fields are not zero-ed in a nested slice.
// Accepts only nested slice of Env (e.g. [][]Env), otherwise ErrTypeAssertionError is thrown.
func AssertRecurseEnvRequired(objSlice interface{}) error {
return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error {
aEnv, ok := obj.(Env)
if !ok {
return ErrTypeAssertionError
}
return AssertEnvRequired(aEnv)
})
}

View File

@@ -15,11 +15,17 @@ func (s *DevstateApiService) DevstateContainerPost(ctx context.Context, containe
container.Image,
container.Command,
container.Args,
container.Env,
container.MemReq,
container.MemLimit,
container.CpuReq,
container.CpuLimit,
container.VolumeMounts,
container.ConfigureSources,
container.MountSources,
container.SourceMapping,
container.Annotation,
container.Endpoints,
)
if err != nil {
return openapi.Response(http.StatusInternalServerError, openapi.GeneralError{

View File

@@ -33,11 +33,17 @@ func TestDevfileState_AddExecCommand(t *testing.T) {
"an-image",
[]string{"run", "command"},
[]string{"arg1", "arg2"},
nil,
"1Gi",
"2Gi",
"100m",
"200m",
nil,
true,
true,
"",
openapi.Annotation{},
nil,
)
if err != nil {
t.Fatal(err)
@@ -72,6 +78,7 @@ components:
image: an-image
memoryLimit: 2Gi
memoryRequest: 1Gi
mountSources: true
name: a-container
metadata: {}
schemaVersion: 2.2.0
@@ -90,15 +97,19 @@ schemaVersion: 2.2.0
},
Containers: []Container{
{
Name: "a-container",
Image: "an-image",
Command: []string{"run", "command"},
Args: []string{"arg1", "arg2"},
MemoryRequest: "1Gi",
MemoryLimit: "2Gi",
CpuRequest: "100m",
CpuLimit: "200m",
VolumeMounts: []openapi.VolumeMount{},
Name: "a-container",
Image: "an-image",
Command: []string{"run", "command"},
Args: []string{"arg1", "arg2"},
MemoryRequest: "1Gi",
MemoryLimit: "2Gi",
CpuRequest: "100m",
CpuLimit: "200m",
VolumeMounts: []openapi.VolumeMount{},
Endpoints: []openapi.Endpoint{},
Env: []openapi.Env{},
ConfigureSources: true,
MountSources: true,
},
},
Images: []Image{},
@@ -236,11 +247,17 @@ func TestDevfileState_AddCompositeCommand(t *testing.T) {
"an-image",
[]string{"run", "command"},
[]string{"arg1", "arg2"},
nil,
"1Gi",
"2Gi",
"100m",
"200m",
nil,
true,
true,
"",
openapi.Annotation{},
nil,
)
if err != nil {
t.Fatal(err)
@@ -288,6 +305,7 @@ components:
image: an-image
memoryLimit: 2Gi
memoryRequest: 1Gi
mountSources: true
name: a-container
metadata: {}
schemaVersion: 2.2.0
@@ -311,15 +329,19 @@ schemaVersion: 2.2.0
},
Containers: []Container{
{
Name: "a-container",
Image: "an-image",
Command: []string{"run", "command"},
Args: []string{"arg1", "arg2"},
MemoryRequest: "1Gi",
MemoryLimit: "2Gi",
CpuRequest: "100m",
CpuLimit: "200m",
VolumeMounts: []openapi.VolumeMount{},
Name: "a-container",
Image: "an-image",
Command: []string{"run", "command"},
Args: []string{"arg1", "arg2"},
MemoryRequest: "1Gi",
MemoryLimit: "2Gi",
CpuRequest: "100m",
CpuLimit: "200m",
VolumeMounts: []openapi.VolumeMount{},
Endpoints: []openapi.Endpoint{},
Env: []openapi.Env{},
ConfigureSources: true,
MountSources: true,
},
},
Images: []Image{},
@@ -368,11 +390,17 @@ func TestDevfileState_DeleteCommand(t *testing.T) {
"an-image",
[]string{"run", "command"},
[]string{"arg1", "arg2"},
nil,
"1Gi",
"2Gi",
"100m",
"200m",
nil,
true,
true,
"",
openapi.Annotation{},
nil,
)
if err != nil {
t.Fatal(err)
@@ -406,6 +434,7 @@ func TestDevfileState_DeleteCommand(t *testing.T) {
image: an-image
memoryLimit: 2Gi
memoryRequest: 1Gi
mountSources: true
name: a-container
metadata: {}
schemaVersion: 2.2.0
@@ -413,15 +442,19 @@ schemaVersion: 2.2.0
Commands: []Command{},
Containers: []Container{
{
Name: "a-container",
Image: "an-image",
Command: []string{"run", "command"},
Args: []string{"arg1", "arg2"},
MemoryRequest: "1Gi",
MemoryLimit: "2Gi",
CpuRequest: "100m",
CpuLimit: "200m",
VolumeMounts: []openapi.VolumeMount{},
Name: "a-container",
Image: "an-image",
Command: []string{"run", "command"},
Args: []string{"arg1", "arg2"},
MemoryRequest: "1Gi",
MemoryLimit: "2Gi",
CpuRequest: "100m",
CpuLimit: "200m",
VolumeMounts: []openapi.VolumeMount{},
Endpoints: []openapi.Endpoint{},
Env: []openapi.Env{},
ConfigureSources: true,
MountSources: true,
},
},
Images: []Image{},

View File

@@ -14,11 +14,17 @@ func (o *DevfileState) AddContainer(
image string,
command []string,
args []string,
envs []Env,
memRequest string,
memLimit string,
cpuRequest string,
cpuLimit string,
volumeMounts []VolumeMount,
configureSources bool,
mountSources bool,
sourceMapping string,
annotation Annotation,
endpoints []Endpoint,
) (DevfileContent, error) {
v1alpha2VolumeMounts := make([]v1alpha2.VolumeMount, 0, len(volumeMounts))
for _, vm := range volumeMounts {
@@ -27,6 +33,38 @@ func (o *DevfileState) AddContainer(
Path: vm.Path,
})
}
v1alpha2Envs := make([]v1alpha2.EnvVar, 0, len(envs))
for _, env := range envs {
v1alpha2Envs = append(v1alpha2Envs, v1alpha2.EnvVar{
Name: env.Name,
Value: env.Value,
})
}
var annotations *v1alpha2.Annotation
if len(annotation.Deployment) > 0 || len(annotation.Service) > 0 {
annotations = &v1alpha2.Annotation{}
if len(annotation.Deployment) > 0 {
annotations.Deployment = annotation.Deployment
}
if len(annotation.Service) > 0 {
annotations.Service = annotation.Service
}
}
v1alpha2Endpoints := make([]v1alpha2.Endpoint, 0, len(endpoints))
for _, endpoint := range endpoints {
endpoint := endpoint
v1alpha2Endpoints = append(v1alpha2Endpoints, v1alpha2.Endpoint{
Name: endpoint.Name,
TargetPort: int(endpoint.TargetPort),
Exposure: v1alpha2.EndpointExposure(endpoint.Exposure),
Protocol: v1alpha2.EndpointProtocol(endpoint.Protocol),
Secure: &endpoint.Secure,
Path: endpoint.Path,
})
}
container := v1alpha2.Component{
Name: name,
ComponentUnion: v1alpha2.ComponentUnion{
@@ -35,15 +73,22 @@ func (o *DevfileState) AddContainer(
Image: image,
Command: command,
Args: args,
Env: v1alpha2Envs,
MemoryRequest: memRequest,
MemoryLimit: memLimit,
CpuRequest: cpuRequest,
CpuLimit: cpuLimit,
VolumeMounts: v1alpha2VolumeMounts,
Annotation: annotations,
},
Endpoints: v1alpha2Endpoints,
},
},
}
if configureSources {
container.Container.MountSources = &mountSources
container.Container.SourceMapping = sourceMapping
}
err := o.Devfile.Data.AddComponents([]v1alpha2.Component{container})
if err != nil {
return DevfileContent{}, err

View File

@@ -10,15 +10,21 @@ import (
func TestDevfileState_AddContainer(t *testing.T) {
type args struct {
name string
image string
command []string
args []string
memRequest string
memLimit string
cpuRequest string
cpuLimit string
volumeMounts []openapi.VolumeMount
name string
image string
command []string
args []string
envs []Env
memRequest string
memLimit string
cpuRequest string
cpuLimit string
volumeMounts []openapi.VolumeMount
configureSources bool
mountSources bool
sourceMapping string
annotation Annotation
endpoints []Endpoint
}
tests := []struct {
name string
@@ -28,7 +34,7 @@ func TestDevfileState_AddContainer(t *testing.T) {
wantErr bool
}{
{
name: "Add a container",
name: "Add a container, with sources configured",
state: func() DevfileState {
return NewDevfileState()
},
@@ -47,6 +53,81 @@ func TestDevfileState_AddContainer(t *testing.T) {
Path: "/mnt/volume1",
},
},
configureSources: true,
mountSources: false,
},
want: DevfileContent{
Content: `components:
- container:
args:
- arg1
- arg2
command:
- run
- command
cpuLimit: 200m
cpuRequest: 100m
image: an-image
memoryLimit: 2Gi
memoryRequest: 1Gi
mountSources: false
volumeMounts:
- name: vol1
path: /mnt/volume1
name: a-name
metadata: {}
schemaVersion: 2.2.0
`,
Commands: []Command{},
Containers: []Container{
{
Name: "a-name",
Image: "an-image",
Command: []string{"run", "command"},
Args: []string{"arg1", "arg2"},
MemoryRequest: "1Gi",
MemoryLimit: "2Gi",
CpuRequest: "100m",
CpuLimit: "200m",
VolumeMounts: []openapi.VolumeMount{
{
Name: "vol1",
Path: "/mnt/volume1",
},
},
Endpoints: []openapi.Endpoint{},
Env: []openapi.Env{},
ConfigureSources: true,
MountSources: false,
},
},
Images: []Image{},
Resources: []Resource{},
Volumes: []Volume{},
Events: Events{},
},
},
{
name: "Add a container, without sources configured",
state: func() DevfileState {
return NewDevfileState()
},
args: args{
name: "a-name",
image: "an-image",
command: []string{"run", "command"},
args: []string{"arg1", "arg2"},
memRequest: "1Gi",
memLimit: "2Gi",
cpuRequest: "100m",
cpuLimit: "200m",
volumeMounts: []openapi.VolumeMount{
{
Name: "vol1",
Path: "/mnt/volume1",
},
},
configureSources: false,
},
want: DevfileContent{
Content: `components:
@@ -86,6 +167,10 @@ schemaVersion: 2.2.0
Path: "/mnt/volume1",
},
},
Endpoints: []openapi.Endpoint{},
Env: []openapi.Env{},
ConfigureSources: false,
MountSources: true,
},
},
Images: []Image{},
@@ -99,7 +184,7 @@ schemaVersion: 2.2.0
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := tt.state()
got, err := o.AddContainer(tt.args.name, tt.args.image, tt.args.command, tt.args.args, tt.args.memRequest, tt.args.memLimit, tt.args.cpuRequest, tt.args.cpuLimit, tt.args.volumeMounts)
got, err := o.AddContainer(tt.args.name, tt.args.image, tt.args.command, tt.args.args, tt.args.envs, tt.args.memRequest, tt.args.memLimit, tt.args.cpuRequest, tt.args.cpuLimit, tt.args.volumeMounts, tt.args.configureSources, tt.args.mountSources, tt.args.sourceMapping, tt.args.annotation, tt.args.endpoints)
if (err != nil) != tt.wantErr {
t.Errorf("DevfileState.AddContainer() error = %v, wantErr %v", err, tt.wantErr)
return
@@ -134,11 +219,17 @@ func TestDevfileState_DeleteContainer(t *testing.T) {
"an-image",
[]string{"run", "command"},
[]string{"arg1", "arg2"},
nil,
"1Gi",
"2Gi",
"100m",
"200m",
nil,
true,
false,
"",
Annotation{},
nil,
)
if err != nil {
t.Fatal(err)
@@ -169,11 +260,17 @@ schemaVersion: 2.2.0
"an-image",
[]string{"run", "command"},
[]string{"arg1", "arg2"},
nil,
"1Gi",
"2Gi",
"100m",
"200m",
nil,
true,
false,
"",
Annotation{},
nil,
)
if err != nil {
t.Fatal(err)

View File

@@ -162,15 +162,21 @@ func (o *DevfileState) getContainers() ([]Container, error) {
result := make([]Container, 0, len(containers))
for _, container := range containers {
result = append(result, Container{
Name: container.Name,
Image: container.ComponentUnion.Container.Image,
Command: container.ComponentUnion.Container.Command,
Args: container.ComponentUnion.Container.Args,
MemoryRequest: container.ComponentUnion.Container.MemoryRequest,
MemoryLimit: container.ComponentUnion.Container.MemoryLimit,
CpuRequest: container.ComponentUnion.Container.CpuRequest,
CpuLimit: container.ComponentUnion.Container.CpuLimit,
VolumeMounts: o.getVolumeMounts(container.Container.Container),
Name: container.Name,
Image: container.ComponentUnion.Container.Image,
Command: container.ComponentUnion.Container.Command,
Args: container.ComponentUnion.Container.Args,
MemoryRequest: container.ComponentUnion.Container.MemoryRequest,
MemoryLimit: container.ComponentUnion.Container.MemoryLimit,
CpuRequest: container.ComponentUnion.Container.CpuRequest,
CpuLimit: container.ComponentUnion.Container.CpuLimit,
VolumeMounts: o.getVolumeMounts(container.Container.Container),
Annotation: o.getAnnotation(container.Container.Annotation),
Endpoints: o.getEndpoints(container.Container.Endpoints),
Env: o.getEnv(container.Container.Env),
ConfigureSources: container.Container.MountSources != nil,
MountSources: pointer.BoolDeref(container.Container.MountSources, true), // TODO(feloy) default value will depend on dedicatedPod
SourceMapping: container.Container.SourceMapping,
})
}
return result, nil
@@ -187,6 +193,42 @@ func (o *DevfileState) getVolumeMounts(container v1alpha2.Container) []VolumeMou
return result
}
func (o *DevfileState) getAnnotation(annotation *v1alpha2.Annotation) Annotation {
if annotation == nil {
return Annotation{}
}
return Annotation{
Deployment: annotation.Deployment,
Service: annotation.Service,
}
}
func (o *DevfileState) getEndpoints(endpoints []v1alpha2.Endpoint) []Endpoint {
result := make([]Endpoint, 0, len(endpoints))
for _, ep := range endpoints {
result = append(result, Endpoint{
Name: ep.Name,
Exposure: string(ep.Exposure),
Path: ep.Path,
Protocol: string(ep.Protocol),
Secure: pointer.BoolDeref(ep.Secure, false),
TargetPort: int32(ep.TargetPort),
})
}
return result
}
func (o *DevfileState) getEnv(envs []v1alpha2.EnvVar) []Env {
result := make([]Env, 0, len(envs))
for _, env := range envs {
result = append(result, Env{
Name: env.Name,
Value: env.Value,
})
}
return result
}
func (o *DevfileState) getImages() ([]Image, error) {
images, err := o.Devfile.Data.GetComponents(common.DevfileOptions{
ComponentOptions: common.ComponentOptions{

View File

@@ -512,6 +512,9 @@ paths:
application/json:
schema:
type: object
required:
- name
- image
properties:
name:
description: Name of the container
@@ -531,6 +534,11 @@ paths:
items: {
type: string
}
env:
description: Environment variables to define
type: array
items:
$ref: '#/components/schemas/Env'
memReq:
description: Requested memory for the deployed container
type: string
@@ -548,6 +556,24 @@ paths:
type: array
items:
$ref: '#/components/schemas/VolumeMount'
configureSources:
description: If false, mountSources and sourceMapping values are not considered
type: boolean
mountSources:
description: If true, sources are mounted into container's filesystem
type: boolean
sourceMapping:
description: Specific directory on which to mount sources
type: string
annotation:
description: Annotations added to the resources created for this container
$ref: '#/components/schemas/Annotation'
endpoints:
description: Endpoints exposed by the container
type: array
items:
$ref: '#/components/schemas/Endpoint'
responses:
'200':
description: container was successfully added to the devfile
@@ -1522,6 +1548,12 @@ components:
- cpuRequest
- cpuLimit
- volumeMounts
- annotation
- endpoints
- env
- configureSources
- mountSources
- sourceMapping
properties:
name:
type: string
@@ -1547,6 +1579,22 @@ components:
type: array
items:
$ref: '#/components/schemas/VolumeMount'
annotation:
$ref: '#/components/schemas/Annotation'
endpoints:
type: array
items:
$ref: '#/components/schemas/Endpoint'
env:
type: array
items:
$ref: '#/components/schemas/Env'
configureSources:
type: boolean
mountSources:
type: boolean
sourceMapping:
type: string
VolumeMount:
type: object
required:
@@ -1557,6 +1605,50 @@ components:
type: string
path:
type: string
Annotation:
type: object
required:
- deployment
- service
properties:
deployment:
type: object
additionalProperties:
type: string
service:
type: object
additionalProperties:
type: string
Endpoint:
type: object
required:
- name
- targetPort
properties:
name:
type: string
exposure:
type: string
enum: [public,internal,none]
path:
type: string
protocol:
type: string
enum: [http,https,ws,wss,tcp,udp]
secure:
type: boolean
targetPort:
type: integer
Env:
type: object
required:
- name
- value
properties:
name:
type: string
value:
type: string
Image:
type: object
required:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -39,7 +39,7 @@ describe('devfile editor spec', () => {
.should('contain.text', 'with arg');
});
it('displays a created container', () => {
it('displays a created container without source configuration', () => {
cy.init();
cy.selectTab(TAB_VOLUMES);
@@ -51,6 +51,87 @@ describe('devfile editor spec', () => {
cy.selectTab(TAB_CONTAINERS);
cy.getByDataCy('container-name').type('created-container');
cy.getByDataCy('container-image').type('an-image');
cy.getByDataCy('container-env-add').click();
cy.getByDataCy('container-env-name-0').type("VAR1");
cy.getByDataCy('container-env-value-0').type("val1");
cy.getByDataCy('container-env-plus').click();
cy.getByDataCy('container-env-name-1').type("VAR2");
cy.getByDataCy('container-env-value-1').type("val2");
cy.getByDataCy('container-env-plus').click();
cy.getByDataCy('container-env-name-2').type("VAR3");
cy.getByDataCy('container-env-value-2').type("val3");
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('endpoints-add').click();
cy.getByDataCy('endpoint-name-0').type("ep1");
cy.getByDataCy('endpoint-targetPort-0').type("4001");
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-more-params').click();
cy.getByDataCy('container-deploy-anno-add').click();
cy.getByDataCy('container-deploy-anno-name-0').type("DEPANNO1");
cy.getByDataCy('container-deploy-anno-value-0').type("depval1");
cy.getByDataCy('container-deploy-anno-plus').click();
cy.getByDataCy('container-deploy-anno-name-1').type("DEPANNO2");
cy.getByDataCy('container-deploy-anno-value-1').type("depval2");
cy.getByDataCy('container-svc-anno-add').click();
cy.getByDataCy('container-svc-anno-name-0').type("SVCANNO1");
cy.getByDataCy('container-svc-anno-value-0').type("svcval1");
cy.getByDataCy('container-svc-anno-plus').click();
cy.getByDataCy('container-svc-anno-name-1').type("SVCANNO2");
cy.getByDataCy('container-svc-anno-value-1').type("svcval2");
cy.getByDataCy('container-create').click();
cy.getByDataCy('container-info').first()
.should('contain.text', 'created-container')
.should('contain.text', 'an-image')
.should('contain.text', 'VAR1: val1')
.should('contain.text', 'VAR2: val2')
.should('contain.text', 'VAR3: val3')
.should('contain.text', 'volume1')
.should('contain.text', '/mnt/vol1')
.should('contain.text', 'volume2')
.should('contain.text', '/mnt/vol2')
.should('not.contain.text', 'Mount Sources')
.should('contain.text', 'ep1')
.should('contain.text', '4001')
.should('contain.text', 'Deployment Annotations')
.should('contain.text', 'DEPANNO1: depval1')
.should('contain.text', 'DEPANNO2: depval2')
.should('contain.text', 'Service Annotations')
.should('contain.text', 'SVCANNO1: svcval1')
.should('contain.text', 'SVCANNO2: svcval2');
cy.selectTab(TAB_VOLUMES);
cy.getByDataCy('volume-info').eq(1)
.should('contain.text', 'volume2');
});
it('displays a created container with source configuration', () => {
cy.init();
cy.selectTab(TAB_VOLUMES);
cy.getByDataCy('volume-name').type('volume1');
cy.getByDataCy('volume-size').type('512Mi');
cy.getByDataCy('volume-ephemeral').click();
cy.getByDataCy('volume-create').click();
cy.selectTab(TAB_CONTAINERS);
cy.getByDataCy('container-name').type('created-container');
cy.getByDataCy('container-image').type('an-image');
cy.getByDataCy('container-more-params').click();
cy.getByDataCy('container-sources-configuration').click();
cy.getByDataCy('container-sources-specific-directory').click();
cy.getByDataCy('container-source-mapping').type('/mnt/sources');
cy.getByDataCy('volume-mount-add').click();
cy.getByDataCy('volume-mount-path-0').type("/mnt/vol1");
@@ -70,7 +151,9 @@ describe('devfile editor spec', () => {
.should('contain.text', 'volume1')
.should('contain.text', '/mnt/vol1')
.should('contain.text', 'volume2')
.should('contain.text', '/mnt/vol2');
.should('contain.text', '/mnt/vol2')
.should('contain.text', 'Mount Sources')
.should('contain.text', '/mnt/sources');
cy.selectTab(TAB_VOLUMES);
cy.getByDataCy('volume-info').eq(1)

View File

@@ -8,6 +8,7 @@ configuration.ts
encoder.ts
git_push.sh
index.ts
model/annotation.ts
model/applyCommand.ts
model/command.ts
model/componentCommandPostRequest.ts
@@ -30,6 +31,8 @@ model/devstateImagePostRequest.ts
model/devstateQuantityValidPostRequest.ts
model/devstateResourcePostRequest.ts
model/devstateVolumePostRequest.ts
model/endpoint.ts
model/env.ts
model/events.ts
model/execCommand.ts
model/generalError.ts

18
ui/src/app/api-gen/model/annotation.ts generated Normal file
View 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 Annotation {
deployment: { [key: string]: string; };
service: { [key: string]: string; };
}

View File

@@ -9,7 +9,10 @@
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { Endpoint } from './endpoint';
import { VolumeMount } from './volumeMount';
import { Env } from './env';
import { Annotation } from './annotation';
export interface Container {
@@ -22,5 +25,11 @@ export interface Container {
cpuRequest: string;
cpuLimit: string;
volumeMounts: Array<VolumeMount>;
annotation: Annotation;
endpoints: Array<Endpoint>;
env: Array<Env>;
configureSources: boolean;
mountSources: boolean;
sourceMapping: string;
}

View File

@@ -9,18 +9,21 @@
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { Endpoint } from './endpoint';
import { VolumeMount } from './volumeMount';
import { Env } from './env';
import { Annotation } from './annotation';
export interface DevstateContainerPostRequest {
/**
* Name of the container
*/
name?: string;
name: string;
/**
* Container image
*/
image?: string;
image: string;
/**
* Entrypoint of the container
*/
@@ -29,6 +32,10 @@ export interface DevstateContainerPostRequest {
* Args passed to the Container entrypoint
*/
args?: Array<string>;
/**
* Environment variables to define
*/
env?: Array<Env>;
/**
* Requested memory for the deployed container
*/
@@ -49,5 +56,22 @@ export interface DevstateContainerPostRequest {
* Volume to mount into the container filesystem
*/
volumeMounts?: Array<VolumeMount>;
/**
* If false, mountSources and sourceMapping values are not considered
*/
configureSources?: boolean;
/**
* If true, sources are mounted into container\'s filesystem
*/
mountSources?: boolean;
/**
* Specific directory on which to mount sources
*/
sourceMapping?: string;
annotation?: Annotation;
/**
* Endpoints exposed by the container
*/
endpoints?: Array<Endpoint>;
}

40
ui/src/app/api-gen/model/endpoint.ts generated Normal file
View File

@@ -0,0 +1,40 @@
/**
* 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 Endpoint {
name: string;
exposure?: Endpoint.ExposureEnum;
path?: string;
protocol?: Endpoint.ProtocolEnum;
secure?: boolean;
targetPort: number;
}
export namespace Endpoint {
export type ExposureEnum = 'public' | 'internal' | 'none';
export const ExposureEnum = {
Public: 'public' as ExposureEnum,
Internal: 'internal' as ExposureEnum,
None: 'none' as ExposureEnum
};
export type ProtocolEnum = 'http' | 'https' | 'ws' | 'wss' | 'tcp' | 'udp';
export const ProtocolEnum = {
Http: 'http' as ProtocolEnum,
Https: 'https' as ProtocolEnum,
Ws: 'ws' as ProtocolEnum,
Wss: 'wss' as ProtocolEnum,
Tcp: 'tcp' as ProtocolEnum,
Udp: 'udp' as ProtocolEnum
};
}

18
ui/src/app/api-gen/model/env.ts generated Normal file
View 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 Env {
name: string;
value: string;
}

View File

@@ -1,3 +1,4 @@
export * from './annotation';
export * from './applyCommand';
export * from './command';
export * from './componentCommandPostRequest';
@@ -20,6 +21,8 @@ export * from './devstateImagePostRequest';
export * from './devstateQuantityValidPostRequest';
export * from './devstateResourcePostRequest';
export * from './devstateVolumePostRequest';
export * from './endpoint';
export * from './env';
export * from './events';
export * from './execCommand';
export * from './generalError';

View File

@@ -48,6 +48,8 @@ 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';
import { MultiKeyValueComponent } from './controls/multi-key-value/multi-key-value.component';
import { EndpointsComponent } from './controls/endpoints/endpoints.component';
@NgModule({
declarations: [
@@ -74,6 +76,8 @@ import { VolumeMountsComponent } from './controls/volume-mounts/volume-mounts.co
VolumesComponent,
VolumeComponent,
VolumeMountsComponent,
MultiKeyValueComponent,
EndpointsComponent,
],
imports: [
BrowserModule,

View File

@@ -0,0 +1,2 @@
.mid-width { width: 50%; }
.quart-width { width: 25%; }

View File

@@ -0,0 +1,43 @@
<div *ngFor="let control of form.controls; index as i">
<ng-container [formGroup]="control">
<mat-form-field class="mid-width" appearance="outline">
<mat-label><span>Name</span></mat-label>
<input [attr.data-cy]="'endpoint-name-'+i" matInput formControlName="name">
</mat-form-field>
<mat-form-field class="quart-width" appearance="outline">
<mat-label><span>Target Port</span></mat-label>
<input [attr.data-cy]="'endpoint-targetPort-'+i" type="number" matInput formControlName="targetPort">
</mat-form-field>
<mat-form-field class="quart-width" appearance="outline">
<mat-label>Exposure</mat-label>
<mat-select [attr.data-cy]="'endpoint-exposure-'+i" formControlName="exposure">
<mat-option value="">(default, public)</mat-option>
<mat-option value="public">public</mat-option>
<mat-option value="internal">internal</mat-option>
<mat-option value="none">none</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mid-width" appearance="outline">
<mat-label><span>Path</span></mat-label>
<input [attr.data-cy]="'endpoint-path-'+i" matInput formControlName="path">
</mat-form-field>
<mat-form-field class="quart-width" appearance="outline">
<mat-label>Protocol</mat-label>
<mat-select [attr.data-cy]="'endpoint-protocol-'+i" formControlName="protocol">
<mat-option value="">(default, http)</mat-option>
<mat-option value="http">http</mat-option>
<mat-option value="https">https</mat-option>
<mat-option value="ws">ws</mat-option>
<mat-option value="wss">wss</mat-option>
<mat-option value="tcp">tcp</mat-option>
<mat-option value="udp">udp</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox [attr.data-cy]="'endpoint-secure-'+i" formControlName="secure">Protocol Is Secure</mat-checkbox>
</ng-container>
</div>
<button data-cy="endpoints-plus" *ngIf="form.value.length > 0" mat-icon-button (click)="addEndpoint()">
<mat-icon class="tab-icon material-icons-outlined">add</mat-icon>
</button>
<button data-cy="endpoints-add" *ngIf="form.value.length == 0" mat-flat-button (click)="addEndpoint()">Add an Endpoint</button>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EndpointsComponent } from './endpoints.component';
describe('EndpointsComponent', () => {
let component: EndpointsComponent;
let fixture: ComponentFixture<EndpointsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ EndpointsComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(EndpointsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,74 @@
import { Component, forwardRef } from '@angular/core';
import { AbstractControl, ControlValueAccessor, FormArray, FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator, Validators } from '@angular/forms';
interface Endpoint {
}
@Component({
selector: 'app-endpoints',
templateUrl: './endpoints.component.html',
styleUrls: ['./endpoints.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: EndpointsComponent
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => EndpointsComponent),
multi: true,
},
]
})
export class EndpointsComponent implements ControlValueAccessor, Validator {
onChange = (_: Endpoint[]) => {};
onValidatorChange = () => {};
form = new FormArray<FormGroup>([]);
constructor() {
this.form.valueChanges.subscribe(value => {
this.onChange(value);
});
}
newEndpoint(): FormGroup {
return new FormGroup({
name: new FormControl("", [Validators.required]),
targetPort: new FormControl("", [Validators.required, Validators.pattern("^[0-9]*$")]),
exposure: new FormControl(""),
path: new FormControl(""),
protocol: new FormControl(""),
secure: new FormControl(false),
});
}
addEndpoint() {
this.form.push(this.newEndpoint());
}
/* ControlValueAccessor implementation */
writeValue(value: Endpoint[]) {
}
registerOnChange(onChange: any) {
this.onChange = onChange;
}
registerOnTouched(_: any) {}
/* Validator implementation */
validate(control: AbstractControl): ValidationErrors | null {
if (!this.form.valid) {
return {'internal': true};
}
return null;
}
registerOnValidatorChange?(onValidatorChange: () => void): void {
this.onValidatorChange = onValidatorChange;
}
}

View File

@@ -0,0 +1,2 @@
div.group { margin-bottom: 16px; }
.mid-width { width: 50%; }

View File

@@ -0,0 +1,16 @@
<div class="group">
<span *ngFor="let entry of entries; let i=index">
<mat-form-field class="mid-width" appearance="outline">
<mat-label><span>Name</span></mat-label>
<input [attr.data-cy]="dataCyPrefix+'-name-'+i" matInput [value]="entry.name" (change)="onKeyChange(i, $event)" (input)="onKeyChange(i, $event)">
</mat-form-field>
<mat-form-field class="mid-width" appearance="outline">
<mat-label><span>Value</span></mat-label>
<input [attr.data-cy]="dataCyPrefix+'-value-'+i" matInput [value]="entry.value" (change)="onValueChange(i, $event)" (input)="onValueChange(i, $event)">
</mat-form-field>
</span>
<button [attr.data-cy]="dataCyPrefix+'-plus'" *ngIf="entries.length > 0" mat-icon-button (click)="addEntry()">
<mat-icon class="tab-icon material-icons-outlined">add</mat-icon>
</button>
<button [attr.data-cy]="dataCyPrefix+'-add'" *ngIf="entries.length == 0" mat-flat-button (click)="addEntry()">{{addLabel}}</button>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MultiKeyValueComponent } from './multi-key-value.component';
describe('MultiKeyValueComponent', () => {
let component: MultiKeyValueComponent;
let fixture: ComponentFixture<MultiKeyValueComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MultiKeyValueComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(MultiKeyValueComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,77 @@
import { Component, Input, forwardRef } from '@angular/core';
import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator, Validators } from '@angular/forms';
interface KeyValue {
name: string;
value: string;
}
@Component({
selector: 'app-multi-key-value',
templateUrl: './multi-key-value.component.html',
styleUrls: ['./multi-key-value.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: MultiKeyValueComponent
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MultiKeyValueComponent),
multi: true,
},
]
})
export class MultiKeyValueComponent implements ControlValueAccessor, Validator {
@Input() dataCyPrefix: string = "";
@Input() addLabel: string = "";
onChange = (_: KeyValue[]) => {};
onValidatorChange = () => {};
entries: KeyValue[] = [];
writeValue(value: KeyValue[]) {
this.entries = value;
}
registerOnChange(onChange: any) {
this.onChange = onChange;
}
registerOnTouched(_: any) {}
addEntry() {
this.entries.push({name: "", value: ""});
this.onChange(this.entries);
}
onKeyChange(i: number, e: Event) {
const target = e.target as HTMLInputElement;
this.entries[i].name = target.value;
this.onChange(this.entries);
}
onValueChange(i: number, e: Event) {
const target = e.target as HTMLInputElement;
this.entries[i].value = target.value;
this.onChange(this.entries);
}
/* Validator implementation */
validate(control: AbstractControl): ValidationErrors | null {
for (let i=0; i<this.entries.length; i++) {
const entry = this.entries[i];
if (entry.name == "" || entry.value == "") {
return {'internal': true};
}
}
return null;
}
registerOnValidatorChange?(onValidatorChange: () => void): void {
this.onValidatorChange = onValidatorChange;
}
}

View File

@@ -12,7 +12,6 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
useExisting: MultiTextComponent
}
]
})
export class MultiTextComponent implements ControlValueAccessor {

View File

@@ -1,4 +1,3 @@
<h3>Volume Mounts</h3>
<div class="group">
<div *ngFor="let vm of volumeMounts; let i=index">
<mat-form-field class="inline" appearance="outline">

View File

@@ -16,7 +16,7 @@ import { Volume, VolumeMount } from 'src/app/api-gen';
provide: NG_VALIDATORS,
useExisting: forwardRef(() => VolumeMountsComponent),
multi: true,
},
},
]
})
export class VolumeMountsComponent implements Validator {

View File

@@ -1,3 +1,12 @@
.main { padding: 16px; }
mat-form-field.full-width { width: 100%; }
mat-form-field.mid-width { width: 50%; }
.mid-width { width: 50%; }
.source-configuration-details {
margin-left: 16px;
}
div.buttonbar {
margin-top: 16px;
}
.outbutton {
text-align: right;
}

View File

@@ -12,36 +12,81 @@
<input placeholder="Image to start the container" data-cy="container-image" matInput formControlName="image">
</mat-form-field>
<h3>Command and Arguments</h3>
<div class="description">Command and Arguments can be used to override the entrypoint of the image</div>
<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>
<h3>Environment Variables</h3>
<div class="description">Environment Variables to define in the running container</div>
<app-multi-key-value dataCyPrefix="container-env" addLabel="Add Environment Variable" formControlName="env"></app-multi-key-value>
<h3>Volume Mounts</h3>
<div class="description">Volumes to mount into the container's filesystem</div>
<app-volume-mounts
[volumes]="volumeNames"
formControlName="volumeMounts"
(createNewVolume)="onCreateNewVolume($event)"></app-volume-mounts>
<h3>Resource Usage</h3>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>Memory Request</span></mat-label>
<mat-error>{{quantityErrMsgMemory}}</mat-error>
<input placeholder="memory requested for the container. Ex: 1Gi" data-cy="container-memory-request" matInput formControlName="memoryRequest">
</mat-form-field>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>Memory Limit</span></mat-label>
<mat-error>{{quantityErrMsgMemory}}</mat-error>
<input placeholder="memory limit for the container. Ex: 1Gi" data-cy="container-memory-limit" matInput formControlName="memoryLimit">
</mat-form-field>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>CPU Request</span></mat-label>
<mat-error>{{quantityErrMsgCPU}}</mat-error>
<input placeholder="CPU requested for the container. Ex: 500m" data-cy="container-cpu-request" matInput formControlName="cpuRequest">
</mat-form-field>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>CPU Limit</span></mat-label>
<mat-error>{{quantityErrMsgCPU}}</mat-error>
<input placeholder="CPU limit for the container. Ex: 1" data-cy="container-cpu-limit" matInput formControlName="cpuLimit">
</mat-form-field>
<h3>Endpoints</h3>
<div class="description">Endpoints exposed by the container</div>
<app-endpoints formControlName="endpoints"></app-endpoints>
<div class="outbutton"><button data-cy="container-more-params" *ngIf="!seeMore" mat-flat-button (click)="more()">More parameters...</button></div>
<div *ngIf="seeMore">
<h3>Resource Usage</h3>
<div class="description">CPU and Memory resources necessary for container's execution</div>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>Memory Request</span></mat-label>
<mat-error>{{quantityErrMsgMemory}}</mat-error>
<input placeholder="memory requested for the container. Ex: 1Gi" data-cy="container-memory-request" matInput formControlName="memoryRequest">
</mat-form-field>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>Memory Limit</span></mat-label>
<mat-error>{{quantityErrMsgMemory}}</mat-error>
<input placeholder="memory limit for the container. Ex: 1Gi" data-cy="container-memory-limit" matInput formControlName="memoryLimit">
</mat-form-field>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>CPU Request</span></mat-label>
<mat-error>{{quantityErrMsgCPU}}</mat-error>
<input placeholder="CPU requested for the container. Ex: 500m" data-cy="container-cpu-request" matInput formControlName="cpuRequest">
</mat-form-field>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>CPU Limit</span></mat-label>
<mat-error>{{quantityErrMsgCPU}}</mat-error>
<input placeholder="CPU limit for the container. Ex: 1" data-cy="container-cpu-limit" matInput formControlName="cpuLimit">
</mat-form-field>
<h3>Sources</h3>
<div class="description">Declare if and how sources are mounted into the container's filesystem. By default, sources are automatically mounted into $PROJECTS_ROOT or /projects directory</div>
<div><mat-checkbox data-cy="container-sources-configuration" formControlName="configureSources">Configure Source mount</mat-checkbox></div>
<div *ngIf="form.get('configureSources')?.value" class="source-configuration-details">
<div style="display: inline-flex" class="mid-width">
<mat-checkbox data-cy="container-mount-sources" formControlName="mountSources">Mount sources into container</mat-checkbox>
<mat-checkbox data-cy="container-sources-specific-directory" matTooltip="${PROJECTS_ROOT} or /projects by default" formControlName="_specificDir">Into specific directory</mat-checkbox>
</div>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>Mount sources into</span></mat-label>
<input placeholder="Container's directory on which to mount sources" data-cy="container-source-mapping" matInput formControlName="sourceMapping">
</mat-form-field>
</div>
<h3>Deployment Annotations</h3>
<div class="description">Annotations added to the Kubernetes Deployment created for running this container</div>
<app-multi-key-value dataCyPrefix="container-deploy-anno" addLabel="Add Annotation" formControlName="deployAnnotations"></app-multi-key-value>
<h3>Service Annotations</h3>
<div class="description">Annotations added to the Kubernetes Service created for accessing this container</div>
<app-multi-key-value dataCyPrefix="container-svc-anno" addLabel="Add Annotation" formControlName="svcAnnotations"></app-multi-key-value>
</div>
<div class="outbutton"><button data-cy="container-less-params" *ngIf="seeMore" mat-flat-button (click)="less()">Less parameters...</button></div>
</form>
<button data-cy="container-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new container" (click)="create()">Create</button>
<button *ngIf="cancelable" mat-flat-button (click)="cancel()">Cancel</button>
<div class="buttonbar">
<button data-cy="container-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new container" (click)="create()">Create</button>
<button *ngIf="cancelable" mat-flat-button (click)="cancel()">Cancel</button>
</div>
</div>

View File

@@ -27,6 +27,7 @@ export class ContainerComponent {
quantityErrMsgCPU = 'Numeric value, with optional unit m, k, M, G, T, P, E';
volumesToCreate: Volume[] = [];
seeMore: boolean = false;
constructor(
private devstate: DevstateService,
@@ -37,16 +38,59 @@ export class ContainerComponent {
image: new FormControl("", [Validators.required]),
command: new FormControl([]),
args: new FormControl([]),
env: new FormControl([]),
volumeMounts: new FormControl([]),
memoryRequest: new FormControl("", null, [this.devstate.isQuantity()]),
memoryLimit: new FormControl("", null, [this.devstate.isQuantity()]),
cpuRequest: new FormControl("", null, [this.devstate.isQuantity()]),
cpuLimit: new FormControl("", null, [this.devstate.isQuantity()]),
volumeMounts: new FormControl([]),
})
configureSources: new FormControl(false),
mountSources: new FormControl(true),
_specificDir: new FormControl(false),
sourceMapping: new FormControl(""),
deployAnnotations: new FormControl([]),
svcAnnotations: new FormControl([]),
endpoints: new FormControl([]),
});
this.form.valueChanges.subscribe((value: any) => {
this.updateSourceFields(value);
});
this.updateSourceFields(this.form.value);
}
updateSourceFields(value: any) {
const sourceMappingEnabled = value.mountSources && value._specificDir;
if (!sourceMappingEnabled && !this.form.get('sourceMapping')?.disabled) {
this.form.get('sourceMapping')?.disable();
this.form.get('sourceMapping')?.setValue('');
this.form.get('_specificDir')?.setValue(false);
}
if (sourceMappingEnabled && !this.form.get('sourceMapping')?.enabled ) {
this.form.get('sourceMapping')?.enable();
}
const specificDirEnabled = value.mountSources;
if (!specificDirEnabled && !this.form.get('_specificDir')?.disabled) {
this.form.get('_specificDir')?.disable();
}
if (specificDirEnabled && !this.form.get('_specificDir')?.enabled ) {
this.form.get('_specificDir')?.enable();
}
}
create() {
this.telemetry.track("[ui] create container");
const toObject = (o: {name: string, value: string}[]) => {
return o.reduce((acc: any, val: {name: string, value: string}) => { acc[val.name] = val.value; return acc; }, {});
};
const container = this.form.value;
container.annotation = {
deployment: toObject(container.deployAnnotations),
service: toObject(container.svcAnnotations),
};
this.created.emit({
container: this.form.value,
volumes: this.volumesToCreate,
@@ -60,4 +104,11 @@ export class ContainerComponent {
onCreateNewVolume(v: Volume) {
this.volumesToCreate.push(v);
}
more() {
this.seeMore = true;
}
less() {
this.seeMore = false;
}
}

View File

@@ -19,11 +19,20 @@ export class DevstateService {
image: container.image,
command: container.command,
args: container.args,
env: container.env,
memReq: container.memoryRequest,
memLimit: container.memoryLimit,
cpuReq: container.cpuRequest,
cpuLimit: container.cpuLimit,
volumeMounts: container.volumeMounts,
configureSources: container.configureSources,
mountSources: container.mountSources,
sourceMapping: container.sourceMapping,
annotation: {
deployment: container.annotation.deployment,
service: container.annotation.service
},
endpoints: container.endpoints,
});
}

View File

@@ -14,3 +14,26 @@ mat-card-content { padding: 16px; }
table.aligned > tr > td {
vertical-align: top;
}
div.endpoint-list {
display: float;
}
mat-card.endpoint {
width: fit-content;
float: left;
margin: 0 8px;
}
mat-card.endpoint mat-card-header {
padding: 8px 8px 0 8px;
}
mat-card.endpoint mat-card-title {
font-size: 16px;
line-height: 24px;
}
mat-card.endpoint mat-card-subtitle {
font-size: 12px;
line-height: 24px;
}
mat-card.endpoint mat-card-content {
padding: 8px;
}

View File

@@ -18,6 +18,14 @@
<td>Args:</td>
<td><code>{{container.args.join(" ")}}</code></td>
</tr>
<tr *ngIf="container.env.length">
<td>Environment variables:</td>
<td>
<div *ngFor="let env of container.env">
{{env.name}}: {{env.value}}
</div>
</td>
</tr>
<tr *ngIf="container.volumeMounts.length > 0">
<td>Volume Mounts:</td>
<td>
@@ -44,6 +52,47 @@
<td>CPU Limit:</td>
<td><code>{{container.cpuLimit}}</code></td>
</tr>
<tr *ngIf="container.annotation.deployment">
<td>Deployment Annotations:</td>
<td>
<div *ngFor="let anno of container.annotation.deployment | keyvalue">
{{anno.key}}: {{anno.value}}
</div>
</td>
</tr>
<tr *ngIf="container.annotation.service">
<td>Service Annotations:</td>
<td>
<div *ngFor="let anno of container.annotation.service | keyvalue">
{{anno.key}}: {{anno.value}}
</div>
</td>
</tr>
<tr *ngIf="container.configureSources">
<td>Mount Sources:</td>
<td><code>{{container.mountSources ? "Yes" : "No"}}</code></td>
</tr>
<tr *ngIf="container.configureSources && container.mountSources && container.sourceMapping">
<td>Mount Sources Into:</td>
<td><code>{{container.sourceMapping}}</code></td>
</tr>
<tr *ngIf="container.endpoints.length">
<td>Endpoints:</td>
<td class="container-list">
<mat-card class="endpoint" *ngFor="let ep of container.endpoints">
<mat-card-header>
<mat-card-title>{{ep.name}}</mat-card-title>
<mat-card-subtitle>{{ep.targetPort}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div>exposure: {{ep.exposure ?? 'public'}}</div>
<div>protocol: {{ep.protocol ?? 'http'}}</div>
<div *ngIf="ep.secure">secure</div>
<div *ngIf="ep.path">path: {{ep.path}}</div>
</mat-card-content>
</mat-card>
</td>
</tr>
</table>
</mat-card-content>

View File

@@ -25,6 +25,14 @@ h2:has(+.description) {
margin-bottom: 0;
}
h3 {
color: #3f51b5;
}
h3:has(+.description) {
margin-bottom: 0;
}
.description {
font-style: italic;
font-size: smaller;