mirror of
https://github.com/redhat-developer/odo.git
synced 2025-10-19 03:06:19 +03:00
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:
76
pkg/component/delete/delete.go
Normal file
76
pkg/component/delete/delete.go
Normal 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
|
||||
}
|
||||
239
pkg/component/delete/delete_test.go
Normal file
239
pkg/component/delete/delete_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
10
pkg/component/delete/interface.go
Normal file
10
pkg/component/delete/interface.go
Normal 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
|
||||
}
|
||||
64
pkg/component/delete/mock.go
Normal file
64
pkg/component/delete/mock.go
Normal 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)
|
||||
}
|
||||
5
pkg/component/delete/mock_handler.go
Normal file
5
pkg/component/delete/mock_handler.go
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
92
pkg/odo/cli/delete/component_test.go
Normal file
92
pkg/odo/cli/delete/component_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
96
tests/integration/devfile/cmd_delete_test.go
Normal file
96
tests/integration/devfile/cmd_delete_test.go
Normal 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")))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user