Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deploy, Events without devfile/adapters #5460

Merged
Merged
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
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Among all the packages we have created the client interface for at business layer, component package has not been modified so far. Does it make sense to do it here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The component one is the more complex to refactor. I'll need to sleep before ;) Joke aside, no, it will deserve PR all to itself

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to need some sleep myself to form a mental picture of odo's code architecture. You're changing it fast (not complaining, complimenting), and it's tough to keep up at times. 😅

// 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