Skip to content

Commit

Permalink
Deploy, Events without devfile/adapters (#5460)
Browse files Browse the repository at this point in the history
* Execute devfile command

* Undeploy

* cleanup devfile/adapters

* refactor

* Move GetOnePod to component package

* Move DoesComponentExist and Log from devfile/adapter to component package

* Exec without devfile/adapters

* Move Delete from devfile/adapters to component

* Remove old Deploy code

* review

* Add tests for issue 5454

* Review
  • Loading branch information
feloy authored Feb 23, 2022
1 parent ff03b8e commit 0e4e55b
Show file tree
Hide file tree
Showing 61 changed files with 1,951 additions and 2,583 deletions.
90 changes: 90 additions & 0 deletions pkg/component/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package component
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"strings"

"github.com/pkg/errors"

"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/api/v2/pkg/devfile"
"github.com/devfile/library/pkg/devfile/parser"
parsercommon "github.com/devfile/library/pkg/devfile/parser/data/v2/common"
Expand All @@ -20,7 +22,9 @@ import (
"github.com/redhat-developer/odo/pkg/devfile/location"
"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/localConfigProvider"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/preference"
"github.com/redhat-developer/odo/pkg/service"
urlpkg "github.com/redhat-developer/odo/pkg/url"
Expand All @@ -30,6 +34,8 @@ import (

v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/klog"
)

const componentRandomNamePartsMaxLen = 12
Expand Down Expand Up @@ -503,3 +509,87 @@ func setLinksServiceNames(client kclient.ClientInterface, linkedSecrets []Secret
}
return nil
}

// GetOnePod gets a pod using the component and app name
func GetOnePod(client kclient.ClientInterface, componentName string, appName string) (*corev1.Pod, error) {
return client.GetOnePodFromSelector(componentlabels.GetSelector(componentName, appName))
}

// ComponentExists checks whether a deployment by the given name exists in the given app
func ComponentExists(client kclient.ClientInterface, name string, app string) (bool, error) {
deployment, err := client.GetOneDeployment(name, app)
if _, ok := err.(*kclient.DeploymentNotFoundError); ok {
klog.V(2).Infof("Deployment %s not found for belonging to the %s app ", name, app)
return false, nil
}
return deployment != nil, err
}

// Log returns log from component
func Log(client kclient.ClientInterface, componentName string, appName string, follow bool, command v1alpha2.Command) (io.ReadCloser, error) {

pod, err := GetOnePod(client, componentName, appName)
if err != nil {
return nil, errors.Errorf("the component %s doesn't exist on the cluster", componentName)
}

if pod.Status.Phase != corev1.PodRunning {
return nil, errors.Errorf("unable to show logs, component is not in running state. current status=%v", pod.Status.Phase)
}

containerName := command.Exec.Component

return client.GetPodLogs(pod.Name, containerName, follow)
}

// Delete deletes the component
func Delete(kubeClient kclient.ClientInterface, devfileObj parser.DevfileObj, componentName string, appName string, labels map[string]string, show bool, wait bool) error {
if labels == nil {
return fmt.Errorf("cannot delete with labels being nil")
}
log.Printf("Gathering information for component: %q", componentName)
podSpinner := log.Spinner("Checking status for component")
defer podSpinner.End(false)

pod, err := GetOnePod(kubeClient, componentName, appName)
if kerrors.IsForbidden(err) {
klog.V(2).Infof("Resource for %s forbidden", componentName)
// log the error if it failed to determine if the component exists due to insufficient RBACs
podSpinner.End(false)
log.Warningf("%v", err)
return nil
} else if e, ok := err.(*kclient.PodNotFoundError); ok {
podSpinner.End(false)
log.Warningf("%v", e)
return nil
} else if err != nil {
return errors.Wrapf(err, "unable to determine if component %s exists", componentName)
}

podSpinner.End(true)

// if there are preStop events, execute them before deleting the deployment
if libdevfile.HasPreStopEvents(devfileObj) {
if pod.Status.Phase != corev1.PodRunning {
return fmt.Errorf("unable to execute preStop events, pod for component %s is not running", componentName)
}
log.Infof("\nExecuting %s event commands for component %s", libdevfile.PreStop, componentName)
err = libdevfile.ExecPreStopEvents(devfileObj, componentName, NewExecHandler(kubeClient, pod.Name, show))
if err != nil {
return err
}
}

log.Infof("\nDeleting component %s", componentName)
spinner := log.Spinner("Deleting Kubernetes resources for component")
defer spinner.End(false)

err = kubeClient.Delete(labels, wait)
if err != nil {
return err
}

spinner.End(true)
log.Successf("Successfully deleted component")
return nil
}
156 changes: 156 additions & 0 deletions pkg/component/exec_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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 {
// deal with environment variables
var cmdLine string
setEnvVariable := util.GetCommandStringFromEnvs(command.Exec.Env)

if setEnvVariable == "" {
cmdLine = command.Exec.CommandLine
} else {
cmdLine = setEnvVariable + " && " + command.Exec.CommandLine
}

// Change to the workdir and execute the command
var cmd []string
if command.Exec.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 " + command.Exec.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

}
82 changes: 82 additions & 0 deletions pkg/deploy/deploy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package deploy

import (
"strings"

"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/pkg/devfile/parser"
devfilefs "github.com/devfile/library/pkg/testingutil/filesystem"

"github.com/pkg/errors"

componentlabels "github.com/redhat-developer/odo/pkg/component/labels"
"github.com/redhat-developer/odo/pkg/devfile/image"
"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/service"
)

type DeployClient struct {
kubeClient kclient.ClientInterface
}

func NewDeployClient(kubeClient kclient.ClientInterface) *DeployClient {
return &DeployClient{
kubeClient: kubeClient,
}
}

func (o *DeployClient) Deploy(devfileObj parser.DevfileObj, path string, appName string) error {
deployHandler := newDeployHandler(devfileObj, path, o.kubeClient, appName)
return libdevfile.Deploy(devfileObj, deployHandler)
}

type deployHandler struct {
devfileObj parser.DevfileObj
path string
kubeClient kclient.ClientInterface
appName string
}

func newDeployHandler(devfileObj parser.DevfileObj, path string, kubeClient kclient.ClientInterface, appName string) *deployHandler {
return &deployHandler{
devfileObj: devfileObj,
path: path,
kubeClient: kubeClient,
appName: appName,
}
}

func (o *deployHandler) ApplyImage(img v1alpha2.Component) error {
return image.BuildPushSpecificImage(o.devfileObj, o.path, img, true)
}

func (o *deployHandler) ApplyKubernetes(kubernetes v1alpha2.Component) error {
// validate if the GVRs represented by Kubernetes inlined components are supported by the underlying cluster
_, err := service.ValidateResourceExist(o.kubeClient, kubernetes, o.path)
if err != nil {
return err
}

labels := componentlabels.GetLabels(kubernetes.Name, o.appName, true)
u, err := service.GetK8sComponentAsUnstructured(kubernetes.Kubernetes, o.path, devfilefs.DefaultFs{})
if err != nil {
return err
}

log.Infof("\nDeploying Kubernetes %s: %s", u.GetKind(), u.GetName())
isOperatorBackedService, err := service.PushKubernetesResource(o.kubeClient, u, labels)
if err != nil {
return errors.Wrap(err, "failed to create service(s) associated with the component")
}
if isOperatorBackedService {
log.Successf("Kubernetes resource %q on the cluster; refer %q to know how to link it to the component", strings.Join([]string{u.GetKind(), u.GetName()}, "/"), "odo link -h")

}
return nil
}

func (o *deployHandler) Execute(command v1alpha2.Command) error {
return errors.New("Exec command is not implemented for Deploy")
}
8 changes: 8 additions & 0 deletions pkg/deploy/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package deploy

import "github.com/devfile/library/pkg/devfile/parser"

type Client interface {
// Deploy resources from a devfile located in path, for the specified appName
Deploy(devfileObj parser.DevfileObj, path string, appName string) error
}
Loading

0 comments on commit 0e4e55b

Please sign in to comment.