odo delete component without a devfile (#5511)

* Namespace

* Get all resources with selector

* Delete resources

* Display errors

* Add start message

* Set correct labels for odo deploy

* Add unit tests

* Add integration tests

* Delete managed by odo only

* Fix rebase

* Review + small fixes

* AdD/fix logs
This commit is contained in:
Philippe Martin
2022-03-10 11:44:18 +01:00
committed by GitHub
parent 53ef7f9438
commit daa9c61a41
14 changed files with 680 additions and 14 deletions

View File

@@ -0,0 +1,76 @@
package delete
import (
applabels "github.com/redhat-developer/odo/pkg/application/labels"
componentlabels "github.com/redhat-developer/odo/pkg/component/labels"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/klog"
)
type DeleteComponentClient struct {
kubeClient kclient.ClientInterface
}
func NewDeleteComponentClient(kubeClient kclient.ClientInterface) *DeleteComponentClient {
return &DeleteComponentClient{
kubeClient: kubeClient,
}
}
// ListResourcesToDelete lists Kubernetes resources from cluster in namespace for a given odo component
// It only returns resources not owned by another resource of the component, letting the garbage collector do its job
func (do *DeleteComponentClient) ListResourcesToDelete(componentName string, namespace string) ([]unstructured.Unstructured, error) {
var result []unstructured.Unstructured
labels := componentlabels.GetLabels(componentName, "app", false)
labels[applabels.ManagedBy] = "odo"
selector := util.ConvertLabelsToSelector(labels)
list, err := do.kubeClient.GetAllResourcesFromSelector(selector, namespace)
if err != nil {
return nil, err
}
for _, resource := range list {
referenced := false
for _, ownerRef := range resource.GetOwnerReferences() {
if references(list, ownerRef) {
referenced = true
break
}
}
if !referenced {
result = append(result, resource)
}
}
return result, nil
}
func (do *DeleteComponentClient) DeleteResources(resources []unstructured.Unstructured) []unstructured.Unstructured {
var failed []unstructured.Unstructured
for _, resource := range resources {
gvr, err := do.kubeClient.GetRestMappingFromUnstructured(resource)
if err != nil {
failed = append(failed, resource)
continue
}
err = do.kubeClient.DeleteDynamicResource(resource.GetName(), gvr.Resource.Group, gvr.Resource.Version, gvr.Resource.Resource)
if err != nil {
klog.V(3).Infof("failed to delete resource %q (%s.%s.%s): %v", resource.GetName(), gvr.Resource.Group, gvr.Resource.Version, gvr.Resource.Resource, err)
failed = append(failed, resource)
}
}
return failed
}
// references returns true if ownerRef references a resource in the list
func references(list []unstructured.Unstructured, ownerRef metav1.OwnerReference) bool {
for _, resource := range list {
if ownerRef.APIVersion == resource.GetAPIVersion() && ownerRef.Kind == resource.GetKind() && ownerRef.Name == resource.GetName() {
return true
}
}
return false
}

View File

@@ -0,0 +1,239 @@
package delete
import (
"errors"
"reflect"
"testing"
"github.com/golang/mock/gomock"
"github.com/redhat-developer/odo/pkg/kclient"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
)
func TestDeleteComponentClient_ListResourcesToDelete(t *testing.T) {
res1 := unstructured.Unstructured{}
res1.SetAPIVersion("v1")
res1.SetKind("deployment")
res1.SetName("dep1")
res2 := unstructured.Unstructured{}
res2.SetAPIVersion("v1")
res2.SetKind("service")
res2.SetName("svc1")
type fields struct {
kubeClient func(ctrl *gomock.Controller) kclient.ClientInterface
}
type args struct {
componentName string
namespace string
}
tests := []struct {
name string
fields fields
args args
want []unstructured.Unstructured
wantErr bool
}{
{
name: "no resource found",
fields: fields{
kubeClient: func(ctrl *gomock.Controller) kclient.ClientInterface {
client := kclient.NewMockClientInterface(ctrl)
selector := "app.kubernetes.io/instance=my-component,app.kubernetes.io/managed-by=odo,app.kubernetes.io/part-of=app"
client.EXPECT().GetAllResourcesFromSelector(selector, "my-ns").Return(nil, nil)
return client
},
},
args: args{
componentName: "my-component",
namespace: "my-ns",
},
wantErr: false,
want: nil,
},
{
name: "2 unrelated resources found",
fields: fields{
kubeClient: func(ctrl *gomock.Controller) kclient.ClientInterface {
var resources []unstructured.Unstructured
resources = append(resources, res1, res2)
client := kclient.NewMockClientInterface(ctrl)
selector := "app.kubernetes.io/instance=my-component,app.kubernetes.io/managed-by=odo,app.kubernetes.io/part-of=app"
client.EXPECT().GetAllResourcesFromSelector(selector, "my-ns").Return(resources, nil)
return client
},
},
args: args{
componentName: "my-component",
namespace: "my-ns",
},
wantErr: false,
want: []unstructured.Unstructured{res1, res2},
},
{
name: "2 resources found, one owned by the other",
fields: fields{
kubeClient: func(ctrl *gomock.Controller) kclient.ClientInterface {
var resources []unstructured.Unstructured
res1.SetOwnerReferences([]metav1.OwnerReference{
{
APIVersion: res2.GetAPIVersion(),
Kind: res2.GetKind(),
Name: res2.GetName(),
},
})
resources = append(resources, res1, res2)
client := kclient.NewMockClientInterface(ctrl)
selector := "app.kubernetes.io/instance=my-component,app.kubernetes.io/managed-by=odo,app.kubernetes.io/part-of=app"
client.EXPECT().GetAllResourcesFromSelector(selector, "my-ns").Return(resources, nil)
return client
},
},
args: args{
componentName: "my-component",
namespace: "my-ns",
},
wantErr: false,
want: []unstructured.Unstructured{res2},
},
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
do := &DeleteComponentClient{
kubeClient: tt.fields.kubeClient(ctrl),
}
got, err := do.ListResourcesToDelete(tt.args.componentName, tt.args.namespace)
if (err != nil) != tt.wantErr {
t.Errorf("DeleteComponentClient.ListResourcesToDelete() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("DeleteComponentClient.ListResourcesToDelete() = %v, want %v", got, tt.want)
}
})
}
}
func TestDeleteComponentClient_DeleteResources(t *testing.T) {
res1 := unstructured.Unstructured{}
res1.SetAPIVersion("v1")
res1.SetKind("deployment")
res1.SetName("dep1")
res2 := unstructured.Unstructured{}
res2.SetAPIVersion("v1")
res2.SetKind("service")
res2.SetName("svc1")
type fields struct {
kubeClient func(ctrl *gomock.Controller) kclient.ClientInterface
}
type args struct {
resources []unstructured.Unstructured
}
tests := []struct {
name string
fields fields
args args
want []unstructured.Unstructured
}{
{
name: "2 resources deleted succesfully",
fields: fields{
kubeClient: func(ctrl *gomock.Controller) kclient.ClientInterface {
client := kclient.NewMockClientInterface(ctrl)
client.EXPECT().GetRestMappingFromUnstructured(res1).Return(&meta.RESTMapping{
Resource: schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: res1.GetKind(),
},
}, nil)
client.EXPECT().GetRestMappingFromUnstructured(res2).Return(&meta.RESTMapping{
Resource: schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: res2.GetKind(),
},
}, nil)
client.EXPECT().DeleteDynamicResource(res1.GetName(), "", "v1", res1.GetKind())
client.EXPECT().DeleteDynamicResource(res2.GetName(), "", "v1", res2.GetKind())
return client
},
},
args: args{
resources: []unstructured.Unstructured{res1, res2},
},
want: nil,
},
{
name: "2 resources, 1 deleted succesfully, 1 failed during restmapping",
fields: fields{
kubeClient: func(ctrl *gomock.Controller) kclient.ClientInterface {
client := kclient.NewMockClientInterface(ctrl)
client.EXPECT().GetRestMappingFromUnstructured(res1).Return(nil, errors.New("some restmapping error"))
client.EXPECT().GetRestMappingFromUnstructured(res2).Return(&meta.RESTMapping{
Resource: schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: res2.GetKind(),
},
}, nil)
client.EXPECT().DeleteDynamicResource(res2.GetName(), "", "v1", res2.GetKind())
return client
},
},
args: args{
resources: []unstructured.Unstructured{res1, res2},
},
want: []unstructured.Unstructured{res1},
},
{
name: "2 resources, 1 deleted succesfully, 1 failed during deletion",
fields: fields{
kubeClient: func(ctrl *gomock.Controller) kclient.ClientInterface {
client := kclient.NewMockClientInterface(ctrl)
client.EXPECT().GetRestMappingFromUnstructured(res1).Return(&meta.RESTMapping{
Resource: schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: res1.GetKind(),
},
}, nil)
client.EXPECT().GetRestMappingFromUnstructured(res2).Return(&meta.RESTMapping{
Resource: schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: res2.GetKind(),
},
}, nil)
client.EXPECT().DeleteDynamicResource(res1.GetName(), "", "v1", res1.GetKind()).Return(errors.New("some error"))
client.EXPECT().DeleteDynamicResource(res2.GetName(), "", "v1", res2.GetKind())
return client
},
},
args: args{
resources: []unstructured.Unstructured{res1, res2},
},
want: []unstructured.Unstructured{res1},
},
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
do := &DeleteComponentClient{
kubeClient: tt.fields.kubeClient(ctrl),
}
if got := do.DeleteResources(tt.args.resources); !reflect.DeepEqual(got, tt.want) {
t.Errorf("DeleteComponentClient.DeleteResources() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,10 @@
package delete
import "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
type Client interface {
// ListResourcesToDelete lists Kubernetes resources from cluster in namespace for a given odo component
ListResourcesToDelete(componentName string, namespace string) ([]unstructured.Unstructured, error)
// DeleteResources deletes the unstuctured resources and return the resources that failed to be deleted
DeleteResources([]unstructured.Unstructured) []unstructured.Unstructured
}

View File

@@ -0,0 +1,64 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: pkg/component/delete/interface.go
// Package delete is a generated GoMock package.
package delete
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// MockClient is a mock of Client interface.
type MockClient struct {
ctrl *gomock.Controller
recorder *MockClientMockRecorder
}
// MockClientMockRecorder is the mock recorder for MockClient.
type MockClientMockRecorder struct {
mock *MockClient
}
// NewMockClient creates a new mock instance.
func NewMockClient(ctrl *gomock.Controller) *MockClient {
mock := &MockClient{ctrl: ctrl}
mock.recorder = &MockClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockClient) EXPECT() *MockClientMockRecorder {
return m.recorder
}
// DeleteResources mocks base method.
func (m *MockClient) DeleteResources(arg0 []unstructured.Unstructured) []unstructured.Unstructured {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteResources", arg0)
ret0, _ := ret[0].([]unstructured.Unstructured)
return ret0
}
// DeleteResources indicates an expected call of DeleteResources.
func (mr *MockClientMockRecorder) DeleteResources(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteResources", reflect.TypeOf((*MockClient)(nil).DeleteResources), arg0)
}
// ListResourcesToDelete mocks base method.
func (m *MockClient) ListResourcesToDelete(componentName, namespace string) ([]unstructured.Unstructured, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListResourcesToDelete", componentName, namespace)
ret0, _ := ret[0].([]unstructured.Unstructured)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListResourcesToDelete indicates an expected call of ListResourcesToDelete.
func (mr *MockClientMockRecorder) ListResourcesToDelete(componentName, namespace interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListResourcesToDelete", reflect.TypeOf((*MockClient)(nil).ListResourcesToDelete), componentName, namespace)
}

View File

@@ -0,0 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: pkg/component/delete/undeploy_handler.go
// Package delete is a generated GoMock package.
package delete

View File

@@ -13,6 +13,15 @@ const ComponentTypeLabel = "app.kubernetes.io/name"
const ComponentTypeAnnotation = "odo.dev/project-type"
// ComponentDeployLabel ...
const ComponentDeployLabel = "Deploy"
// ComponentModeLabel ...
const ComponentModeLabel = "odo.dev/mode"
// ComponentProjectTypeLabel ...
const ComponentProjectTypeLabel = "odo.dev/project-type"
// GetLabels return labels that should be applied to every object for given component in active application
// additional labels are used only for creating object
// if you are creating something use additional=true

View File

@@ -6,9 +6,11 @@ import (
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/pkg/devfile/parser"
devfilefs "github.com/devfile/library/pkg/testingutil/filesystem"
"k8s.io/klog"
"github.com/pkg/errors"
"github.com/redhat-developer/odo/pkg/component"
componentlabels "github.com/redhat-developer/odo/pkg/component/labels"
"github.com/redhat-developer/odo/pkg/devfile/image"
"github.com/redhat-developer/odo/pkg/kclient"
@@ -48,6 +50,7 @@ func newDeployHandler(devfileObj parser.DevfileObj, path string, kubeClient kcli
}
}
// ApplyImage builds and pushes the OCI image to be used on Kubernetes
func (o *deployHandler) ApplyImage(img v1alpha2.Component) error {
return image.BuildPushSpecificImage(o.devfileObj, o.path, img, true)
}
@@ -59,17 +62,28 @@ func (o *deployHandler) ApplyKubernetes(kubernetes v1alpha2.Component) error {
return err
}
labels := componentlabels.GetLabels(kubernetes.Name, o.appName, true)
// Get the most common labels that's applicable to all resources being deployed.
// Retrieve the component type from the devfile and also inject it into the list of labels
// Set the mode to DEPLOY. Regardless of what Kubernetes resource we are deploying.
labels := componentlabels.GetLabels(o.devfileObj.Data.GetMetadata().Name, o.appName, true)
componentType := component.GetComponentTypeFromDevfileMetadata(o.devfileObj.Data.GetMetadata())
labels[componentlabels.ComponentProjectTypeLabel] = componentType
labels[componentlabels.ComponentModeLabel] = componentlabels.ComponentDeployLabel
klog.V(4).Infof("Injecting labels: %+v into k8s artifact", labels)
// Get the Kubernetes component
u, err := service.GetK8sComponentAsUnstructured(kubernetes.Kubernetes, o.path, devfilefs.DefaultFs{})
if err != nil {
return err
}
// Deploy the actual Kubernetes component and error out if there's an issue.
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")
@@ -77,6 +91,11 @@ func (o *deployHandler) ApplyKubernetes(kubernetes v1alpha2.Component) error {
return nil
}
// 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")
}

View File

@@ -1,8 +1,12 @@
package delete
import (
"fmt"
"github.com/spf13/cobra"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/odo/cli/ui"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions/clientset"
@@ -14,6 +18,15 @@ const ComponentRecommendedCommandName = "component"
type ComponentOptions struct {
// name of the component to delete, optional
name string
// namespace on which to find the component to delete, optional, defaults to current namespace
namespace string
// forceFlag forces deletion
forceFlag bool
// Clients
clientset *clientset.Clientset
}
// NewComponentOptions returns new instance of ComponentOptions
@@ -22,9 +35,19 @@ func NewComponentOptions() *ComponentOptions {
}
func (o *ComponentOptions) SetClientset(clientset *clientset.Clientset) {
o.clientset = clientset
}
func (o *ComponentOptions) Complete(cmdline cmdline.Cmdline, args []string) (err error) {
if o.name == "" {
// TODO #5478
return nil
}
if o.namespace != "" {
o.clientset.KubernetesClient.SetNamespace(o.namespace)
} else {
o.namespace = o.clientset.KubernetesClient.GetCurrentNamespace()
}
return nil
}
@@ -42,6 +65,29 @@ func (o *ComponentOptions) Run() error {
// deleteNamedComponent deletes a component given its name
func (o *ComponentOptions) deleteNamedComponent() error {
log.Info("Searching resources to delete, please wait...")
list, err := o.clientset.DeleteClient.ListResourcesToDelete(o.name, o.namespace)
if err != nil {
return err
}
if len(list) == 0 {
log.Infof("No resource found for component %q in namespace %q\n", o.name, o.namespace)
return nil
}
log.Info("The following resources will be deleted: ")
for _, resource := range list {
fmt.Printf("\t%s: %s\n", resource.GetKind(), resource.GetName())
}
if o.forceFlag || ui.Proceed("Are you sure you want to delete these resources?") {
failed := o.clientset.DeleteClient.DeleteResources(list)
for _, fail := range failed {
log.Warningf("Failed to delete the %q resource: %s\n", fail.GetKind(), fail.GetName())
}
log.Infof("The component %q is successfully deleted from namespace %q", o.name, o.namespace)
return nil
}
log.Error("Aborting deletion of component")
return nil
}
@@ -64,6 +110,9 @@ func NewCmdComponent(name, fullName string) *cobra.Command {
},
}
componentCmd.Flags().StringVar(&o.name, "name", "", "Name of the component to delete, optional. By default, the component described in the local devfile is deleted")
componentCmd.Flags().StringVar(&o.namespace, "namespace", "", "Namespace in which to find the component to delete, optional. By default, the current namespace defined in kube config is used")
componentCmd.Flags().BoolVarP(&o.forceFlag, "force", "f", false, "Delete component without prompting")
clientset.Add(componentCmd, clientset.DELETE_COMPONENT, clientset.KUBERNETES)
return componentCmd
}

View File

@@ -0,0 +1,92 @@
package delete
import (
"testing"
"github.com/golang/mock/gomock"
_delete "github.com/redhat-developer/odo/pkg/component/delete"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions/clientset"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestComponentOptions_deleteNamedComponent(t *testing.T) {
type fields struct {
name string
namespace string
forceFlag bool
kubernetesClient func(ctrl *gomock.Controller) kclient.ClientInterface
deleteComponentClient func(ctrl *gomock.Controller) _delete.Client
}
tests := []struct {
name string
fields fields
wantErr bool
}{
{
name: "No resource found",
fields: fields{
name: "my-component",
namespace: "my-namespace",
forceFlag: false,
kubernetesClient: func(ctrl *gomock.Controller) kclient.ClientInterface {
client := kclient.NewMockClientInterface(ctrl)
return client
},
deleteComponentClient: func(ctrl *gomock.Controller) _delete.Client {
client := _delete.NewMockClient(ctrl)
client.EXPECT().ListResourcesToDelete("my-component", "my-namespace").Return(nil, nil)
client.EXPECT().DeleteResources(gomock.Any()).Times(0)
return client
},
},
wantErr: false,
},
{
name: "2 resources to delete",
fields: fields{
name: "my-component",
namespace: "my-namespace",
forceFlag: true,
kubernetesClient: func(ctrl *gomock.Controller) kclient.ClientInterface {
client := kclient.NewMockClientInterface(ctrl)
return client
},
deleteComponentClient: func(ctrl *gomock.Controller) _delete.Client {
var resources []unstructured.Unstructured
res1 := unstructured.Unstructured{}
res1.SetAPIVersion("v1")
res1.SetKind("deployment")
res1.SetName("dep1")
res2 := unstructured.Unstructured{}
res2.SetAPIVersion("v1")
res2.SetKind("service")
res2.SetName("svc1")
resources = append(resources, res1, res2)
client := _delete.NewMockClient(ctrl)
client.EXPECT().ListResourcesToDelete("my-component", "my-namespace").Return(resources, nil)
client.EXPECT().DeleteResources([]unstructured.Unstructured{res1, res2}).Times(1)
return client
},
},
},
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
o := &ComponentOptions{
name: tt.fields.name,
namespace: tt.fields.namespace,
forceFlag: tt.fields.forceFlag,
clientset: &clientset.Clientset{
KubernetesClient: tt.fields.kubernetesClient(ctrl),
DeleteClient: tt.fields.deleteComponentClient(ctrl),
},
}
if err := o.deleteNamedComponent(); (err != nil) != tt.wantErr {
t.Errorf("ComponentOptions.deleteNamedComponent() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@@ -69,7 +69,7 @@ func (o *DeployOptions) Complete(cmdline cmdline.Cmdline, args []string) (err er
return err
}
o.Context, err = genericclioptions.New(genericclioptions.NewCreateParameters(cmdline).NeedDevfile(o.contextDir))
o.Context, err = genericclioptions.New(genericclioptions.NewCreateParameters(cmdline).NeedDevfile(o.contextDir).CreateAppIfNeeded())
if err != nil {
return err
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/spf13/cobra"
"github.com/redhat-developer/odo/pkg/catalog"
_delete "github.com/redhat-developer/odo/pkg/component/delete"
"github.com/redhat-developer/odo/pkg/deploy"
_init "github.com/redhat-developer/odo/pkg/init"
"github.com/redhat-developer/odo/pkg/init/registry"
@@ -29,6 +30,8 @@ import (
const (
// CATALOG instantiates client for pkg/catalog
CATALOG = "DEP_CATALOG"
// DELETE_COMPONENT instantiates client for pkg/component/delete
DELETE_COMPONENT = "DEP_DELETE_COMPONENT"
// DEPLOY instantiates client for pkg/deploy
DEPLOY = "DEP_DEPLOY"
// DEV instantiates client for pkg/dev
@@ -56,16 +59,18 @@ const (
// subdeps defines the sub-dependencies
// Clients will be created only once and be reused for sub-dependencies
var subdeps map[string][]string = map[string][]string{
CATALOG: {FILESYSTEM, PREFERENCE},
DEPLOY: {KUBERNETES},
DEV: {WATCH},
INIT: {FILESYSTEM, PREFERENCE, REGISTRY, CATALOG},
PROJECT: {KUBERNETES_NULLABLE},
CATALOG: {FILESYSTEM, PREFERENCE},
DELETE_COMPONENT: {KUBERNETES},
DEPLOY: {KUBERNETES},
DEV: {WATCH},
INIT: {FILESYSTEM, PREFERENCE, REGISTRY, CATALOG},
PROJECT: {KUBERNETES_NULLABLE},
/* Add sub-dependencies here, if any */
}
type Clientset struct {
CatalogClient catalog.Client
DeleteClient _delete.Client
DeployClient deploy.Client
DevClient dev.Client
FS filesystem.Filesystem
@@ -128,6 +133,9 @@ func Fetch(command *cobra.Command) (*Clientset, error) {
if isDefined(command, CATALOG) {
dep.CatalogClient = catalog.NewCatalogClient(dep.FS, dep.PreferenceClient)
}
if isDefined(command, DELETE_COMPONENT) {
dep.DeleteClient = _delete.NewDeleteComponentClient(dep.KubernetesClient)
}
if isDefined(command, DEPLOY) {
dep.DeployClient = deploy.NewDeployClient(dep.KubernetesClient)
}

View File

@@ -264,14 +264,9 @@ func PushKubernetesResource(client kclient.ClientInterface, u unstructured.Unstr
return false, err
}
// add labels to the CRD before creation
// Add all passed in labels to the deployment regardless if it's an operator or not
existingLabels := u.GetLabels()
if isOp {
u.SetLabels(mergeLabels(existingLabels, labels))
} else {
// Kubernetes built-in resource; only set managed-by label to it
u.SetLabels(mergeLabels(existingLabels, map[string]string{"app.kubernetes.io/managed-by": "odo"}))
}
u.SetLabels(mergeLabels(existingLabels, labels))
err = createOperatorService(client, u)
return isOp, err

View File

@@ -70,3 +70,7 @@ mockgen -source=pkg/libdevfile/libdevfile.go \
mockgen -source=pkg/watch/interface.go \
-package watch \
-destination pkg/watch/mock.go
mockgen -source=pkg/component/delete/interface.go \
-package delete \
-destination pkg/component/delete/mock.go

View File

@@ -0,0 +1,96 @@
package devfile
import (
"path/filepath"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/redhat-developer/odo/tests/helper"
)
var _ = Describe("odo delete command tests", func() {
var cmpName string
var commonVar helper.CommonVar
// This is run before every Spec (It)
var _ = BeforeEach(func() {
commonVar = helper.CommonBeforeEach()
cmpName = helper.RandString(6)
helper.Chdir(commonVar.Context)
})
// This is run after every Spec (It)
var _ = AfterEach(func() {
helper.CommonAfterEach(commonVar)
})
When("a component is bootstrapped", func() {
BeforeEach(func() {
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), commonVar.Context)
helper.Cmd("odo", "project", "set", commonVar.Project).ShouldPass()
helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-deploy.yaml")).ShouldPass()
})
When("the component is deployed in DEV mode", func() {
BeforeEach(func() {
session := helper.CmdRunner("odo", "dev")
defer session.Kill()
helper.WaitForOutputToContain("Waiting for something to change", 180, 10, session)
list := commonVar.CliRunner.Run("get", "deployment", "-n", commonVar.Project).Out.Contents()
Expect(list).To(ContainSubstring(cmpName))
})
When("the component is deleted using its name and namespace from another directory", func() {
var out string
BeforeEach(func() {
otherDir := filepath.Join(commonVar.Context, "tmp")
helper.MakeDir(otherDir)
helper.Chdir(otherDir)
out = helper.Cmd("odo", "delete", "component", "--name", cmpName, "--namespace", commonVar.Project, "-f").ShouldPass().Out()
})
It("should have deleted the component", func() {
By("listing the resource to delete", func() {
Expect(out).To(ContainSubstring("Deployment: " + cmpName))
})
By("deleting the deployment", func() {
list := commonVar.CliRunner.Run("get", "deployment", "-n", commonVar.Project).Out.Contents()
Expect(list).To(Not(ContainSubstring(cmpName)))
})
})
})
})
When("the component is deployed in DEPLOY mode", func() {
BeforeEach(func() {
helper.Cmd("odo", "deploy").AddEnv("PODMAN_CMD=echo").ShouldPass()
list := commonVar.CliRunner.Run("get", "deployment", "-n", commonVar.Project).Out.Contents()
Expect(list).To(ContainSubstring("my-component"))
commonVar.CliRunner.Run("get", "deployment", "-n", commonVar.Project, "-o", "yaml")
})
When("the component is deleted using its name and namespace from another directory", func() {
var out string
BeforeEach(func() {
otherDir := filepath.Join(commonVar.Context, "tmp")
helper.MakeDir(otherDir)
helper.Chdir(otherDir)
out = helper.Cmd("odo", "delete", "component", "--name", cmpName, "--namespace", commonVar.Project, "-f").ShouldPass().Out()
})
It("should have deleted the component", func() {
By("listing the resource to delete", func() {
Expect(out).To(ContainSubstring("Deployment: my-component"))
})
By("deleting the deployment", func() {
list := commonVar.CliRunner.Run("get", "deployment", "-n", commonVar.Project).Out.Contents()
Expect(list).To(Not(ContainSubstring("my-component")))
})
})
})
})
})
})