From 330b724753c9d267d5110cff4f3a537a4a38ae02 Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Fri, 2 Jun 2023 15:33:21 +0200 Subject: [PATCH] Add an `odo run` command to manually execute command during `odo dev` (#6857) * Add a run command * Check command name passed as arg * Check platform is available * Add a Run method to the DevClient * Run command on cluster * Add test with run command on cluster * Implement and test run on podman * Enhance test to check that command has been executed in container * Fix `odo help` test * Refactor common code for podman/cluster * Test Apply commands on Kubernetes/Images * Test a msg is displayed when executing odo run without odo dev * Review * makes GetRunningPodFromSelector return only Running pods on Podman --- cmd/odo/help_test.go | 1 + pkg/component/component.go | 6 +- pkg/component/delete/delete.go | 6 - pkg/component/delete/delete_test.go | 19 --- pkg/dev/common/run.go | 53 +++++++ pkg/dev/interface.go | 5 + pkg/dev/kubedev/run.go | 23 +++ pkg/dev/mock.go | 14 ++ pkg/dev/podmandev/run.go | 23 +++ pkg/exec/exec_test.go | 4 + pkg/libdevfile/errors.go | 3 + pkg/libdevfile/libdevfile.go | 19 +++ pkg/odo/cli/cli.go | 2 + pkg/odo/cli/errors/errors.go | 14 ++ pkg/odo/cli/run/run.go | 122 +++++++++++++++ pkg/platform/interface.go | 2 + pkg/platform/mock.go | 15 ++ pkg/podman/component.go | 15 +- pkg/podman/mock.go | 15 ++ pkg/podman/pods.go | 14 +- .../devfiles/nodejs/devfile-for-run.yaml | 86 ++++++++++ tests/integration/cmd_run_test.go | 147 ++++++++++++++++++ 22 files changed, 574 insertions(+), 34 deletions(-) create mode 100644 pkg/dev/common/run.go create mode 100644 pkg/dev/kubedev/run.go create mode 100644 pkg/dev/podmandev/run.go create mode 100644 pkg/odo/cli/run/run.go create mode 100644 tests/examples/source/devfiles/nodejs/devfile-for-run.yaml create mode 100644 tests/integration/cmd_run_test.go diff --git a/cmd/odo/help_test.go b/cmd/odo/help_test.go index 03c927997b4..91efbd67bed 100644 --- a/cmd/odo/help_test.go +++ b/cmd/odo/help_test.go @@ -34,6 +34,7 @@ Examples: init Init bootstraps a new project logs Show logs of all containers of the component registry List all components from the Devfile registry + run Run a specific command in the Dev mode ` diff --git a/pkg/component/component.go b/pkg/component/component.go index 9581687cb85..96a7d02bd12 100644 --- a/pkg/component/component.go +++ b/pkg/component/component.go @@ -94,11 +94,7 @@ func Log(platformClient platform.Client, componentName string, appName string, f pod, err := platformClient.GetRunningPodFromSelector(odolabels.GetSelector(componentName, appName, odolabels.ComponentDevMode, false)) if err != nil { - return nil, fmt.Errorf("the component %s doesn't exist on the cluster", componentName) - } - - if pod.Status.Phase != corev1.PodRunning { - return nil, fmt.Errorf("unable to show logs, component is not in running state. current status=%v", pod.Status.Phase) + return nil, fmt.Errorf("a running component %s doesn't exist on the cluster: %w", componentName, err) } containerName := command.Exec.Component diff --git a/pkg/component/delete/delete.go b/pkg/component/delete/delete.go index ffac61e4467..9756cd82dc1 100644 --- a/pkg/component/delete/delete.go +++ b/pkg/component/delete/delete.go @@ -212,12 +212,6 @@ func (do *DeleteComponentClient) ExecutePreStopEvents(ctx context.Context, devfi return fmt.Errorf("unable to determine if component %s exists; cause: %v", componentName, err.Error()) } - // do not fail Delete operation if if the pod is not running or if the event execution fails - if pod.Status.Phase != corev1.PodRunning { - klog.V(4).Infof("unable to execute preStop events, pod for component %q is not running", componentName) - return nil - } - klog.V(4).Infof("Executing %q event commands for component %q", libdevfile.PreStop, componentName) // ignore the failures if any; delete should not fail because preStop events failed to execute handler := component.NewRunHandler( diff --git a/pkg/component/delete/delete_test.go b/pkg/component/delete/delete_test.go index cdf4641ee21..c153ea0fbee 100644 --- a/pkg/component/delete/delete_test.go +++ b/pkg/component/delete/delete_test.go @@ -700,25 +700,6 @@ func TestDeleteComponentClient_ExecutePreStopEvents(t *testing.T) { }, wantErr: false, }, - { - name: "did not execute PreStopEvents because the pod is not in the running state", - fields: fields{ - kubeClient: func(ctrl *gomock.Controller) kclient.ClientInterface { - client := kclient.NewMockClientInterface(ctrl) - - selector := odolabels.GetSelector(componentName, "app", odolabels.ComponentDevMode, false) - pod := odoTestingUtil.CreateFakePod(componentName, "mypod", "runtime") - pod.Status.Phase = corev1.PodFailed - client.EXPECT().GetRunningPodFromSelector(selector).Return(pod, nil) - return client - }, - }, - args: args{ - devfileObj: devfileObjWithPreStopEvents, - appName: appName, - }, - wantErr: false, - }, { name: "failed to execute PreStopEvents because it failed to execute the command inside the container, but no error returned", fields: fields{ diff --git a/pkg/dev/common/run.go b/pkg/dev/common/run.go new file mode 100644 index 00000000000..ddc89be77f4 --- /dev/null +++ b/pkg/dev/common/run.go @@ -0,0 +1,53 @@ +package common + +import ( + "context" + "fmt" + + "github.com/redhat-developer/odo/pkg/component" + "github.com/redhat-developer/odo/pkg/configAutomount" + "github.com/redhat-developer/odo/pkg/devfile/image" + "github.com/redhat-developer/odo/pkg/exec" + "github.com/redhat-developer/odo/pkg/libdevfile" + odocontext "github.com/redhat-developer/odo/pkg/odo/context" + "github.com/redhat-developer/odo/pkg/platform" + "github.com/redhat-developer/odo/pkg/testingutil/filesystem" +) + +func Run( + ctx context.Context, + commandName string, + platformClient platform.Client, + execClient exec.Client, + configAutomountClient configAutomount.Client, + filesystem filesystem.Filesystem, +) error { + var ( + componentName = odocontext.GetComponentName(ctx) + devfileObj = odocontext.GetEffectiveDevfileObj(ctx) + devfilePath = odocontext.GetDevfilePath(ctx) + ) + + pod, err := platformClient.GetPodUsingComponentName(componentName) + if err != nil { + return fmt.Errorf("unable to get pod for component %s: %w. Please check the command 'odo dev' is running", componentName, err) + } + + handler := component.NewRunHandler( + ctx, + platformClient, + execClient, + configAutomountClient, + pod.Name, + false, + component.GetContainersNames(pod), + "Executing command in container", + + filesystem, + image.SelectBackend(ctx), + *devfileObj, + devfilePath, + ) + + return libdevfile.ExecuteCommandByName(ctx, *devfileObj, commandName, handler, false) +} diff --git a/pkg/dev/interface.go b/pkg/dev/interface.go index 177831b2b36..02cde68a596 100644 --- a/pkg/dev/interface.go +++ b/pkg/dev/interface.go @@ -48,6 +48,11 @@ type Client interface { options StartOptions, ) error + Run( + ctx context.Context, + commandName string, + ) error + // CleanupResources deletes the component created using the context's devfile and writes any outputs to out CleanupResources(ctx context.Context, out io.Writer) error } diff --git a/pkg/dev/kubedev/run.go b/pkg/dev/kubedev/run.go new file mode 100644 index 00000000000..6dee6972108 --- /dev/null +++ b/pkg/dev/kubedev/run.go @@ -0,0 +1,23 @@ +package kubedev + +import ( + "context" + + "github.com/redhat-developer/odo/pkg/dev/common" + "k8s.io/klog" +) + +func (o *DevClient) Run( + ctx context.Context, + commandName string, +) error { + klog.V(4).Infof("running command %q on cluster", commandName) + return common.Run( + ctx, + commandName, + o.kubernetesClient, + o.execClient, + o.configAutomountClient, + o.filesystem, + ) +} diff --git a/pkg/dev/mock.go b/pkg/dev/mock.go index c615af8e388..b9ad7e49778 100644 --- a/pkg/dev/mock.go +++ b/pkg/dev/mock.go @@ -49,6 +49,20 @@ func (mr *MockClientMockRecorder) CleanupResources(ctx, out interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanupResources", reflect.TypeOf((*MockClient)(nil).CleanupResources), ctx, out) } +// Run mocks base method. +func (m *MockClient) Run(ctx context.Context, commandName string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Run", ctx, commandName) + ret0, _ := ret[0].(error) + return ret0 +} + +// Run indicates an expected call of Run. +func (mr *MockClientMockRecorder) Run(ctx, commandName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockClient)(nil).Run), ctx, commandName) +} + // Start mocks base method. func (m *MockClient) Start(ctx context.Context, options StartOptions) error { m.ctrl.T.Helper() diff --git a/pkg/dev/podmandev/run.go b/pkg/dev/podmandev/run.go new file mode 100644 index 00000000000..c1b5af77cc4 --- /dev/null +++ b/pkg/dev/podmandev/run.go @@ -0,0 +1,23 @@ +package podmandev + +import ( + "context" + + "github.com/redhat-developer/odo/pkg/dev/common" + "k8s.io/klog" +) + +func (o *DevClient) Run( + ctx context.Context, + commandName string, +) error { + klog.V(4).Infof("running command %q on podman", commandName) + return common.Run( + ctx, + commandName, + o.podmanClient, + o.execClient, + nil, // TODO(feloy) set when running on new container is supported on podman + o.fs, + ) +} diff --git a/pkg/exec/exec_test.go b/pkg/exec/exec_test.go index 866902706f1..4b5119f6deb 100644 --- a/pkg/exec/exec_test.go +++ b/pkg/exec/exec_test.go @@ -44,6 +44,10 @@ func (o fakePlatform) GetRunningPodFromSelector(selector string) (*corev1.Pod, e panic("not implemented yet") } +func (o fakePlatform) GetPodUsingComponentName(componentName string) (*corev1.Pod, error) { + panic("not implemented yet") +} + func TestExecuteCommand(t *testing.T) { for _, tt := range []struct { name string diff --git a/pkg/libdevfile/errors.go b/pkg/libdevfile/errors.go index 491af5d56f8..dd6806cd230 100644 --- a/pkg/libdevfile/errors.go +++ b/pkg/libdevfile/errors.go @@ -22,6 +22,9 @@ func (e NoCommandFoundError) Error() string { if e.name == "" { return fmt.Sprintf("no %s command found in devfile", e.kind) } + if e.kind == "" { + return fmt.Sprintf("no command named %q found in devfile", e.name) + } return fmt.Sprintf("no %s command with name %q found in Devfile", e.kind, e.name) } diff --git a/pkg/libdevfile/libdevfile.go b/pkg/libdevfile/libdevfile.go index da4409bb6b3..48489498e6d 100644 --- a/pkg/libdevfile/libdevfile.go +++ b/pkg/libdevfile/libdevfile.go @@ -66,6 +66,25 @@ func ExecuteCommandByNameAndKind(ctx context.Context, devfileObj parser.DevfileO return executeCommand(ctx, devfileObj, cmd, handler) } +// ExecuteCommandByName executes the specified command cmdName in the Devfile. +// If ignoreCommandNotFound is true, nothing is executed if the command is not found and no error is returned. +func ExecuteCommandByName(ctx context.Context, devfileObj parser.DevfileObj, cmdName string, handler Handler, ignoreCommandNotFound bool) error { + commands, err := devfileObj.Data.GetCommands( + common.DevfileOptions{ + FilterByName: cmdName, + }, + ) + if err != nil { + return err + } + if len(commands) != 1 { + return NewNoCommandFoundError("", cmdName) + } + + cmd := commands[0] + return executeCommand(ctx, devfileObj, cmd, handler) +} + // executeCommand executes a specific command of a devfile using handler as backend func executeCommand(ctx context.Context, devfileObj parser.DevfileObj, command v1alpha2.Command, handler Handler) error { cmd, err := newCommand(devfileObj, command) diff --git a/pkg/odo/cli/cli.go b/pkg/odo/cli/cli.go index c4e7aea18f2..859c19037a7 100644 --- a/pkg/odo/cli/cli.go +++ b/pkg/odo/cli/cli.go @@ -10,6 +10,7 @@ import ( "unicode" "github.com/redhat-developer/odo/pkg/odo/cli/logs" + "github.com/redhat-developer/odo/pkg/odo/cli/run" "github.com/redhat-developer/odo/pkg/odo/commonflags" "github.com/redhat-developer/odo/pkg/log" @@ -195,6 +196,7 @@ func odoRootCmd(ctx context.Context, name, fullName string) *cobra.Command { set.NewCmdSet(set.RecommendedCommandName, util.GetFullName(fullName, set.RecommendedCommandName)), logs.NewCmdLogs(logs.RecommendedCommandName, util.GetFullName(fullName, logs.RecommendedCommandName)), completion.NewCmdCompletion(completion.RecommendedCommandName, util.GetFullName(fullName, completion.RecommendedCommandName)), + run.NewCmdRun(run.RecommendedCommandName, util.GetFullName(fullName, run.RecommendedCommandName)), ) // Add all subcommands to base commands diff --git a/pkg/odo/cli/errors/errors.go b/pkg/odo/cli/errors/errors.go index 48d105b0794..3a553536e97 100644 --- a/pkg/odo/cli/errors/errors.go +++ b/pkg/odo/cli/errors/errors.go @@ -19,6 +19,20 @@ func (o NoCommandInDevfileError) Error() string { return fmt.Sprintf("no command of kind %q found in the devfile", o.command) } +type NoCommandNameInDevfileError struct { + name string +} + +func NewNoCommandNameInDevfileError(name string) NoCommandNameInDevfileError { + return NoCommandNameInDevfileError{ + name: name, + } +} + +func (o NoCommandNameInDevfileError) Error() string { + return fmt.Sprintf("no command named %q found in the devfile", o.name) +} + type Warning struct { msg string err error diff --git a/pkg/odo/cli/run/run.go b/pkg/odo/cli/run/run.go new file mode 100644 index 00000000000..c150e8b3dd7 --- /dev/null +++ b/pkg/odo/cli/run/run.go @@ -0,0 +1,122 @@ +package run + +import ( + "context" + "fmt" + + "github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common" + "github.com/spf13/cobra" + + "github.com/redhat-developer/odo/pkg/kclient" + "github.com/redhat-developer/odo/pkg/odo/cli/errors" + "github.com/redhat-developer/odo/pkg/odo/cmdline" + "github.com/redhat-developer/odo/pkg/odo/commonflags" + fcontext "github.com/redhat-developer/odo/pkg/odo/commonflags/context" + odocontext "github.com/redhat-developer/odo/pkg/odo/context" + "github.com/redhat-developer/odo/pkg/odo/genericclioptions" + "github.com/redhat-developer/odo/pkg/odo/genericclioptions/clientset" + odoutil "github.com/redhat-developer/odo/pkg/odo/util" + "github.com/redhat-developer/odo/pkg/podman" + scontext "github.com/redhat-developer/odo/pkg/segment/context" + + ktemplates "k8s.io/kubectl/pkg/util/templates" +) + +const ( + RecommendedCommandName = "run" +) + +type RunOptions struct { + // Clients + clientset *clientset.Clientset + + // Args + commandName string +} + +var _ genericclioptions.Runnable = (*RunOptions)(nil) + +func NewRunOptions() *RunOptions { + return &RunOptions{} +} + +var runExample = ktemplates.Examples(` + # Run the command "my-command" in the Dev mode + %[1]s my-command + +`) + +func (o *RunOptions) SetClientset(clientset *clientset.Clientset) { + o.clientset = clientset +} + +func (o *RunOptions) Complete(ctx context.Context, cmdline cmdline.Cmdline, args []string) error { + o.commandName = args[0] // Value at 0 is expected to exist, thanks to ExactArgs(1) + return nil +} + +func (o *RunOptions) Validate(ctx context.Context) error { + var ( + devfileObj = odocontext.GetEffectiveDevfileObj(ctx) + platform = fcontext.GetPlatform(ctx, commonflags.PlatformCluster) + ) + + if devfileObj == nil { + return genericclioptions.NewNoDevfileError(odocontext.GetWorkingDirectory(ctx)) + } + + commands, err := devfileObj.Data.GetCommands(common.DevfileOptions{ + FilterByName: o.commandName, + }) + if err != nil { + return err + } + if len(commands) != 1 { + return errors.NewNoCommandNameInDevfileError(o.commandName) + } + + switch platform { + + case commonflags.PlatformCluster: + if o.clientset.KubernetesClient == nil { + return kclient.NewNoConnectionError() + } + scontext.SetPlatform(ctx, o.clientset.KubernetesClient) + + case commonflags.PlatformPodman: + if o.clientset.PodmanClient == nil { + return podman.NewPodmanNotFoundError(nil) + } + scontext.SetPlatform(ctx, o.clientset.PodmanClient) + } + return nil +} + +func (o *RunOptions) Run(ctx context.Context) (err error) { + return o.clientset.DevClient.Run(ctx, o.commandName) +} + +func NewCmdRun(name, fullName string) *cobra.Command { + o := NewRunOptions() + runCmd := &cobra.Command{ + Use: name, + Short: "Run a specific command in the Dev mode", + Long: `odo run executes a specific command of the Devfile during the Dev mode ("odo dev" needs to be running)`, + Example: fmt.Sprintf(runExample, fullName), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return genericclioptions.GenericRun(o, cmd, args) + }, + } + clientset.Add(runCmd, + clientset.FILESYSTEM, + clientset.KUBERNETES_NULLABLE, + clientset.PODMAN_NULLABLE, + clientset.DEV, + ) + + odoutil.SetCommandGroup(runCmd, odoutil.MainGroup) + runCmd.SetUsageTemplate(odoutil.CmdUsageTemplate) + commonflags.UsePlatformFlag(runCmd) + return runCmd +} diff --git a/pkg/platform/interface.go b/pkg/platform/interface.go index 20b0d03c5f3..baf5f6489eb 100644 --- a/pkg/platform/interface.go +++ b/pkg/platform/interface.go @@ -31,4 +31,6 @@ type Client interface { // GetRunningPodFromSelector returns any pod matching the given label selector. // If multiple pods are found, implementations might have different behavior, by either returning an error or returning any element. GetRunningPodFromSelector(selector string) (*corev1.Pod, error) + + GetPodUsingComponentName(componentName string) (*corev1.Pod, error) } diff --git a/pkg/platform/mock.go b/pkg/platform/mock.go index 317e645b2d0..82cc718b363 100644 --- a/pkg/platform/mock.go +++ b/pkg/platform/mock.go @@ -96,6 +96,21 @@ func (mr *MockClientMockRecorder) GetPodLogs(podName, containerName, followLog i return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPodLogs", reflect.TypeOf((*MockClient)(nil).GetPodLogs), podName, containerName, followLog) } +// GetPodUsingComponentName mocks base method. +func (m *MockClient) GetPodUsingComponentName(componentName string) (*v1.Pod, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPodUsingComponentName", componentName) + ret0, _ := ret[0].(*v1.Pod) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPodUsingComponentName indicates an expected call of GetPodUsingComponentName. +func (mr *MockClientMockRecorder) GetPodUsingComponentName(componentName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPodUsingComponentName", reflect.TypeOf((*MockClient)(nil).GetPodUsingComponentName), componentName) +} + // GetPodsMatchingSelector mocks base method. func (m *MockClient) GetPodsMatchingSelector(selector string) (*v1.PodList, error) { m.ctrl.T.Helper() diff --git a/pkg/podman/component.go b/pkg/podman/component.go index e0633ad040e..2ab99d7637a 100644 --- a/pkg/podman/component.go +++ b/pkg/podman/component.go @@ -6,6 +6,7 @@ import ( "os/exec" "strings" + corev1 "k8s.io/api/core/v1" "k8s.io/klog" "github.com/redhat-developer/odo/pkg/api" @@ -15,8 +16,13 @@ import ( // ListPodsReport contains the result of the `podman pod ps --format json` command type ListPodsReport struct { - Name string - Labels map[string]string + Name string + Labels map[string]string + Containers []ListPodsContainer `json:"Containers,omitempty"` +} + +type ListPodsContainer struct { + Names string `json:"Names,omitempty"` } func (o *PodmanCli) ListAllComponents() ([]api.ComponentAbstract, error) { @@ -86,3 +92,8 @@ func (o *PodmanCli) ListAllComponents() ([]api.ComponentAbstract, error) { return components, nil } + +func (o *PodmanCli) GetPodUsingComponentName(componentName string) (*corev1.Pod, error) { + podSelector := fmt.Sprintf("component=%s", componentName) + return o.GetRunningPodFromSelector(podSelector) +} diff --git a/pkg/podman/mock.go b/pkg/podman/mock.go index aecc762329c..0eba45104db 100644 --- a/pkg/podman/mock.go +++ b/pkg/podman/mock.go @@ -111,6 +111,21 @@ func (mr *MockClientMockRecorder) GetPodLogs(podName, containerName, followLog i return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPodLogs", reflect.TypeOf((*MockClient)(nil).GetPodLogs), podName, containerName, followLog) } +// GetPodUsingComponentName mocks base method. +func (m *MockClient) GetPodUsingComponentName(componentName string) (*v1.Pod, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPodUsingComponentName", componentName) + ret0, _ := ret[0].(*v1.Pod) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPodUsingComponentName indicates an expected call of GetPodUsingComponentName. +func (mr *MockClientMockRecorder) GetPodUsingComponentName(componentName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPodUsingComponentName", reflect.TypeOf((*MockClient)(nil).GetPodUsingComponentName), componentName) +} + // GetPodsMatchingSelector mocks base method. func (m *MockClient) GetPodsMatchingSelector(selector string) (*v1.PodList, error) { m.ctrl.T.Helper() diff --git a/pkg/podman/pods.go b/pkg/podman/pods.go index 44c4eaf331e..e4bae249f4e 100644 --- a/pkg/podman/pods.go +++ b/pkg/podman/pods.go @@ -92,8 +92,18 @@ func (o *PodmanCli) GetRunningPodFromSelector(selector string) (*corev1.Pod, err if err != nil { return nil, err } - if inspect.State == "Running" { - pod.Status.Phase = corev1.PodRunning + if inspect.State != "Running" { + return nil, fmt.Errorf("a pod exists but is not in Running state. Current status=%v", inspect.State) + } + + for _, container := range podReport.Containers { + // Names of users containers are prefixed with pod name by podman + if !strings.HasPrefix(container.Names, podReport.Name+"-") { + continue + } + pod.Spec.Containers = append(pod.Spec.Containers, corev1.Container{ + Name: strings.TrimPrefix(container.Names, podReport.Name+"-"), + }) } return &pod, nil } diff --git a/tests/examples/source/devfiles/nodejs/devfile-for-run.yaml b/tests/examples/source/devfiles/nodejs/devfile-for-run.yaml new file mode 100644 index 00000000000..2dfadf93705 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-for-run.yaml @@ -0,0 +1,86 @@ +schemaVersion: 2.2.0 +metadata: + name: nodejs + projectType: nodejs + language: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true + - name: other-container + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + command: ["tail"] + args: ["-f", "/dev/null"] + memoryLimit: 1024Mi + - name: config + kubernetes: + inlined: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: my-config + - name: image + image: + imageName: my-image + dockerfile: + uri: ./Dockerfile + buildContext: ${PROJECTS_ROOT} + rootRequired: false +commands: + - id: devbuild + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: true + - id: build + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + - id: devrun + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true + - id: run + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + - id: create-file + exec: + component: runtime + commandLine: touch /tmp/new-file + workingDir: ${PROJECTS_ROOT} + - id: create-file-in-other-container + exec: + component: other-container + commandLine: touch /tmp/new-file-in-other-container + workingDir: ${PROJECTS_ROOT} + - id: deploy-config + apply: + component: config + - id: build-image + apply: + component: image diff --git a/tests/integration/cmd_run_test.go b/tests/integration/cmd_run_test.go new file mode 100644 index 00000000000..2aa2e50df1c --- /dev/null +++ b/tests/integration/cmd_run_test.go @@ -0,0 +1,147 @@ +package integration + +import ( + "path/filepath" + + "github.com/redhat-developer/odo/pkg/labels" + "github.com/redhat-developer/odo/tests/helper" + "k8s.io/utils/pointer" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("odo run command tests", func() { + var cmpName string + var commonVar helper.CommonVar + + // This is run before every Spec (It) + var _ = BeforeEach(func() { + commonVar = helper.CommonBeforeEach() + cmpName = helper.RandString(6) + _ = cmpName // TODO remove when used + helper.Chdir(commonVar.Context) + Expect(helper.VerifyFileExists(".odo/env/env.yaml")).To(BeFalse()) + }) + + // This is run after every Spec (It) + var _ = AfterEach(func() { + helper.CommonAfterEach(commonVar) + }) + + When("directory is empty", Label(helper.LabelNoCluster), func() { + BeforeEach(func() { + Expect(helper.ListFilesInDir(commonVar.Context)).To(HaveLen(0)) + }) + + It("should error", func() { + output := helper.Cmd("odo", "run", "my-command").ShouldFail().Err() + Expect(output).To(ContainSubstring("The current directory does not represent an odo component")) + }) + }) + + When("a component is bootstrapped", func() { + BeforeEach(func() { + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), commonVar.Context) + helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-for-run.yaml")).ShouldPass() + Expect(helper.VerifyFileExists(".odo/env/env.yaml")).To(BeFalse()) + }) + + It("should fail if command is not found in devfile", Label(helper.LabelNoCluster), func() { + output := helper.Cmd("odo", "run", "unknown-command").ShouldFail().Err() + Expect(output).To(ContainSubstring(`no command named "unknown-command" found in the devfile`)) + + }) + + It("should fail if platform is not available", Label(helper.LabelNoCluster), func() { + By("failing when trying to run on default platform", func() { + output := helper.Cmd("odo", "run", "build").ShouldFail().Err() + Expect(output).To(ContainSubstring(`unable to access the cluster`)) + + }) + By("failing when trying to run on cluster", func() { + output := helper.Cmd("odo", "run", "build", "--platform", "cluster").ShouldFail().Err() + Expect(output).To(ContainSubstring(`unable to access the cluster`)) + + }) + By("failing when trying to run on podman", func() { + output := helper.Cmd("odo", "run", "build", "--platform", "podman").ShouldFail().Err() + Expect(output).To(ContainSubstring(`unable to access podman`)) + }) + }) + + It("should fail if odo dev is not running", func() { + output := helper.Cmd("odo", "run", "build").ShouldFail().Err() + Expect(output).To(ContainSubstring(`unable to get pod for component`)) + Expect(output).To(ContainSubstring(`Please check the command 'odo dev' is running`)) + }) + + for _, podman := range []bool{false, true} { + podman := podman + When("odo dev is executed and ready", helper.LabelPodmanIf(podman, func() { + + var devSession helper.DevSession + + BeforeEach(func() { + var err error + devSession, _, _, _, err = helper.StartDevMode(helper.DevSessionOpts{ + RunOnPodman: podman, + }) + Expect(err).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + devSession.Stop() + devSession.WaitEnd() + }) + + It("should execute commands", func() { + platform := "cluster" + if podman { + platform = "podman" + } + + By("executing an exec command", func() { + output := helper.Cmd("odo", "run", "create-file", "--platform", platform).ShouldPass().Out() + Expect(output).To(ContainSubstring("Executing command in container (command: create-file)")) + component := helper.NewComponent(cmpName, "app", labels.ComponentDevMode, commonVar.Project, commonVar.CliRunner) + component.Exec("runtime", []string{"ls", "/tmp/new-file"}, pointer.Bool(true)) + }) + + By("executing an exec command in another container", func() { + output := helper.Cmd("odo", "run", "create-file-in-other-container", "--platform", platform).ShouldPass().Out() + Expect(output).To(ContainSubstring("Executing command in container (command: create-file-in-other-container)")) + component := helper.NewComponent(cmpName, "app", labels.ComponentDevMode, commonVar.Project, commonVar.CliRunner) + component.Exec("other-container", []string{"ls", "/tmp/new-file-in-other-container"}, pointer.Bool(true)) + }) + + if !podman { + By("executing apply command on Kubernetes component", func() { + output := helper.Cmd("odo", "run", "deploy-config", "--platform", platform).ShouldPass().Out() + Expect(output).To(ContainSubstring("Creating resource ConfigMap/my-config")) + out := commonVar.CliRunner.Run("get", "configmap", "my-config", "-n", + commonVar.Project).Wait().Out.Contents() + Expect(out).To(ContainSubstring("my-config")) + }) + } + + if podman { + By("executing apply command on Image component", func() { + // Will fail because Dockerfile is not present, but we just want to check the build is started + // We cannot use PODMAN_CMD=echo with --platform=podman + output := helper.Cmd("odo", "run", "build-image", "--platform", platform).ShouldFail().Out() + Expect(output).To(ContainSubstring("Building image locally")) + }) + } else { + By("executing apply command on Image component", func() { + output := helper.Cmd("odo", "run", "build-image", "--platform", platform).AddEnv("PODMAN_CMD=echo").ShouldPass().Out() + Expect(output).To(ContainSubstring("Building image locally")) + Expect(output).To(ContainSubstring("Pushing image to container registry")) + + }) + } + }) + })) + } + }) +})