Skip to content

Commit

Permalink
Exec without devfile/adapters
Browse files Browse the repository at this point in the history
  • Loading branch information
feloy committed Feb 15, 2022
1 parent 5b1f99b commit d5e9f17
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 12 deletions.
4 changes: 4 additions & 0 deletions pkg/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
18 changes: 10 additions & 8 deletions pkg/devfile/adapters/kubernetes/component/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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

}
}

Expand Down Expand Up @@ -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
}
Expand Down
158 changes: 158 additions & 0 deletions pkg/devfile/adapters/kubernetes/component/exec_handler.go
Original file line number Diff line number Diff line change
@@ -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

}
4 changes: 3 additions & 1 deletion pkg/libdevfile/command.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
}
5 changes: 3 additions & 2 deletions pkg/libdevfile/command_composite.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
}
}
Expand All @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/libdevfile/command_exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 14 additions & 0 deletions pkg/libdevfile/handler_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 56 additions & 0 deletions pkg/libdevfile/libdevfile.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
17 changes: 17 additions & 0 deletions pkg/libdevfile/types.go
Original file line number Diff line number Diff line change
@@ -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"
)
4 changes: 4 additions & 0 deletions pkg/odo/cli/component/devfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

0 comments on commit d5e9f17

Please sign in to comment.