Skip to content

Commit

Permalink
Merge 7e9add1 into 3adf8e5
Browse files Browse the repository at this point in the history
  • Loading branch information
valaparthvi authored Mar 2, 2023
2 parents 3adf8e5 + 7e9add1 commit fe15d56
Show file tree
Hide file tree
Showing 14 changed files with 692 additions and 98 deletions.
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})
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
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
return c.KubeClient.BatchV1().Jobs(c.Namespace).Delete(context.Background(), jobName, metav1.DeleteOptions{PropagationPolicy: &propagationPolicy})
}
Loading

0 comments on commit fe15d56

Please sign in to comment.