diff --git a/pkg/devfile/parser/configurables.go b/pkg/devfile/parser/configurables.go index f25437dd..e05b4646 100644 --- a/pkg/devfile/parser/configurables.go +++ b/pkg/devfile/parser/configurables.go @@ -1,13 +1,8 @@ package parser import ( - "fmt" - "strconv" - "strings" - v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/devfile/library/pkg/devfile/parser/data/v2/common" - corev1 "k8s.io/api/core/v1" ) const ( @@ -27,70 +22,46 @@ func (d DevfileObj) SetMetadataName(name string) error { return d.WriteYamlDevfile() } -// AddEnvVars adds environment variables to all the components in a devfile -func (d DevfileObj) AddEnvVars(otherList []v1.EnvVar) error { - components, err := d.Data.GetComponents(common.DevfileOptions{}) +// AddEnvVars accepts a map of container name mapped to an array of the env vars to be set; +// it adds the envirnoment variables to a given container name, and writes to the devfile +// Example of containerEnvMap : {"runtime": {{Name: "Foo", Value: "Bar"}}} +func (d DevfileObj) AddEnvVars(containerEnvMap map[string][]v1.EnvVar) error { + err := d.Data.AddEnvVars(containerEnvMap) if err != nil { return err } - for _, component := range components { - if component.Container != nil { - component.Container.Env = Merge(component.Container.Env, otherList) - d.Data.UpdateComponent(component) - } - } return d.WriteYamlDevfile() } -// RemoveEnvVars removes the environment variables which have the keys from all the components in a devfile -func (d DevfileObj) RemoveEnvVars(keys []string) (err error) { - components, err := d.Data.GetComponents(common.DevfileOptions{}) +// RemoveEnvVars accepts a map of container name mapped to an array of environment variables to be removed; +// it removes the env vars from the specified container name and writes it to the devfile +func (d DevfileObj) RemoveEnvVars(containerEnvMap map[string][]string) (err error) { + err = d.Data.RemoveEnvVars(containerEnvMap) if err != nil { return err } - for _, component := range components { - if component.Container != nil { - component.Container.Env, err = RemoveEnvVarsFromList(component.Container.Env, keys) - if err != nil { - return err - } - d.Data.UpdateComponent(component) - } - } return d.WriteYamlDevfile() } -// SetPorts converts ports to endpoints, adds to a devfile -func (d DevfileObj) SetPorts(ports ...string) error { - components, err := d.Data.GetComponents(common.DevfileOptions{}) +// SetPorts accepts a map of container name mapped to an array of port numbers to be set; +// it converts ports to endpoints, sets the endpoint to a given container name, and writes to the devfile +// Example of containerPortsMap: {"runtime": {"8080", "9000"}, "wildfly": {"12956"}} +func (d DevfileObj) SetPorts(containerPortsMap map[string][]string) error { + err := d.Data.SetPorts(containerPortsMap) if err != nil { return err } - endpoints, err := portsToEndpoints(ports...) - if err != nil { - return err - } - for _, component := range components { - if component.Container != nil { - component.Container.Endpoints = addEndpoints(component.Container.Endpoints, endpoints) - d.Data.UpdateComponent(component) - } - } return d.WriteYamlDevfile() } -// RemovePorts removes all container endpoints from a devfile -func (d DevfileObj) RemovePorts() error { - components, err := d.Data.GetComponents(common.DevfileOptions{}) +// RemovePorts accepts a map of container name mapped to an array of port numbers to be removed; +// it removes the container endpoints with the specified port numbers of the specified container, and writes to the devfile +// Example of containerPortsMap: {"runtime": {"8080", "9000"}, "wildfly": {"12956"}} +func (d DevfileObj) RemovePorts(containerPortsMap map[string][]string) error { + err := d.Data.RemovePorts(containerPortsMap) if err != nil { return err } - for _, component := range components { - if component.Container != nil { - component.Container.Endpoints = []v1.Endpoint{} - d.Data.UpdateComponent(component) - } - } return d.WriteYamlDevfile() } @@ -146,152 +117,3 @@ func (d DevfileObj) GetMemory() string { func (d DevfileObj) GetMetadataName() string { return d.Data.GetMetadata().Name } - -func portsToEndpoints(ports ...string) ([]v1.Endpoint, error) { - var endpoints []v1.Endpoint - conPorts, err := GetContainerPortsFromStrings(ports) - if err != nil { - return nil, err - } - for _, port := range conPorts { - - endpoint := v1.Endpoint{ - Name: fmt.Sprintf("port-%d-%s", port.ContainerPort, strings.ToLower(string(port.Protocol))), - TargetPort: int(port.ContainerPort), - Protocol: v1.EndpointProtocol(strings.ToLower(string(port.Protocol))), - } - endpoints = append(endpoints, endpoint) - } - return endpoints, nil - -} - -func addEndpoints(current []v1.Endpoint, other []v1.Endpoint) []v1.Endpoint { - newList := make([]v1.Endpoint, len(current)) - copy(newList, current) - for _, ep := range other { - present := false - - for _, presentep := range newList { - - protocol := presentep.Protocol - if protocol == "" { - // endpoint protocol default value is http - protocol = "http" - } - // if the target port and protocol match, we add a case where the protocol is not provided and hence we assume that to be "tcp" - if presentep.TargetPort == ep.TargetPort && (ep.Protocol == protocol) { - present = true - break - } - } - if !present { - newList = append(newList, ep) - } - } - - return newList -} - -// GetContainerPortsFromStrings generates ContainerPort values from the array of string port values -// ports is the array containing the string port values -func GetContainerPortsFromStrings(ports []string) ([]corev1.ContainerPort, error) { - var containerPorts []corev1.ContainerPort - for _, port := range ports { - splits := strings.Split(port, "/") - if len(splits) < 1 || len(splits) > 2 { - return nil, fmt.Errorf("unable to parse the port string %s", port) - } - - portNumberI64, err := strconv.ParseInt(splits[0], 10, 32) - if err != nil { - return nil, fmt.Errorf("invalid port number %s", splits[0]) - } - portNumber := int32(portNumberI64) - - var portProto corev1.Protocol - if len(splits) == 2 { - switch strings.ToUpper(splits[1]) { - case "TCP": - portProto = corev1.ProtocolTCP - case "UDP": - portProto = corev1.ProtocolUDP - default: - return nil, fmt.Errorf("invalid port protocol %s", splits[1]) - } - } else { - portProto = corev1.ProtocolTCP - } - - port := corev1.ContainerPort{ - Name: fmt.Sprintf("%d-%s", portNumber, strings.ToLower(string(portProto))), - ContainerPort: portNumber, - Protocol: portProto, - } - containerPorts = append(containerPorts, port) - } - return containerPorts, nil -} - -// RemoveEnvVarsFromList removes the env variables based on the keys provided -// and returns a new EnvVarList -func RemoveEnvVarsFromList(envVarList []v1.EnvVar, keys []string) ([]v1.EnvVar, error) { - // convert the envVarList map to an array to easily search for env var(s) - // to remove from the component - envVarListArray := []string{} - for _, env := range envVarList { - envVarListArray = append(envVarListArray, env.Name) - } - - // now check if the environment variable(s) requested for removal exists in - // the env vars set for the component by odo - for _, key := range keys { - if !InArray(envVarListArray, key) { - return nil, fmt.Errorf("unable to find environment variable %s in the component", key) - } - } - - // finally, let's remove the environment variables(s) requested by the user - newEnvVarList := []v1.EnvVar{} - for _, envVar := range envVarList { - // if the env is in the keys we skip it - if InArray(keys, envVar.Name) { - continue - } - newEnvVarList = append(newEnvVarList, envVar) - } - return newEnvVarList, nil -} - -// Merge merges the other EnvVarlist with keeping last value for duplicate EnvVars -// and returns a new EnvVarList -func Merge(original []v1.EnvVar, other []v1.EnvVar) []v1.EnvVar { - - var dedupNewEvl []v1.EnvVar - newEvl := append(original, other...) - uniqueMap := make(map[string]string) - // last value will be kept in case of duplicate env vars - for _, envVar := range newEvl { - uniqueMap[envVar.Name] = envVar.Value - } - - for key, value := range uniqueMap { - dedupNewEvl = append(dedupNewEvl, v1.EnvVar{ - Name: key, - Value: value, - }) - } - - return dedupNewEvl - -} - -// In checks if the value is in the array -func InArray(arr []string, value string) bool { - for _, item := range arr { - if item == value { - return true - } - } - return false -} diff --git a/pkg/devfile/parser/configurables_test.go b/pkg/devfile/parser/configurables_test.go deleted file mode 100644 index 899a869e..00000000 --- a/pkg/devfile/parser/configurables_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package parser - -import ( - "reflect" - "testing" - - v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" - devfileCtx "github.com/devfile/library/pkg/devfile/parser/context" - v2 "github.com/devfile/library/pkg/devfile/parser/data/v2" - "github.com/devfile/library/pkg/testingutil/filesystem" - "github.com/kylelemons/godebug/pretty" -) - -func TestAddAndRemoveEnvVars(t *testing.T) { - - // Use fakeFs - fs := filesystem.NewFakeFs() - - tests := []struct { - name string - listToAdd []v1.EnvVar - listToRemove []string - currentDevfile DevfileObj - wantDevFile DevfileObj - }{ - { - name: "add and remove env vars", - listToAdd: []v1.EnvVar{ - { - Name: "DATABASE_PASSWORD", - Value: "苦痛", - }, - { - Name: "PORT", - Value: "3003", - }, - { - Name: "PORT", - Value: "4342", - }, - }, - listToRemove: []string{ - "PORT", - }, - currentDevfile: testDevfileObj(fs), - wantDevFile: DevfileObj{ - Ctx: devfileCtx.FakeContext(fs, OutputDevfileYamlPath), - Data: &v2.DevfileV2{ - Devfile: v1.Devfile{ - DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ - DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ - Commands: []v1.Command{ - { - Id: "devbuild", - CommandUnion: v1.CommandUnion{ - Exec: &v1.ExecCommand{ - WorkingDir: "/projects/nodejs-starter", - }, - }, - }, - }, - Components: []v1.Component{ - { - Name: "runtime", - ComponentUnion: v1.ComponentUnion{ - Container: &v1.ContainerComponent{ - Container: v1.Container{ - Image: "quay.io/nodejs-12", - Env: []v1.EnvVar{ - { - Name: "DATABASE_PASSWORD", - Value: "苦痛", - }, - }, - }, - Endpoints: []v1.Endpoint{ - { - Name: "port-3030", - TargetPort: 3000, - }, - }, - }, - }, - }, - { - Name: "loadbalancer", - ComponentUnion: v1.ComponentUnion{ - Container: &v1.ContainerComponent{ - Container: v1.Container{ - Image: "quay.io/nginx", - Env: []v1.EnvVar{ - { - Name: "DATABASE_PASSWORD", - Value: "苦痛", - }, - }, - }, - }, - }, - }, - }, - Events: &v1.Events{ - DevWorkspaceEvents: v1.DevWorkspaceEvents{ - PostStop: []string{"post-stop"}, - }, - }, - Projects: []v1.Project{ - { - ClonePath: "/projects", - Name: "nodejs-starter-build", - }, - }, - StarterProjects: []v1.StarterProject{ - { - SubDir: "/projects", - Name: "starter-project-2", - }, - }, - }, - }, - }, - }, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - - err := tt.currentDevfile.AddEnvVars(tt.listToAdd) - - if err != nil { - t.Errorf("TestAddAndRemoveEnvVars() unexpected error while adding env vars %+v", err.Error()) - } - - err = tt.currentDevfile.RemoveEnvVars(tt.listToRemove) - - if err != nil { - t.Errorf("TestAddAndRemoveEnvVars() unexpected error while removing env vars %+v", err.Error()) - } - - if !reflect.DeepEqual(tt.currentDevfile.Data, tt.wantDevFile.Data) { - t.Errorf("TestAddAndRemoveEnvVars() error: wanted: %v, got: %v, difference at %v", tt.wantDevFile, tt.currentDevfile, pretty.Compare(tt.currentDevfile.Data, tt.wantDevFile.Data)) - } - - }) - } - -} - -func testDevfileObj(fs filesystem.Filesystem) DevfileObj { - return DevfileObj{ - Ctx: devfileCtx.FakeContext(fs, OutputDevfileYamlPath), - Data: &v2.DevfileV2{ - Devfile: v1.Devfile{ - DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ - DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ - Commands: []v1.Command{ - { - Id: "devbuild", - CommandUnion: v1.CommandUnion{ - Exec: &v1.ExecCommand{ - WorkingDir: "/projects/nodejs-starter", - }, - }, - }, - }, - Components: []v1.Component{ - { - Name: "runtime", - ComponentUnion: v1.ComponentUnion{ - Container: &v1.ContainerComponent{ - Container: v1.Container{ - Image: "quay.io/nodejs-12", - }, - Endpoints: []v1.Endpoint{ - { - Name: "port-3030", - TargetPort: 3000, - }, - }, - }, - }, - }, - { - Name: "loadbalancer", - ComponentUnion: v1.ComponentUnion{ - Container: &v1.ContainerComponent{ - Container: v1.Container{ - Image: "quay.io/nginx", - }, - }, - }, - }, - }, - Events: &v1.Events{ - DevWorkspaceEvents: v1.DevWorkspaceEvents{ - PostStop: []string{"post-stop"}, - }, - }, - Projects: []v1.Project{ - { - ClonePath: "/projects", - Name: "nodejs-starter-build", - }, - }, - StarterProjects: []v1.StarterProject{ - { - SubDir: "/projects", - Name: "starter-project-2", - }, - }, - }, - }, - }, - }, - } -} diff --git a/pkg/devfile/parser/data/interface.go b/pkg/devfile/parser/data/interface.go index 5721ca1d..743077e8 100644 --- a/pkg/devfile/parser/data/interface.go +++ b/pkg/devfile/parser/data/interface.go @@ -82,4 +82,10 @@ type DevfileData interface { GetDevfileContainerComponents(common.DevfileOptions) ([]v1.Component, error) GetDevfileVolumeComponents(common.DevfileOptions) ([]v1.Component, error) + + // containers + RemoveEnvVars(containerEnvMap map[string][]string) error + SetPorts(containerPortsMap map[string][]string) error + AddEnvVars(containerEnvMap map[string][]v1.EnvVar) error + RemovePorts(containerPortsMap map[string][]string) error } diff --git a/pkg/devfile/parser/data/mock_interface.go b/pkg/devfile/parser/data/mock_interface.go index 89951d41..fb21d6a1 100644 --- a/pkg/devfile/parser/data/mock_interface.go +++ b/pkg/devfile/parser/data/mock_interface.go @@ -550,3 +550,59 @@ func (mr *MockDevfileDataMockRecorder) UpdateStarterProject(project interface{}) mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStarterProject", reflect.TypeOf((*MockDevfileData)(nil).UpdateStarterProject), project) } + +// RemoveEnvVars mocks base method +func (m *MockDevfileData) RemoveEnvVars(containerEnvMap map[string][]string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveEnvVars", containerEnvMap) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveEnvVars indicates an expected call of RemoveEnvVars +func (mr *MockDevfileDataMockRecorder) RemoveEnvVars(containerEnvMap interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveEnvVars", reflect.TypeOf((*MockDevfileData)(nil).RemoveEnvVars), containerEnvMap) +} + +// SetPorts mocks base method +func (m *MockDevfileData) SetPorts(containerPortsMap map[string][]string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetPorts", containerPortsMap) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetPorts indicates an expected call of SetPorts +func (mr *MockDevfileDataMockRecorder) SetPorts(containerPortsMap interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPorts", reflect.TypeOf((*MockDevfileData)(nil).SetPorts), containerPortsMap) +} + +// AddEnvVars mocks base method +func (m *MockDevfileData) AddEnvVars(containerEnvMap map[string][]v1alpha2.EnvVar) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddEnvVars", containerEnvMap) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddEnvVars indicates an expected call of AddEnvVars +func (mr *MockDevfileDataMockRecorder) AddEnvVars(containerEnvMap interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddEnvVars", reflect.TypeOf((*MockDevfileData)(nil).AddEnvVars), containerEnvMap) +} + +// RemovePorts mocks base method +func (m *MockDevfileData) RemovePorts(containerPortsMap map[string][]string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemovePorts", containerPortsMap) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemovePorts indicates an expected call of RemovePorts +func (mr *MockDevfileDataMockRecorder) RemovePorts(containerPortsMap interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePorts", reflect.TypeOf((*MockDevfileData)(nil).RemovePorts), containerPortsMap) +} diff --git a/pkg/devfile/parser/data/v2/containers.go b/pkg/devfile/parser/data/v2/containers.go new file mode 100644 index 00000000..7671f180 --- /dev/null +++ b/pkg/devfile/parser/data/v2/containers.go @@ -0,0 +1,279 @@ +package v2 + +import ( + "fmt" + "strconv" + "strings" + + v1alpha2 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + corev1 "k8s.io/api/core/v1" +) + +// AddEnvVars accepts a map of container name mapped to an array of the env vars to be set; +// it adds the envirnoment variables to a given container name of the DevfileV2 object +// Example of containerEnvMap : {"runtime": {{Name: "Foo", Value: "Bar"}}} +func (d *DevfileV2) AddEnvVars(containerEnvMap map[string][]v1alpha2.EnvVar) error { + components, err := d.GetComponents(common.DevfileOptions{}) + if err != nil { + return err + } + for _, component := range components { + if component.Container != nil { + component.Container.Env = merge(component.Container.Env, containerEnvMap[component.Name]) + d.UpdateComponent(component) + } + } + return nil +} + +// RemoveEnvVars accepts a map of container name mapped to an array of environment variables to be removed; +// it removes the env vars from the specified container name of the DevfileV2 object +func (d *DevfileV2) RemoveEnvVars(containerEnvMap map[string][]string) error { + components, err := d.GetComponents(common.DevfileOptions{}) + if err != nil { + return err + } + for _, component := range components { + if component.Container != nil { + component.Container.Env, err = removeEnvVarsFromList(component.Container.Env, containerEnvMap[component.Name]) + if err != nil { + return err + } + d.UpdateComponent(component) + } + } + return nil +} + +// SetPorts accepts a map of container name mapped to an array of port numbers to be set; +// it converts ports to endpoints, sets the endpoint to a given container name of the DevfileV2 object +// Example of containerPortsMap: {"runtime": {"8080", "9000"}, "wildfly": {"12956"}} +func (d *DevfileV2) SetPorts(containerPortsMap map[string][]string) error { + components, err := d.GetComponents(common.DevfileOptions{}) + if err != nil { + return err + } + for _, component := range components { + endpoints, err := portsToEndpoints(containerPortsMap[component.Name]...) + if err != nil { + return err + } + if component.Container != nil { + component.Container.Endpoints = addEndpoints(component.Container.Endpoints, endpoints) + d.UpdateComponent(component) + } + } + + return nil +} + +// RemovePorts accepts a map of container name mapped to an array of port numbers to be removed; +// it removes the container endpoints with the specified port numbers of the specified container of the DevfileV2 object +// Example of containerPortsMap: {"runtime": {"8080", "9000"}, "wildfly": {"12956"}} +func (d *DevfileV2) RemovePorts(containerPortsMap map[string][]string) error { + components, err := d.GetComponents(common.DevfileOptions{}) + if err != nil { + return err + } + for _, component := range components { + if component.Container != nil { + component.Container.Endpoints, err = removePortsFromList(component.Container.Endpoints, containerPortsMap[component.Name]) + if err != nil { + return err + } + d.UpdateComponent(component) + } + } + + return nil +} + +// removeEnvVarsFromList removes the env variables based on the keys provided +// and returns a new EnvVarList +func removeEnvVarsFromList(envVarList []v1alpha2.EnvVar, keys []string) ([]v1alpha2.EnvVar, error) { + // convert the array of envVarList to a map such that it can easily search for env var(s) + // to remove from the component + envVarListMap := map[string]bool{} + for _, env := range envVarList { + if !envVarListMap[env.Name] { + envVarListMap[env.Name] = true + } + } + + // convert the array of keys to a map so that it can do a fast search for environment variable(s) + // to remove from the component + envVarToBeRemoved := map[string]bool{} + // now check if the environment variable(s) requested for removal exists in + // the env vars currently set in the component + // if an env var requested for removal is not currently set, then raise an error + // else add the env var to the envVarToBeRemoved map + for _, key := range keys { + if !envVarListMap[key] { + return envVarList, fmt.Errorf("unable to find environment variable %s in the component", key) + } + envVarToBeRemoved[key] = true + } + + // finally, let's remove the environment variables(s) requested by the user + newEnvVarList := []v1alpha2.EnvVar{} + for _, envVar := range envVarList { + // if the env is in the keys(env var(s) to be removed), we skip it + if envVarToBeRemoved[envVar.Name] { + continue + } + newEnvVarList = append(newEnvVarList, envVar) + } + return newEnvVarList, nil +} + +// removePortsFromList removes the ports from a given Endpoint list based on the provided port numbers +// and returns a new list of Endpoint +func removePortsFromList(endpoints []v1alpha2.Endpoint, ports []string) ([]v1alpha2.Endpoint, error) { + // convert the array of Endpoint to a map such that it can easily search for port(s) + // to remove from the component + portInEndpoint := map[string]bool{} + for _, ep := range endpoints { + port := strconv.Itoa(ep.TargetPort) + if !portInEndpoint[port] { + portInEndpoint[port] = true + } + } + + // convert the array of ports to a map so that it can do a fast search for port(s) + // to remove from the component + portsToBeRemoved := map[string]bool{} + + // now check if the port(s) requested for removal exists in + // the ports currently present in the component; + // if a port requested for removal is not currently present, then raise an error + // else add the port to the portsToBeRemoved map + for _, port := range ports { + if !portInEndpoint[port] { + return endpoints, fmt.Errorf("unable to find port %q in the component", port) + } + portsToBeRemoved[port] = true + } + + // finally, let's remove the port(s) requested by the user + newEndpointsList := []v1alpha2.Endpoint{} + for _, ep := range endpoints { + // if the port is in the port(s)(to be removed), we skip it + if portsToBeRemoved[strconv.Itoa(ep.TargetPort)] { + continue + } + newEndpointsList = append(newEndpointsList, ep) + } + return newEndpointsList, nil +} + +// merge merges the other EnvVarlist with keeping last value for duplicate EnvVars +// and returns a new EnvVarList +func merge(original []v1alpha2.EnvVar, other []v1alpha2.EnvVar) []v1alpha2.EnvVar { + + var dedupNewEvl []v1alpha2.EnvVar + newEvl := append(original, other...) + uniqueMap := make(map[string]string) + // last value will be kept in case of duplicate env vars + for _, envVar := range newEvl { + uniqueMap[envVar.Name] = envVar.Value + } + + for key, value := range uniqueMap { + dedupNewEvl = append(dedupNewEvl, v1alpha2.EnvVar{ + Name: key, + Value: value, + }) + } + + return dedupNewEvl + +} + +// portsToEndpoints converts an array of ports to an array of v1alpha2.Endpoint +func portsToEndpoints(ports ...string) ([]v1alpha2.Endpoint, error) { + var endpoints []v1alpha2.Endpoint + conPorts, err := getContainerPortsFromStrings(ports) + if err != nil { + return nil, err + } + for _, port := range conPorts { + + endpoint := v1alpha2.Endpoint{ + Name: fmt.Sprintf("port-%d-%s", port.ContainerPort, strings.ToLower(string(port.Protocol))), + TargetPort: int(port.ContainerPort), + Protocol: v1alpha2.EndpointProtocol(strings.ToLower(string(port.Protocol))), + } + endpoints = append(endpoints, endpoint) + } + return endpoints, nil + +} + +// addEndpoints appends two arrays of v1alpha2.Endpoint objects +func addEndpoints(current []v1alpha2.Endpoint, other []v1alpha2.Endpoint) []v1alpha2.Endpoint { + newList := make([]v1alpha2.Endpoint, len(current)) + copy(newList, current) + for _, ep := range other { + present := false + + for _, presentep := range newList { + + protocol := presentep.Protocol + if protocol == "" { + // endpoint protocol default value is http + protocol = "http" + } + // if the target port and protocol match, we add a case where the protocol is not provided and hence we assume that to be "tcp" + if presentep.TargetPort == ep.TargetPort && (ep.Protocol == protocol) { + present = true + break + } + } + if !present { + newList = append(newList, ep) + } + } + + return newList +} + +// getContainerPortsFromStrings generates ContainerPort values from the array of string port values +// ports is the array containing the string port values +func getContainerPortsFromStrings(ports []string) ([]corev1.ContainerPort, error) { + var containerPorts []corev1.ContainerPort + for _, port := range ports { + splits := strings.Split(port, "/") + if len(splits) < 1 || len(splits) > 2 { + return nil, fmt.Errorf("unable to parse the port string %s", port) + } + + portNumberI64, err := strconv.ParseInt(splits[0], 10, 32) + if err != nil { + return nil, fmt.Errorf("invalid port number %s", splits[0]) + } + portNumber := int32(portNumberI64) + + var portProto corev1.Protocol + if len(splits) == 2 { + switch strings.ToUpper(splits[1]) { + case "TCP": + portProto = corev1.ProtocolTCP + case "UDP": + portProto = corev1.ProtocolUDP + default: + return nil, fmt.Errorf("invalid port protocol %s", splits[1]) + } + } else { + portProto = corev1.ProtocolTCP + } + + port := corev1.ContainerPort{ + Name: fmt.Sprintf("%d-%s", portNumber, strings.ToLower(string(portProto))), + ContainerPort: portNumber, + Protocol: portProto, + } + containerPorts = append(containerPorts, port) + } + return containerPorts, nil +} diff --git a/pkg/devfile/parser/data/v2/containers_test.go b/pkg/devfile/parser/data/v2/containers_test.go new file mode 100644 index 00000000..5f79e23b --- /dev/null +++ b/pkg/devfile/parser/data/v2/containers_test.go @@ -0,0 +1,662 @@ +package v2 + +import ( + "reflect" + "testing" + + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/kylelemons/godebug/pretty" +) + +func TestAddEnvVars(t *testing.T) { + + tests := []struct { + name string + listToAdd map[string][]v1alpha2.EnvVar + currentDevfile *DevfileV2 + wantDevFile *DevfileV2 + }{ + { + name: "add env vars", + listToAdd: map[string][]v1alpha2.EnvVar{ + "loadbalancer": { + { + Name: "DATABASE_PASSWORD", + Value: "苦痛", + }, + }, + }, + currentDevfile: testDevfileData(), + wantDevFile: &DevfileV2{ + Devfile: v1alpha2.Devfile{ + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Commands: []v1alpha2.Command{ + { + Id: "devbuild", + CommandUnion: v1alpha2.CommandUnion{ + Exec: &v1alpha2.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1alpha2.Component{ + { + Name: "runtime", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nodejs-12", + Env: []v1alpha2.EnvVar{ + { + Name: "DATABASE_PASSWORD", + Value: "苦痛", + }, + }, + }, + Endpoints: []v1alpha2.Endpoint{ + { + Name: "port-3030", + TargetPort: 3030, + }, + }, + }, + }, + }, + { + Name: "loadbalancer", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nginx", + Env: []v1alpha2.EnvVar{ + { + Name: "DATABASE_PASSWORD", + Value: "苦痛", + }, + }, + }, + }, + }, + }, + }, + Events: &v1alpha2.Events{ + DevWorkspaceEvents: v1alpha2.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1alpha2.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + StarterProjects: []v1alpha2.StarterProject{ + { + SubDir: "/projects", + Name: "starter-project-2", + }, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + err := tt.currentDevfile.AddEnvVars(tt.listToAdd) + + if err != nil { + t.Errorf("TestAddAndRemoveEnvVars() unexpected error while adding env vars %+v", err.Error()) + } + + if !reflect.DeepEqual(tt.currentDevfile, tt.wantDevFile) { + t.Errorf("TestAddAndRemoveEnvVars() error: wanted: %v, got: %v, difference at %v", tt.wantDevFile, tt.currentDevfile, pretty.Compare(tt.currentDevfile, tt.wantDevFile)) + } + + }) + } + +} + +func TestRemoveEnvVars(t *testing.T) { + + tests := []struct { + name string + listToRemove map[string][]string + currentDevfile *DevfileV2 + wantDevFile *DevfileV2 + wantRemoveErr bool + }{ + { + name: "remove env vars", + listToRemove: map[string][]string{ + "runtime": { + "DATABASE_PASSWORD", + }, + }, + currentDevfile: testDevfileData(), + wantDevFile: &DevfileV2{ + Devfile: v1alpha2.Devfile{ + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Commands: []v1alpha2.Command{ + { + Id: "devbuild", + CommandUnion: v1alpha2.CommandUnion{ + Exec: &v1alpha2.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1alpha2.Component{ + { + Name: "runtime", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nodejs-12", + Env: []v1alpha2.EnvVar{}, + }, + Endpoints: []v1alpha2.Endpoint{ + { + Name: "port-3030", + TargetPort: 3030, + }, + }, + }, + }, + }, + { + Name: "loadbalancer", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nginx", + Env: []v1alpha2.EnvVar{}, + }, + }, + }, + }, + }, + Events: &v1alpha2.Events{ + DevWorkspaceEvents: v1alpha2.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1alpha2.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + StarterProjects: []v1alpha2.StarterProject{ + { + SubDir: "/projects", + Name: "starter-project-2", + }, + }, + }, + }, + }, + }, + }, + { + name: "remove non-existent env vars", + listToRemove: map[string][]string{ + "runtime": { + "NON_EXISTENT_KEY", + }, + }, + currentDevfile: testDevfileData(), + wantDevFile: &DevfileV2{ + Devfile: v1alpha2.Devfile{ + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Commands: []v1alpha2.Command{ + { + Id: "devbuild", + CommandUnion: v1alpha2.CommandUnion{ + Exec: &v1alpha2.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1alpha2.Component{ + { + Name: "runtime", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nodejs-12", + Env: []v1alpha2.EnvVar{ + { + Name: "DATABASE_PASSWORD", + Value: "苦痛", + }, + }, + }, + Endpoints: []v1alpha2.Endpoint{ + { + Name: "port-3030", + TargetPort: 3030, + }, + }, + }, + }, + }, + { + Name: "loadbalancer", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nginx", + }, + }, + }, + }, + }, + Events: &v1alpha2.Events{ + DevWorkspaceEvents: v1alpha2.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1alpha2.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + StarterProjects: []v1alpha2.StarterProject{ + { + SubDir: "/projects", + Name: "starter-project-2", + }, + }, + }, + }, + }, + }, + wantRemoveErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.currentDevfile.RemoveEnvVars(tt.listToRemove) + + if (err != nil) != tt.wantRemoveErr { + t.Errorf("TestAddAndRemoveEnvVars() unexpected error while removing env vars %+v", err.Error()) + } + + if !reflect.DeepEqual(tt.currentDevfile, tt.wantDevFile) { + t.Errorf("TestAddAndRemoveEnvVars() error: wanted: %v, got: %v, difference at %v", tt.wantDevFile, tt.currentDevfile, pretty.Compare(tt.currentDevfile, tt.wantDevFile)) + } + + }) + } + +} + +func TestSetPorts(t *testing.T) { + + tests := []struct { + name string + portToSet map[string][]string + currentDevfile *DevfileV2 + wantDevFile *DevfileV2 + }{ + { + name: "set ports", + portToSet: map[string][]string{"runtime": {"9000"}, "loadbalancer": {"8000"}}, + currentDevfile: testDevfileData(), + wantDevFile: &DevfileV2{ + Devfile: v1alpha2.Devfile{ + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Commands: []v1alpha2.Command{ + { + Id: "devbuild", + CommandUnion: v1alpha2.CommandUnion{ + Exec: &v1alpha2.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1alpha2.Component{ + { + Name: "runtime", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nodejs-12", + Env: []v1alpha2.EnvVar{ + { + Name: "DATABASE_PASSWORD", + Value: "苦痛", + }, + }, + }, + Endpoints: []v1alpha2.Endpoint{ + { + Name: "port-3030", + TargetPort: 3030, + }, + { + Name: "port-9000-tcp", + TargetPort: 9000, + Protocol: "tcp", + }, + }, + }, + }, + }, + { + Name: "loadbalancer", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nginx", + }, + Endpoints: []v1alpha2.Endpoint{ + { + Name: "port-8000-tcp", + TargetPort: 8000, + Protocol: "tcp", + }, + }, + }, + }, + }, + }, + Events: &v1alpha2.Events{ + DevWorkspaceEvents: v1alpha2.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1alpha2.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + StarterProjects: []v1alpha2.StarterProject{ + { + SubDir: "/projects", + Name: "starter-project-2", + }, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + err := tt.currentDevfile.SetPorts(tt.portToSet) + + if err != nil { + t.Errorf("TestSetAndRemovePorts() unexpected error while adding ports %+v", err.Error()) + } + + if !reflect.DeepEqual(tt.currentDevfile, tt.wantDevFile) { + t.Errorf("TestSetAndRemovePorts() error: wanted: %v, got: %v, difference at %v", tt.wantDevFile, tt.currentDevfile, pretty.Compare(tt.currentDevfile, tt.wantDevFile)) + } + }) + } + +} + +func TestRemovePorts(t *testing.T) { + + tests := []struct { + name string + portToRemove map[string][]string + currentDevfile *DevfileV2 + wantDevFile *DevfileV2 + wantRemoveErr bool + }{ + { + name: "remove ports", + portToRemove: map[string][]string{"runtime": {"3030"}}, + currentDevfile: testDevfileData(), + wantDevFile: &DevfileV2{ + Devfile: v1alpha2.Devfile{ + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Commands: []v1alpha2.Command{ + { + Id: "devbuild", + CommandUnion: v1alpha2.CommandUnion{ + Exec: &v1alpha2.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1alpha2.Component{ + { + Name: "runtime", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nodejs-12", + Env: []v1alpha2.EnvVar{ + { + Name: "DATABASE_PASSWORD", + Value: "苦痛", + }, + }, + }, + Endpoints: []v1alpha2.Endpoint{}, + }, + }, + }, + { + Name: "loadbalancer", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nginx", + }, + Endpoints: []v1alpha2.Endpoint{}, + }, + }, + }, + }, + Events: &v1alpha2.Events{ + DevWorkspaceEvents: v1alpha2.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1alpha2.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + StarterProjects: []v1alpha2.StarterProject{ + { + SubDir: "/projects", + Name: "starter-project-2", + }, + }, + }, + }, + }, + }, + }, + { + name: "remove non-existent ports", + portToRemove: map[string][]string{"runtime": {"3050"}}, + currentDevfile: testDevfileData(), + wantDevFile: &DevfileV2{ + Devfile: v1alpha2.Devfile{ + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Commands: []v1alpha2.Command{ + { + Id: "devbuild", + CommandUnion: v1alpha2.CommandUnion{ + Exec: &v1alpha2.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1alpha2.Component{ + { + Name: "runtime", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nodejs-12", + Env: []v1alpha2.EnvVar{ + { + Name: "DATABASE_PASSWORD", + Value: "苦痛", + }, + }, + }, + Endpoints: []v1alpha2.Endpoint{ + { + Name: "port-3030", + TargetPort: 3030, + }, + }, + }, + }, + }, + { + Name: "loadbalancer", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nginx", + }, + }, + }, + }, + }, + Events: &v1alpha2.Events{ + DevWorkspaceEvents: v1alpha2.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1alpha2.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + StarterProjects: []v1alpha2.StarterProject{ + { + SubDir: "/projects", + Name: "starter-project-2", + }, + }, + }, + }, + }, + }, + wantRemoveErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.currentDevfile.RemovePorts(tt.portToRemove) + + if (err != nil) != tt.wantRemoveErr { + t.Errorf("TestSetAndRemovePorts() unexpected error while removing ports %+v", err.Error()) + } + + if !reflect.DeepEqual(tt.currentDevfile, tt.wantDevFile) { + t.Errorf("TestSetAndRemovePorts() error: wanted: %v, got: %v, difference at %v", tt.wantDevFile, tt.currentDevfile, pretty.Compare(tt.currentDevfile, tt.wantDevFile)) + } + }) + } + +} + +func testDevfileData() *DevfileV2 { + return &DevfileV2{ + Devfile: v1alpha2.Devfile{ + DevWorkspaceTemplateSpec: v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Commands: []v1alpha2.Command{ + { + Id: "devbuild", + CommandUnion: v1alpha2.CommandUnion{ + Exec: &v1alpha2.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1alpha2.Component{ + { + Name: "runtime", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nodejs-12", + Env: []v1alpha2.EnvVar{ + { + Name: "DATABASE_PASSWORD", + Value: "苦痛", + }, + }, + }, + Endpoints: []v1alpha2.Endpoint{ + { + Name: "port-3030", + TargetPort: 3030, + }, + }, + }, + }, + }, + { + Name: "loadbalancer", + ComponentUnion: v1alpha2.ComponentUnion{ + Container: &v1alpha2.ContainerComponent{ + Container: v1alpha2.Container{ + Image: "quay.io/nginx", + }, + }, + }, + }, + }, + Events: &v1alpha2.Events{ + DevWorkspaceEvents: v1alpha2.DevWorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1alpha2.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + StarterProjects: []v1alpha2.StarterProject{ + { + SubDir: "/projects", + Name: "starter-project-2", + }, + }, + }, + }, + }, + } +}