diff --git a/.ibm/images/Dockerfile b/.ibm/images/Dockerfile index 9e51ed93897..817cd63e628 100644 --- a/.ibm/images/Dockerfile +++ b/.ibm/images/Dockerfile @@ -1,8 +1,6 @@ FROM golang:1.17 RUN curl -fsSL https://clis.cloud.ibm.com/install/linux | sh && \ - ibmcloud plugin install -f cloud-object-storage && \ - ibmcloud plugin install -f kubernetes-service && \ curl -sLO https://github.com/cli/cli/releases/download/v2.1.0/gh_2.1.0_linux_amd64.deb && \ apt install ./gh_2.1.0_linux_amd64.deb && \ curl -sLO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl && \ @@ -15,4 +13,11 @@ RUN curl -fsSL https://clis.cloud.ibm.com/install/linux | sh && \ apt-get install -y sshpass && \ rm -rf /var/lib/apt/lists/* -RUN go get github.com/kadel/odo-robot@965ea0dd848856691bfc76e6824a8b787b950045 +# Create non-root user and associated home directory +RUN useradd -u 2001 --create-home tester +# Change to non-root privilege +USER tester + +RUN go get github.com/kadel/odo-robot@965ea0dd848856691bfc76e6824a8b787b950045 && \ + ibmcloud plugin install -f cloud-object-storage && \ + ibmcloud plugin install -f kubernetes-service diff --git a/docs/website/versioned_docs/version-3.0.0/command-reference/dev.md b/docs/website/versioned_docs/version-3.0.0/command-reference/dev.md index 6df356c3a21..3cf323b429a 100644 --- a/docs/website/versioned_docs/version-3.0.0/command-reference/dev.md +++ b/docs/website/versioned_docs/version-3.0.0/command-reference/dev.md @@ -226,3 +226,22 @@ components: memoryLimit: 1024Mi mountSources: true ``` + +### State file + +When the command `odo dev` is executed, the state of the command is saved in the file `./.odo/devstate.json`. + +This state file contains the currently forwarded ports. + +``` +{ + "forwardedPorts": [ + { + "containerName": "runtime", + "localAddress": "127.0.0.1", + "localPort": 40001, + "containerPort": 3000 + } + ] +} +``` diff --git a/pkg/api/component.go b/pkg/api/component.go index a1e563f2753..4ad4d62215c 100644 --- a/pkg/api/component.go +++ b/pkg/api/component.go @@ -19,6 +19,7 @@ type Component struct { type ForwardedPort struct { ContainerName string `json:"containerName"` + LocalAddress string `json:"localAddress"` LocalPort int `json:"localPort"` ContainerPort int `json:"containerPort"` } diff --git a/pkg/odo/cli/dev/dev.go b/pkg/odo/cli/dev/dev.go index 48d0316555e..4c8f148919d 100644 --- a/pkg/odo/cli/dev/dev.go +++ b/pkg/odo/cli/dev/dev.go @@ -178,14 +178,22 @@ func (o *DevOptions) Validate() error { return nil } -func (o *DevOptions) Run(ctx context.Context) error { - var err error - var platformContext = kubernetes.KubernetesContext{ - Namespace: o.Context.GetProject(), - } - var path = filepath.Dir(o.Context.EnvSpecificInfo.GetDevfilePath()) - devfileName := o.EnvSpecificInfo.GetDevfileObj().GetMetadataName() - namespace := o.GetProject() +func (o *DevOptions) Run(ctx context.Context) (err error) { + var ( + devFileObj = o.Context.EnvSpecificInfo.GetDevfileObj() + platformContext = kubernetes.KubernetesContext{ + Namespace: o.Context.GetProject(), + } + path = filepath.Dir(o.Context.EnvSpecificInfo.GetDevfilePath()) + devfileName = devFileObj.GetMetadataName() + namespace = o.GetProject() + ) + + defer func() { + if err != nil { + _ = o.clientset.WatchClient.Cleanup(devFileObj, log.GetStdout()) + } + }() // Output what the command is doing / information log.Title("Developing using the "+devfileName+" Devfile", @@ -193,13 +201,13 @@ func (o *DevOptions) Run(ctx context.Context) error { "odo version: "+version.VERSION) log.Section("Deploying to the cluster in developer mode") - err = o.clientset.DevClient.Start(o.Context.EnvSpecificInfo.GetDevfileObj(), platformContext, o.ignorePaths, path, o.debugFlag) + err = o.clientset.DevClient.Start(devFileObj, platformContext, o.ignorePaths, path, o.debugFlag) if err != nil { return err } // get the endpoint/port information for containers in devfile and setup port-forwarding - containers, err := o.Context.EnvSpecificInfo.GetDevfileObj().Data.GetComponents(parsercommon.DevfileOptions{ + containers, err := devFileObj.Data.GetComponents(parsercommon.DevfileOptions{ ComponentOptions: parsercommon.ComponentOptions{ComponentType: v1alpha2.ContainerComponentType}, }) if err != nil { @@ -216,7 +224,7 @@ func (o *DevOptions) Run(ctx context.Context) error { for _, v1 := range portPairs { portPairsSlice = append(portPairsSlice, v1...) } - pod, err := o.clientset.KubernetesClient.GetPodUsingComponentName(o.Context.EnvSpecificInfo.GetDevfileObj().GetMetadataName()) + pod, err := o.clientset.KubernetesClient.GetPodUsingComponentName(devFileObj.GetMetadataName()) if err != nil { return err } @@ -224,7 +232,7 @@ func (o *DevOptions) Run(ctx context.Context) error { // Output that the application is running, and then show the port-forwarding information log.Info("\nYour application is now running on the cluster") - portsBuf := NewPortWriter(log.GetStdout(), len(portPairsSlice)) + portsBuf := NewPortWriter(log.GetStdout(), len(portPairsSlice), ceMapping) go func() { err = o.clientset.KubernetesClient.SetupPortForwarding(pod, portPairsSlice, portsBuf, o.errOut) if err != nil { @@ -233,8 +241,10 @@ func (o *DevOptions) Run(ctx context.Context) error { }() portsBuf.Wait() - - devFileObj := o.Context.EnvSpecificInfo.GetDevfileObj() + err = o.clientset.StateClient.SetForwardedPorts(portsBuf.GetForwardedPorts()) + if err != nil { + return fmt.Errorf("unable to save forwarded ports to state file: %v", err) + } scontext.SetComponentType(ctx, component.GetComponentTypeFromDevfileMetadata(devFileObj.Data.GetMetadata())) scontext.SetLanguage(ctx, devFileObj.Data.GetMetadata().Language) @@ -312,7 +322,7 @@ It forwards endpoints with exposure values 'public' or 'internal' to a port on l devCmd.Flags().BoolVar(&o.randomPortsFlag, "random-ports", false, "Assign random ports to redirected ports") devCmd.Flags().BoolVar(&o.debugFlag, "debug", false, "Execute the debug command within the component") - clientset.Add(devCmd, clientset.DEV, clientset.INIT, clientset.KUBERNETES) + clientset.Add(devCmd, clientset.DEV, clientset.INIT, clientset.KUBERNETES, clientset.STATE) // Add a defined annotation in order to appear in the help menu devCmd.Annotations["command"] = "main" devCmd.SetUsageTemplate(odoutil.CmdUsageTemplate) diff --git a/pkg/odo/cli/dev/writer.go b/pkg/odo/cli/dev/writer.go index 1ac552055cd..4d09236426f 100644 --- a/pkg/odo/cli/dev/writer.go +++ b/pkg/odo/cli/dev/writer.go @@ -1,26 +1,37 @@ package dev import ( + "errors" "fmt" "io" + "regexp" + "strconv" "strings" "github.com/fatih/color" + + "github.com/redhat-developer/odo/pkg/api" + + "k8s.io/klog" ) type PortWriter struct { buffer io.Writer end chan bool len int + // mapping indicates the list of ports open by containers (ex: mapping["runtime"] = {3000, 3030}) + mapping map[string][]int + fwPorts []api.ForwardedPort } // NewPortWriter creates a writer that will write the content in buffer, // and Wait will return after strings "Forwarding from 127.0.0.1:" has been written "len" times -func NewPortWriter(buffer io.Writer, len int) *PortWriter { +func NewPortWriter(buffer io.Writer, len int, mapping map[string][]int) *PortWriter { return &PortWriter{ - buffer: buffer, - len: len, - end: make(chan bool), + buffer: buffer, + len: len, + end: make(chan bool), + mapping: mapping, } } @@ -33,6 +44,14 @@ func (o *PortWriter) Write(buf []byte) (n int, err error) { defer color.Unset() // Use it in your function s := string(buf) if strings.HasPrefix(s, "Forwarding from 127.0.0.1") { + + fwPort, err := getForwardedPort(o.mapping, s) + if err == nil { + o.fwPorts = append(o.fwPorts, fwPort) + } else { + klog.V(4).Infof("unable to get forwarded port: %v", err) + } + fmt.Fprintf(o.buffer, " - %s", s) o.len-- if o.len == 0 { @@ -45,3 +64,38 @@ func (o *PortWriter) Write(buf []byte) (n int, err error) { func (o *PortWriter) Wait() { <-o.end } + +func (o *PortWriter) GetForwardedPorts() []api.ForwardedPort { + return o.fwPorts +} + +func getForwardedPort(mapping map[string][]int, s string) (api.ForwardedPort, error) { + regex := regexp.MustCompile(`Forwarding from 127.0.0.1:([0-9]+) -> ([0-9]+)`) + matches := regex.FindStringSubmatch(s) + if len(matches) < 3 { + return api.ForwardedPort{}, errors.New("unable to analyze port forwarding string") + } + localPort, err := strconv.Atoi(matches[1]) + if err != nil { + return api.ForwardedPort{}, err + } + remotePort, err := strconv.Atoi(matches[2]) + if err != nil { + return api.ForwardedPort{}, err + } + containerName := "" + for container, ports := range mapping { + for _, port := range ports { + if port == remotePort { + containerName = container + break + } + } + } + return api.ForwardedPort{ + ContainerName: containerName, + LocalAddress: "127.0.0.1", + LocalPort: localPort, + ContainerPort: remotePort, + }, nil +} diff --git a/pkg/odo/cli/dev/writer_test.go b/pkg/odo/cli/dev/writer_test.go new file mode 100644 index 00000000000..b5dfb461e59 --- /dev/null +++ b/pkg/odo/cli/dev/writer_test.go @@ -0,0 +1,63 @@ +package dev + +import ( + "reflect" + "testing" + + "github.com/redhat-developer/odo/pkg/api" +) + +func Test_getForwardedPort(t *testing.T) { + type args struct { + mapping map[string][]int + s string + } + tests := []struct { + name string + args args + want api.ForwardedPort + wantErr bool + }{ + { + name: "find port in container", + args: args{ + mapping: map[string][]int{ + "container1": {3000, 4200}, + "container2": {80, 8080}, + }, + s: "Forwarding from 127.0.0.1:40407 -> 3000", + }, + want: api.ForwardedPort{ + ContainerName: "container1", + LocalAddress: "127.0.0.1", + LocalPort: 40407, + ContainerPort: 3000, + }, + wantErr: false, + }, + { + name: "string error", + args: args{ + mapping: map[string][]int{ + "container1": {3000, 4200}, + "container2": {80, 8080}, + }, + s: "Forwarding from 127.0.0.1:40407 => 3000", + }, + want: api.ForwardedPort{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getForwardedPort(tt.args.mapping, tt.args.s) + if (err != nil) != tt.wantErr { + t.Errorf("getForwardedPort() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getForwardedPort() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/odo/genericclioptions/clientset/clientset.go b/pkg/odo/genericclioptions/clientset/clientset.go index 799b4bccd83..3758875edf4 100644 --- a/pkg/odo/genericclioptions/clientset/clientset.go +++ b/pkg/odo/genericclioptions/clientset/clientset.go @@ -14,6 +14,7 @@ package clientset import ( "github.com/redhat-developer/odo/pkg/alizer" "github.com/redhat-developer/odo/pkg/dev" + "github.com/redhat-developer/odo/pkg/state" "github.com/spf13/cobra" _delete "github.com/redhat-developer/odo/pkg/component/delete" @@ -50,6 +51,8 @@ const ( PROJECT = "DEP_PROJECT" // REGISTRY instantiates client for pkg/registry REGISTRY = "DEP_REGISTRY" + // STATE instantiates client for pkg/state + STATE = "DEP_STATE" // WATCH instantiates client for pkg/watch WATCH = "DEP_WATCH" @@ -66,7 +69,8 @@ var subdeps map[string][]string = map[string][]string{ INIT: {ALIZER, FILESYSTEM, PREFERENCE, REGISTRY}, PROJECT: {KUBERNETES_NULLABLE}, REGISTRY: {FILESYSTEM, PREFERENCE}, - WATCH: {DELETE_COMPONENT}, + STATE: {FILESYSTEM}, + WATCH: {DELETE_COMPONENT, STATE}, /* Add sub-dependencies here, if any */ } @@ -81,6 +85,7 @@ type Clientset struct { PreferenceClient preference.Client ProjectClient project.Client RegistryClient registry.Client + StateClient state.Client WatchClient watch.Client /* Add client here */ } @@ -144,8 +149,11 @@ func Fetch(command *cobra.Command) (*Clientset, error) { if isDefined(command, PROJECT) { dep.ProjectClient = project.NewClient(dep.KubernetesClient) } + if isDefined(command, STATE) { + dep.StateClient = state.NewStateClient(dep.FS) + } if isDefined(command, WATCH) { - dep.WatchClient = watch.NewWatchClient(dep.DeleteClient) + dep.WatchClient = watch.NewWatchClient(dep.DeleteClient, dep.StateClient) } if isDefined(command, DEV) { dep.DevClient = dev.NewDevClient(dep.WatchClient) diff --git a/pkg/state/const.go b/pkg/state/const.go new file mode 100644 index 00000000000..f81a14eaf4f --- /dev/null +++ b/pkg/state/const.go @@ -0,0 +1,3 @@ +package state + +const _filepath = "./.odo/devstate.json" diff --git a/pkg/state/doc.go b/pkg/state/doc.go new file mode 100644 index 00000000000..cea421b0d98 --- /dev/null +++ b/pkg/state/doc.go @@ -0,0 +1,2 @@ +// Package state gives access to the state of the odo process stored in a local file +package state diff --git a/pkg/state/interface.go b/pkg/state/interface.go new file mode 100644 index 00000000000..aa4cf458e5f --- /dev/null +++ b/pkg/state/interface.go @@ -0,0 +1,11 @@ +package state + +import "github.com/redhat-developer/odo/pkg/api" + +type Client interface { + // SetForwardedPorts sets the forwarded ports in the state file and saves it to the file, updating the metadata + SetForwardedPorts(fwPorts []api.ForwardedPort) error + + // SaveExit resets the state file to indicate odo is not running + SaveExit() error +} diff --git a/pkg/state/state.go b/pkg/state/state.go new file mode 100644 index 00000000000..e291660e55f --- /dev/null +++ b/pkg/state/state.go @@ -0,0 +1,40 @@ +package state + +import ( + "encoding/json" + + "github.com/redhat-developer/odo/pkg/api" + "github.com/redhat-developer/odo/pkg/testingutil/filesystem" +) + +type State struct { + content Content + fs filesystem.Filesystem +} + +func NewStateClient(fs filesystem.Filesystem) *State { + return &State{ + fs: fs, + } +} + +func (o *State) SetForwardedPorts(fwPorts []api.ForwardedPort) error { + // TODO(feloy) When other data is persisted into the state file, it will be needed to read the file first + o.content.ForwardedPorts = fwPorts + return o.save() +} + +func (o *State) SaveExit() error { + o.content.ForwardedPorts = nil + return o.save() +} + +// save writes the content structure in json format in file +func (o *State) save() error { + jsonContent, err := json.MarshalIndent(o.content, "", " ") + if err != nil { + return err + } + // .odo directory is supposed to exist, don't create it + return o.fs.WriteFile(_filepath, jsonContent, 0644) +} diff --git a/pkg/state/state_test.go b/pkg/state/state_test.go new file mode 100644 index 00000000000..f629743d028 --- /dev/null +++ b/pkg/state/state_test.go @@ -0,0 +1,146 @@ +package state + +import ( + "encoding/json" + "fmt" + "reflect" + "testing" + + "github.com/redhat-developer/odo/pkg/api" + "github.com/redhat-developer/odo/pkg/testingutil/filesystem" +) + +func TestState_SetForwardedPorts(t *testing.T) { + + forwardedPort1 := api.ForwardedPort{ + ContainerName: "acontainer", + LocalAddress: "localhost", + LocalPort: 40001, + ContainerPort: 3000, + } + + type fields struct { + fs func() filesystem.Filesystem + getSecondsFromEpoch func() int64 + getpid func() int + } + type args struct { + fwPorts []api.ForwardedPort + } + tests := []struct { + name string + fields fields + args args + wantErr bool + checkState func(fs filesystem.Filesystem) error + }{ + // TODO: Add test cases. + { + name: "set forwarded ports", + fields: fields{ + fs: func() filesystem.Filesystem { + return filesystem.NewFakeFs() + }, + getSecondsFromEpoch: func() int64 { + return 13000 + }, + getpid: func() int { + return 100 + }, + }, + args: args{ + fwPorts: []api.ForwardedPort{forwardedPort1}, + }, + wantErr: false, + checkState: func(fs filesystem.Filesystem) error { + jsonContent, err := fs.ReadFile(_filepath) + if err != nil { + return err + } + var content Content + err = json.Unmarshal(jsonContent, &content) + if err != nil { + return err + } + expected := []api.ForwardedPort{forwardedPort1} + if !reflect.DeepEqual(content.ForwardedPorts, expected) { + return fmt.Errorf("Forwarded ports is %+v, should be %+v", content.ForwardedPorts, expected) + } + return nil + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := tt.fields.fs() + o := State{ + fs: fs, + } + if err := o.SetForwardedPorts(tt.args.fwPorts); (err != nil) != tt.wantErr { + t.Errorf("State.SetForwardedPorts() error = %v, wantErr %v", err, tt.wantErr) + } + if check := tt.checkState(fs); check != nil { + t.Error(check) + } + }) + } +} + +func TestState_SaveExit(t *testing.T) { + type fields struct { + fs func() filesystem.Filesystem + getSecondsFromEpoch func() int64 + getpid func() int + } + tests := []struct { + name string + fields fields + wantErr bool + checkState func(fs filesystem.Filesystem) error + }{ + { + name: "save exit", + fields: fields{ + fs: func() filesystem.Filesystem { + return filesystem.NewFakeFs() + }, + getSecondsFromEpoch: func() int64 { + return 13000 + }, + getpid: func() int { + return 100 + }, + }, + wantErr: false, + checkState: func(fs filesystem.Filesystem) error { + jsonContent, err := fs.ReadFile(_filepath) + if err != nil { + return err + } + var content Content + err = json.Unmarshal(jsonContent, &content) + if err != nil { + return err + } + if len(content.ForwardedPorts) != 0 { + return fmt.Errorf("Forwarded ports is %+v, should be empty", content.ForwardedPorts) + } + return nil + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := tt.fields.fs() + o := State{ + fs: fs, + } + if err := o.SaveExit(); (err != nil) != tt.wantErr { + t.Errorf("State.SaveExit() error = %v, wantErr %v", err, tt.wantErr) + } + if check := tt.checkState(fs); check != nil { + t.Error(check) + } + }) + } +} diff --git a/pkg/state/types.go b/pkg/state/types.go new file mode 100644 index 00000000000..1a68bebe629 --- /dev/null +++ b/pkg/state/types.go @@ -0,0 +1,10 @@ +package state + +import ( + "github.com/redhat-developer/odo/pkg/api" +) + +type Content struct { + // ForwardedPorts are the ports forwarded during odo dev session + ForwardedPorts []api.ForwardedPort `json:"forwardedPorts"` +} diff --git a/pkg/watch/watch.go b/pkg/watch/watch.go index c40bd2b4c52..724827bcde2 100644 --- a/pkg/watch/watch.go +++ b/pkg/watch/watch.go @@ -10,6 +10,7 @@ import ( "github.com/devfile/library/pkg/devfile/parser" _delete "github.com/redhat-developer/odo/pkg/component/delete" + "github.com/redhat-developer/odo/pkg/state" "github.com/fsnotify/fsnotify" gitignore "github.com/sabhiram/go-gitignore" @@ -32,11 +33,13 @@ const ( type WatchClient struct { deleteClient _delete.Client + stateClient state.Client } -func NewWatchClient(deleteClient _delete.Client) *WatchClient { +func NewWatchClient(deleteClient _delete.Client, stateClient state.Client) *WatchClient { return &WatchClient{ deleteClient: deleteClient, + stateClient: stateClient, } } @@ -356,6 +359,7 @@ func processEvents(changedFiles, deletedPaths []string, parameters WatchParamete } func (o *WatchClient) Cleanup(devfileObj parser.DevfileObj, out io.Writer) error { + fmt.Fprintln(out, "Cleaning resources, please wait") isInnerLoopDeployed, resources, err := o.deleteClient.ListResourcesToDeleteFromDevfile(devfileObj, "app") if err != nil { fmt.Fprintf(out, "failed to delete inner loop resources: %v", err) @@ -373,7 +377,8 @@ func (o *WatchClient) Cleanup(devfileObj parser.DevfileObj, out io.Writer) error for _, fail := range failed { fmt.Fprintf(out, "Failed to delete the %q resource: %s\n", fail.GetKind(), fail.GetName()) } - return nil + + return o.stateClient.SaveExit() } func shouldIgnoreEvent(event fsnotify.Event) (ignoreEvent bool) { diff --git a/tests/helper/helper_generic.go b/tests/helper/helper_generic.go index b458b5bc3df..d0ce641490e 100644 --- a/tests/helper/helper_generic.go +++ b/tests/helper/helper_generic.go @@ -258,17 +258,25 @@ func CommonAfterEach(commonVar CommonVar) { } // JsonPathContentIs expects that the content of the path to equal value -func JsonPathContentIs(json string, path string, value string) bool { +func JsonPathContentIs(json string, path string, value string) { result := gjson.Get(json, path) Expect(result.String()).To(Equal(value), fmt.Sprintf("content of path %q should be %q but is %q", path, value, result.String())) - return true } // JsonPathContentContain expects that the content of the path to contain value -func JsonPathContentContain(json string, path string, value string) bool { +func JsonPathContentContain(json string, path string, value string) { result := gjson.Get(json, path) Expect(result.String()).To(ContainSubstring(value), fmt.Sprintf("content of path %q should contain %q but is %q", path, value, result.String())) - return true +} + +func JsonPathContentIsValidUserPort(json string, path string) { + result := gjson.Get(json, path) + intVal, err := strconv.Atoi(result.String()) + Expect(err).ToNot(HaveOccurred()) + Expect(intVal).To(SatisfyAll( + BeNumerically(">=", 1024), + BeNumerically("<=", 65535), + )) } // SetProjectName sets projectNames based on the neame of the test file name (withouth path and replacing _ with -), line number of current ginkgo execution, and a random string of 3 letters diff --git a/tests/integration/devfile/cmd_alizer_test.go b/tests/integration/devfile/cmd_alizer_test.go index 7dde22a9db8..cc83ecbda4d 100644 --- a/tests/integration/devfile/cmd_alizer_test.go +++ b/tests/integration/devfile/cmd_alizer_test.go @@ -33,8 +33,8 @@ var _ = Describe("odo alizer command tests", func() { stdout, stderr := res.Out(), res.Err() Expect(stderr).To(BeEmpty()) Expect(helper.IsJSON(stdout)).To(BeTrue()) - Expect(helper.JsonPathContentIs(stdout, "devfile", "nodejs")) - Expect(helper.JsonPathContentIs(stdout, "devfileRegistry", "DefaultDevfileRegistry")) + helper.JsonPathContentIs(stdout, "devfile", "nodejs") + helper.JsonPathContentIs(stdout, "devfileRegistry", "DefaultDevfileRegistry") }) }) @@ -43,7 +43,7 @@ var _ = Describe("odo alizer command tests", func() { stdout, stderr := res.Out(), res.Err() Expect(stdout).To(BeEmpty()) Expect(helper.IsJSON(stderr)).To(BeTrue()) - Expect(helper.JsonPathContentContain(stderr, "message", "No valid devfile found for project in")) + helper.JsonPathContentContain(stderr, "message", "No valid devfile found for project in") }) It("alizer should fail without json output", func() { diff --git a/tests/integration/devfile/cmd_dev_test.go b/tests/integration/devfile/cmd_dev_test.go index 379927552ec..177b04f4e33 100644 --- a/tests/integration/devfile/cmd_dev_test.go +++ b/tests/integration/devfile/cmd_dev_test.go @@ -3,6 +3,7 @@ package devfile import ( "fmt" "io" + "io/ioutil" "net/http" "os" "path/filepath" @@ -166,6 +167,22 @@ var _ = Describe("odo dev command tests", func() { Expect(err).ToNot(HaveOccurred()) }) + When("a state file is not writable", func() { + BeforeEach(func() { + stateFile := filepath.Join(commonVar.Context, ".odo", "devstate.json") + helper.MakeDir(filepath.Dir(stateFile)) + Expect(helper.CreateFileWithContent(stateFile, "")).ToNot(HaveOccurred()) + Expect(os.Chmod(stateFile, 0400)).ToNot(HaveOccurred()) + }) + It("should fail running odo dev", func() { + res := helper.Cmd("odo", "dev", "--random-ports").ShouldFail() + stdout := res.Out() + stderr := res.Err() + Expect(stdout).To(ContainSubstring("Cleaning")) + Expect(stderr).To(ContainSubstring("unable to save forwarded ports to state file")) + }) + }) + When("odo dev is executed", func() { BeforeEach(func() { @@ -1398,4 +1415,52 @@ var _ = Describe("odo dev command tests", func() { } }) }) + + When("a component with multiple endpoints is run", func() { + stateFile := ".odo/devstate.json" + var devSession helper.DevSession + BeforeEach(func() { + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project-with-multiple-endpoints"), commonVar.Context) + helper.Cmd("odo", "project", "set", commonVar.Project).ShouldPass() + helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-with-multiple-endpoints.yaml")).ShouldPass() + Expect(helper.VerifyFileExists(".odo/devstate.json")).To(BeFalse()) + var err error + devSession, _, _, _, err = helper.StartDevMode() + Expect(err).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + // We stop the process so the process does not remain after the end of the tests + devSession.Kill() + devSession.WaitEnd() + }) + + It("should create a state file containing forwarded ports", func() { + Expect(helper.VerifyFileExists(stateFile)).To(BeTrue()) + contentJSON, err := ioutil.ReadFile(stateFile) + Expect(err).ToNot(HaveOccurred()) + helper.JsonPathContentIs(string(contentJSON), "forwardedPorts.0.containerName", "runtime") + helper.JsonPathContentIs(string(contentJSON), "forwardedPorts.1.containerName", "runtime") + helper.JsonPathContentIs(string(contentJSON), "forwardedPorts.0.localAddress", "127.0.0.1") + helper.JsonPathContentIs(string(contentJSON), "forwardedPorts.1.localAddress", "127.0.0.1") + helper.JsonPathContentIs(string(contentJSON), "forwardedPorts.0.containerPort", "3000") + helper.JsonPathContentIs(string(contentJSON), "forwardedPorts.1.containerPort", "4567") + helper.JsonPathContentIsValidUserPort(string(contentJSON), "forwardedPorts.0.localPort") + helper.JsonPathContentIsValidUserPort(string(contentJSON), "forwardedPorts.1.localPort") + }) + + When("odo dev is stopped", func() { + BeforeEach(func() { + devSession.Stop() + devSession.WaitEnd() + }) + + It("should remove forwarded ports from state file", func() { + Expect(helper.VerifyFileExists(stateFile)).To(BeTrue()) + contentJSON, err := ioutil.ReadFile(stateFile) + Expect(err).ToNot(HaveOccurred()) + helper.JsonPathContentIs(string(contentJSON), "forwardedPorts", "") + }) + }) + }) }) diff --git a/tests/integration/devfile/cmd_devfile_init_test.go b/tests/integration/devfile/cmd_devfile_init_test.go index 482ac1e655e..be9fbed64f1 100644 --- a/tests/integration/devfile/cmd_devfile_init_test.go +++ b/tests/integration/devfile/cmd_devfile_init_test.go @@ -40,7 +40,7 @@ var _ = Describe("odo devfile init command tests", func() { stdout, stderr := res.Out(), res.Err() Expect(stdout).To(BeEmpty()) Expect(helper.IsJSON(stderr)).To(BeTrue()) - Expect(helper.JsonPathContentIs(stderr, "message", "parameters are expected to select a devfile")) + helper.JsonPathContentIs(stderr, "message", "parameters are expected to select a devfile") }) By("running odo init with incomplete flags and JSON output", func() { @@ -48,7 +48,7 @@ var _ = Describe("odo devfile init command tests", func() { stdout, stderr := res.Out(), res.Err() Expect(stdout).To(BeEmpty()) Expect(helper.IsJSON(stderr)).To(BeTrue()) - Expect(helper.JsonPathContentContain(stderr, "message", "either --devfile or --devfile-path parameter should be specified")) + helper.JsonPathContentContain(stderr, "message", "either --devfile or --devfile-path parameter should be specified") }) By("keeping an empty directory when running odo init with wrong starter name", func() { @@ -124,12 +124,12 @@ var _ = Describe("odo devfile init command tests", func() { stdout, stderr := res.Out(), res.Err() Expect(stderr).To(BeEmpty()) Expect(helper.IsJSON(stdout)).To(BeTrue()) - Expect(helper.JsonPathContentIs(stdout, "devfilePath", filepath.Join(commonVar.Context, "devfile.yaml"))) - Expect(helper.JsonPathContentIs(stdout, "devfileData.devfile.schemaVersion", "2.0.0")) - Expect(helper.JsonPathContentIs(stdout, "devfileData.supportedOdoFeatures.dev", "true")) - Expect(helper.JsonPathContentIs(stdout, "devfileData.supportedOdoFeatures.debug", "false")) - Expect(helper.JsonPathContentIs(stdout, "devfileData.supportedOdoFeatures.deploy", "false")) - Expect(helper.JsonPathContentIs(stdout, "managedBy", "odo")) + helper.JsonPathContentIs(stdout, "devfilePath", filepath.Join(commonVar.Context, "devfile.yaml")) + helper.JsonPathContentIs(stdout, "devfileData.devfile.schemaVersion", "2.0.0") + helper.JsonPathContentIs(stdout, "devfileData.supportedOdoFeatures.dev", "true") + helper.JsonPathContentIs(stdout, "devfileData.supportedOdoFeatures.debug", "false") + helper.JsonPathContentIs(stdout, "devfileData.supportedOdoFeatures.deploy", "false") + helper.JsonPathContentIs(stdout, "managedBy", "odo") }) }) When("using --devfile-path flag with a local devfile", func() {