Skip to content

Commit

Permalink
odo experimental debug port-forward command (#2043)
Browse files Browse the repository at this point in the history
* add port-forward code from oc

* added command for debug and experimental

* integrating with odo - in progress

* add build portfoward request in occlient

* resolved all references

* extract the port forwarder into a different package

* some cleanup

* integrating the portforward command in odo cli

* removed i18n

* add debug port support in the config

* use the debug port in portforward cli

* resolved failing unit test

* added debug_port in the env var list

* remove the pod timeout option

* resolved long desc for port forward

* sending corrupt debug port

* better logging

* add tests for odo debug

* minor cleanup

* removed focus

* resolved failing tests

* added comments

* small error in tests

* removed experimental, changed the tests and add warning for tech preview

* added logging for debugging

* added more comments

* removed the port forward interface

* use init:0.12.0 bootstrap image

* made port forwarder more independent and added comment

* major refactor to resolve mrinal's comments

* moved logging

* doc typo

* doc typo
  • Loading branch information
girishramnani authored and openshift-merge-robot committed Sep 20, 2019
1 parent 567ad81 commit 8b253d0
Show file tree
Hide file tree
Showing 20 changed files with 476 additions and 31 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,15 @@ jobs:

- <<: *base-test
stage: test
name: "preference, config and url command integration tests"
name: "preference, config, url and debug command integration tests"
script:
- ./scripts/oc-cluster.sh
- make bin
- sudo cp odo /usr/bin
- odo login -u developer
- travis_wait make test-cmd-pref-config
- travis_wait make test-cmd-url
- travis_wait make test-cmd-debug
- odo logout

# Run service-catalog e2e tests
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ test-cmd-watch:
ginkgo -v -nodes=$(TEST_EXEC_NODES) -focus="odo watch command tests" \
slowSpecThreshold=$(SLOW_SPEC_THRESHOLD) -randomizeAllSpecs tests/integration/ -timeout $(TIMEOUT)

# Run odo debug command tests
test-cmd-debug:
ginkgo -v -nodes=$(TEST_EXEC_NODES) -focus="odo debug command tests" \
slowSpecThreshold=$(SLOW_SPEC_THRESHOLD) -randomizeAllSpecs tests/integration/ -timeout $(TIMEOUT)

# Run command's integration tests irrespective of service catalog status in the cluster.
# Service, link and login/logout command tests are not the part of this test run
.PHONY: test-integration
Expand Down
67 changes: 67 additions & 0 deletions docs/getting-started.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,70 @@ To list all enviroment variables in the current local configuration:
----
odo config view
----

== Using debug mode to interactively debug (Tech Preview)

Odo allows the user to attach a debugger for remotely debugging your application. Currently this feature is only supported by nodejs and java.

Components created using odo by default run in debug mode (kind of debug mode which doesn't break into debug session unless attached to). This means that a debugger agent is already running on the component side on a specific port. So user just needs to start the port forwarding using
----
odo debug port-forward
----
and attach their local debugger (usually bundled in an IDE).

Diagram to explain how it works -
----
Nodejs Component
+-------------------------------------------+
| |
| |
| node --inspect server.js |
| |
| |
| Runs a debugging agent on port 5858 |
| |
| |
+-----------------------+-------------------+
^
|
|
|
|
|
|
+-------------------+ |
| | Connects to +--------------+------------------+
| | localhost port | |
| VSCode/ | 5858 | |
| Intellij/ | | Odo debug port-forward |
| Netbeans +-------------------> |
| | | Forwards the localhost 5858 |
| | | port to the component's 5858 |
| | | |
| | | |
+-------------------+ +---------------------------------+
User's Machine
----

=== Odo provides you ways to configure some of parameters like

To set remote port on which debugging agent should run:
----
odo config set DebugPort 9292
----
This value gets persisted in the local config and stays same between runs.
Note - A re-deployment is needed for this value to be refected on the component


To set the local port which would be port forwarded:
----
odo debug port-forward --local-port 9292
----
This value is not persisted and hence needs to provided whenever the port needs to be changed.

=== References to attaching debugger for different IDEs

- link:https://stackify.com/java-remote-debugging/[Java Debugging using CLI and VS Code]
- link:https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_attaching-to-nodejs[NodeJs Debugging using VS Code]
36 changes: 23 additions & 13 deletions pkg/component/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ func CreateComponent(client *occlient.Client, componentConfig config.LocalConfig
cmpSrcRef := componentConfig.GetRef()
appName := componentConfig.GetApplication()
envVarsList := componentConfig.GetEnvVars()
addDebugPortToEnv(&envVarsList, componentConfig)

// create and get the storage to be created/mounted during the component creation
storageList := getStorageFromConfig(&componentConfig)
Expand Down Expand Up @@ -1018,29 +1019,30 @@ func GetComponentSource(client *occlient.Client, componentName string, applicati
// Update updates the requested component
// Parameters:
// client: occlient instance
// componentSettings: Component configuration
// componentConfig: Component configuration
// newSource: Location of component source resolved to absolute path
// stdout: io pipe to write logs to
// Returns:
// errors if any
func Update(client *occlient.Client, componentSettings config.LocalConfigInfo, newSource string, stdout io.Writer) error {
func Update(client *occlient.Client, componentConfig config.LocalConfigInfo, newSource string, stdout io.Writer) error {

retrievingSpinner := log.Spinner("Retrieving component data")
defer retrievingSpinner.End(false)

// STEP 1. Create the common Object Meta for updating.

componentName := componentSettings.GetName()
applicationName := componentSettings.GetApplication()
newSourceType := componentSettings.GetSourceType()
newSourceRef := componentSettings.GetRef()
componentImageType := componentSettings.GetType()
cmpPorts := componentSettings.GetPorts()
envVarsList := componentSettings.GetEnvVars()
componentName := componentConfig.GetName()
applicationName := componentConfig.GetApplication()
newSourceType := componentConfig.GetSourceType()
newSourceRef := componentConfig.GetRef()
componentImageType := componentConfig.GetType()
cmpPorts := componentConfig.GetPorts()
envVarsList := componentConfig.GetEnvVars()
addDebugPortToEnv(&envVarsList, componentConfig)

// retrieve the list of storages to create/mount and unmount
storageList := getStorageFromConfig(&componentSettings)
storageToMount, storageToUnMount, err := storage.Push(client, storageList, componentSettings.GetName(), componentSettings.GetApplication(), true)
storageList := getStorageFromConfig(&componentConfig)
storageToMount, storageToUnMount, err := storage.Push(client, storageList, componentConfig.GetName(), componentConfig.GetApplication(), true)
if err != nil {
return errors.Wrapf(err, "unable to get storage to mount and unmount")
}
Expand Down Expand Up @@ -1096,7 +1098,7 @@ func Update(client *occlient.Client, componentSettings config.LocalConfigInfo, n
if len(cmpPorts) > 0 {
ports, err = util.GetContainerPortsFromStrings(cmpPorts)
if err != nil {
return errors.Wrapf(err, "failed to apply component config %+v to component %s", componentSettings, commonObjectMeta.Name)
return errors.Wrapf(err, "failed to apply component config %+v to component %s", componentConfig, commonObjectMeta.Name)
}
}

Expand All @@ -1109,7 +1111,7 @@ func Update(client *occlient.Client, componentSettings config.LocalConfigInfo, n

// Generate the new DeploymentConfig
resourceLimits := occlient.FetchContainerResourceLimits(foundCurrentDCContainer)
resLts, err := occlient.GetResourceRequirementsFromCmpSettings(componentSettings)
resLts, err := occlient.GetResourceRequirementsFromCmpSettings(componentConfig)
if err != nil {
return errors.Wrap(err, "failed to update component")
}
Expand Down Expand Up @@ -1487,3 +1489,11 @@ func checkIfURLChangesWillBeMade(client *occlient.Client, componentConfig config

return false, nil
}

func addDebugPortToEnv(envVarList *config.EnvVarList, componentConfig config.LocalConfigInfo) {
// adding the debug port as an env variable
*envVarList = append(*envVarList, config.EnvVar{
Name: "DEBUG_PORT",
Value: fmt.Sprint(componentConfig.GetDebugPort()),
})
}
24 changes: 24 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const (
configFileName = "config.yaml"
localConfigKind = "LocalConfig"
localConfigAPIVersion = "odo.openshift.io/v1alpha1"
// DefaultDebugPort is the default port used for debugging on remote pod
DefaultDebugPort = 5858
)

type ComponentStorageSettings struct {
Expand Down Expand Up @@ -58,6 +60,9 @@ type ComponentSettings struct {

MaxMemory *string `yaml:"MaxMemory,omitempty"`

// DebugPort controls the port used by the pod to run the debugging agent on
DebugPort *int `yaml:"DebugPort,omitempty"`

Storage *[]ComponentStorageSettings `yaml:"Storage,omitempty"`

// Ignore if set to true then odoignore file should be considered
Expand Down Expand Up @@ -213,6 +218,12 @@ func (lci *LocalConfigInfo) SetConfiguration(parameter string, value interface{}
case "memory":
lci.componentSettings.MaxMemory = &strValue
lci.componentSettings.MinMemory = &strValue
case "debugport":
dbgPort, err := strconv.Atoi(strValue)
if err != nil {
return err
}
lci.componentSettings.DebugPort = &dbgPort
case "ignore":
val, err := strconv.ParseBool(strings.ToLower(strValue))
if err != nil {
Expand Down Expand Up @@ -438,6 +449,14 @@ func (lc *LocalConfig) GetMaxMemory() string {
return *lc.componentSettings.MaxMemory
}

// GetDebugPort returns the DebugPort, returns default if nil
func (lc *LocalConfig) GetDebugPort() int {
if lc.componentSettings.DebugPort == nil {
return DefaultDebugPort
}
return *lc.componentSettings.DebugPort
}

// GetIgnore returns the Ignore, returns default if nil
func (lc *LocalConfig) GetIgnore() bool {
if lc.componentSettings.Ignore == nil {
Expand Down Expand Up @@ -507,6 +526,10 @@ const (
Memory = "Memory"
// MemoryDescription is the description of the setting controlling the min and max memory to same value
MemoryDescription = "The minimum and maximum memory a component can consume"
// DebugPort is the port where the application is set to listen for debugger
DebugPort = "DebugPort"
// DebugPortDescription is the description for debug port
DebugPortDescription = "The port on which the debugger will listen on the component"
// Ignore is the name of the setting controlling the min memory a component consumes
Ignore = "Ignore"
// IgnoreDescription is the description of the setting controlling the use of .odoignore file
Expand Down Expand Up @@ -570,6 +593,7 @@ var (
MinMemory: MinMemoryDescription,
MaxMemory: MaxMemoryDescription,
Memory: MemoryDescription,
DebugPort: DebugPortDescription,
Ignore: IgnoreDescription,
MinCPU: MinCPUDescription,
MaxCPU: MaxCPUDescription,
Expand Down
2 changes: 1 addition & 1 deletion pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ func TestLocalUnsetConfiguration(t *testing.T) {
func TestLowerCaseParameterForLocalParameters(t *testing.T) {
expected := map[string]bool{"name": true, "minmemory": true, "ignore": true, "project": true,
"application": true, "type": true, "ref": true, "mincpu": true, "cpu": true, "ports": true, "maxmemory": true,
"maxcpu": true, "sourcetype": true, "sourcelocation": true, "memory": true, "storage": true, "url": true}
"maxcpu": true, "sourcetype": true, "sourcelocation": true, "memory": true, "storage": true, "url": true, "debugport": true}
actual := util.GetLowerCaseParameters(GetLocallySupportedParameters())
if !reflect.DeepEqual(expected, actual) {
t.Errorf("expected '%v', got '%v'", expected, actual)
Expand Down
82 changes: 82 additions & 0 deletions pkg/debug/portforward.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package debug

import (
"github.com/openshift/odo/pkg/occlient"

componentlabels "github.com/openshift/odo/pkg/component/labels"

"fmt"
"net/http"

"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/util"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"

"k8s.io/client-go/tools/portforward"
"k8s.io/client-go/transport/spdy"
k8sgenclioptions "k8s.io/kubernetes/pkg/kubectl/genericclioptions"
)

// DefaultPortForwarder implements the SPDY based port forwarder
type DefaultPortForwarder struct {
client *occlient.Client
k8sgenclioptions.IOStreams
componentName string
appName string
}

func NewDefaultPortForwarder(componentName, appName string, client *occlient.Client, streams k8sgenclioptions.IOStreams) *DefaultPortForwarder {
return &DefaultPortForwarder{
client: client,
IOStreams: streams,
componentName: componentName,
appName: appName,
}
}

// ForwardPorts forwards the port using the url for the remote pod.
// portPair is a pair of port in format "localPort:RemotePort" that is to be forwarded
// stop Chan is used to stop port forwarding
// ready Chan is used to signal failure to the channel receiver
func (f *DefaultPortForwarder) ForwardPorts(portPair string, stopChan, readyChan chan struct{}) error {
conf, err := f.client.KubeConfig.ClientConfig()
if err != nil {
return err
}

pod, err := f.getPodUsingComponentName()
if err != nil {
return err
}

if pod.Status.Phase != corev1.PodRunning {
return fmt.Errorf("unable to forward port because pod is not running. Current status=%v", pod.Status.Phase)
}

transport, upgrader, err := spdy.RoundTripperFor(conf)
if err != nil {
return err
}
req := f.client.BuildPortForwardReq(pod.Name)
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", req.URL())
fw, err := portforward.New(dialer, []string{portPair}, stopChan, readyChan, f.Out, f.ErrOut)
if err != nil {
return err
}
log.Info("Started port forwarding at ports -", portPair)
return fw.ForwardPorts()
}

func (f *DefaultPortForwarder) getPodUsingComponentName() (*corev1.Pod, error) {
componentLabels := componentlabels.GetLabels(f.componentName, f.appName, false)
componentSelector := util.ConvertLabelsToSelector(componentLabels)
dc, err := f.client.GetOneDeploymentConfigFromSelector(componentSelector)
if err != nil {
return nil, errors.Wrap(err, "unable to get deployment for component")
}
// Find Pod for component
podSelector := fmt.Sprintf("deploymentconfig=%s", dc.Name)

return f.client.GetOnePodFromSelector(podSelector)
}
13 changes: 12 additions & 1 deletion pkg/occlient/occlient.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import (

"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/remotecommand"
"k8s.io/client-go/util/retry"
Expand Down Expand Up @@ -95,7 +96,7 @@ const (

// Default Image that will be used containing the supervisord binary and assembly scripts
// use getBoostrapperImage() function instead of this variable
defaultBootstrapperImage = "quay.io/openshiftdo/init:0.11.0"
defaultBootstrapperImage = "quay.io/openshiftdo/init:0.12.0"

// ENV variable to overwrite image used to bootstrap SupervisorD in S2I builder Image
bootstrapperImageEnvName = "ODO_BOOTSTRAPPER_IMAGE"
Expand Down Expand Up @@ -3016,6 +3017,16 @@ func (c *Client) ExecCMDInContainer(podName string, cmd []string, stdout io.Writ
return nil
}

// BuildPortForwardReq builds a port forward request
func (c *Client) BuildPortForwardReq(podName string) *rest.Request {
return c.kubeClient.CoreV1().RESTClient().
Post().
Resource("pods").
Namespace(c.Namespace).
Name(podName).
SubResource("portforward")
}

// GetVolumeMountsFromDC returns a list of all volume mounts in the given DC
func (c *Client) GetVolumeMountsFromDC(dc *appsv1.DeploymentConfig) []corev1.VolumeMount {
var volumeMounts []corev1.VolumeMount
Expand Down
3 changes: 3 additions & 0 deletions pkg/odo/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import (
"github.com/openshift/odo/pkg/odo/cli/catalog"
"github.com/openshift/odo/pkg/odo/cli/component"
"github.com/openshift/odo/pkg/odo/cli/config"
"github.com/openshift/odo/pkg/odo/cli/debug"
"github.com/openshift/odo/pkg/odo/cli/login"
"github.com/openshift/odo/pkg/odo/cli/logout"
"github.com/openshift/odo/pkg/odo/cli/preference"
"github.com/openshift/odo/pkg/odo/cli/project"
"github.com/openshift/odo/pkg/odo/cli/service"
"github.com/openshift/odo/pkg/odo/cli/storage"

"github.com/openshift/odo/pkg/odo/cli/url"
"github.com/openshift/odo/pkg/odo/cli/utils"
"github.com/openshift/odo/pkg/odo/cli/version"
Expand Down Expand Up @@ -133,6 +135,7 @@ func NewCmdOdo(name, fullName string) *cobra.Command {
version.NewCmdVersion(version.RecommendedCommandName, util.GetFullName(fullName, version.RecommendedCommandName)),
config.NewCmdConfiguration(config.RecommendedCommandName, util.GetFullName(fullName, config.RecommendedCommandName)),
preference.NewCmdPreference(preference.RecommendedCommandName, util.GetFullName(fullName, preference.RecommendedCommandName)),
debug.NewCmdDebug(debug.RecommendedCommandName, util.GetFullName(fullName, debug.RecommendedCommandName)),
)

odoutil.VisitCommands(rootCmd, reconfigureCmdWithSubcmd)
Expand Down
Loading

0 comments on commit 8b253d0

Please sign in to comment.