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

Support exec command #6579

Merged
merged 30 commits into from
Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ae17b89
Support exec command for deploy
valaparthvi Feb 8, 2023
177d8f5
Print log after timeout
valaparthvi Feb 8, 2023
85af90b
Add helper function to form proper commandLine
valaparthvi Feb 8, 2023
889d801
Mockgen kclient
valaparthvi Feb 8, 2023
4e996b7
Enhance error message
valaparthvi Feb 8, 2023
a71fe82
Attempt at fixing unit test failures
valaparthvi Feb 9, 2023
21beb54
Rename import v1 to batchv1
valaparthvi Feb 9, 2023
d5901e3
Remove TODOs
valaparthvi Feb 9, 2023
897562b
Add integration tests and cleanup on user interrupt
valaparthvi Feb 13, 2023
37dc3c7
Temp changes
valaparthvi Feb 13, 2023
4b90009
Log tip to run odo logs after a minute
valaparthvi Feb 17, 2023
e8968d3
List components to delete even if there are no devfile resources
valaparthvi Feb 17, 2023
563554a
Fix integration tests
valaparthvi Feb 17, 2023
a5b68be
Fix deploy exec delete integration test
valaparthvi Feb 17, 2023
14daad5
Temp Change
valaparthvi Feb 17, 2023
cdde976
Fix delete command tests
valaparthvi Feb 17, 2023
ef8e083
Fix mockgen client
valaparthvi Feb 17, 2023
d9ea411
Fix validation errors
valaparthvi Feb 17, 2023
3e4eecc
Fix unit test failure
valaparthvi Feb 17, 2023
caadaa5
Attemp at writing less flaky integration test
valaparthvi Feb 17, 2023
a3c8048
Remove TODOs
valaparthvi Feb 17, 2023
8f3accc
Add tip after 1 minute and return the go routine if job finishes befo…
valaparthvi Feb 21, 2023
855d79e
Use the container as it is so that container-overrides can be taken i…
valaparthvi Feb 21, 2023
04834e9
Move job spec code to a different helper function inside the libdevfi…
valaparthvi Feb 21, 2023
231d59d
Modify the Execute method to use the new helper function and refactoring
valaparthvi Feb 21, 2023
90dee37
Attempt at fixing integration and unit tests
valaparthvi Feb 21, 2023
f4c2a92
Move defer to print remaining resources to a separate function, fix f…
valaparthvi Feb 22, 2023
857a0db
Fix test failures
valaparthvi Feb 22, 2023
e60f841
Cleanup
valaparthvi Mar 2, 2023
c54a522
Cleanup unused functions
valaparthvi Mar 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 143 additions & 6 deletions pkg/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,31 @@ package deploy

import (
"context"
"errors"
"fmt"
"path/filepath"
"strings"
"time"

"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/v2/pkg/devfile/generator"
"github.com/devfile/library/v2/pkg/devfile/parser"
"github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"
"k8s.io/utils/pointer"

"github.com/redhat-developer/odo/pkg/component"
"github.com/redhat-developer/odo/pkg/devfile/image"
"github.com/redhat-developer/odo/pkg/kclient"
odolabels "github.com/redhat-developer/odo/pkg/labels"
"github.com/redhat-developer/odo/pkg/libdevfile"
odogenerator "github.com/redhat-developer/odo/pkg/libdevfile/generator"
"github.com/redhat-developer/odo/pkg/log"
odocontext "github.com/redhat-developer/odo/pkg/odo/context"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
"github.com/redhat-developer/odo/pkg/util"
)

type DeployClient struct {
Expand Down Expand Up @@ -83,10 +95,135 @@ func (o *deployHandler) ApplyOpenShift(openshift v1alpha2.Component) error {
}

// Execute will deploy the listed information in the `exec` section of devfile.yaml
// We currently do NOT support this in `odo deploy`.
func (o *deployHandler) Execute(command v1alpha2.Command) error {
// TODO:
// * Make sure we inject the "deploy" mode label once we implement exec in `odo deploy`
// * Make sure you inject the "component type" label once we implement exec.
return errors.New("exec command is not implemented for Deploy")
containerComps, err := generator.GetContainers(o.devfileObj, common.DevfileOptions{FilterByName: command.Exec.Component})
if err != nil {
return err
}
if len(containerComps) != 1 {
return fmt.Errorf("could not find the component")
}

containerComp := containerComps[0]
containerComp.Command = []string{"/bin/sh"}
containerComp.Args = getCmdline(command)

// Create a Kubernetes Job and use the container image referenced by command.Exec.Component
// Get the component for the command with command.Exec.Component
getJobName := func() string {
maxLen := kclient.JobNameOdoMaxLength - len(command.Id)
// We ignore the error here because our component name or app name will never be empty; which are the only cases when an error might be raised.
name, _ := util.NamespaceKubernetesObjectWithTrim(o.componentName, o.appName, maxLen)
name += "-" + command.Id
return name
}
completionMode := batchv1.CompletionMode("Indexed")
jobParams := odogenerator.JobParams{
TypeMeta: generator.GetTypeMeta(kclient.JobsKind, kclient.JobsAPIVersion),
ObjectMeta: metav1.ObjectMeta{
Name: getJobName(),
},
PodTemplateSpec: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{containerComp},
// Setting the restart policy to "never" so that pods are kept around after the job finishes execution; this is helpful in obtaining logs to debug.
RestartPolicy: "Never",
},
},
SpecParams: odogenerator.JobSpecParams{
CompletionMode: &completionMode,
TTLSecondsAfterFinished: pointer.Int32(60),
BackOffLimit: pointer.Int32(1),
},
}
job := odogenerator.GetJob(jobParams)
// Set labels and annotations
job.SetLabels(odolabels.GetLabels(o.componentName, o.appName, component.GetComponentRuntimeFromDevfileMetadata(o.devfileObj.Data.GetMetadata()), odolabels.ComponentDeployMode, false))
job.Annotations = map[string]string{}
odolabels.AddCommonAnnotations(job.Annotations)
odolabels.SetProjectType(job.Annotations, component.GetComponentTypeFromDevfileMetadata(o.devfileObj.Data.GetMetadata()))

// Make sure there are no existing jobs
checkAndDeleteExistingJob := func() {
items, dErr := o.kubeClient.ListJobs(odolabels.GetSelector(o.componentName, o.appName, odolabels.ComponentDeployMode, false))
if dErr != nil {
klog.V(4).Infof("failed to list jobs; cause: %s", dErr.Error())
return
}
jobName := getJobName()
for _, item := range items.Items {
if strings.Contains(item.Name, jobName) {
dErr = o.kubeClient.DeleteJob(item.Name)
if dErr != nil {
klog.V(4).Infof("failed to delete job %q; cause: %s", item.Name, dErr.Error())
}
}
}
}
checkAndDeleteExistingJob()

log.Sectionf("Executing command:")
spinner := log.Spinnerf("Executing command in container (command: %s)", command.Id)
defer spinner.End(false)

var createdJob *batchv1.Job
createdJob, err = o.kubeClient.CreateJob(job, "")
if err != nil {
return err
}
defer func() {
err = o.kubeClient.DeleteJob(createdJob.Name)
if err != nil {
klog.V(4).Infof("failed to delete job %q; cause: %s", createdJob.Name, err)
}
}()

var done = make(chan struct{}, 1)
// Print the tip to use `odo logs` if the command is still running after 1 minute
go func() {
select {
case <-time.After(1 * time.Minute):
log.Info("\nTip: Run `odo logs --deploy --follow` to get the logs of the command output.")
case <-done:
return
}
}()

// Wait for the command to complete execution
_, err = o.kubeClient.WaitForJobToComplete(createdJob)
done <- struct{}{}
if err != nil {
err = fmt.Errorf("failed to execute (command: %s)", command.Id)
// Print the job logs if the job failed
jobLogs, logErr := o.kubeClient.GetJobLogs(createdJob, command.Exec.Component)
if logErr != nil {
log.Warningf("failed to fetch the logs of execution; cause: %s", logErr)
}
fmt.Println("Execution output:")
_ = util.DisplayLog(false, jobLogs, log.GetStderr(), o.componentName, 100)
}

spinner.End(err == nil)

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
}
var args []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"
args = []string{"-c", "cd " + command.Exec.WorkingDir + " && " + cmdLine}
} else {
args = []string{"-c", cmdLine}
}
return args
}
2 changes: 1 addition & 1 deletion pkg/devfile/adapters/kubernetes/component/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ func (a *Adapter) createOrUpdateComponent(
serviceAnnotations["service.binding/backend_ip"] = "path={.spec.clusterIP}"
serviceAnnotations["service.binding/backend_port"] = "path={.spec.ports},elementType=sliceOfMaps,sourceKey=name,sourceValue=port"

serviceName, err := util.NamespaceKubernetesObjectWithTrim(componentName, a.AppName)
serviceName, err := util.NamespaceKubernetesObjectWithTrim(componentName, a.AppName, 63)
if err != nil {
return nil, false, err
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/kclient/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
projectv1 "github.com/openshift/api/project/v1"
olm "github.com/operator-framework/api/pkg/operators/v1alpha1"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/api/meta"
Expand Down Expand Up @@ -162,4 +163,13 @@ type ClientInterface interface {

// ingress_routes.go
ListIngresses(namespace, selector string) (*v1.IngressList, error)

ListJobs(selector string) (*batchv1.JobList, error)
// CreateJob creates a K8s job to execute task
CreateJob(job batchv1.Job, namespace string) (*batchv1.Job, error)
// WaitForJobToComplete to wait until a job completes or fails; it starts printing log or error if the job does not complete execution after 1 minute
WaitForJobToComplete(job *batchv1.Job) (*batchv1.Job, error)
// GetJobLogs retrieves pod logs of a job
GetJobLogs(job *batchv1.Job, containerName string) (io.ReadCloser, error)
DeleteJob(jobName string) error
}
98 changes: 98 additions & 0 deletions pkg/kclient/jobs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package kclient

import (
"context"
"fmt"
"io"

batchv1 "k8s.io/api/batch/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/klog"
)

// constants for volumes
const (
JobsKind = "Job"
JobsAPIVersion = "batch/v1"
// JobNameOdoMaxLength is the max length of a job name
// To be on the safe side, we keep the max length less than the original(k8s) max length;
// we do this because k8s job in odo is created to run exec commands in Deploy mode and this is not a user created resource,
// so we do not want to break because of any error with job
JobNameOdoMaxLength = 60
)

func (c *Client) ListJobs(selector string) (*batchv1.JobList, error) {
return c.KubeClient.BatchV1().Jobs(c.Namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selector})
}

// CreateJobs creates a K8s job to execute task
func (c *Client) CreateJob(job batchv1.Job, namespace string) (*batchv1.Job, error) {
if namespace == "" {
namespace = c.Namespace
}
createdJob, err := c.KubeClient.BatchV1().Jobs(namespace).Create(context.TODO(), &job, metav1.CreateOptions{FieldManager: FieldManager})
rm3l marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, fmt.Errorf("unable to create Jobs: %w", err)
}
return createdJob, nil
}

// WaitForJobToComplete to wait until a job completes or fails; it starts printing log or error if the job does not complete execution after 2 minutes
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fix the error msg because we're not using timeout.

func (c *Client) WaitForJobToComplete(job *batchv1.Job) (*batchv1.Job, error) {
klog.V(3).Infof("Waiting for Job %s to complete successfully", job.Name)

w, err := c.KubeClient.BatchV1().Jobs(c.Namespace).Watch(context.TODO(), metav1.ListOptions{
FieldSelector: fields.Set{"metadata.name": job.Name}.AsSelector().String(),
})
if err != nil {
return nil, fmt.Errorf("unable to watch job: %w", err)
}
defer w.Stop()

for {
val, ok := <-w.ResultChan()
if !ok {
break
}

wJob, ok := val.Object.(*batchv1.Job)
if !ok {
klog.V(4).Infof("did not receive job object, received: %v", val)
continue
}
for _, condition := range wJob.Status.Conditions {
if condition.Type == batchv1.JobFailed {
klog.V(4).Infof("Failed to execute the job, reason: %s", condition.String())
// we return the job as it is in case the caller requires it for further investigation.
return wJob, fmt.Errorf("failed to execute the job")
}
if condition.Type == batchv1.JobComplete {
return wJob, nil
}
}
}
return nil, nil
}

// GetJobLogs retrieves pod logs of a job
func (c *Client) GetJobLogs(job *batchv1.Job, containerName string) (io.ReadCloser, error) {
// Set standard log options
// RESTClient call to kubernetes
selector := labels.Set{"controller-uid": string(job.UID), "job-name": job.Name}.AsSelector().String()
pods, err := c.GetPodsMatchingSelector(selector)
if err != nil {
return nil, err
}
if len(pods.Items) == 0 {
return nil, fmt.Errorf("no pod found for job %q", job.Name)
}
pod := pods.Items[0]
return c.GetPodLogs(pod.Name, containerName, false)
}

func (c *Client) DeleteJob(jobName string) error {
propagationPolicy := metav1.DeletePropagationBackground
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Add a comment about the policy.

return c.KubeClient.BatchV1().Jobs(c.Namespace).Delete(context.Background(), jobName, metav1.DeleteOptions{PropagationPolicy: &propagationPolicy})
}
Loading