Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

odo dev store information about currently forwarded ports #5703

Merged
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",
rm3l marked this conversation as resolved.
Show resolved Hide resolved
"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
Comment on lines +6 to +7
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking out loud - why do we want business layer to get involved with storing the state? Isn't this something that odo "CLI" is interested in doing? Maybe a different implementation using odo as library would not want to store the state.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In any case, we want to create an abstraction for the state, hence this package.
For me, it is a business concern, as it is a way to be able to give to the CLI the state of the Dev session at any time. Any other CLI would be interested in getting this state.


// SaveExit resets the state file to indicate odo is not running
SaveExit() error
}
Loading