Support exec command (#6579)

* Support exec command for deploy

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Print log after timeout

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Add helper function to form proper commandLine

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Mockgen kclient

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Enhance error message

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Attempt at fixing unit test failures

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Rename import v1 to batchv1

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Remove TODOs

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Add integration tests and cleanup on user interrupt

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Temp changes

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Log tip to run odo logs after a minute

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* List components to delete even if there are no devfile resources

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Fix integration tests

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Fix deploy exec delete integration test

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Temp Change

* Fix delete command tests

* Fix mockgen client

* Fix validation errors

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Fix unit test failure

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Attemp at writing less flaky integration test

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Remove TODOs

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Add tip after 1 minute and return the go routine if job finishes before that

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Use the container as it is so that container-overrides can be taken into account

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Move job spec code to a different helper function inside the libdevfile pkg

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Modify the Execute method to use the new helper function and refactoring

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Attempt at fixing integration and unit tests

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Move defer to print remaining resources to a separate function, fix func doc and gofmt the files

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Fix test failures

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Cleanup

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Cleanup unused functions

Signed-off-by: Parthvi Vala <pvala@redhat.com>

---------

Signed-off-by: Parthvi Vala <pvala@redhat.com>
This commit is contained in:
Parthvi Vala
2023-03-03 15:58:16 +05:30
committed by GitHub
parent ff920107f9
commit 775adfd01e
14 changed files with 692 additions and 98 deletions

View File

@@ -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 {
@@ -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
}

View File

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

View File

@@ -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"
@@ -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
pkg/kclient/jobs.go Normal file
View File

@@ -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})
}

View File

@@ -17,10 +17,11 @@ import (
v1alpha10 "github.com/redhat-developer/service-binding-operator/apis/binding/v1alpha1"
v1alpha3 "github.com/redhat-developer/service-binding-operator/apis/spec/v1alpha3"
v10 "k8s.io/api/apps/v1"
v11 "k8s.io/api/core/v1"
v12 "k8s.io/api/networking/v1"
v11 "k8s.io/api/batch/v1"
v12 "k8s.io/api/core/v1"
v13 "k8s.io/api/networking/v1"
meta "k8s.io/apimachinery/pkg/api/meta"
v13 "k8s.io/apimachinery/pkg/apis/meta/v1"
v14 "k8s.io/apimachinery/pkg/apis/meta/v1"
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
schema "k8s.io/apimachinery/pkg/runtime/schema"
watch "k8s.io/apimachinery/pkg/watch"
@@ -84,11 +85,26 @@ func (mr *MockClientInterfaceMockRecorder) CreateDeployment(deploy interface{})
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDeployment", reflect.TypeOf((*MockClientInterface)(nil).CreateDeployment), deploy)
}
// CreateJob mocks base method.
func (m *MockClientInterface) CreateJob(job v11.Job, namespace string) (*v11.Job, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateJob", job, namespace)
ret0, _ := ret[0].(*v11.Job)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateJob indicates an expected call of CreateJob.
func (mr *MockClientInterfaceMockRecorder) CreateJob(job, namespace interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateJob", reflect.TypeOf((*MockClientInterface)(nil).CreateJob), job, namespace)
}
// CreateNamespace mocks base method.
func (m *MockClientInterface) CreateNamespace(name string) (*v11.Namespace, error) {
func (m *MockClientInterface) CreateNamespace(name string) (*v12.Namespace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateNamespace", name)
ret0, _ := ret[0].(*v11.Namespace)
ret0, _ := ret[0].(*v12.Namespace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -114,10 +130,10 @@ func (mr *MockClientInterfaceMockRecorder) CreateNewProject(projectName, wait in
}
// CreatePVC mocks base method.
func (m *MockClientInterface) CreatePVC(pvc v11.PersistentVolumeClaim) (*v11.PersistentVolumeClaim, error) {
func (m *MockClientInterface) CreatePVC(pvc v12.PersistentVolumeClaim) (*v12.PersistentVolumeClaim, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreatePVC", pvc)
ret0, _ := ret[0].(*v11.PersistentVolumeClaim)
ret0, _ := ret[0].(*v12.PersistentVolumeClaim)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -129,7 +145,7 @@ func (mr *MockClientInterfaceMockRecorder) CreatePVC(pvc interface{}) *gomock.Ca
}
// CreateSecret mocks base method.
func (m *MockClientInterface) CreateSecret(objectMeta v13.ObjectMeta, data map[string]string, ownerReference v13.OwnerReference) error {
func (m *MockClientInterface) CreateSecret(objectMeta v14.ObjectMeta, data map[string]string, ownerReference v14.OwnerReference) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateSecret", objectMeta, data, ownerReference)
ret0, _ := ret[0].(error)
@@ -143,7 +159,7 @@ func (mr *MockClientInterfaceMockRecorder) CreateSecret(objectMeta, data, ownerR
}
// CreateSecrets mocks base method.
func (m *MockClientInterface) CreateSecrets(componentName string, commonObjectMeta v13.ObjectMeta, svc *v11.Service, ownerReference v13.OwnerReference) error {
func (m *MockClientInterface) CreateSecrets(componentName string, commonObjectMeta v14.ObjectMeta, svc *v12.Service, ownerReference v14.OwnerReference) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateSecrets", componentName, commonObjectMeta, svc, ownerReference)
ret0, _ := ret[0].(error)
@@ -157,10 +173,10 @@ func (mr *MockClientInterfaceMockRecorder) CreateSecrets(componentName, commonOb
}
// CreateService mocks base method.
func (m *MockClientInterface) CreateService(svc v11.Service) (*v11.Service, error) {
func (m *MockClientInterface) CreateService(svc v12.Service) (*v12.Service, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateService", svc)
ret0, _ := ret[0].(*v11.Service)
ret0, _ := ret[0].(*v12.Service)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -172,10 +188,10 @@ func (mr *MockClientInterfaceMockRecorder) CreateService(svc interface{}) *gomoc
}
// CreateTLSSecret mocks base method.
func (m *MockClientInterface) CreateTLSSecret(tlsCertificate, tlsPrivKey []byte, objectMeta v13.ObjectMeta) (*v11.Secret, error) {
func (m *MockClientInterface) CreateTLSSecret(tlsCertificate, tlsPrivKey []byte, objectMeta v14.ObjectMeta) (*v12.Secret, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateTLSSecret", tlsCertificate, tlsPrivKey, objectMeta)
ret0, _ := ret[0].(*v11.Secret)
ret0, _ := ret[0].(*v12.Secret)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -200,6 +216,20 @@ func (mr *MockClientInterfaceMockRecorder) DeleteDynamicResource(name, gvr, wait
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDynamicResource", reflect.TypeOf((*MockClientInterface)(nil).DeleteDynamicResource), name, gvr, wait)
}
// DeleteJob mocks base method.
func (m *MockClientInterface) DeleteJob(jobName string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteJob", jobName)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteJob indicates an expected call of DeleteJob.
func (mr *MockClientInterfaceMockRecorder) DeleteJob(jobName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteJob", reflect.TypeOf((*MockClientInterface)(nil).DeleteJob), jobName)
}
// DeleteNamespace mocks base method.
func (m *MockClientInterface) DeleteNamespace(name string, wait bool) error {
m.ctrl.T.Helper()
@@ -314,10 +344,10 @@ func (mr *MockClientInterfaceMockRecorder) GeneratePortForwardReq(podName interf
}
// GetAllPodsInNamespaceMatchingSelector mocks base method.
func (m *MockClientInterface) GetAllPodsInNamespaceMatchingSelector(selector, ns string) (*v11.PodList, error) {
func (m *MockClientInterface) GetAllPodsInNamespaceMatchingSelector(selector, ns string) (*v12.PodList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAllPodsInNamespaceMatchingSelector", selector, ns)
ret0, _ := ret[0].(*v11.PodList)
ret0, _ := ret[0].(*v12.PodList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -591,11 +621,26 @@ func (mr *MockClientInterfaceMockRecorder) GetGVRFromGVK(gvk interface{}) *gomoc
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGVRFromGVK", reflect.TypeOf((*MockClientInterface)(nil).GetGVRFromGVK), gvk)
}
// GetJobLogs mocks base method.
func (m *MockClientInterface) GetJobLogs(job *v11.Job, containerName string) (io.ReadCloser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetJobLogs", job, containerName)
ret0, _ := ret[0].(io.ReadCloser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetJobLogs indicates an expected call of GetJobLogs.
func (mr *MockClientInterfaceMockRecorder) GetJobLogs(job, containerName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetJobLogs", reflect.TypeOf((*MockClientInterface)(nil).GetJobLogs), job, containerName)
}
// GetNamespace mocks base method.
func (m *MockClientInterface) GetNamespace(name string) (*v11.Namespace, error) {
func (m *MockClientInterface) GetNamespace(name string) (*v12.Namespace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetNamespace", name)
ret0, _ := ret[0].(*v11.Namespace)
ret0, _ := ret[0].(*v12.Namespace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -607,10 +652,10 @@ func (mr *MockClientInterfaceMockRecorder) GetNamespace(name interface{}) *gomoc
}
// GetNamespaceNormal mocks base method.
func (m *MockClientInterface) GetNamespaceNormal(name string) (*v11.Namespace, error) {
func (m *MockClientInterface) GetNamespaceNormal(name string) (*v12.Namespace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetNamespaceNormal", name)
ret0, _ := ret[0].(*v11.Namespace)
ret0, _ := ret[0].(*v12.Namespace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -682,10 +727,10 @@ func (mr *MockClientInterfaceMockRecorder) GetOneDeploymentFromSelector(selector
}
// GetOneService mocks base method.
func (m *MockClientInterface) GetOneService(componentName, appName string, isPartOfComponent bool) (*v11.Service, error) {
func (m *MockClientInterface) GetOneService(componentName, appName string, isPartOfComponent bool) (*v12.Service, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetOneService", componentName, appName, isPartOfComponent)
ret0, _ := ret[0].(*v11.Service)
ret0, _ := ret[0].(*v12.Service)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -697,10 +742,10 @@ func (mr *MockClientInterfaceMockRecorder) GetOneService(componentName, appName,
}
// GetOneServiceFromSelector mocks base method.
func (m *MockClientInterface) GetOneServiceFromSelector(selector string) (*v11.Service, error) {
func (m *MockClientInterface) GetOneServiceFromSelector(selector string) (*v12.Service, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetOneServiceFromSelector", selector)
ret0, _ := ret[0].(*v11.Service)
ret0, _ := ret[0].(*v12.Service)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -727,10 +772,10 @@ func (mr *MockClientInterfaceMockRecorder) GetOperatorGVRList() *gomock.Call {
}
// GetPVCFromName mocks base method.
func (m *MockClientInterface) GetPVCFromName(pvcName string) (*v11.PersistentVolumeClaim, error) {
func (m *MockClientInterface) GetPVCFromName(pvcName string) (*v12.PersistentVolumeClaim, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPVCFromName", pvcName)
ret0, _ := ret[0].(*v11.PersistentVolumeClaim)
ret0, _ := ret[0].(*v12.PersistentVolumeClaim)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -757,10 +802,10 @@ func (mr *MockClientInterfaceMockRecorder) GetPodLogs(podName, containerName, fo
}
// GetPodUsingComponentName mocks base method.
func (m *MockClientInterface) GetPodUsingComponentName(componentName string) (*v11.Pod, error) {
func (m *MockClientInterface) GetPodUsingComponentName(componentName string) (*v12.Pod, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPodUsingComponentName", componentName)
ret0, _ := ret[0].(*v11.Pod)
ret0, _ := ret[0].(*v12.Pod)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -772,10 +817,10 @@ func (mr *MockClientInterfaceMockRecorder) GetPodUsingComponentName(componentNam
}
// GetPodsMatchingSelector mocks base method.
func (m *MockClientInterface) GetPodsMatchingSelector(selector string) (*v11.PodList, error) {
func (m *MockClientInterface) GetPodsMatchingSelector(selector string) (*v12.PodList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPodsMatchingSelector", selector)
ret0, _ := ret[0].(*v11.PodList)
ret0, _ := ret[0].(*v12.PodList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -847,10 +892,10 @@ func (mr *MockClientInterfaceMockRecorder) GetRestMappingFromUnstructured(arg0 i
}
// GetRunningPodFromSelector mocks base method.
func (m *MockClientInterface) GetRunningPodFromSelector(selector string) (*v11.Pod, error) {
func (m *MockClientInterface) GetRunningPodFromSelector(selector string) (*v12.Pod, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetRunningPodFromSelector", selector)
ret0, _ := ret[0].(*v11.Pod)
ret0, _ := ret[0].(*v12.Pod)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -862,10 +907,10 @@ func (mr *MockClientInterfaceMockRecorder) GetRunningPodFromSelector(selector in
}
// GetSecret mocks base method.
func (m *MockClientInterface) GetSecret(name, namespace string) (*v11.Secret, error) {
func (m *MockClientInterface) GetSecret(name, namespace string) (*v12.Secret, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSecret", name, namespace)
ret0, _ := ret[0].(*v11.Secret)
ret0, _ := ret[0].(*v12.Secret)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -1057,10 +1102,10 @@ func (mr *MockClientInterfaceMockRecorder) ListDynamicResources(namespace, gvr,
}
// ListIngresses mocks base method.
func (m *MockClientInterface) ListIngresses(namespace, selector string) (*v12.IngressList, error) {
func (m *MockClientInterface) ListIngresses(namespace, selector string) (*v13.IngressList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListIngresses", namespace, selector)
ret0, _ := ret[0].(*v12.IngressList)
ret0, _ := ret[0].(*v13.IngressList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -1071,6 +1116,21 @@ func (mr *MockClientInterfaceMockRecorder) ListIngresses(namespace, selector int
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListIngresses", reflect.TypeOf((*MockClientInterface)(nil).ListIngresses), namespace, selector)
}
// ListJobs mocks base method.
func (m *MockClientInterface) ListJobs(selector string) (*v11.JobList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListJobs", selector)
ret0, _ := ret[0].(*v11.JobList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListJobs indicates an expected call of ListJobs.
func (mr *MockClientInterfaceMockRecorder) ListJobs(selector interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListJobs", reflect.TypeOf((*MockClientInterface)(nil).ListJobs), selector)
}
// ListPVCNames mocks base method.
func (m *MockClientInterface) ListPVCNames(selector string) ([]string, error) {
m.ctrl.T.Helper()
@@ -1087,10 +1147,10 @@ func (mr *MockClientInterfaceMockRecorder) ListPVCNames(selector interface{}) *g
}
// ListPVCs mocks base method.
func (m *MockClientInterface) ListPVCs(selector string) ([]v11.PersistentVolumeClaim, error) {
func (m *MockClientInterface) ListPVCs(selector string) ([]v12.PersistentVolumeClaim, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListPVCs", selector)
ret0, _ := ret[0].([]v11.PersistentVolumeClaim)
ret0, _ := ret[0].([]v12.PersistentVolumeClaim)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -1117,10 +1177,10 @@ func (mr *MockClientInterfaceMockRecorder) ListProjectNames() *gomock.Call {
}
// ListSecrets mocks base method.
func (m *MockClientInterface) ListSecrets(labelSelector string) ([]v11.Secret, error) {
func (m *MockClientInterface) ListSecrets(labelSelector string) ([]v12.Secret, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListSecrets", labelSelector)
ret0, _ := ret[0].([]v11.Secret)
ret0, _ := ret[0].([]v12.Secret)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -1148,10 +1208,10 @@ func (mr *MockClientInterfaceMockRecorder) ListServiceBindingsFromAllGroups() *g
}
// ListServices mocks base method.
func (m *MockClientInterface) ListServices(selector string) ([]v11.Service, error) {
func (m *MockClientInterface) ListServices(selector string) ([]v12.Service, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListServices", selector)
ret0, _ := ret[0].([]v11.Service)
ret0, _ := ret[0].([]v12.Service)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -1291,7 +1351,7 @@ func (mr *MockClientInterfaceMockRecorder) SetNamespace(ns interface{}) *gomock.
}
// SetupPortForwarding mocks base method.
func (m *MockClientInterface) SetupPortForwarding(pod *v11.Pod, portPairs []string, out, errOut io.Writer, stopChan chan struct{}) error {
func (m *MockClientInterface) SetupPortForwarding(pod *v12.Pod, portPairs []string, out, errOut io.Writer, stopChan chan struct{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetupPortForwarding", pod, portPairs, out, errOut, stopChan)
ret0, _ := ret[0].(error)
@@ -1305,7 +1365,7 @@ func (mr *MockClientInterfaceMockRecorder) SetupPortForwarding(pod, portPairs, o
}
// TryWithBlockOwnerDeletion mocks base method.
func (m *MockClientInterface) TryWithBlockOwnerDeletion(ownerReference v13.OwnerReference, exec func(v13.OwnerReference) error) error {
func (m *MockClientInterface) TryWithBlockOwnerDeletion(ownerReference v14.OwnerReference, exec func(v14.OwnerReference) error) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TryWithBlockOwnerDeletion", ownerReference, exec)
ret0, _ := ret[0].(error)
@@ -1348,7 +1408,7 @@ func (mr *MockClientInterfaceMockRecorder) UpdateDynamicResource(gvr, name, u in
}
// UpdatePVCLabels mocks base method.
func (m *MockClientInterface) UpdatePVCLabels(pvc *v11.PersistentVolumeClaim, labels map[string]string) error {
func (m *MockClientInterface) UpdatePVCLabels(pvc *v12.PersistentVolumeClaim, labels map[string]string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdatePVCLabels", pvc, labels)
ret0, _ := ret[0].(error)
@@ -1362,10 +1422,10 @@ func (mr *MockClientInterfaceMockRecorder) UpdatePVCLabels(pvc, labels interface
}
// UpdateSecret mocks base method.
func (m *MockClientInterface) UpdateSecret(secret *v11.Secret, namespace string) (*v11.Secret, error) {
func (m *MockClientInterface) UpdateSecret(secret *v12.Secret, namespace string) (*v12.Secret, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateSecret", secret, namespace)
ret0, _ := ret[0].(*v11.Secret)
ret0, _ := ret[0].(*v12.Secret)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -1377,10 +1437,10 @@ func (mr *MockClientInterfaceMockRecorder) UpdateSecret(secret, namespace interf
}
// UpdateService mocks base method.
func (m *MockClientInterface) UpdateService(svc v11.Service) (*v11.Service, error) {
func (m *MockClientInterface) UpdateService(svc v12.Service) (*v12.Service, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateService", svc)
ret0, _ := ret[0].(*v11.Service)
ret0, _ := ret[0].(*v12.Service)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -1392,7 +1452,7 @@ func (mr *MockClientInterfaceMockRecorder) UpdateService(svc interface{}) *gomoc
}
// UpdateStorageOwnerReference mocks base method.
func (m *MockClientInterface) UpdateStorageOwnerReference(pvc *v11.PersistentVolumeClaim, ownerReference ...v13.OwnerReference) error {
func (m *MockClientInterface) UpdateStorageOwnerReference(pvc *v12.PersistentVolumeClaim, ownerReference ...v14.OwnerReference) error {
m.ctrl.T.Helper()
varargs := []interface{}{pvc}
for _, a := range ownerReference {
@@ -1411,10 +1471,10 @@ func (mr *MockClientInterfaceMockRecorder) UpdateStorageOwnerReference(pvc inter
}
// WaitAndGetSecret mocks base method.
func (m *MockClientInterface) WaitAndGetSecret(name, namespace string) (*v11.Secret, error) {
func (m *MockClientInterface) WaitAndGetSecret(name, namespace string) (*v12.Secret, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WaitAndGetSecret", name, namespace)
ret0, _ := ret[0].(*v11.Secret)
ret0, _ := ret[0].(*v12.Secret)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -1425,6 +1485,21 @@ func (mr *MockClientInterfaceMockRecorder) WaitAndGetSecret(name, namespace inte
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitAndGetSecret", reflect.TypeOf((*MockClientInterface)(nil).WaitAndGetSecret), name, namespace)
}
// WaitForJobToComplete mocks base method.
func (m *MockClientInterface) WaitForJobToComplete(job *v11.Job) (*v11.Job, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WaitForJobToComplete", job)
ret0, _ := ret[0].(*v11.Job)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// WaitForJobToComplete indicates an expected call of WaitForJobToComplete.
func (mr *MockClientInterfaceMockRecorder) WaitForJobToComplete(job interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForJobToComplete", reflect.TypeOf((*MockClientInterface)(nil).WaitForJobToComplete), job)
}
// WaitForServiceAccountInNamespace mocks base method.
func (m *MockClientInterface) WaitForServiceAccountInNamespace(namespace, serviceAccountName string) error {
m.ctrl.T.Helper()

View File

@@ -0,0 +1,40 @@
package generator
import (
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type JobParams struct {
TypeMeta metav1.TypeMeta
ObjectMeta metav1.ObjectMeta
PodTemplateSpec corev1.PodTemplateSpec
SpecParams JobSpecParams
}
type JobSpecParams struct {
CompletionMode *batchv1.CompletionMode
TTLSecondsAfterFinished *int32
BackOffLimit *int32
Parallelism *int32
Completion *int32
ActiveDeadlineSeconds *int64
}
func GetJob(jobParams JobParams) batchv1.Job {
return batchv1.Job{
TypeMeta: jobParams.TypeMeta,
ObjectMeta: jobParams.ObjectMeta,
Spec: batchv1.JobSpec{
Template: jobParams.PodTemplateSpec,
Parallelism: jobParams.SpecParams.Parallelism,
Completions: jobParams.SpecParams.Completion,
ActiveDeadlineSeconds: jobParams.SpecParams.ActiveDeadlineSeconds,
BackoffLimit: jobParams.SpecParams.BackOffLimit,
// we delete jobs before exiting this function but setting this as a backup in case DeleteJob fails
TTLSecondsAfterFinished: jobParams.SpecParams.TTLSecondsAfterFinished,
CompletionMode: jobParams.SpecParams.CompletionMode,
},
}
}

View File

@@ -139,7 +139,11 @@ func (o *ComponentOptions) Run(ctx context.Context) error {
if o.name != "" {
return o.deleteNamedComponent(ctx)
}
return o.deleteDevfileComponent(ctx)
remainingResources, err := o.deleteDevfileComponent(ctx)
if err == nil {
printRemainingResources(ctx, remainingResources)
}
return err
}
// deleteNamedComponent deletes a component given its name
@@ -226,9 +230,23 @@ func messageWithPlatforms(cluster, podman bool, name, namespace string) string {
return fmt.Sprintf("No resource found for component %q%s\n", name, strings.Join(details, " or"))
}
// printRemainingResources lists the remaining cluster resources that are not found in the devfile.
func printRemainingResources(ctx context.Context, remainingResources []unstructured.Unstructured) {
if len(remainingResources) == 0 {
return
}
componentName := odocontext.GetComponentName(ctx)
namespace := odocontext.GetNamespace(ctx)
log.Printf("There are still resources left in the cluster that might be belonging to the deleted component.")
for _, resource := range remainingResources {
fmt.Printf("\t- %s: %s\n", resource.GetKind(), resource.GetName())
}
log.Infof("If you want to delete those, execute `odo delete component --name %s --namespace %s`\n", componentName, namespace)
}
// deleteDevfileComponent deletes all the components defined by the devfile in the current directory
// devfileObj in context must not be nil when this method is called
func (o *ComponentOptions) deleteDevfileComponent(ctx context.Context) error {
func (o *ComponentOptions) deleteDevfileComponent(ctx context.Context) ([]unstructured.Unstructured, error) {
var (
devfileObj = odocontext.GetDevfileObj(ctx)
componentName = odocontext.GetComponentName(ctx)
@@ -238,6 +256,7 @@ func (o *ComponentOptions) deleteDevfileComponent(ctx context.Context) error {
isClusterInnerLoopDeployed bool
hasClusterResources bool
clusterResources []unstructured.Unstructured
remainingResources []unstructured.Unstructured
isPodmanInnerLoopDeployed bool
hasPodmanResources bool
@@ -247,6 +266,7 @@ func (o *ComponentOptions) deleteDevfileComponent(ctx context.Context) error {
)
log.Info("Searching resources to delete, please wait...")
if o.clientset.KubernetesClient != nil {
isClusterInnerLoopDeployed, clusterResources, err = o.clientset.DeleteClient.ListClusterResourcesToDeleteFromDevfile(
*devfileObj, appName, componentName, o.runningIn)
@@ -254,21 +274,26 @@ func (o *ComponentOptions) deleteDevfileComponent(ctx context.Context) error {
if clierrors.AsWarning(err) {
log.Warning(err.Error())
} else {
return err
return nil, err
}
}
namespace = odocontext.GetNamespace(ctx)
hasClusterResources = len(clusterResources) != 0
// Get a list of component's resources present on the cluster
deployedResources, _ := o.clientset.DeleteClient.ListClusterResourcesToDelete(ctx, componentName, namespace, o.runningIn)
// Get a list of component's resources absent from the devfile, but present on the cluster
remainingResources = listResourcesMissingFromDevfilePresentOnCluster(componentName, clusterResources, deployedResources)
}
// 2. get podman resources
if o.clientset.PodmanClient != nil {
isPodmanInnerLoopDeployed, podmanPods, err = o.clientset.DeleteClient.ListPodmanResourcesToDelete(appName, componentName, o.runningIn)
if err != nil {
if clierrors.AsWarning(err) {
log.Warning(err.Error())
} else {
return err
return nil, err
}
}
hasPodmanResources = len(podmanPods) != 0
@@ -277,7 +302,8 @@ func (o *ComponentOptions) deleteDevfileComponent(ctx context.Context) error {
if !(hasClusterResources || hasPodmanResources) {
log.Infof(messageWithPlatforms(o.clientset.KubernetesClient != nil, o.clientset.PodmanClient != nil, componentName, namespace))
if !o.withFilesFlag {
return nil
// check for resources here
return remainingResources, nil
}
}
@@ -287,7 +313,7 @@ func (o *ComponentOptions) deleteDevfileComponent(ctx context.Context) error {
if o.withFilesFlag {
filesToDelete, err = getFilesCreatedByOdo(o.clientset.FS, ctx)
if err != nil {
return err
return nil, err
}
printFileCreatedByOdo(filesToDelete, hasClusterResources)
}
@@ -295,7 +321,7 @@ func (o *ComponentOptions) deleteDevfileComponent(ctx context.Context) error {
if !(hasClusterResources || hasPodmanResources || hasFilesToDelete) {
klog.V(2).Info("no cluster resources and no files to delete")
return nil
return remainingResources, nil
}
msg := fmt.Sprintf("Are you sure you want to delete %q and all its resources?", componentName)
@@ -306,10 +332,6 @@ func (o *ComponentOptions) deleteDevfileComponent(ctx context.Context) error {
if hasClusterResources {
spinner := log.Spinnerf("Deleting resources from cluster")
// Get a list of component's resources present on the cluster
deployedResources, _ := o.clientset.DeleteClient.ListClusterResourcesToDelete(ctx, componentName, namespace, o.runningIn)
// Get a list of component's resources absent from the devfile, but present on the cluster
remainingResources := listResourcesMissingFromDevfilePresentOnCluster(componentName, clusterResources, deployedResources)
// if innerloop deployment resource is present, then execute preStop events
if isClusterInnerLoopDeployed {
@@ -328,13 +350,6 @@ func (o *ComponentOptions) deleteDevfileComponent(ctx context.Context) error {
spinner.End(true)
log.Infof("The component %q is successfully deleted from namespace %q\n", componentName, namespace)
if len(remainingResources) != 0 {
log.Printf("There are still resources left in the cluster that might be belonging to the deleted component.")
for _, resource := range remainingResources {
fmt.Printf("\t- %s: %s\n", resource.GetKind(), resource.GetName())
}
log.Infof("If you want to delete those, execute `odo delete component --name %s --namespace %s`\n", componentName, namespace)
}
}
if hasPodmanResources {
@@ -354,7 +369,7 @@ func (o *ComponentOptions) deleteDevfileComponent(ctx context.Context) error {
}
if o.withFilesFlag {
//Delete files
// Delete files
remainingFiles := o.deleteFilesCreatedByOdo(o.clientset.FS, filesToDelete)
var listOfFiles []string
for f, e := range remainingFiles {
@@ -367,12 +382,11 @@ func (o *ComponentOptions) deleteDevfileComponent(ctx context.Context) error {
log.Info("You need to manually delete those.")
}
}
return nil
return remainingResources, nil
}
log.Error("Aborting deletion of component")
return nil
return remainingResources, nil
}
// listResourcesMissingFromDevfilePresentOnCluster returns a list of resources belonging to a component name that are present on cluster, but missing from devfile

View File

@@ -413,10 +413,11 @@ func TestComponentOptions_deleteDevfileComponent(t *testing.T) {
runningIn string
}
tests := []struct {
name string
fields fields
wantErr bool
deleteClient func(ctrl *gomock.Controller) _delete.Client
name string
fields fields
remainingResources []unstructured.Unstructured
wantErr bool
deleteClient func(ctrl *gomock.Controller) _delete.Client
}{
{
name: "deleting a component with access to devfile",
@@ -471,6 +472,23 @@ func TestComponentOptions_deleteDevfileComponent(t *testing.T) {
},
wantErr: false,
},
{
name: "deleting a component running in Deploy with access to devfile, with no resource present in the devfile but some present on the cluster",
deleteClient: func(ctrl *gomock.Controller) _delete.Client {
deleteClient := _delete.NewMockClient(ctrl)
deleteClient.EXPECT().ListClusterResourcesToDeleteFromDevfile(gomock.Any(), appName, gomock.Any(), labels.ComponentDeployMode).
Return(true, nil, nil)
deleteClient.EXPECT().ListClusterResourcesToDelete(gomock.Any(), compName, projectName, labels.ComponentDeployMode).
Return(resources, nil)
return deleteClient
},
fields: fields{
forceFlag: true,
runningIn: labels.ComponentDeployMode,
},
wantErr: false,
remainingResources: resources,
},
{
name: "deleting a component should not fail even if ExecutePreStopEvents fails",
deleteClient: func(ctrl *gomock.Controller) _delete.Client {
@@ -529,6 +547,7 @@ func TestComponentOptions_deleteDevfileComponent(t *testing.T) {
deleteClient: func(ctrl *gomock.Controller) _delete.Client {
deleteClient := _delete.NewMockClient(ctrl)
deleteClient.EXPECT().ListClusterResourcesToDeleteFromDevfile(gomock.Any(), appName, gomock.Any(), labels.ComponentAnyMode).Return(true, resources, nil)
deleteClient.EXPECT().ListClusterResourcesToDelete(gomock.Any(), gomock.Any(), gomock.Any(), labels.ComponentAnyMode)
return deleteClient
},
fields: fields{
@@ -541,6 +560,7 @@ func TestComponentOptions_deleteDevfileComponent(t *testing.T) {
deleteClient: func(ctrl *gomock.Controller) _delete.Client {
deleteClient := _delete.NewMockClient(ctrl)
deleteClient.EXPECT().ListClusterResourcesToDeleteFromDevfile(gomock.Any(), appName, gomock.Any(), labels.ComponentDevMode).Return(true, resources, nil)
deleteClient.EXPECT().ListClusterResourcesToDelete(gomock.Any(), gomock.Any(), gomock.Any(), labels.ComponentDevMode)
return deleteClient
},
fields: fields{
@@ -554,6 +574,7 @@ func TestComponentOptions_deleteDevfileComponent(t *testing.T) {
deleteClient: func(ctrl *gomock.Controller) _delete.Client {
deleteClient := _delete.NewMockClient(ctrl)
deleteClient.EXPECT().ListClusterResourcesToDeleteFromDevfile(gomock.Any(), appName, gomock.Any(), labels.ComponentDeployMode).Return(true, resources, nil)
deleteClient.EXPECT().ListClusterResourcesToDelete(gomock.Any(), gomock.Any(), gomock.Any(), labels.ComponentDeployMode)
return deleteClient
},
fields: fields{
@@ -568,6 +589,7 @@ func TestComponentOptions_deleteDevfileComponent(t *testing.T) {
deleteClient := _delete.NewMockClient(ctrl)
deleteClient.EXPECT().ListClusterResourcesToDeleteFromDevfile(gomock.Any(), appName, gomock.Any(), labels.ComponentAnyMode).
Return(false, nil, nil)
deleteClient.EXPECT().ListClusterResourcesToDelete(gomock.Any(), gomock.Any(), gomock.Any(), labels.ComponentAnyMode)
return deleteClient
},
fields: fields{
@@ -581,6 +603,7 @@ func TestComponentOptions_deleteDevfileComponent(t *testing.T) {
deleteClient := _delete.NewMockClient(ctrl)
deleteClient.EXPECT().ListClusterResourcesToDeleteFromDevfile(gomock.Any(), appName, gomock.Any(), labels.ComponentDevMode).
Return(false, nil, nil)
deleteClient.EXPECT().ListClusterResourcesToDelete(gomock.Any(), gomock.Any(), gomock.Any(), labels.ComponentDevMode)
return deleteClient
},
fields: fields{
@@ -595,6 +618,7 @@ func TestComponentOptions_deleteDevfileComponent(t *testing.T) {
deleteClient := _delete.NewMockClient(ctrl)
deleteClient.EXPECT().ListClusterResourcesToDeleteFromDevfile(gomock.Any(), appName, gomock.Any(), labels.ComponentDeployMode).
Return(false, nil, nil)
deleteClient.EXPECT().ListClusterResourcesToDelete(gomock.Any(), gomock.Any(), gomock.Any(), labels.ComponentDeployMode)
return deleteClient
},
fields: fields{
@@ -628,9 +652,13 @@ func TestComponentOptions_deleteDevfileComponent(t *testing.T) {
ctx = odocontext.WithWorkingDirectory(ctx, workingDir)
ctx = odocontext.WithComponentName(ctx, compName)
ctx = odocontext.WithDevfileObj(ctx, &info)
if err = o.deleteDevfileComponent(ctx); (err != nil) != tt.wantErr {
remainingResources, err := o.deleteDevfileComponent(ctx)
if (err != nil) != tt.wantErr {
t.Errorf("deleteDevfileComponent() error = %v, wantErr %v", err, tt.wantErr)
}
if diff := cmp.Diff(remainingResources, tt.remainingResources); diff != "" {
t.Errorf("deleteDevfileComponent() did not return expected resources: %s", diff)
}
})
}
}

View File

@@ -3,6 +3,7 @@ package deploy
import (
"context"
"fmt"
dfutil "github.com/devfile/library/v2/pkg/util"
"github.com/redhat-developer/odo/pkg/component"
"github.com/redhat-developer/odo/pkg/log"
@@ -62,7 +63,9 @@ func (o *DeployOptions) Validate(ctx context.Context) error {
if devfileObj == nil {
return genericclioptions.NewNoDevfileError(odocontext.GetWorkingDirectory(ctx))
}
return nil
componentName := odocontext.GetComponentName(ctx)
err := dfutil.ValidateK8sResourceName("component name", componentName)
return err
}
// Run contains the logic for the odo command

View File

@@ -104,10 +104,7 @@ func NamespaceKubernetesObject(componentName string, applicationName string) (st
// NamespaceKubernetesObjectWithTrim hyphenates applicationName and componentName
// if the resultant name is greater than 63 characters
// it trims app name then component name
func NamespaceKubernetesObjectWithTrim(componentName, applicationName string) (string, error) {
var (
maxLen = 63
)
func NamespaceKubernetesObjectWithTrim(componentName, applicationName string, maxLen int) (string, error) {
value, err := NamespaceKubernetesObject(componentName, applicationName)
if err != nil {
return "", err

View File

@@ -2564,7 +2564,7 @@ func TestNamespaceKubernetesObjectWithTrim(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NamespaceKubernetesObjectWithTrim(tt.args.componentName, tt.args.applicationName)
got, err := NamespaceKubernetesObjectWithTrim(tt.args.componentName, tt.args.applicationName, 63)
if (err != nil) != tt.wantErr {
t.Errorf("NamespaceKubernetesObjectWithTrim() error = %v, wantErr %v", err, tt.wantErr)
return

View File

@@ -0,0 +1,74 @@
commands:
- exec:
commandLine: npm install
component: runtime
group:
isDefault: true
kind: build
workingDir: /project
id: install
- exec:
commandLine: npm start
component: runtime
group:
isDefault: true
kind: run
workingDir: /project
id: run
- exec:
commandLine: npm run debug
component: runtime
group:
isDefault: true
kind: debug
workingDir: /project
id: debug
- exec:
commandLine: npm test
component: runtime
group:
isDefault: true
kind: test
workingDir: /project
id: test
- exec:
commandLine: echo Hello world
component: runtime
id: deploy-exec
- id: deploy
composite:
commands:
- deploy-exec
group:
kind: deploy
isDefault: true
components:
- container:
endpoints:
- name: http-3000
targetPort: 3000
image: registry.access.redhat.com/ubi8/nodejs-14:latest
memoryLimit: 1024Mi
mountSources: true
sourceMapping: /project
name: runtime
metadata:
description: Stack with Node.js 14
displayName: Node.js Runtime
icon: https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg
language: javascript
name: nodejs-prj1-api-abhz
projectType: nodejs
tags:
- NodeJS
- Express
- ubi8
version: 1.0.1
schemaVersion: 2.2.0
starterProjects:
- git:
remotes:
origin: https://github.com/odo-devfiles/nodejs-ex.git
name: nodejs-starter
variables:
CONTAINER_IMAGE: quay.io/unknown-account/myimage

View File

@@ -132,10 +132,15 @@ var _ = Describe("odo delete command tests", func() {
title string
devfileName string
setupFunc func()
// TODO(pvala): Find a better solution to renaming a resource when the data is in a different location
renameServiceFunc func(newName string)
}{
{
title: "a component is bootstrapped",
devfileName: "devfile-deploy-with-multiple-resources.yaml",
renameServiceFunc: func(newName string) {
helper.ReplaceString(filepath.Join(commonVar.Context, "devfile.yaml"), fmt.Sprintf("name: %s", serviceName), fmt.Sprintf("name: %s", newName))
},
},
{
title: "a component is bootstrapped using a devfile.yaml with URI-referenced Kubernetes components",
@@ -145,6 +150,9 @@ var _ = Describe("odo delete command tests", func() {
filepath.Join("source", "devfiles", "nodejs", "kubernetes", "devfile-deploy-with-multiple-resources-and-k8s-uri"),
filepath.Join(commonVar.Context, "kubernetes", "devfile-deploy-with-multiple-resources-and-k8s-uri"))
},
renameServiceFunc: func(newName string) {
helper.ReplaceString(filepath.Join(commonVar.Context, "kubernetes", "devfile-deploy-with-multiple-resources-and-k8s-uri", "outerloop-deploy-2.yaml"), fmt.Sprintf("name: %s", serviceName), fmt.Sprintf("name: %s", newName))
},
},
} {
// this is a workaround to ensure that the for loop works with `It` blocks
@@ -451,12 +459,12 @@ var _ = Describe("odo delete command tests", func() {
for _, withFiles := range []bool{true, false} {
withFiles := withFiles
When(fmt.Sprintf("a resource is changed in the devfile and the component is deleted while having access to the devfile.yaml with --files=%v",
withFiles), func() {
When(fmt.Sprintf("a resource is changed in the devfile and the component is deleted while having access to the devfile.yaml with --files=%v --running-in=%v",
withFiles, runningIn), func() {
var changedServiceName, stdout string
BeforeEach(func() {
changedServiceName = "my-changed-cs"
helper.ReplaceString(filepath.Join(commonVar.Context, "devfile.yaml"), fmt.Sprintf("name: %s", serviceName), fmt.Sprintf("name: %s", changedServiceName))
ctx.renameServiceFunc(changedServiceName)
args := []string{"delete", "component"}
if withFiles {
@@ -477,7 +485,7 @@ var _ = Describe("odo delete command tests", func() {
Expect(stdout).To(SatisfyAll(
ContainSubstring("There are still resources left in the cluster that might be belonging to the deleted component"),
Not(ContainSubstring(changedServiceName)),
ContainSubstring(serviceName),
ContainSubstring(fmt.Sprintf("Service: %s", serviceName)),
ContainSubstring("odo delete component --name %s --namespace %s", cmpName, commonVar.Project),
))
})
@@ -729,4 +737,21 @@ var _ = Describe("odo delete command tests", func() {
})
})
}
When("running odo deploy for an exec command bound to fail", func() {
BeforeEach(func() {
helper.CopyExampleDevFile(
filepath.Join("source", "devfiles", "nodejs", "devfile-deploy-exec.yaml"),
path.Join(commonVar.Context, "devfile.yaml"),
helper.DevfileMetadataNameSetter(cmpName))
helper.ReplaceString(filepath.Join(commonVar.Context, "devfile.yaml"), `image: registry.access.redhat.com/ubi8/nodejs-14:latest`, `image: registry.access.redhat.com/ubi8/nodejs-does-not-exist-14:latest`)
// We terminate after 5 seconds because the job should have been created by then and is bound to fail.
helper.Cmd("odo", "deploy").WithTerminate(5, nil).ShouldRun()
})
It("should print the job in the list of resources to be deleted with named delete command", func() {
out := helper.Cmd("odo", "delete", "component", "-f").ShouldPass().Out()
Expect(out).To(SatisfyAll(
ContainSubstring("There are still resources left in the cluster that might be belonging to the deleted component."),
ContainSubstring(fmt.Sprintf("Job: %s-app-deploy-exec", cmpName))))
})
})
})

View File

@@ -9,11 +9,9 @@ import (
"path/filepath"
"regexp"
segment "github.com/redhat-developer/odo/pkg/segment/context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
segment "github.com/redhat-developer/odo/pkg/segment/context"
"github.com/redhat-developer/odo/tests/helper"
)
@@ -483,6 +481,101 @@ CMD ["npm", "start"]
})
})
})
})
}
Context("deploying devfile with exec", func() {
BeforeEach(func() {
helper.CopyExampleDevFile(
filepath.Join("source", "devfiles", "nodejs", "devfile-deploy-exec.yaml"),
path.Join(commonVar.Context, "devfile.yaml"),
helper.DevfileMetadataNameSetter(cmpName))
})
for _, ctx := range []struct {
title, compName string
}{
{
title: "component name of at max(63) characters length",
compName: "document-how-odo-translates-container-component-to-deploymentss",
},
{
title: "component name of a normal character length",
compName: helper.RandString(6),
},
} {
ctx := ctx
When(fmt.Sprintf("using devfile that works; with %s", ctx.title), func() {
BeforeEach(func() {
helper.UpdateDevfileContent(filepath.Join(commonVar.Context, "devfile.yaml"), []helper.DevfileUpdater{helper.DevfileMetadataNameSetter(ctx.compName)})
})
It("should complete the command execution successfully", func() {
out := helper.Cmd("odo", "deploy").ShouldPass().Out()
Expect(out).To(ContainSubstring("Executing command in container (command: deploy-exec)"))
})
})
// We check the following tests for character length as long as 63 and for normal character length because for 63 char,
// the job name will be truncated, and we want to ensure the correct truncated name is used to delete the old job before running a new one so that `odo deploy` does not fail
When(fmt.Sprintf("the deploy command terminates abruptly; %s", ctx.title), func() {
BeforeEach(func() {
helper.UpdateDevfileContent(filepath.Join(commonVar.Context, "devfile.yaml"), []helper.DevfileUpdater{helper.DevfileMetadataNameSetter(ctx.compName)})
helper.ReplaceString(filepath.Join(commonVar.Context, "devfile.yaml"), `image: registry.access.redhat.com/ubi8/nodejs-14:latest`, `image: registry.access.redhat.com/ubi8/nodejs-does-not-exist-14:latest`)
helper.Cmd("odo", "deploy").WithTimeout(10).ShouldFail()
})
When("odo deploy command is run again", func() {
BeforeEach(func() {
// Restore the Devfile; this is not a required step to test, but we do it to not abruptly terminate the command again
helper.ReplaceString(filepath.Join(commonVar.Context, "devfile.yaml"), `image: registry.access.redhat.com/ubi8/nodejs-does-not-exist-14:latest`, `image: registry.access.redhat.com/ubi8/nodejs-14:latest`)
})
It("should run successfully", func() {
helper.Cmd("odo", "deploy").ShouldPass()
})
})
})
}
When("using a devfile name with length more than 63", func() {
const (
unacceptableLongName = "document-how-odo-translates-container-component-to-deploymentsss"
)
BeforeEach(func() {
helper.UpdateDevfileContent(filepath.Join(commonVar.Context, "devfile.yaml"), []helper.DevfileUpdater{helper.DevfileMetadataNameSetter(unacceptableLongName)})
})
It("should fail with invalid component name error", func() {
errOut := helper.Cmd("odo", "deploy").ShouldFail().Err()
Expect(errOut).To(SatisfyAll(ContainSubstring(fmt.Sprintf("component name %q is not valid", unacceptableLongName)),
ContainSubstring("Contain at most 63 characters"),
ContainSubstring("Start with an alphanumeric character"),
ContainSubstring("End with an alphanumeric character"),
ContainSubstring("Must not contain all numeric values")))
})
})
When("using devfile with a long running command in exec", func() {
BeforeEach(func() {
helper.ReplaceString(filepath.Join(commonVar.Context, "devfile.yaml"), `commandLine: echo Hello world`, `commandLine: sleep 62; echo hello world`)
})
It("should print the tip to run odo logs after 1 minute of execution", func() {
out := helper.Cmd("odo", "deploy").ShouldPass().Out()
Expect(out).To(ContainSubstring("Tip: Run `odo logs --deploy --follow` to get the logs of the command output."))
})
})
When("using devfile where the exec command is bound to fail", func() {
BeforeEach(func() {
// the following new commandLine ensures "counter $i counter" is printed on 99 lines of the output and the last line is a failure from running a non-existent binary
helper.ReplaceString(filepath.Join(commonVar.Context, "devfile.yaml"), `commandLine: echo Hello world`, `commandLine: for i in {1..100}; do echo counter $i counter; done; run-non-existent-binary`)
})
It("should print the last 100 lines of the log to the output", func() {
out, errOut := helper.Cmd("odo", "deploy").ShouldFail().OutAndErr()
Expect(out).To(ContainSubstring("Execution output:"))
// checking 'counter 1 counter' does not exist in the log output ensures that only the last 100 lines are printed
Expect(errOut).ToNot(ContainSubstring("counter 1 counter"))
Expect(errOut).To(ContainSubstring("/bin/sh: run-non-existent-binary: command not found"))
})
})
})
})