diff --git a/pkg/deploy/deploy.go b/pkg/deploy/deploy.go index 4b9f04bc5a4..5c07de407c2 100644 --- a/pkg/deploy/deploy.go +++ b/pkg/deploy/deploy.go @@ -76,3 +76,7 @@ func (o *deployHandler) ApplyKubernetes(kubernetes v1alpha2.Component) error { } return nil } + +func (o *deployHandler) Execute(command v1alpha2.Command) error { + return errors.New("Exec command is not implemented for Deploy") +} diff --git a/pkg/devfile/adapters/kubernetes/component/adapter.go b/pkg/devfile/adapters/kubernetes/component/adapter.go index 393fed06953..057d60d0c62 100644 --- a/pkg/devfile/adapters/kubernetes/component/adapter.go +++ b/pkg/devfile/adapters/kubernetes/component/adapter.go @@ -21,6 +21,7 @@ import ( "github.com/redhat-developer/odo/pkg/devfile/adapters/kubernetes/utils" "github.com/redhat-developer/odo/pkg/envinfo" "github.com/redhat-developer/odo/pkg/kclient" + "github.com/redhat-developer/odo/pkg/libdevfile" "github.com/redhat-developer/odo/pkg/log" "github.com/redhat-developer/odo/pkg/preference" "github.com/redhat-developer/odo/pkg/service" @@ -312,12 +313,11 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) { // PostStart events from the devfile will only be executed when the component // didn't previously exist - postStartEvents := a.Devfile.Data.GetEvents().PostStart - if !componentExists && len(postStartEvents) > 0 { - err = a.ExecDevfileEvent(postStartEvents, common.PostStart, parameters.Show) + if !componentExists && libdevfile.HasPostStartEvents(a.Devfile) { + log.Infof("\nExecuting %s event commands for component %s", string(libdevfile.PostStart), a.ComponentName) + err = libdevfile.ExecPostStartEvents(a.Devfile, a.ComponentName, newExecHandler(a.Client, a.pod.Name, parameters.Show)) if err != nil { return err - } } @@ -673,13 +673,15 @@ func (a Adapter) Delete(labels map[string]string, show bool, wait bool) error { podSpinner.End(true) // if there are preStop events, execute them before deleting the deployment - preStopEvents := a.Devfile.Data.GetEvents().PreStop - if len(preStopEvents) > 0 { + if libdevfile.HasPreStopEvents(a.Devfile) { if pod.Status.Phase != corev1.PodRunning { return fmt.Errorf("unable to execute preStop events, pod for component %s is not running", a.ComponentName) } - - err = a.ExecDevfileEvent(preStopEvents, common.PreStop, show) + _, err = a.getPod(false) + if err != nil { + return err + } + err = libdevfile.ExecPreStopEvents(a.Devfile, a.ComponentName, newExecHandler(a.Client, a.pod.Name, show)) if err != nil { return err } diff --git a/pkg/devfile/adapters/kubernetes/component/exec_handler.go b/pkg/devfile/adapters/kubernetes/component/exec_handler.go new file mode 100644 index 00000000000..d7c45fdcef3 --- /dev/null +++ b/pkg/devfile/adapters/kubernetes/component/exec_handler.go @@ -0,0 +1,158 @@ +package component + +import ( + "bufio" + "fmt" + "io" + "os" + + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/pkg/errors" + "github.com/redhat-developer/odo/pkg/kclient" + "github.com/redhat-developer/odo/pkg/log" + "github.com/redhat-developer/odo/pkg/machineoutput" + "github.com/redhat-developer/odo/pkg/util" + "k8s.io/klog" +) + +type execHandler struct { + kubeClient kclient.ClientInterface + podName string + show bool +} + +const ShellExecutable string = "/bin/sh" + +func newExecHandler(kubeClient kclient.ClientInterface, podName string, show bool) *execHandler { + return &execHandler{ + kubeClient: kubeClient, + podName: podName, + show: show, + } +} + +func (o *execHandler) ApplyImage(image v1alpha2.Component) error { + return nil +} + +func (o *execHandler) ApplyKubernetes(kubernetes v1alpha2.Component) error { + return nil +} + +func (o *execHandler) Execute(command v1alpha2.Command) error { + msg := fmt.Sprintf("Executing command %q on container %q", command.Exec.CommandLine, command.Exec.Component) + spinner := log.Spinner(msg) + defer spinner.End(false) + + logger := machineoutput.NewMachineEventLoggingClient() + stdoutWriter, stdoutChannel, stderrWriter, stderrChannel := logger.CreateContainerOutputWriter() + + cmdline := getCmdline(command) + err := executeCommand(o.kubeClient, command.Exec.Component, o.podName, cmdline, o.show, stdoutWriter, stderrWriter) + + closeWriterAndWaitForAck(stdoutWriter, stdoutChannel, stderrWriter, stderrChannel) + + spinner.End(true) + return err +} + +func getCmdline(command v1alpha2.Command) []string { + exe := command.Exec + + // deal with environment variables + var cmdLine string + setEnvVariable := util.GetCommandStringFromEnvs(exe.Env) + + if setEnvVariable == "" { + cmdLine = exe.CommandLine + } else { + cmdLine = setEnvVariable + " && " + exe.CommandLine + } + + // Change to the workdir and execute the command + var cmd []string + if exe.WorkingDir != "" { + // since we are using /bin/sh -c, the command needs to be within a single double quote instance, for example "cd /tmp && pwd" + cmd = []string{ShellExecutable, "-c", "cd " + exe.WorkingDir + " && " + cmdLine} + } else { + cmd = []string{ShellExecutable, "-c", cmdLine} + } + return cmd +} + +func closeWriterAndWaitForAck(stdoutWriter *io.PipeWriter, stdoutChannel chan interface{}, stderrWriter *io.PipeWriter, stderrChannel chan interface{}) { + if stdoutWriter != nil { + _ = stdoutWriter.Close() + <-stdoutChannel + } + if stderrWriter != nil { + _ = stderrWriter.Close() + <-stderrChannel + } +} + +// ExecuteCommand executes the given command in the pod's container +func executeCommand(client kclient.ClientInterface, containerName string, podName string, command []string, show bool, consoleOutputStdout *io.PipeWriter, consoleOutputStderr *io.PipeWriter) (err error) { + stdoutReader, stdoutWriter := io.Pipe() + stderrReader, stderrWriter := io.Pipe() + + var cmdOutput string + + klog.V(2).Infof("Executing command %v for pod: %v in container: %v", command, podName, containerName) + + // Read stdout and stderr, store their output in cmdOutput, and also pass output to consoleOutput Writers (if non-nil) + stdoutCompleteChannel := startReaderGoroutine(stdoutReader, show, &cmdOutput, consoleOutputStdout) + stderrCompleteChannel := startReaderGoroutine(stderrReader, show, &cmdOutput, consoleOutputStderr) + + err = client.ExecCMDInContainer(containerName, podName, command, stdoutWriter, stderrWriter, nil, false) + + // Block until we have received all the container output from each stream + _ = stdoutWriter.Close() + <-stdoutCompleteChannel + _ = stderrWriter.Close() + <-stderrCompleteChannel + + if err != nil { + // It is safe to read from cmdOutput here, as the goroutines are guaranteed to have terminated at this point. + klog.V(2).Infof("ExecuteCommand returned an an err: %v. for command '%v'. output: %v", err, command, cmdOutput) + + return errors.Wrapf(err, "unable to exec command %v: \n%v", command, cmdOutput) + } + + return +} + +// This goroutine will automatically pipe the output from the writer (passed into ExecCMDInContainer) to +// the loggers. +// The returned channel will contain a single nil entry once the reader has closed. +func startReaderGoroutine(reader io.Reader, show bool, cmdOutput *string, consoleOutput *io.PipeWriter) chan interface{} { + + result := make(chan interface{}) + + go func() { + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + line := scanner.Text() + + if log.IsDebug() || show { + _, err := fmt.Fprintln(os.Stdout, line) + if err != nil { + log.Errorf("Unable to print to stdout: %s", err.Error()) + } + } + + *cmdOutput += fmt.Sprintln(line) + + if consoleOutput != nil { + _, err := consoleOutput.Write([]byte(line + "\n")) + if err != nil { + log.Errorf("Error occurred on writing string to consoleOutput writer: %s", err.Error()) + } + } + } + result <- nil + }() + + return result + +} diff --git a/pkg/libdevfile/command.go b/pkg/libdevfile/command.go index 6e89d104968..c1bbd908d31 100644 --- a/pkg/libdevfile/command.go +++ b/pkg/libdevfile/command.go @@ -1,6 +1,8 @@ package libdevfile import ( + "strings" + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/devfile/library/pkg/devfile/parser" "github.com/devfile/library/pkg/devfile/parser/data/v2/common" @@ -52,7 +54,7 @@ func allCommandsMap(devfileObj parser.DevfileObj) (map[string]v1alpha2.Command, commandMap := make(map[string]v1alpha2.Command, len(commands)) for _, command := range commands { - commandMap[command.Id] = command + commandMap[strings.ToLower(command.Id)] = command } return commandMap, nil } diff --git a/pkg/libdevfile/command_composite.go b/pkg/libdevfile/command_composite.go index 755bb2ba501..93f1711f0fe 100644 --- a/pkg/libdevfile/command_composite.go +++ b/pkg/libdevfile/command_composite.go @@ -2,6 +2,7 @@ package libdevfile import ( "fmt" + "strings" "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/devfile/library/pkg/devfile/parser" @@ -28,7 +29,7 @@ func (o *compositeCommand) CheckValidity() error { } cmds := o.command.Composite.Commands for _, cmd := range cmds { - if _, ok := allCommands[cmd]; !ok { + if _, ok := allCommands[strings.ToLower(cmd)]; !ok { return fmt.Errorf("composite command %q references command %q not found in devfile", o.command.Id, cmd) } } @@ -42,7 +43,7 @@ func (o *compositeCommand) Execute(handler Handler) error { return err } for _, devfileCmd := range o.command.Composite.Commands { - cmd, err := newCommand(o.devfileObj, allCommands[devfileCmd]) + cmd, err := newCommand(o.devfileObj, allCommands[strings.ToLower(devfileCmd)]) if err != nil { return err } diff --git a/pkg/libdevfile/command_exec.go b/pkg/libdevfile/command_exec.go index 8cc56a97014..aec8073f25e 100644 --- a/pkg/libdevfile/command_exec.go +++ b/pkg/libdevfile/command_exec.go @@ -25,7 +25,7 @@ func (o *execCommand) CheckValidity() error { } func (o *execCommand) Execute(handler Handler) error { - return nil + return handler.Execute(o.command) } func (o *execCommand) UnExecute() error { diff --git a/pkg/libdevfile/handler_mock.go b/pkg/libdevfile/handler_mock.go index 2816c43e4a1..792ac6df914 100644 --- a/pkg/libdevfile/handler_mock.go +++ b/pkg/libdevfile/handler_mock.go @@ -61,3 +61,17 @@ func (mr *MockHandlerMockRecorder) ApplyKubernetes(kubernetes interface{}) *gomo mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyKubernetes", reflect.TypeOf((*MockHandler)(nil).ApplyKubernetes), kubernetes) } + +// Execute mocks base method. +func (m *MockHandler) Execute(command v1alpha2.Command) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Execute", command) + ret0, _ := ret[0].(error) + return ret0 +} + +// Execute indicates an expected call of Execute. +func (mr *MockHandlerMockRecorder) Execute(command interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockHandler)(nil).Execute), command) +} diff --git a/pkg/libdevfile/libdevfile.go b/pkg/libdevfile/libdevfile.go index e0a8d214fe3..4f9528d698f 100644 --- a/pkg/libdevfile/libdevfile.go +++ b/pkg/libdevfile/libdevfile.go @@ -1,14 +1,19 @@ package libdevfile import ( + "fmt" + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/devfile/library/pkg/devfile/parser" "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + + "github.com/redhat-developer/odo/pkg/log" ) type Handler interface { ApplyImage(image v1alpha2.Component) error ApplyKubernetes(kubernetes v1alpha2.Component) error + Execute(command v1alpha2.Command) error } // Deploy executes the default Deploy command of the devfile @@ -67,3 +72,54 @@ func executeCommand(devfileObj parser.DevfileObj, command v1alpha2.Command, hand } return cmd.Execute(handler) } + +func HasPostStartEvents(devfileObj parser.DevfileObj) bool { + postStartEvents := devfileObj.Data.GetEvents().PostStart + return len(postStartEvents) > 0 +} + +func HasPreStopEvents(devfileObj parser.DevfileObj) bool { + preStopEvents := devfileObj.Data.GetEvents().PreStop + return len(preStopEvents) > 0 +} + +func ExecPostStartEvents(devfileObj parser.DevfileObj, componentName string, handler Handler) error { + postStartEvents := devfileObj.Data.GetEvents().PostStart + return execDevfileEvent(devfileObj, componentName, postStartEvents, PostStart, handler) +} + +func ExecPreStopEvents(devfileObj parser.DevfileObj, componentName string, handler Handler) error { + preStopEvents := devfileObj.Data.GetEvents().PreStop + return execDevfileEvent(devfileObj, componentName, preStopEvents, PreStop, handler) +} + +// execDevfileEvent receives a Devfile Event (PostStart, PreStop etc.) and loops through them +// Each Devfile Command associated with the given event is retrieved, and executed in the container specified +// in the command +func execDevfileEvent(devfileObj parser.DevfileObj, componentName string, events []string, eventType DevfileEventType, handler Handler) error { + if len(events) > 0 { + // TODO move to caller + log.Infof("\nExecuting %s event commands for component %s", string(eventType), componentName) + commandMap, err := allCommandsMap(devfileObj) + if err != nil { + return err + } + for _, commandName := range events { + command, ok := commandMap[commandName] + if !ok { + return fmt.Errorf("unable to find devfile command %q", commandName) + } + + c, err := newCommand(devfileObj, command) + if err != nil { + return err + } + // Execute command in container + err = c.Execute(handler) + if err != nil { + return fmt.Errorf("unable to execute devfile command %q: %w", commandName, err) + } + } + } + return nil +} diff --git a/pkg/libdevfile/types.go b/pkg/libdevfile/types.go new file mode 100644 index 00000000000..728c39d0df3 --- /dev/null +++ b/pkg/libdevfile/types.go @@ -0,0 +1,17 @@ +package libdevfile + +type DevfileEventType string + +const ( + // PreStart is a devfile event + PreStart DevfileEventType = "preStart" + + // PostStart is a devfile event + PostStart DevfileEventType = "postStart" + + // PreStop is a devfile event + PreStop DevfileEventType = "preStop" + + // PostStop is a devfile event + PostStop DevfileEventType = "postStop" +) diff --git a/pkg/odo/cli/component/devfile.go b/pkg/odo/cli/component/devfile.go index 712ab1f08e1..25ae09a91a2 100644 --- a/pkg/odo/cli/component/devfile.go +++ b/pkg/odo/cli/component/devfile.go @@ -179,3 +179,7 @@ func (o *undeployHandler) ApplyKubernetes(kubernetes v1alpha2.Component) error { // Un-deploy the K8s manifest return o.kubeClient.DeleteDynamicResource(u.GetName(), gvr.Resource.Group, gvr.Resource.Version, gvr.Resource.Resource) } + +func (o *undeployHandler) Execute(command v1alpha2.Command) error { + return errors.New("Exec command is not implemented for Deploy") +}