From 1517d4ff8a1d96de88f6177d7df6a8f3a34bf1b4 Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Tue, 15 Feb 2022 12:01:27 +0100 Subject: [PATCH] Exec without devfile/adapters --- pkg/deploy/deploy.go | 4 + pkg/devfile/adapters/common/command.go | 5 - pkg/devfile/adapters/common/errors.go | 18 - pkg/devfile/adapters/common/generic.go | 44 --- pkg/devfile/adapters/common/utils.go | 35 -- .../adapters/kubernetes/component/adapter.go | 50 +-- .../adapters/kubernetes/component/errors.go | 13 - .../kubernetes/component/exec_handler.go | 158 +++++++++ .../adapters/kubernetes/component/status.go | 323 ------------------ .../kubernetes/component/status_test.go | 284 --------------- pkg/libdevfile/command.go | 4 +- pkg/libdevfile/command_composite.go | 5 +- pkg/libdevfile/command_exec.go | 2 +- pkg/libdevfile/handler_mock.go | 14 + pkg/libdevfile/libdevfile.go | 52 +++ pkg/libdevfile/types.go | 17 + pkg/odo/cli/component/devfile.go | 4 + 17 files changed, 263 insertions(+), 769 deletions(-) delete mode 100644 pkg/devfile/adapters/kubernetes/component/errors.go create mode 100644 pkg/devfile/adapters/kubernetes/component/exec_handler.go delete mode 100644 pkg/devfile/adapters/kubernetes/component/status_test.go create mode 100644 pkg/libdevfile/types.go 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/common/command.go b/pkg/devfile/adapters/common/command.go index d7d19a922a9..6443afe24e0 100644 --- a/pkg/devfile/adapters/common/command.go +++ b/pkg/devfile/adapters/common/command.go @@ -153,11 +153,6 @@ func GetTestCommand(data data.DevfileData, devfileTestCmd string) (runCommand de return getCommand(data, devfileTestCmd, devfilev1.TestCommandGroupKind) } -// GetDeployCommand iterates through the components in the devfile and returns the deploy command -func GetDeployCommand(data data.DevfileData, devfileDeployCmd string) (deployCommand devfilev1.Command, err error) { - return getCommand(data, devfileDeployCmd, devfilev1.DeployCommandGroupKind) -} - // ValidateAndGetPushDevfileCommands validates the build and the run command, // if provided through odo push or else checks the devfile for devBuild and devRun. // It returns the build and run commands if its validated successfully, error otherwise. diff --git a/pkg/devfile/adapters/common/errors.go b/pkg/devfile/adapters/common/errors.go index bc099f06ac4..7344d05cc5d 100644 --- a/pkg/devfile/adapters/common/errors.go +++ b/pkg/devfile/adapters/common/errors.go @@ -6,24 +6,6 @@ import ( "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" ) -// NoDefaultForGroup indicates a error when no default command was found for the given Group -type NoDefaultForGroup struct { - Group v1alpha2.CommandGroupKind -} - -func (n NoDefaultForGroup) Error() string { - return fmt.Sprintf("there should be exactly one default command for command group %v, currently there is no default command", n.Group) -} - -// MoreDefaultForGroup indicates a error when more than one default command was found for the given Group -type MoreDefaultForGroup struct { - Group v1alpha2.CommandGroupKind -} - -func (m MoreDefaultForGroup) Error() string { - return fmt.Sprintf("there should be exactly one default command for command group %v, currently there is more than one default command", m.Group) -} - // NoCommandForGroup indicates a error when no command was found for the given Group type NoCommandForGroup struct { Group v1alpha2.CommandGroupKind diff --git a/pkg/devfile/adapters/common/generic.go b/pkg/devfile/adapters/common/generic.go index ca853ea250e..b1f6be0da96 100644 --- a/pkg/devfile/adapters/common/generic.go +++ b/pkg/devfile/adapters/common/generic.go @@ -2,12 +2,10 @@ package common import ( "io" - "strings" devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/devfile/library/pkg/devfile/parser/data/v2/common" "github.com/pkg/errors" - "github.com/redhat-developer/odo/pkg/log" "github.com/redhat-developer/odo/pkg/machineoutput" "github.com/redhat-developer/odo/pkg/util" "k8s.io/klog" @@ -51,10 +49,6 @@ func (a GenericAdapter) Logger() machineoutput.MachineEventLoggingClient { return a.logger } -func (a *GenericAdapter) SetLogger(loggingClient machineoutput.MachineEventLoggingClient) { - a.logger = loggingClient -} - func (a GenericAdapter) ComponentInfo(command devfilev1.Command) (ComponentInfo, error) { return a.componentInfo(command) } @@ -63,11 +57,6 @@ func (a GenericAdapter) SupervisorComponentInfo(command devfilev1.Command) (Comp return a.supervisordComponentInfo(command) } -// ExecuteCommand simply calls exec.ExecuteCommand using the GenericAdapter's client -func (a GenericAdapter) ExecuteCommand(compInfo ComponentInfo, command []string, show bool, consoleOutputStdout *io.PipeWriter, consoleOutputStderr *io.PipeWriter) (err error) { - return ExecuteCommand(a.client, compInfo, command, show, consoleOutputStdout, consoleOutputStderr) -} - // closeWriterAndWaitForAck closes the PipeWriter and then waits for a channel response from the ContainerOutputWriter (indicating that the reader had closed). // This ensures that we always get the full stderr/stdout output from the container process BEFORE we output the devfileCommandExecution event. func closeWriterAndWaitForAck(stdoutWriter *io.PipeWriter, stdoutChannel chan interface{}, stderrWriter *io.PipeWriter, stderrChannel chan interface{}) { @@ -178,39 +167,6 @@ func (a GenericAdapter) addToComposite(commandsMap PushCommandsMap, groupType de return commands, nil } -// 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 (a GenericAdapter) ExecDevfileEvent(events []string, eventType DevfileEventType, show bool) error { - if len(events) > 0 { - log.Infof("\nExecuting %s event commands for component %s", string(eventType), a.ComponentName) - commands, err := a.Devfile.Data.GetCommands(common.DevfileOptions{}) - if err != nil { - return err - } - - commandMap := GetCommandsMap(commands) - for _, commandName := range events { - // Convert commandName to lower because GetCommands converts Command.Exec.Id's to lower - command, ok := commandMap[strings.ToLower(commandName)] - if !ok { - return errors.New("unable to find devfile command " + commandName) - } - - c, err := New(command, commandMap, a) - if err != nil { - return err - } - // Execute command in container - err = c.Execute(show) - if err != nil { - return errors.Wrapf(err, "unable to execute devfile command %s", commandName) - } - } - } - return nil -} - func (a GenericAdapter) ApplyComponent(component string) error { return nil } diff --git a/pkg/devfile/adapters/common/utils.go b/pkg/devfile/adapters/common/utils.go index 6a4ce31cde8..4f61be57582 100644 --- a/pkg/devfile/adapters/common/utils.go +++ b/pkg/devfile/adapters/common/utils.go @@ -21,11 +21,6 @@ type PredefinedDevfileCommands string type DevfileEventType string const ( - // DefaultDevfileInitCommand is a predefined devfile command for init - DefaultDevfileInitCommand PredefinedDevfileCommands = "devinit" - - // DefaultDevfileBuildCommand is a predefined devfile command for build - DefaultDevfileBuildCommand PredefinedDevfileCommands = "devbuild" // DefaultDevfileRunCommand is a predefined devfile command for run DefaultDevfileRunCommand PredefinedDevfileCommands = "devrun" @@ -40,9 +35,6 @@ const ( // use GetBootstrapperImage() function instead of this variable defaultBootstrapperImage = "registry.access.redhat.com/ocp-tools-4/odo-init-container-rhel8:1.1.11" - // SupervisordControlCommand sub command which stands for control - SupervisordControlCommand = "ctl" - // SupervisordVolumeName Create a custom name and (hope) that users don't use the *exact* same name in their deployment (occlient.go) SupervisordVolumeName = "odo-supervisord-shared-data" @@ -61,18 +53,9 @@ const ( // ENV variable to overwrite image used to bootstrap SupervisorD in S2I and Devfile builder Image bootstrapperImageEnvName = "ODO_BOOTSTRAPPER_IMAGE" - // BinBash The path to sh executable - BinBash = "/bin/sh" - - // DefaultVolumeSize Default volume size for volumes defined in a devfile - DefaultVolumeSize = "1Gi" - // EnvProjectsRoot is the env defined for project mount in a component container when component's mountSources=true EnvProjectsRoot = "PROJECTS_ROOT" - // EnvProjectsSrc is the env defined for path to the project source in a component container - EnvProjectsSrc = "PROJECT_SOURCE" - // EnvOdoCommandRunWorkingDir is the env defined in the runtime component container which holds the work dir for the run command EnvOdoCommandRunWorkingDir = "ODO_COMMAND_RUN_WORKING_DIR" @@ -93,26 +76,8 @@ const ( // SupervisordCtlSubCommand is the supervisord sub command ctl SupervisordCtlSubCommand = "ctl" - - // 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" ) -// CommandNames is a struct to store the default and adapter names for devfile commands -type CommandNames struct { - DefaultName string - AdapterName string -} - // GetBootstrapperImage returns the odo-init bootstrapper image func GetBootstrapperImage() string { if env, ok := os.LookupEnv(bootstrapperImageEnvName); ok { diff --git a/pkg/devfile/adapters/kubernetes/component/adapter.go b/pkg/devfile/adapters/kubernetes/component/adapter.go index 393fed06953..5610a1b3b3f 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,12 @@ 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) + log.Infof("\nExecuting %s event commands for component %s", libdevfile.PreStop, a.ComponentName) + err = libdevfile.ExecPreStopEvents(a.Devfile, a.ComponentName, newExecHandler(a.Client, pod.Name, show)) if err != nil { return err } @@ -699,41 +698,6 @@ func (a Adapter) Delete(labels map[string]string, show bool, wait bool) error { return nil } -// Exec executes a command in the component -func (a Adapter) Exec(command []string) error { - exists, err := component.ComponentExists(a.Client, a.ComponentName, a.AppName) - if err != nil { - return err - } - - if !exists { - return errors.Errorf("the component %s doesn't exist on the cluster", a.ComponentName) - } - - runCommand, err := common.GetRunCommand(a.Devfile.Data, "") - if err != nil { - return err - } - containerName := runCommand.Exec.Component - - // get the pod - pod, err := component.GetOnePod(a.Client, a.ComponentName, a.AppName) - if err != nil { - return errors.Wrapf(err, "unable to get pod for component %s", a.ComponentName) - } - - if pod.Status.Phase != corev1.PodRunning { - return fmt.Errorf("unable to exec as the component is not running. Current status=%v", pod.Status.Phase) - } - - componentInfo := common.ComponentInfo{ - PodName: pod.Name, - ContainerName: containerName, - } - - return a.ExecuteCommand(componentInfo, command, true, nil, nil) -} - func (a Adapter) ExecCMDInContainer(componentInfo common.ComponentInfo, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error { return a.Client.ExecCMDInContainer(componentInfo.ContainerName, componentInfo.PodName, cmd, stdout, stderr, stdin, tty) } diff --git a/pkg/devfile/adapters/kubernetes/component/errors.go b/pkg/devfile/adapters/kubernetes/component/errors.go deleted file mode 100644 index e672af3ca8b..00000000000 --- a/pkg/devfile/adapters/kubernetes/component/errors.go +++ /dev/null @@ -1,13 +0,0 @@ -package component - -type NoDefaultDeployCommandFoundError struct{} - -func (e NoDefaultDeployCommandFoundError) Error() string { - return "error deploying, no default deploy command found in devfile" -} - -type MoreThanOneDefaultDeployCommandFoundError struct{} - -func (e MoreThanOneDefaultDeployCommandFoundError) Error() string { - return "more than one default deploy command found in devfile, should not happen" -} 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..c28062996bf --- /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 %s command %q on container %q", command.Id, 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/devfile/adapters/kubernetes/component/status.go b/pkg/devfile/adapters/kubernetes/component/status.go index acda0fb0f22..64357e0ddba 100644 --- a/pkg/devfile/adapters/kubernetes/component/status.go +++ b/pkg/devfile/adapters/kubernetes/component/status.go @@ -1,330 +1,14 @@ package component import ( - "context" - "reflect" - "sort" "strings" - "time" - - v1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" "github.com/pkg/errors" - "k8s.io/klog" "github.com/redhat-developer/odo/pkg/devfile/adapters/common" "github.com/redhat-developer/odo/pkg/machineoutput" ) -// KubernetesDeploymentStatus is a simplified representation of the component's cluster resources -type KubernetesDeploymentStatus struct { - DeploymentUID types.UID - ReplicaSetUID types.UID - Pods []*corev1.Pod -} - -// KubernetesPodStatus is a representation of corev1.Pod, but only containing the fields we are interested in (for later marshalling to JSON) -type KubernetesPodStatus struct { - Name string - UID string - Phase string - Labels map[string]string - StartTime *time.Time - Containers []corev1.ContainerStatus - InitContainers []corev1.ContainerStatus -} - -// Find the pod for the component and convert to KubernetesDeploymentStatus -func (a Adapter) getDeploymentStatus() (*KubernetesDeploymentStatus, error) { - - // 1) Retrieve the deployment - deployment, err := a.Client.GetOneDeployment(a.ComponentName, a.AppName) - if err != nil { - klog.V(4).Infof("Unable to retrieve deployment %s in %s ", a.ComponentName, a.Client.GetCurrentNamespace()) - return nil, err - } - - if deployment == nil { - return nil, errors.New("deployment status from Kubernetes API was nil") - } - - deploymentUID := deployment.UID - - // 2) Retrieve the replica set that is owned by the deployment; if multiple, go with one with largest generation - replicaSetList, err := a.Client.GetClient().AppsV1().ReplicaSets(a.Client.GetCurrentNamespace()).List(context.TODO(), metav1.ListOptions{}) - if err != nil { - return nil, err - } - - matchingReplicaSets := []v1.ReplicaSet{} - sort.Slice(replicaSetList.Items, func(i, j int) bool { - iGen := replicaSetList.Items[i].Generation - jGen := replicaSetList.Items[j].Generation - - // Sort descending by generation - return iGen > jGen - }) - - // Locate the first matching replica, after above sort -outer: - for _, replicaSet := range replicaSetList.Items { - for _, ownerRef := range replicaSet.OwnerReferences { - if ownerRef.UID == deploymentUID { - matchingReplicaSets = append(matchingReplicaSets, replicaSet) - break outer - } - } - } - - if len(matchingReplicaSets) == 0 { - return nil, errors.New("no replica sets found") - } - - replicaSetUID := matchingReplicaSets[0].UID - - // 3) Retrieves the pods that are owned by the ReplicaSet and return - podList, err := a.Client.GetClient().CoreV1().Pods(a.Client.GetCurrentNamespace()).List(context.TODO(), metav1.ListOptions{}) - if err != nil { - return nil, err - } - - matchingPods := []*corev1.Pod{} - for i, podItem := range podList.Items { - for _, ownerRef := range podItem.OwnerReferences { - - if string(ownerRef.UID) == string(replicaSetUID) { - matchingPods = append(matchingPods, &podList.Items[i]) - } - } - } - result := KubernetesDeploymentStatus{} - result.Pods = append(result.Pods, matchingPods...) - - result.DeploymentUID = deploymentUID - result.ReplicaSetUID = replicaSetUID - - return &result, nil - -} - -// CreateKubernetesPodStatusFromPod extracts only the fields we are interested in from corev1.Pod -func CreateKubernetesPodStatusFromPod(pod corev1.Pod) KubernetesPodStatus { - podStatus := KubernetesPodStatus{ - Name: pod.Name, - UID: string(pod.UID), - Phase: string(pod.Status.Phase), - Labels: pod.Labels, - InitContainers: []corev1.ContainerStatus{}, - Containers: []corev1.ContainerStatus{}, - } - - if pod.Status.StartTime != nil { - podStatus.StartTime = &pod.Status.StartTime.Time - } - - podStatus.InitContainers = pod.Status.InitContainerStatuses - - podStatus.Containers = pod.Status.ContainerStatuses - - return podStatus - -} - -const ( - // SupervisordCheckInterval is the time we wait before we check the supervisord statuses each time, after the first call - SupervisordCheckInterval = time.Duration(10) * time.Second -) - -// StartSupervisordCtlStatusWatch kicks off a goroutine which calls 'supervisord ctl status' within every odo-managed container, every X seconds, -// and reports the result to the console. -func (a Adapter) StartSupervisordCtlStatusWatch() { - - watcher := newSupervisordStatusWatch(a.Logger()) - - ticker := time.NewTicker(SupervisordCheckInterval) - - go func() { - - for { - // On initial goroutine start, perform a query - watcher.querySupervisordStatusFromContainers(a) - <-ticker.C - } - - }() - -} - -type supervisordStatusWatcher struct { - // See 'createSupervisordStatusReconciler' for a description of the reconciler - statusReconcilerChannel chan supervisordStatusEvent -} - -func newSupervisordStatusWatch(loggingClient machineoutput.MachineEventLoggingClient) *supervisordStatusWatcher { - inputChan := createSupervisordStatusReconciler(loggingClient) - - return &supervisordStatusWatcher{ - statusReconcilerChannel: inputChan, - } -} - -// createSupervisordStatusReconciler contains the status reconciler implementation. -// The reconciler receives (is sent) channel messages that contains the 'supervisord ctl status' values for each odo-managed container, -// with the result reported to the console. -func createSupervisordStatusReconciler(loggingClient machineoutput.MachineEventLoggingClient) chan supervisordStatusEvent { - - senderChannel := make(chan supervisordStatusEvent) - - go func() { - // Map key: 'podUID:containerName' (within pod) -> list of statuses from 'supervisord ctl status' - lastContainerStatus := map[string][]supervisordStatus{} - - for { - - event := <-senderChannel - - key := event.podUID + ":" + event.containerName - - previousStatus, hasLastContainerStatus := lastContainerStatus[key] - lastContainerStatus[key] = event.status - - reportChange := false - - if hasLastContainerStatus { - // If we saw a status for this container previously... - if !supervisordStatusesEqual(previousStatus, event.status) { - reportChange = true - } else { - reportChange = false - } - - } else { - // No status from the container previously... - reportChange = true - } - - entries := []machineoutput.SupervisordStatusEntry{} - - for _, status := range event.status { - entries = append(entries, machineoutput.SupervisordStatusEntry{ - Program: status.program, - Status: status.status, - }) - } - - loggingClient.SupervisordStatus(entries, machineoutput.TimestampNow()) - - if reportChange { - klog.V(4).Infof("Ccontainer %v status has changed - is: %v", event.containerName, event.status) - } - - } - - }() - - return senderChannel -} - -// querySupervisordStatusFromContainers locates the correct component's pod, and for each container within the pod queries the supervisord ctl status. -// The status results are sent to the reconciler. -func (sw *supervisordStatusWatcher) querySupervisordStatusFromContainers(a Adapter) { - - status, err := a.getDeploymentStatus() - if err != nil { - a.Logger().ReportError(errors.Wrap(err, "unable to retrieve container status"), machineoutput.TimestampNow()) - return - } - - if status == nil { - return - } - - // Given a list of odo-managed pods, we want to find the newest; if there are multiple with the same age, then find the most - // alive by container status. - var podPhaseSortOrder = map[corev1.PodPhase]int{ - corev1.PodFailed: 0, - corev1.PodSucceeded: 1, - corev1.PodUnknown: 2, - corev1.PodPending: 3, - corev1.PodRunning: 4, - } - sort.Slice(status.Pods, func(i, j int) bool { - - iPod := status.Pods[i] - jPod := status.Pods[j] - - iTime := iPod.CreationTimestamp.Time - jTime := jPod.CreationTimestamp.Time - - if !jTime.Equal(iTime) { - // Sort descending by creation timestamp - return jTime.After(iTime) - } - - // Next, sort descending to find the pod with most successful pod phase: - // PodRunning > PodPending > PodUnknown > PodSucceeded > PodFailed - return podPhaseSortOrder[jPod.Status.Phase] > podPhaseSortOrder[iPod.Status.Phase] - }) - - if len(status.Pods) < 1 { - return - } - - // Retrieve the first pod, which post-sort should be the most recent and most alive - pod := status.Pods[0] - - debugCommand, err := common.GetDebugCommand(a.Devfile.Data, a.devfileDebugCmd) - if err != nil { - a.Logger().ReportError(errors.Wrap(err, "unable to retrieve debug command"), machineoutput.TimestampNow()) - return - } - - runCommand, err := common.GetRunCommand(a.Devfile.Data, a.devfileRunCmd) - if err != nil { - a.Logger().ReportError(errors.Wrap(err, "unable to retrieve run command"), machineoutput.TimestampNow()) - return - } - - // For each of the containers, retrieve the status of the tasks and send that status back to the status reconciler - for _, container := range pod.Status.ContainerStatuses { - - if (runCommand.Exec != nil && container.Name == runCommand.Exec.Component) || (debugCommand.Exec != nil && container.Name == debugCommand.Exec.Component) { - status := getSupervisordStatusInContainer(pod.Name, container.Name, a) - - sw.statusReconcilerChannel <- supervisordStatusEvent{ - containerName: container.Name, - status: status, - podUID: string(pod.UID), - } - } - } -} - -// supervisordStatusesEqual is a simple comparison of []supervisord that ignores slice element order -func supervisordStatusesEqual(one []supervisordStatus, two []supervisordStatus) bool { - if len(one) != len(two) { - return false - } - - for _, oneVal := range one { - - match := false - for _, twoVal := range two { - - if reflect.DeepEqual(oneVal, twoVal) { - match = true - } - } - if !match { - return false - } - } - return true -} - // getSupervisordStatusInContainer executes 'supervisord ctl status' within the pod and container, parses the output, // and returns the status for the container func getSupervisordStatusInContainer(podName string, containerName string, a Adapter) []supervisordStatus { @@ -375,10 +59,3 @@ type supervisordStatus struct { program string status string } - -// All statuses seen within the container -type supervisordStatusEvent struct { - containerName string - podUID string - status []supervisordStatus -} diff --git a/pkg/devfile/adapters/kubernetes/component/status_test.go b/pkg/devfile/adapters/kubernetes/component/status_test.go deleted file mode 100644 index 803906575e1..00000000000 --- a/pkg/devfile/adapters/kubernetes/component/status_test.go +++ /dev/null @@ -1,284 +0,0 @@ -package component - -import ( - "testing" - - "github.com/redhat-developer/odo/pkg/util" - - "github.com/devfile/library/pkg/devfile/parser/data" - - "github.com/redhat-developer/odo/pkg/envinfo" - - devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" - devfileParser "github.com/devfile/library/pkg/devfile/parser" - "github.com/devfile/library/pkg/testingutil" - applabels "github.com/redhat-developer/odo/pkg/application/labels" - componentlabels "github.com/redhat-developer/odo/pkg/component/labels" - adaptersCommon "github.com/redhat-developer/odo/pkg/devfile/adapters/common" - "github.com/redhat-developer/odo/pkg/kclient" - - v1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - ktesting "k8s.io/client-go/testing" -) - -func TestGetDeploymentStatus(t *testing.T) { - - testComponentName := "component" - testAppName := "app" - - deploymentName, err := util.NamespaceKubernetesObject(testComponentName, testAppName) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - - tests := []struct { - name string - envInfo envinfo.EnvSpecificInfo - running bool - wantErr bool - deployment v1.Deployment - replicaSet v1.ReplicaSetList - podSet corev1.PodList - expectedDeploymentUID string - expectedReplicaSetUID string - expectedPodUID string - }{ - { - name: "Case 1: A single deployment, matching replica, and matching pod", - envInfo: envinfo.EnvSpecificInfo{}, - running: false, - wantErr: false, - deployment: v1.Deployment{ - TypeMeta: metav1.TypeMeta{ - Kind: kclient.DeploymentKind, - APIVersion: kclient.DeploymentAPIVersion, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: deploymentName, - UID: types.UID("deployment-uid"), - Labels: map[string]string{ - componentlabels.ComponentLabel: testComponentName, - applabels.ApplicationLabel: testAppName, - }, - }, - }, - replicaSet: v1.ReplicaSetList{ - Items: []v1.ReplicaSet{ - { - ObjectMeta: metav1.ObjectMeta{ - UID: "replica-set-uid", - OwnerReferences: []metav1.OwnerReference{ - { - UID: types.UID("deployment-uid"), - }, - }, - }, - Spec: v1.ReplicaSetSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{}, - }, - }, - }, - }, - }, - podSet: corev1.PodList{ - Items: []corev1.Pod{ - { - ObjectMeta: metav1.ObjectMeta{ - UID: "pod-uid", - OwnerReferences: []metav1.OwnerReference{ - { - UID: types.UID("replica-set-uid"), - }, - }, - }, - }, - }, - }, - expectedDeploymentUID: "deployment-uid", - expectedReplicaSetUID: "replica-set-uid", - expectedPodUID: "pod-uid", - }, - { - name: "Case 2: A single deployment, multiple replicas with different generations, and a single matching pod", - envInfo: envinfo.EnvSpecificInfo{}, - running: false, - wantErr: false, - deployment: v1.Deployment{ - TypeMeta: metav1.TypeMeta{ - Kind: kclient.DeploymentKind, - APIVersion: kclient.DeploymentAPIVersion, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: deploymentName, - UID: types.UID("deployment-uid"), - Labels: map[string]string{ - componentlabels.ComponentLabel: testComponentName, - applabels.ApplicationLabel: testAppName, - }, - }, - }, - replicaSet: v1.ReplicaSetList{ - Items: []v1.ReplicaSet{ - { - ObjectMeta: metav1.ObjectMeta{ - UID: "replica-set-uid1", - Generation: 1, - OwnerReferences: []metav1.OwnerReference{ - { - UID: types.UID("deployment-uid"), - }, - }, - }, - Spec: v1.ReplicaSetSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{}, - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - UID: "replica-set-uid2", - Generation: 2, - OwnerReferences: []metav1.OwnerReference{ - { - UID: types.UID("deployment-uid"), - }, - }, - }, - Spec: v1.ReplicaSetSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{}, - }, - }, - }, - }, - }, - podSet: corev1.PodList{ - Items: []corev1.Pod{ - { - ObjectMeta: metav1.ObjectMeta{ - UID: "pod-uid", - OwnerReferences: []metav1.OwnerReference{ - { - UID: types.UID("replica-set-uid2"), - }, - }, - }, - }, - }, - }, - expectedDeploymentUID: "deployment-uid", - expectedReplicaSetUID: "replica-set-uid2", - expectedPodUID: "pod-uid", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - - comp := testingutil.GetFakeContainerComponent(testComponentName) - devObj := devfileParser.DevfileObj{ - Data: func() data.DevfileData { - devfileData, err := data.NewDevfileData(string(data.APISchemaVersion200)) - if err != nil { - t.Error(err) - } - err = devfileData.AddComponents([]devfilev1.Component{comp}) - if err != nil { - t.Error(err) - } - err = devfileData.AddCommands([]devfilev1.Command{getExecCommand("run", devfilev1.RunCommandGroupKind)}) - if err != nil { - t.Error(err) - } - return devfileData - }(), - } - - adapterCtx := adaptersCommon.AdapterContext{ - ComponentName: testComponentName, - AppName: testAppName, - Devfile: devObj, - } - - fkclient, fkclientset := kclient.FakeNew() - - // Return test case's deployment, when requested - fkclientset.Kubernetes.PrependReactor("get", "*", func(action ktesting.Action) (bool, runtime.Object, error) { - if getAction, is := action.(ktesting.GetAction); is && getAction.GetName() == deploymentName { - return true, &tt.deployment, nil - } - return false, nil, nil - }) - - // Return test case's deployment, when requested - fkclientset.Kubernetes.PrependReactor("patch", "*", func(action ktesting.Action) (bool, runtime.Object, error) { - if patchAction, is := action.(ktesting.PatchAction); is && patchAction.GetName() == deploymentName { - return true, &tt.deployment, nil - } - return false, nil, nil - }) - - // Return test case's deployment, when requested - fkclientset.Kubernetes.PrependReactor("apply", "*", func(action ktesting.Action) (bool, runtime.Object, error) { - if patchAction, is := action.(ktesting.PatchAction); is && patchAction.GetName() == deploymentName { - return true, &tt.deployment, nil - } - return false, nil, nil - }) - - // Return test cases's replicasets, or pods, when requested - fkclientset.Kubernetes.PrependReactor("list", "*", func(action ktesting.Action) (bool, runtime.Object, error) { - switch action.GetResource().Resource { - case "replicasets": - return true, &tt.replicaSet, nil - case "pods": - return true, &tt.podSet, nil - case "deployments": - return true, &v1.DeploymentList{Items: []v1.Deployment{tt.deployment}}, nil - } - return false, nil, nil - }) - - tt.envInfo.EnvInfo = *envinfo.GetFakeEnvInfo(envinfo.ComponentSettings{ - Name: testComponentName, - AppName: testAppName, - }) - - componentAdapter := New(adapterCtx, fkclient, nil) - fkclient.Namespace = componentAdapter.Client.GetCurrentNamespace() - err := componentAdapter.createOrUpdateComponent(tt.running, tt.envInfo, false) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - // Call the function to test - result, err := componentAdapter.getDeploymentStatus() - // Checks for unexpected error cases - if !tt.wantErr == (err != nil) { - t.Fatalf("unexpected error %v, wantErr %v", err, tt.wantErr) - } - if string(result.DeploymentUID) != tt.expectedDeploymentUID { - t.Fatalf("could not find expected deployment UID %s %s", string(result.DeploymentUID), tt.expectedDeploymentUID) - } - - if string(result.ReplicaSetUID) != tt.expectedReplicaSetUID { - t.Fatalf("could not find expected replica set UID %s %s", string(result.ReplicaSetUID), tt.expectedReplicaSetUID) - } - - if result.Pods == nil || len(result.Pods) != 1 { - t.Fatalf("results of this test should match 1 pod") - } - - if string(result.Pods[0].UID) != tt.expectedPodUID { - t.Fatalf("pod UID did not match expected pod UID: %s %s", string(result.Pods[0].UID), tt.expectedPodUID) - } - - }) - } - -} 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..775e6624b4d 100644 --- a/pkg/libdevfile/libdevfile.go +++ b/pkg/libdevfile/libdevfile.go @@ -1,6 +1,8 @@ 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" @@ -9,6 +11,7 @@ import ( 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 +70,52 @@ 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 { + 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") +}