Skip to content

Commit

Permalink
odo dev store information about currently forwarded ports (redhat-dev…
Browse files Browse the repository at this point in the history
…eloper#5703)

* Save state (forwarded ports + PID + timestamp) to local file and cleanup

* Integration tests

* Documentation

* Update docs/website/versioned_docs/version-3.0.0/command-reference/dev.md

Co-authored-by: Armel Soro <armel@rm3l.org>

* Fail when an error happens writing state file
In case of error during initialization, odo dev cleanup the resources

* Fix typo

* Test localPort matches a number

* Remove PID and timestamp from state + rename to devstate.json

* Run IBM cloud tests as non root

* Fix doc

* Review

* Remove reference to PID/timestamp from doc

Co-authored-by: Armel Soro <armel@rm3l.org>
  • Loading branch information
2 people authored and cdrage committed Aug 31, 2022
1 parent 2ed47cf commit aa71f06
Show file tree
Hide file tree
Showing 18 changed files with 491 additions and 41 deletions.
11 changes: 8 additions & 3 deletions .ibm/images/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 && \
Expand All @@ -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
19 changes: 19 additions & 0 deletions docs/website/versioned_docs/version-3.0.0/command-reference/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
}
```
1 change: 1 addition & 0 deletions pkg/api/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
40 changes: 25 additions & 15 deletions pkg/odo/cli/dev/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,28 +178,36 @@ 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",
"Namespace: "+namespace,
"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 {
Expand All @@ -216,15 +224,15 @@ 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
}

// 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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
62 changes: 58 additions & 4 deletions pkg/odo/cli/dev/writer.go
Original file line number Diff line number Diff line change
@@ -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,
}
}

Expand All @@ -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 {
Expand All @@ -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
}
63 changes: 63 additions & 0 deletions pkg/odo/cli/dev/writer_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
12 changes: 10 additions & 2 deletions pkg/odo/genericclioptions/clientset/clientset.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"

Expand All @@ -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 */
}

Expand All @@ -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 */
}
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions pkg/state/const.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package state

const _filepath = "./.odo/devstate.json"
2 changes: 2 additions & 0 deletions pkg/state/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package state gives access to the state of the odo process stored in a local file
package state
11 changes: 11 additions & 0 deletions pkg/state/interface.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit aa71f06

Please sign in to comment.