Separate layers - Application + Project (#5293)

* Application interface

* Application describe

* Application list

* Fix --output/-o flag

* Test Run()

* Tests on application pkg

* Unit tests on kclient relative to application

* comment

* Add ComponentList method to Application

* Project interface

* Project CLI tests

* Dharmit review
This commit is contained in:
Philippe Martin
2022-01-05 16:08:40 +01:00
committed by GitHub
parent 666af653ef
commit b20103c9f3
39 changed files with 2024 additions and 619 deletions

View File

@@ -1,97 +1,12 @@
package application
import (
"github.com/pkg/errors"
"github.com/redhat-developer/odo/pkg/kclient"
"k8s.io/klog"
import "github.com/redhat-developer/odo/pkg/component"
applabels "github.com/redhat-developer/odo/pkg/application/labels"
"github.com/redhat-developer/odo/pkg/component"
"github.com/redhat-developer/odo/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
appAPIVersion = "odo.dev/v1alpha1"
appKind = "Application"
appList = "List"
)
// List all applications names in current project by looking at `app` labels in deployments
func List(client kclient.ClientInterface) ([]string, error) {
if client == nil {
return nil, nil
}
// Get all Deployments with the "app" label
deploymentAppNames, err := client.GetDeploymentLabelValues(applabels.ApplicationLabel, applabels.ApplicationLabel)
if err != nil {
return nil, errors.Wrap(err, "unable to list applications from deployments")
}
// Filter out any names, as there could be multiple components but within the same application
return util.RemoveDuplicates(deploymentAppNames), nil
}
// Exists checks whether the given app exist or not in the list of applications
func Exists(app string, client kclient.ClientInterface) (bool, error) {
appList, err := List(client)
if err != nil {
return false, err
}
for _, appName := range appList {
if appName == app {
return true, nil
}
}
return false, nil
}
// Delete the given application by deleting deployments and services instances belonging to this application
func Delete(client kclient.ClientInterface, name string) error {
klog.V(4).Infof("Deleting application %s", name)
labels := applabels.GetLabels(name, false)
// delete application from cluster
err := client.Delete(labels, false)
if err != nil {
return errors.Wrapf(err, "unable to delete application %s", name)
}
return nil
}
// GetMachineReadableFormat returns resource information in machine readable format
func GetMachineReadableFormat(client kclient.ClientInterface, appName string, projectName string) App {
componentList, _ := component.GetComponentNames(client, appName)
appDef := App{
TypeMeta: metav1.TypeMeta{
Kind: appKind,
APIVersion: appAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: appName,
Namespace: projectName,
},
Spec: AppSpec{
Components: componentList,
},
}
return appDef
}
// GetMachineReadableFormatForList returns application list in machine readable format
func GetMachineReadableFormatForList(apps []App) AppList {
return AppList{
TypeMeta: metav1.TypeMeta{
Kind: appList,
APIVersion: appAPIVersion,
},
ListMeta: metav1.ListMeta{},
Items: apps,
}
type Client interface {
List() ([]string, error)
Exists(app string) (bool, error)
Delete(name string) error
ComponentList(name string) ([]component.Component, error)
GetMachineReadableFormat(appName string, projectName string) App
GetMachineReadableFormatForList(apps []App) AppList
}

View File

@@ -1,192 +0,0 @@
package application
import (
"reflect"
"testing"
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/testingutil"
"github.com/redhat-developer/odo/pkg/version"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
ktesting "k8s.io/client-go/testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestGetMachineReadableFormat(t *testing.T) {
type args struct {
appName string
projectName string
active bool
}
tests := []struct {
name string
args args
want App
}{
{
name: "Test Case: machine readable output for application",
args: args{
appName: "myapp",
projectName: "myproject",
active: true,
},
want: App{
TypeMeta: metav1.TypeMeta{
Kind: appKind,
APIVersion: appAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "myapp",
Namespace: "myproject",
},
Spec: AppSpec{
Components: []string{"frontend"},
},
},
},
}
deploymentList := appsv1.DeploymentList{
Items: []appsv1.Deployment{
{
ObjectMeta: metav1.ObjectMeta{
Name: "frontend-myapp",
Namespace: "myproject",
Labels: map[string]string{
applabels.ApplicationLabel: "myapp",
componentlabels.ComponentLabel: "frontend",
componentlabels.ComponentTypeLabel: "nodejs",
applabels.ManagedBy: "odo",
applabels.ManagerVersion: version.VERSION,
},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "dummyContainer",
},
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "backend-app",
Namespace: "myproject",
Labels: map[string]string{
applabels.ApplicationLabel: "app",
componentlabels.ComponentLabel: "backend",
componentlabels.ComponentTypeLabel: "java",
applabels.ManagedBy: "odo",
applabels.ManagerVersion: version.VERSION,
},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "dummyContainer",
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Fake the client with the appropriate arguments
client, fakeClientSet := kclient.FakeNew()
// fake the project
fakeClientSet.Kubernetes.PrependReactor("get", "projects", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
return true, &testingutil.FakeOnlyOneExistingProjects().Items[0], nil
})
//fake the deployments
fakeClientSet.Kubernetes.PrependReactor("list", "deployments", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &deploymentList, nil
})
for i := range deploymentList.Items {
fakeClientSet.Kubernetes.PrependReactor("get", "deployments", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &deploymentList.Items[i], nil
})
}
if got := GetMachineReadableFormat(client, tt.args.appName, tt.args.projectName); !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetMachineReadableFormat() = %v,\n want %v", got, tt.want)
}
})
}
}
func TestGetMachineReadableFormatForList(t *testing.T) {
type args struct {
apps []App
}
tests := []struct {
name string
args args
want AppList
}{
{
name: "Test Case: Machine Readable for Application List",
args: args{
apps: []App{
{
TypeMeta: metav1.TypeMeta{
Kind: appKind,
APIVersion: appAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "myapp",
},
Spec: AppSpec{
Components: []string{"frontend"},
},
},
},
},
want: AppList{
TypeMeta: metav1.TypeMeta{
Kind: appList,
APIVersion: appAPIVersion,
},
ListMeta: metav1.ListMeta{},
Items: []App{
{
TypeMeta: metav1.TypeMeta{
Kind: appKind,
APIVersion: appAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "myapp",
},
Spec: AppSpec{
Components: []string{"frontend"},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetMachineReadableFormatForList(tt.args.apps); !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetMachineReadableFormatForList() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,112 @@
package application
import (
"github.com/pkg/errors"
applabels "github.com/redhat-developer/odo/pkg/application/labels"
"github.com/redhat-developer/odo/pkg/component"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/machineoutput"
"github.com/redhat-developer/odo/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog"
)
type kubernetesClient struct {
client kclient.ClientInterface
}
func NewClient(client kclient.ClientInterface) Client {
return kubernetesClient{
client: client,
}
}
// List all applications names in current project by looking at `app` labels in deployments
func (o kubernetesClient) List() ([]string, error) {
if o.client == nil {
return nil, nil
}
// Get all Deployments with the "app" label
deploymentAppNames, err := o.client.GetDeploymentLabelValues(applabels.ApplicationLabel, applabels.ApplicationLabel)
if err != nil {
return nil, errors.Wrap(err, "unable to list applications from deployments")
}
// Filter out any names, as there could be multiple components but within the same application
return util.RemoveDuplicates(deploymentAppNames), nil
}
// Exists checks whether the given app exist or not in the list of applications
func (o kubernetesClient) Exists(app string) (bool, error) {
appList, err := o.List()
if err != nil {
return false, err
}
for _, appName := range appList {
if appName == app {
return true, nil
}
}
return false, nil
}
// Delete the given application by deleting deployments and services instances belonging to this application
func (o kubernetesClient) Delete(name string) error {
klog.V(4).Infof("Deleting application %q", name)
labels := applabels.GetLabels(name, false)
// delete application from cluster
err := o.client.Delete(labels, false)
if err != nil {
return errors.Wrapf(err, "unable to delete application %s", name)
}
return nil
}
// ComponentList returns the list of components for an application
func (o kubernetesClient) ComponentList(name string) ([]component.Component, error) {
selector := applabels.GetSelector(name)
componentList, err := component.List(o.client, selector)
if err != nil {
return nil, errors.Wrap(err, "failed to get Component list")
}
return componentList.Items, nil
}
// GetMachineReadableFormat returns resource information in machine readable format
func (o kubernetesClient) GetMachineReadableFormat(appName string, projectName string) App {
componentList, _ := component.GetComponentNames(o.client, appName)
appDef := App{
TypeMeta: metav1.TypeMeta{
Kind: appKind,
APIVersion: machineoutput.APIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: appName,
Namespace: projectName,
},
Spec: AppSpec{
Components: componentList,
},
}
return appDef
}
// GetMachineReadableFormatForList returns application list in machine readable format
func (o kubernetesClient) GetMachineReadableFormatForList(apps []App) AppList {
return AppList{
TypeMeta: metav1.TypeMeta{
Kind: appList,
APIVersion: machineoutput.APIVersion,
},
ListMeta: metav1.ListMeta{},
Items: apps,
}
}

View File

@@ -0,0 +1,348 @@
package application
import (
"errors"
"reflect"
"strings"
"testing"
gomock "github.com/golang/mock/gomock"
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/machineoutput"
"github.com/redhat-developer/odo/pkg/testingutil"
"github.com/redhat-developer/odo/pkg/unions"
"github.com/redhat-developer/odo/pkg/version"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ktesting "k8s.io/client-go/testing"
)
func TestList(t *testing.T) {
ctrl := gomock.NewController(t)
kubclient := kclient.NewMockClientInterface(ctrl)
kubclient.EXPECT().GetDeploymentLabelValues("app.kubernetes.io/part-of", "app.kubernetes.io/part-of").Return([]string{"app1", "app3", "app1", "app2"}, nil).AnyTimes()
appClient := NewClient(kubclient)
result, err := appClient.List()
expected := []string{"app1", "app2", "app3"}
if err != nil {
t.Errorf("Expected nil error, got %s", err)
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("Got %v, expected %v", result, expected)
}
}
func TestExists(t *testing.T) {
tests := []struct {
name string
search string
result bool
err bool
}{
{
name: "not exists",
search: "an-app",
result: false,
err: false,
},
{
name: "exists",
search: "app1",
result: true,
err: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
kubclient := kclient.NewMockClientInterface(ctrl)
kubclient.EXPECT().GetDeploymentLabelValues("app.kubernetes.io/part-of", "app.kubernetes.io/part-of").Return([]string{"app1", "app3", "app1", "app2"}, nil).AnyTimes()
appClient := NewClient(kubclient)
result, err := appClient.Exists(tt.search)
if err != nil != tt.err {
t.Errorf("Expected %v error, got %v", tt.err, err)
}
if result != tt.result {
t.Errorf("Expected %v, got %v", tt.result, result)
}
})
}
}
func TestDelete(t *testing.T) {
tests := []struct {
name string
deleteReturn error
expectedErr string
}{
{
name: "kubernetes delete works",
deleteReturn: nil,
expectedErr: "",
},
{
name: "kubernetes delete fails",
deleteReturn: errors.New("an error"),
expectedErr: "unable to delete application",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
kubclient := kclient.NewMockClientInterface(ctrl)
appClient := NewClient(kubclient)
labels := map[string]string{
"app.kubernetes.io/part-of": "an-app",
}
kubclient.EXPECT().Delete(labels, false).Return(tt.deleteReturn).Times(1)
// kube Delete works
err := appClient.Delete("an-app")
if err == nil && tt.expectedErr != "" {
t.Errorf("Expected %v, got no error", tt.expectedErr)
return
}
if err != nil && tt.expectedErr == "" {
t.Errorf("Expected no error, got %v", err.Error())
return
}
if err != nil && tt.expectedErr != "" && !strings.Contains(err.Error(), tt.expectedErr) {
t.Errorf("Expected error %v, got %v", tt.expectedErr, err.Error())
return
}
if err != nil {
return
}
})
}
}
func TestComponentList(t *testing.T) {
ctrl := gomock.NewController(t)
kubclient := kclient.NewMockClientInterface(ctrl)
depList := []appsv1.Deployment{
{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app.kubernetes.io/instance": "a-component",
"app.kubernetes.io/part-of": "an-app-name",
},
Annotations: map[string]string{
"odo.dev/project-type": "nodejs",
},
},
},
}
kubclient.EXPECT().GetDeploymentFromSelector("app=an-app-name,app.kubernetes.io/managed-by=odo,app.kubernetes.io/part-of=an-app-name").Return(depList, nil).AnyTimes()
kubclient.EXPECT().GetCurrentNamespace().Return("my-namespace").AnyTimes()
kubclient.EXPECT().GetOneDeployment("a-component", "an-app-name").Return(&depList[0], nil).AnyTimes()
ingresses := &unions.KubernetesIngressList{
Items: nil,
}
kubclient.EXPECT().ListIngresses("app.kubernetes.io/instance=a-component,app.kubernetes.io/part-of=an-app-name").Return(ingresses, nil).AnyTimes()
kubclient.EXPECT().IsServiceBindingSupported().Return(false, nil).AnyTimes()
kubclient.EXPECT().ListSecrets("app.kubernetes.io/instance=a-component,app.kubernetes.io/part-of=an-app-name").Return(nil, nil).AnyTimes()
kubclient.EXPECT().ListServices("").Return(nil, nil).AnyTimes()
appClient := NewClient(kubclient)
result, err := appClient.ComponentList("an-app-name")
if len(result) != 1 {
t.Errorf("expected 1 component in list, got %d", len(result))
}
component := result[0]
if component.Name != "a-component" {
t.Errorf("Expected component name %q, got %q", "a-component", component.Name)
}
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
}
func TestGetMachineReadableFormat(t *testing.T) {
type args struct {
appName string
projectName string
active bool
}
tests := []struct {
name string
args args
want App
}{
{
name: "Test Case: machine readable output for application",
args: args{
appName: "myapp",
projectName: "myproject",
active: true,
},
want: App{
TypeMeta: metav1.TypeMeta{
Kind: appKind,
APIVersion: machineoutput.APIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "myapp",
Namespace: "myproject",
},
Spec: AppSpec{
Components: []string{"frontend"},
},
},
},
}
deploymentList := appsv1.DeploymentList{
Items: []appsv1.Deployment{
{
ObjectMeta: metav1.ObjectMeta{
Name: "frontend-myapp",
Namespace: "myproject",
Labels: map[string]string{
applabels.ApplicationLabel: "myapp",
componentlabels.ComponentLabel: "frontend",
componentlabels.ComponentTypeLabel: "nodejs",
applabels.ManagedBy: "odo",
applabels.ManagerVersion: version.VERSION,
},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "dummyContainer",
},
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "backend-app",
Namespace: "myproject",
Labels: map[string]string{
applabels.ApplicationLabel: "app",
componentlabels.ComponentLabel: "backend",
componentlabels.ComponentTypeLabel: "java",
applabels.ManagedBy: "odo",
applabels.ManagerVersion: version.VERSION,
},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "dummyContainer",
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Fake the client with the appropriate arguments
client, fakeClientSet := kclient.FakeNew()
// fake the project
fakeClientSet.Kubernetes.PrependReactor("get", "projects", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
return true, &testingutil.FakeOnlyOneExistingProjects().Items[0], nil
})
//fake the deployments
fakeClientSet.Kubernetes.PrependReactor("list", "deployments", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &deploymentList, nil
})
for i := range deploymentList.Items {
fakeClientSet.Kubernetes.PrependReactor("get", "deployments", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &deploymentList.Items[i], nil
})
}
kclient := NewClient(client)
if got := kclient.GetMachineReadableFormat(tt.args.appName, tt.args.projectName); !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetMachineReadableFormat() = %v,\n want %v", got, tt.want)
}
})
}
}
func TestGetMachineReadableFormatForList(t *testing.T) {
type args struct {
apps []App
}
tests := []struct {
name string
args args
want AppList
}{
{
name: "Test Case: Machine Readable for Application List",
args: args{
apps: []App{
{
TypeMeta: metav1.TypeMeta{
Kind: appKind,
APIVersion: machineoutput.APIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "myapp",
},
Spec: AppSpec{
Components: []string{"frontend"},
},
},
},
},
want: AppList{
TypeMeta: metav1.TypeMeta{
Kind: appList,
APIVersion: machineoutput.APIVersion,
},
ListMeta: metav1.ListMeta{},
Items: []App{
{
TypeMeta: metav1.TypeMeta{
Kind: appKind,
APIVersion: machineoutput.APIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "myapp",
},
Spec: AppSpec{
Components: []string{"frontend"},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, _ := kclient.FakeNew()
kclient := NewClient(client)
if got := kclient.GetMachineReadableFormatForList(tt.args.apps); !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetMachineReadableFormatForList() = %v, want %v", got, tt.want)
}
})
}
}

122
pkg/application/mock.go Normal file
View File

@@ -0,0 +1,122 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: pkg/application/application.go
// Package application is a generated GoMock package.
package application
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
component "github.com/redhat-developer/odo/pkg/component"
)
// 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
}
// ComponentList mocks base method.
func (m *MockClient) ComponentList(name string) ([]component.Component, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ComponentList", name)
ret0, _ := ret[0].([]component.Component)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ComponentList indicates an expected call of ComponentList.
func (mr *MockClientMockRecorder) ComponentList(name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ComponentList", reflect.TypeOf((*MockClient)(nil).ComponentList), name)
}
// Delete mocks base method.
func (m *MockClient) Delete(name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", name)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockClientMockRecorder) Delete(name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockClient)(nil).Delete), name)
}
// Exists mocks base method.
func (m *MockClient) Exists(app string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Exists", app)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Exists indicates an expected call of Exists.
func (mr *MockClientMockRecorder) Exists(app interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockClient)(nil).Exists), app)
}
// GetMachineReadableFormat mocks base method.
func (m *MockClient) GetMachineReadableFormat(appName, projectName string) App {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMachineReadableFormat", appName, projectName)
ret0, _ := ret[0].(App)
return ret0
}
// GetMachineReadableFormat indicates an expected call of GetMachineReadableFormat.
func (mr *MockClientMockRecorder) GetMachineReadableFormat(appName, projectName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMachineReadableFormat", reflect.TypeOf((*MockClient)(nil).GetMachineReadableFormat), appName, projectName)
}
// GetMachineReadableFormatForList mocks base method.
func (m *MockClient) GetMachineReadableFormatForList(apps []App) AppList {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMachineReadableFormatForList", apps)
ret0, _ := ret[0].(AppList)
return ret0
}
// GetMachineReadableFormatForList indicates an expected call of GetMachineReadableFormatForList.
func (mr *MockClientMockRecorder) GetMachineReadableFormatForList(apps interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMachineReadableFormatForList", reflect.TypeOf((*MockClient)(nil).GetMachineReadableFormatForList), apps)
}
// List mocks base method.
func (m *MockClient) List() ([]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List")
ret0, _ := ret[0].([]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockClientMockRecorder) List() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockClient)(nil).List))
}

View File

@@ -4,6 +4,11 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
appKind = "Application"
appList = "List"
)
// Application
type App struct {
metav1.TypeMeta `json:",inline"`

View File

@@ -1,25 +1,25 @@
package kclient
import (
reflect "reflect"
"testing"
"github.com/devfile/library/pkg/devfile/parser/data"
"github.com/pkg/errors"
devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/pkg/devfile/generator"
devfileParser "github.com/devfile/library/pkg/devfile/parser"
"github.com/devfile/library/pkg/devfile/parser/data"
parsercommon "github.com/devfile/library/pkg/devfile/parser/data/v2/common"
"github.com/devfile/library/pkg/testingutil"
odoTestingUtil "github.com/redhat-developer/odo/pkg/testingutil"
"github.com/redhat-developer/odo/pkg/util"
"github.com/pkg/errors"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ktesting "k8s.io/client-go/testing"
)
@@ -314,3 +314,47 @@ func TestDeleteDeployment(t *testing.T) {
})
}
}
func TestGetDeploymentLabelValues(t *testing.T) {
fkclient, fkclientset := FakeNew()
fkclient.Namespace = "default"
fkclientset.Kubernetes.PrependReactor("list", "deployments", func(action ktesting.Action) (bool, runtime.Object, error) {
depList := appsv1.DeploymentList{
Items: []appsv1.Deployment{
{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"a-selector": "sel-value",
"a-label": "a-value",
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"a-selector": "sel-value-2",
"a-label": "a-value-2",
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"other-selector": "sel-value",
"a-label": "another-value",
},
},
},
},
}
return true, &depList, nil
})
result, err := fkclient.GetDeploymentLabelValues("a-label", "a-selector")
expected := []string{"a-value", "a-value-2"}
if !reflect.DeepEqual(result, expected) {
t.Errorf("Expected %v, got %v", expected, result)
}
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
}

View File

@@ -5,9 +5,11 @@ import (
"runtime"
"testing"
"github.com/pkg/errors"
kruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/discovery/fake"
ktesting "k8s.io/client-go/testing"
)
func (c *fakeDiscovery) ServerVersion() (*version.Info, error) {
@@ -84,3 +86,22 @@ func TestClient_IsSSASupported(t *testing.T) {
})
}
}
func TestDelete(t *testing.T) {
fkclient, fkclientset := FakeNew()
fkclient.Namespace = "default"
fkclientset.Kubernetes.PrependReactor("delete-collection", "deployments", func(action ktesting.Action) (bool, kruntime.Object, error) {
if "a-selector=a-value" != action.(ktesting.DeleteCollectionAction).GetListRestrictions().Labels.String() {
return true, nil, errors.New("not found")
}
return true, nil, nil
})
selectors := map[string]string{
"a-selector": "a-value",
}
err := fkclient.Delete(selectors, false)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
}

View File

@@ -3,15 +3,11 @@ package application
import (
"fmt"
"github.com/pkg/errors"
applabels "github.com/redhat-developer/odo/pkg/application/labels"
"github.com/redhat-developer/odo/pkg/component"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/log"
"github.com/spf13/cobra"
"github.com/redhat-developer/odo/pkg/odo/util"
odoutil "github.com/redhat-developer/odo/pkg/odo/util"
"github.com/redhat-developer/odo/pkg/odo/util/completion"
"github.com/spf13/cobra"
)
// RecommendedCommandName is the recommended app command name
@@ -50,38 +46,3 @@ func AddApplicationFlag(cmd *cobra.Command) {
cmd.Flags().String(util.ApplicationFlagName, "", "Application, defaults to active application")
completion.RegisterCommandFlagHandler(cmd, "app", completion.AppCompletionHandler)
}
// printAppInfo will print things which will be deleted
func printAppInfo(client kclient.ClientInterface, kClient kclient.ClientInterface, appName string, projectName string) error {
var selector string
if appName != "" {
selector = applabels.GetSelector(appName)
}
componentList, err := component.List(client, selector)
if err != nil {
return errors.Wrap(err, "failed to get Component list")
}
if len(componentList.Items) != 0 {
log.Info("This application has following components that will be deleted")
for _, currentComponent := range componentList.Items {
log.Info("component named", currentComponent.Name)
if len(currentComponent.Spec.URL) != 0 {
log.Info("This component has following urls that will be deleted with component")
for _, u := range currentComponent.Spec.URLSpec {
log.Info("URL named", u.GetName(), "with host", u.Spec.Host, "having protocol", u.Spec.Protocol, "at port", u.Spec.Port)
}
}
if len(currentComponent.Spec.Storage) != 0 {
log.Info("The component has following storages which will be deleted with the component")
for _, storage := range currentComponent.Spec.StorageSpec {
store := storage
log.Info("Storage named", store.GetName(), "of size", store.Spec.Size)
}
}
}
}
return nil
}

View File

@@ -3,16 +3,18 @@ package application
import (
"fmt"
odoUtil "github.com/redhat-developer/odo/pkg/odo/util"
"github.com/spf13/cobra"
"github.com/redhat-developer/odo/pkg/application"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/odo/cli/project"
"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"
odoUtil "github.com/redhat-developer/odo/pkg/odo/util"
"github.com/redhat-developer/odo/pkg/odo/util/completion"
"github.com/spf13/cobra"
ktemplates "k8s.io/kubectl/pkg/util/templates"
)
@@ -28,6 +30,9 @@ type DeleteOptions struct {
// Context
*genericclioptions.Context
// Clients
appClient application.Client
// Parameters
appName string
@@ -36,8 +41,10 @@ type DeleteOptions struct {
}
// NewDeleteOptions creates a new DeleteOptions instance
func NewDeleteOptions() *DeleteOptions {
return &DeleteOptions{}
func NewDeleteOptions(appClient application.Client) *DeleteOptions {
return &DeleteOptions{
appClient: appClient,
}
}
// Complete completes DeleteOptions after they've been created
@@ -51,7 +58,6 @@ func (o *DeleteOptions) Complete(cmdline cmdline.Cmdline, args []string) (err er
// If app name passed, consider it for deletion
o.appName = args[0]
}
return
}
@@ -61,7 +67,7 @@ func (o *DeleteOptions) Validate() (err error) {
return odoUtil.ThrowContextError()
}
exist, err := application.Exists(o.appName, o.KClient)
exist, err := o.appClient.Exists(o.appName)
if !exist {
return fmt.Errorf("%s app does not exists", o.appName)
}
@@ -70,22 +76,18 @@ func (o *DeleteOptions) Validate() (err error) {
// Run contains the logic for the odo command
func (o *DeleteOptions) Run() (err error) {
if log.IsJSON() {
err = application.Delete(o.KClient, o.appName)
if err != nil {
return err
}
return nil
if o.IsJSON() {
return o.appClient.Delete(o.appName)
}
// Print App Information which will be deleted
err = printAppInfo(o.KClient, o.KClient, o.appName, o.GetProject())
err = printAppInfo(o.appClient, o.appName, o.GetProject())
if err != nil {
return err
}
if o.forceFlag || ui.Proceed(fmt.Sprintf("Are you sure you want to delete the application: %v from project: %v", o.appName, o.GetProject())) {
err = application.Delete(o.KClient, o.appName)
err = o.appClient.Delete(o.appName)
if err != nil {
return err
}
@@ -93,12 +95,44 @@ func (o *DeleteOptions) Run() (err error) {
} else {
log.Infof("Aborting deletion of application: %v", o.appName)
}
return
return nil
}
// printAppInfo will print information about the app requested for deletion
func printAppInfo(appClient application.Client, appName string, projectName string) error {
components, err := appClient.ComponentList(appName)
if err != nil {
return err
}
if len(components) != 0 {
log.Info("This application has following components that will be deleted")
for _, currentComponent := range components {
log.Info("component named", currentComponent.Name)
if len(currentComponent.Spec.URL) != 0 {
log.Info("This component has following urls that will be deleted with component")
for _, u := range currentComponent.Spec.URLSpec {
log.Info("URL named", u.GetName(), "with host", u.Spec.Host, "having protocol", u.Spec.Protocol, "at port", u.Spec.Port)
}
}
if len(currentComponent.Spec.Storage) != 0 {
log.Info("The component has following storages which will be deleted with the component")
for _, storage := range currentComponent.Spec.StorageSpec {
store := storage
log.Info("Storage named", store.GetName(), "of size", store.Spec.Size)
}
}
}
}
return nil
}
// NewCmdDelete implements the odo command.
func NewCmdDelete(name, fullName string) *cobra.Command {
o := NewDeleteOptions()
// The error is not handled at this point, it will be handled during Context creation
kubclient, _ := kclient.New()
o := NewDeleteOptions(application.NewClient(kubclient))
command := &cobra.Command{
Use: name,
Short: "Delete the given application",

View File

@@ -0,0 +1,203 @@
package application
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/golang/mock/gomock"
"github.com/redhat-developer/odo/pkg/application"
"github.com/redhat-developer/odo/pkg/envinfo"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestDelete(t *testing.T) {
prefixDir, err := os.MkdirTemp(os.TempDir(), "unittests-")
if err != nil {
t.Errorf("Error creating temp directory for tests")
return
}
workingDir := filepath.Join(prefixDir, "myapp")
tests := []struct {
name string
populateWorkingDir func(fs filesystem.Filesystem)
args []string
existingApps []string
wantAppName string
wantErrValidate string
}{
{
name: "default app",
populateWorkingDir: func(fs filesystem.Filesystem) {
_ = fs.MkdirAll(filepath.Join(prefixDir, "myapp", ".odo", "env"), 0755)
env, err := envinfo.NewEnvSpecificInfo(filepath.Join(prefixDir, "myapp"))
if err != nil {
return
}
_ = env.SetComponentSettings(envinfo.ComponentSettings{
Name: "a-name",
Project: "a-project",
AppName: "an-app-name",
})
},
existingApps: []string{"an-app-name", "another-app-name"},
wantAppName: "an-app-name",
},
{
name: "app from args",
populateWorkingDir: func(fs filesystem.Filesystem) {
_ = fs.MkdirAll(filepath.Join(prefixDir, "myapp", ".odo", "env"), 0755)
env, err := envinfo.NewEnvSpecificInfo(filepath.Join(prefixDir, "myapp"))
if err != nil {
return
}
_ = env.SetComponentSettings(envinfo.ComponentSettings{
Name: "a-name",
Project: "a-project",
AppName: "an-app-name",
})
},
args: []string{"another-app-name"},
existingApps: []string{"an-app-name", "another-app-name"},
wantAppName: "another-app-name",
},
{
name: "empty app name",
populateWorkingDir: func(fs filesystem.Filesystem) {
_ = fs.MkdirAll(filepath.Join(prefixDir, "myapp", ".odo", "env"), 0755)
env, err := envinfo.NewEnvSpecificInfo(filepath.Join(prefixDir, "myapp"))
if err != nil {
return
}
_ = env.SetComponentSettings(envinfo.ComponentSettings{
Name: "a-name",
Project: "a-project",
AppName: "",
})
},
existingApps: []string{"an-app-name", "another-app-name"},
wantAppName: "",
wantErrValidate: "Please specify the application name and project name",
},
{
name: "non existing app name",
populateWorkingDir: func(fs filesystem.Filesystem) {
_ = fs.MkdirAll(filepath.Join(prefixDir, "myapp", ".odo", "env"), 0755)
env, err := envinfo.NewEnvSpecificInfo(filepath.Join(prefixDir, "myapp"))
if err != nil {
return
}
_ = env.SetComponentSettings(envinfo.ComponentSettings{
Name: "a-name",
Project: "a-project",
AppName: "an-app-name",
})
},
args: []string{"an-unknown-app-name"},
existingApps: []string{"an-app-name", "another-app-name"},
wantAppName: "an-unknown-app-name",
wantErrValidate: " app does not exists",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// the first one is to cleanup the directory before execution (in case there are remaining files from a previous execution)
os.RemoveAll(prefixDir)
// the second one to cleanup after execution
defer os.RemoveAll(prefixDir)
// Fake Cobra
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmdline := cmdline.NewMockCmdline(ctrl)
// Fake odo Kube client
kclient := kclient.NewMockClientInterface(ctrl)
/* Mocks for Complete */
cmdline.EXPECT().GetWorkingDirectory().Return(workingDir, nil).AnyTimes()
cmdline.EXPECT().CheckIfConfigurationNeeded().Return(false, nil).AnyTimes()
cmdline.EXPECT().FlagValueIfSet("project").Return("").AnyTimes()
cmdline.EXPECT().FlagValueIfSet("app").Return("").AnyTimes()
cmdline.EXPECT().FlagValueIfSet("component").Return("").AnyTimes()
cmdline.EXPECT().FlagValueIfSet("o").Return("").AnyTimes()
cmdline.EXPECT().GetKubeClient().Return(kclient, nil).AnyTimes()
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "a-project",
},
}
kclient.EXPECT().GetNamespaceNormal("a-project").Return(ns, nil).AnyTimes()
kclient.EXPECT().SetNamespace("a-project").AnyTimes()
tt.populateWorkingDir(filesystem.DefaultFs{})
/* Mocks for Complete */
appClient := application.NewMockClient(ctrl)
appClient.EXPECT().Exists(tt.wantAppName).Return(func() bool {
for _, app := range tt.existingApps {
if tt.wantAppName == app {
return true
}
}
return false
}(), nil).AnyTimes()
appClient.EXPECT().ComponentList(tt.wantAppName).AnyTimes()
opts := NewDeleteOptions(appClient)
// Force to disable interactive confirmation
opts.forceFlag = true
/* COMPLETE */
err := opts.Complete(cmdline, tt.args)
if err != nil {
t.Errorf("Expected nil error, got %s", err)
return
}
if opts.appName != tt.wantAppName {
t.Errorf("Got appName %q, expected %q", opts.appName, tt.wantAppName)
}
/* VALIDATE */
err = opts.Validate()
if err == nil && tt.wantErrValidate != "" {
t.Errorf("Expected %v, got no error", tt.wantErrValidate)
return
}
if err != nil && tt.wantErrValidate == "" {
t.Errorf("Expected no error, got %v", err.Error())
return
}
if err != nil && tt.wantErrValidate != "" && !strings.Contains(err.Error(), tt.wantErrValidate) {
t.Errorf("Expected error %v, got %v", tt.wantErrValidate, err.Error())
return
}
if err != nil {
return
}
/* Mocks for Run */
kclient.EXPECT().GetDeploymentFromSelector(fmt.Sprintf("app=%s,app.kubernetes.io/managed-by=odo,app.kubernetes.io/part-of=%s", tt.wantAppName, tt.wantAppName)).AnyTimes()
appClient.EXPECT().Delete(tt.wantAppName).Times(1)
/* RUN */
err = opts.Run()
if err != nil {
t.Errorf("Expected nil err, got %s", err)
}
})
}
}

View File

@@ -3,18 +3,17 @@ package application
import (
"fmt"
applabels "github.com/redhat-developer/odo/pkg/application/labels"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
"github.com/spf13/cobra"
"github.com/redhat-developer/odo/pkg/application"
"github.com/redhat-developer/odo/pkg/component"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/machineoutput"
"github.com/redhat-developer/odo/pkg/odo/cli/project"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions"
"github.com/redhat-developer/odo/pkg/odo/util"
"github.com/redhat-developer/odo/pkg/odo/util/completion"
"github.com/spf13/cobra"
ktemplates "k8s.io/kubectl/pkg/util/templates"
)
@@ -30,13 +29,18 @@ type DescribeOptions struct {
// Context
*genericclioptions.Context
// Clients
appClient application.Client
// Parameters
appName string
}
// NewDescribeOptions creates a new DescribeOptions instance
func NewDescribeOptions() *DescribeOptions {
return &DescribeOptions{}
func NewDescribeOptions(appClient application.Client) *DescribeOptions {
return &DescribeOptions{
appClient: appClient,
}
}
// Complete completes DescribeOptions after they've been created
@@ -57,11 +61,8 @@ func (o *DescribeOptions) Validate() (err error) {
if o.Context.GetProject() == "" || o.appName == "" {
return util.ThrowContextError()
}
if o.appName == "" {
return fmt.Errorf("There's no active application in project: %v", o.GetProject())
}
exist, err := application.Exists(o.appName, o.KClient)
exist, err := o.appClient.Exists(o.appName)
if !exist {
return fmt.Errorf("%s app does not exists", o.appName)
}
@@ -70,42 +71,40 @@ func (o *DescribeOptions) Validate() (err error) {
// Run contains the logic for the odo command
func (o *DescribeOptions) Run() (err error) {
if log.IsJSON() {
appDef := application.GetMachineReadableFormat(o.KClient, o.appName, o.GetProject())
if o.IsJSON() {
appDef := o.appClient.GetMachineReadableFormat(o.appName, o.GetProject())
machineoutput.OutputSuccess(appDef)
} else {
var selector string
if o.appName != "" {
selector = applabels.GetSelector(o.appName)
}
componentList, err := component.List(o.KClient, selector)
return nil
}
componentList, err := o.appClient.ComponentList(o.appName)
if err != nil {
return err
}
if len(componentList) == 0 {
fmt.Printf("Application %s has no components or services deployed.", o.appName)
return
}
fmt.Printf("Application Name: %s has %v component(s):\n--------------------------------------\n",
o.appName, len(componentList))
for _, currentComponent := range componentList {
err := util.PrintComponentInfo(o.KClient, currentComponent.Name, currentComponent, o.appName, o.GetProject())
if err != nil {
return err
}
if len(componentList.Items) == 0 {
fmt.Printf("Application %s has no components or services deployed.", o.appName)
} else {
fmt.Printf("Application Name: %s has %v component(s):\n--------------------------------------\n",
o.appName, len(componentList.Items))
if len(componentList.Items) > 0 {
for _, currentComponent := range componentList.Items {
err := util.PrintComponentInfo(o.KClient, currentComponent.Name, currentComponent, o.appName, o.GetProject())
if err != nil {
return err
}
fmt.Println("--------------------------------------")
}
}
}
fmt.Println("--------------------------------------")
}
return
return nil
}
// NewCmdDescribe implements the odo command.
func NewCmdDescribe(name, fullName string) *cobra.Command {
o := NewDescribeOptions()
// The error is not handled at this point, it will be handled during Context creation
kubclient, _ := kclient.New()
o := NewDescribeOptions(application.NewClient(kubclient))
command := &cobra.Command{
Use: fmt.Sprintf("%s [application_name]", name),
Short: "Describe the given application",

View File

@@ -0,0 +1,197 @@
package application
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/golang/mock/gomock"
"github.com/redhat-developer/odo/pkg/application"
"github.com/redhat-developer/odo/pkg/envinfo"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestDescribe(t *testing.T) {
prefixDir, err := os.MkdirTemp(os.TempDir(), "unittests-")
if err != nil {
t.Errorf("Error creating temp directory for tests")
return
}
workingDir := filepath.Join(prefixDir, "myapp")
tests := []struct {
name string
populateWorkingDir func(fs filesystem.Filesystem)
args []string
existingApps []string
wantAppName string
wantErrValidate string
}{
{
name: "default app",
populateWorkingDir: func(fs filesystem.Filesystem) {
_ = fs.MkdirAll(filepath.Join(prefixDir, "myapp", ".odo", "env"), 0755)
env, err := envinfo.NewEnvSpecificInfo(filepath.Join(prefixDir, "myapp"))
if err != nil {
return
}
_ = env.SetComponentSettings(envinfo.ComponentSettings{
Name: "a-name",
Project: "a-project",
AppName: "an-app-name",
})
},
existingApps: []string{"an-app-name", "another-app-name"},
wantAppName: "an-app-name",
},
{
name: "app from args",
populateWorkingDir: func(fs filesystem.Filesystem) {
_ = fs.MkdirAll(filepath.Join(prefixDir, "myapp", ".odo", "env"), 0755)
env, err := envinfo.NewEnvSpecificInfo(filepath.Join(prefixDir, "myapp"))
if err != nil {
return
}
_ = env.SetComponentSettings(envinfo.ComponentSettings{
Name: "a-name",
Project: "a-project",
AppName: "an-app-name",
})
},
args: []string{"another-app-name"},
existingApps: []string{"an-app-name", "another-app-name"},
wantAppName: "another-app-name",
},
{
name: "empty app name",
populateWorkingDir: func(fs filesystem.Filesystem) {
_ = fs.MkdirAll(filepath.Join(prefixDir, "myapp", ".odo", "env"), 0755)
env, err := envinfo.NewEnvSpecificInfo(filepath.Join(prefixDir, "myapp"))
if err != nil {
return
}
_ = env.SetComponentSettings(envinfo.ComponentSettings{
Name: "a-name",
Project: "a-project",
AppName: "",
})
},
existingApps: []string{"an-app-name", "another-app-name"},
wantAppName: "",
wantErrValidate: "Please specify the application name and project name",
},
{
name: "non existing app name",
populateWorkingDir: func(fs filesystem.Filesystem) {
_ = fs.MkdirAll(filepath.Join(prefixDir, "myapp", ".odo", "env"), 0755)
env, err := envinfo.NewEnvSpecificInfo(filepath.Join(prefixDir, "myapp"))
if err != nil {
return
}
_ = env.SetComponentSettings(envinfo.ComponentSettings{
Name: "a-name",
Project: "a-project",
AppName: "an-app-name",
})
},
args: []string{"an-unknown-app-name"},
existingApps: []string{"an-app-name", "another-app-name"},
wantAppName: "an-unknown-app-name",
wantErrValidate: " app does not exists",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// the first one is to cleanup the directory before execution (in case there are remaining files from a previous execution)
os.RemoveAll(prefixDir)
// the second one to cleanup after execution
defer os.RemoveAll(prefixDir)
// Fake Cobra
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmdline := cmdline.NewMockCmdline(ctrl)
// Fake odo Kube client
kclient := kclient.NewMockClientInterface(ctrl)
/* Mocks for Complete */
cmdline.EXPECT().GetWorkingDirectory().Return(workingDir, nil).AnyTimes()
cmdline.EXPECT().CheckIfConfigurationNeeded().Return(false, nil).AnyTimes()
cmdline.EXPECT().FlagValueIfSet("project").Return("").AnyTimes()
cmdline.EXPECT().FlagValueIfSet("app").Return("").AnyTimes()
cmdline.EXPECT().FlagValueIfSet("component").Return("").AnyTimes()
cmdline.EXPECT().FlagValueIfSet("o").Return("").AnyTimes()
cmdline.EXPECT().GetKubeClient().Return(kclient, nil).AnyTimes()
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "a-project",
},
}
kclient.EXPECT().GetNamespaceNormal("a-project").Return(ns, nil).AnyTimes()
kclient.EXPECT().SetNamespace("a-project").AnyTimes()
tt.populateWorkingDir(filesystem.DefaultFs{})
/* Mocks for Complete */
appClient := application.NewMockClient(ctrl)
appClient.EXPECT().Exists(tt.wantAppName).Return(func() bool {
for _, app := range tt.existingApps {
if tt.wantAppName == app {
return true
}
}
return false
}(), nil).AnyTimes()
opts := NewDescribeOptions(appClient)
/* COMPLETE */
err := opts.Complete(cmdline, tt.args)
if err != nil {
return
}
if opts.appName != tt.wantAppName {
t.Errorf("Got appName %q, expected %q", opts.appName, tt.wantAppName)
}
/* VALIDATE */
err = opts.Validate()
if err == nil && tt.wantErrValidate != "" {
t.Errorf("Expected %v, got no error", tt.wantErrValidate)
return
}
if err != nil && tt.wantErrValidate == "" {
t.Errorf("Expected no error, got %v", err.Error())
return
}
if err != nil && tt.wantErrValidate != "" && !strings.Contains(err.Error(), tt.wantErrValidate) {
t.Errorf("Expected error %v, got %v", tt.wantErrValidate, err.Error())
return
}
if err != nil {
return
}
/* Mocks for Run */
appClient.EXPECT().ComponentList(tt.wantAppName)
/* RUN */
err = opts.Run()
if err != nil {
t.Errorf("Expected nil err, got %s", err)
}
})
}
}

View File

@@ -5,14 +5,17 @@ import (
"os"
"text/tabwriter"
"github.com/spf13/cobra"
"github.com/redhat-developer/odo/pkg/application"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/machineoutput"
"github.com/redhat-developer/odo/pkg/odo/cli/project"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions"
"github.com/redhat-developer/odo/pkg/odo/util"
"github.com/spf13/cobra"
ktemplates "k8s.io/kubectl/pkg/util/templates"
)
@@ -30,17 +33,22 @@ var (
type ListOptions struct {
// Context
*genericclioptions.Context
// Clients
appClient application.Client
}
// NewListOptions creates a new ListOptions instance
func NewListOptions() *ListOptions {
return &ListOptions{}
func NewListOptions(appClient application.Client) *ListOptions {
return &ListOptions{
appClient: appClient,
}
}
// Complete completes ListOptions after they've been created
func (o *ListOptions) Complete(cmdline cmdline.Cmdline, args []string) (err error) {
o.Context, err = genericclioptions.New(genericclioptions.NewCreateParameters(cmdline))
return
return err
}
// Validate validates the ListOptions based on completed values
@@ -54,52 +62,55 @@ func (o *ListOptions) Validate() (err error) {
// Run contains the logic for the odo command
func (o *ListOptions) Run() (err error) {
apps, err := application.List(o.KClient)
apps, err := o.appClient.List()
if err != nil {
return fmt.Errorf("unable to get list of applications: %v", err)
}
if len(apps) > 0 {
if log.IsJSON() {
var appList []application.App
for _, app := range apps {
appDef := application.GetMachineReadableFormat(o.KClient, app, o.GetProject())
appList = append(appList, appDef)
}
appListDef := application.GetMachineReadableFormatForList(appList)
machineoutput.OutputSuccess(appListDef)
} else {
log.Infof("The project '%v' has the following applications:", o.GetProject())
tabWriter := tabwriter.NewWriter(os.Stdout, 5, 2, 3, ' ', tabwriter.TabIndent)
_, err := fmt.Fprintln(tabWriter, "NAME")
if err != nil {
return err
}
for _, app := range apps {
_, err := fmt.Fprintln(tabWriter, app)
if err != nil {
return err
}
}
return tabWriter.Flush()
}
} else {
if log.IsJSON() {
apps := application.GetMachineReadableFormatForList([]application.App{})
if len(apps) == 0 {
if o.IsJSON() {
apps := o.appClient.GetMachineReadableFormatForList([]application.App{})
machineoutput.OutputSuccess(apps)
} else {
log.Infof("There are no applications deployed in the project '%v'", o.GetProject())
return nil
}
log.Infof("There are no applications deployed in the project '%v'", o.GetProject())
return nil
}
if o.IsJSON() {
var appList []application.App
for _, app := range apps {
appDef := o.appClient.GetMachineReadableFormat(app, o.GetProject())
appList = append(appList, appDef)
}
appListDef := o.appClient.GetMachineReadableFormatForList(appList)
machineoutput.OutputSuccess(appListDef)
return nil
}
log.Infof("The project '%v' has the following applications:", o.GetProject())
tabWriter := tabwriter.NewWriter(os.Stdout, 5, 2, 3, ' ', tabwriter.TabIndent)
_, err = fmt.Fprintln(tabWriter, "NAME")
if err != nil {
return err
}
for _, app := range apps {
_, err := fmt.Fprintln(tabWriter, app)
if err != nil {
return err
}
}
return
return tabWriter.Flush()
}
// NewCmdList implements the odo command.
func NewCmdList(name, fullName string) *cobra.Command {
o := NewListOptions()
// The error is not handled at this point, it will be handled during Context creation
kubclient, _ := kclient.New()
o := NewListOptions(application.NewClient(kubclient))
command := &cobra.Command{
Use: name,
Short: "List all applications in the current project",

View File

@@ -0,0 +1,149 @@
package application
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/golang/mock/gomock"
"github.com/redhat-developer/odo/pkg/application"
"github.com/redhat-developer/odo/pkg/envinfo"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestList(t *testing.T) {
prefixDir, err := os.MkdirTemp(os.TempDir(), "unittests-")
if err != nil {
t.Errorf("Error creating temp directory for tests")
return
}
workingDir := filepath.Join(prefixDir, "myapp")
tests := []struct {
name string
populateWorkingDir func(fs filesystem.Filesystem)
currentNamespace string
wantErrValidate string
}{
{
name: "default app",
populateWorkingDir: func(fs filesystem.Filesystem) {
_ = fs.MkdirAll(filepath.Join(prefixDir, "myapp", ".odo", "env"), 0755)
env, err := envinfo.NewEnvSpecificInfo(filepath.Join(prefixDir, "myapp"))
if err != nil {
return
}
_ = env.SetComponentSettings(envinfo.ComponentSettings{
Name: "a-name",
Project: "a-project",
AppName: "an-app-name",
})
},
},
{
name: "empty project name",
populateWorkingDir: func(fs filesystem.Filesystem) {
_ = fs.MkdirAll(filepath.Join(prefixDir, "myapp", ".odo", "env"), 0755)
env, err := envinfo.NewEnvSpecificInfo(filepath.Join(prefixDir, "myapp"))
if err != nil {
return
}
_ = env.SetComponentSettings(envinfo.ComponentSettings{
Name: "a-name",
Project: "",
AppName: "an-app-name",
})
},
currentNamespace: "",
wantErrValidate: "Please specify the application name and project name",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// the first one is to cleanup the directory before execution (in case there are remaining files from a previous execution)
os.RemoveAll(prefixDir)
// the second one to cleanup after execution
defer os.RemoveAll(prefixDir)
// Fake Cobra
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmdline := cmdline.NewMockCmdline(ctrl)
// Fake odo Kube client
kclient := kclient.NewMockClientInterface(ctrl)
/* Mocks for Complete */
cmdline.EXPECT().GetWorkingDirectory().Return(workingDir, nil).AnyTimes()
cmdline.EXPECT().CheckIfConfigurationNeeded().Return(false, nil).AnyTimes()
cmdline.EXPECT().FlagValueIfSet("project").Return("").AnyTimes()
cmdline.EXPECT().FlagValueIfSet("app").Return("").AnyTimes()
cmdline.EXPECT().FlagValueIfSet("component").Return("").AnyTimes()
cmdline.EXPECT().FlagValueIfSet("o").Return("").AnyTimes()
cmdline.EXPECT().GetName().Return("list").AnyTimes()
cmdline.EXPECT().GetParentName().Return("application").AnyTimes()
cmdline.EXPECT().GetKubeClient().Return(kclient, nil).AnyTimes()
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "a-project",
},
}
kclient.EXPECT().GetNamespaceNormal("a-project").Return(ns, nil).AnyTimes()
kclient.EXPECT().GetNamespaceNormal("").Return(nil, nil).AnyTimes()
kclient.EXPECT().SetNamespace("a-project").AnyTimes()
kclient.EXPECT().SetNamespace("").AnyTimes()
kclient.EXPECT().GetCurrentNamespace().Return(tt.currentNamespace).AnyTimes()
tt.populateWorkingDir(filesystem.DefaultFs{})
/* Mocks for Complete */
appClient := application.NewMockClient(ctrl)
opts := NewListOptions(appClient)
/* COMPLETE */
err := opts.Complete(cmdline, []string{})
if err != nil {
return
}
/* VALIDATE */
err = opts.Validate()
if err == nil && tt.wantErrValidate != "" {
t.Errorf("Expected %v, got no error", tt.wantErrValidate)
return
}
if err != nil && tt.wantErrValidate == "" {
t.Errorf("Expected no error, got %v", err.Error())
return
}
if err != nil && tt.wantErrValidate != "" && !strings.Contains(err.Error(), tt.wantErrValidate) {
t.Errorf("Expected error %v, got %v", tt.wantErrValidate, err.Error())
return
}
if err != nil {
return
}
/* Mocks for Run */
appClient.EXPECT().List().Times(1)
/* RUN */
err = opts.Run()
if err != nil {
t.Errorf("Expected nil err, got %s", err)
}
})
}
}

View File

@@ -19,18 +19,27 @@ import (
// CommonPushOptions has data needed for all pushes
type CommonPushOptions struct {
// Context
*genericclioptions.Context
// Clients
prjClient project.Client
//Flags
// TODO(feloy) Fixme
showFlag bool //nolint:structcheck
componentContext string
configFlag bool
sourceFlag bool
EnvSpecificInfo *envinfo.EnvSpecificInfo
*genericclioptions.Context
EnvSpecificInfo *envinfo.EnvSpecificInfo
}
// NewCommonPushOptions instantiates a commonPushOptions object
func NewCommonPushOptions() *CommonPushOptions {
return &CommonPushOptions{}
func NewCommonPushOptions(prjClient project.Client) *CommonPushOptions {
return &CommonPushOptions{
prjClient: prjClient,
}
}
//InitEnvInfoFromContext initializes envinfo from the context
@@ -60,12 +69,12 @@ func (cpo *CommonPushOptions) ResolveSrcAndConfigFlags() {
func (cpo *CommonPushOptions) ResolveProject(prjName string) (err error) {
// check if project exist
isPrjExists, err := project.Exists(cpo.Context.KClient, prjName)
isPrjExists, err := cpo.prjClient.Exists(prjName)
if err != nil {
return errors.Wrapf(err, "failed to check if project with name %s exists", prjName)
}
if !isPrjExists {
err = project.Create(cpo.Context.KClient, prjName, true)
err = cpo.prjClient.Create(prjName, true)
if err != nil {
return errors.Wrapf(
err,

View File

@@ -6,8 +6,10 @@ import (
"path/filepath"
"strings"
"github.com/redhat-developer/odo/pkg/kclient"
registryUtil "github.com/redhat-developer/odo/pkg/odo/cli/registry/util"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
"github.com/redhat-developer/odo/pkg/project"
"github.com/zalando/go-keyring"
"github.com/devfile/library/pkg/devfile"
@@ -113,9 +115,9 @@ odo catalog list components
%[1]s nodejs --app myapp --project myproject`)
// NewCreateOptions returns new instance of CreateOptions
func NewCreateOptions() *CreateOptions {
func NewCreateOptions(prjClient project.Client) *CreateOptions {
return &CreateOptions{
PushOptions: NewPushOptions(),
PushOptions: NewPushOptions(prjClient),
}
}
@@ -345,7 +347,9 @@ func (co *CreateOptions) Run() (err error) {
// NewCmdCreate implements the create odo command
func NewCmdCreate(name, fullName string) *cobra.Command {
co := NewCreateOptions()
// The error is not handled at this point, it will be handled during Context creation
kubclient, _ := kclient.New()
co := NewCreateOptions(project.NewClient(kubclient))
var componentCreateCmd = &cobra.Command{
Use: fmt.Sprintf("%s <component_type> [component_name] [flags]", name),
Short: "Create a new component",

View File

@@ -5,6 +5,7 @@ import (
"path/filepath"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/project"
scontext "github.com/redhat-developer/odo/pkg/segment/context"
"github.com/redhat-developer/odo/pkg/component"
@@ -73,9 +74,9 @@ type PushOptions struct {
// NewPushOptions returns new instance of PushOptions
// with "default" values for certain values, for example, show is "false"
func NewPushOptions() *PushOptions {
func NewPushOptions(prjClient project.Client) *PushOptions {
return &PushOptions{
CommonPushOptions: NewCommonPushOptions(),
CommonPushOptions: NewCommonPushOptions(prjClient),
}
}
@@ -228,7 +229,9 @@ func (po *PushOptions) Run() (err error) {
// NewCmdPush implements the push odo command
func NewCmdPush(name, fullName string) *cobra.Command {
po := NewPushOptions()
// The error is not handled at this point, it will be handled during Context creation
kubclient, _ := kclient.New()
po := NewPushOptions(project.NewClient(kubclient))
var pushCmd = &cobra.Command{
Use: fmt.Sprintf("%s [component name]", name),

View File

@@ -4,7 +4,9 @@ import (
"fmt"
"strings"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
"github.com/redhat-developer/odo/pkg/project"
"github.com/redhat-developer/odo/pkg/util"
"github.com/pkg/errors"
@@ -54,8 +56,10 @@ type SetOptions struct {
}
// NewSetOptions creates a new SetOptions instance
func NewSetOptions() *SetOptions {
return &SetOptions{PushOptions: clicomponent.NewPushOptions()}
func NewSetOptions(prjClient project.Client) *SetOptions {
return &SetOptions{
PushOptions: clicomponent.NewPushOptions(prjClient),
}
}
// Complete completes SetOptions after they've been created
@@ -174,7 +178,9 @@ func isValidArgumentList(args []string) error {
// NewCmdSet implements the config set odo command
func NewCmdSet(name, fullName string) *cobra.Command {
o := NewSetOptions()
// The error is not handled at this point, it will be handled during Context creation
kubclient, _ := kclient.New()
o := NewSetOptions(project.NewClient(kubclient))
configurationSetCmd := &cobra.Command{
Use: name,
Short: "Set a value in odo config file",

View File

@@ -4,6 +4,8 @@ import (
"fmt"
"strings"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/project"
"github.com/redhat-developer/odo/pkg/util"
"github.com/redhat-developer/odo/pkg/config"
@@ -49,8 +51,10 @@ type UnsetOptions struct {
}
// NewUnsetOptions creates a new UnsetOptions instance
func NewUnsetOptions() *UnsetOptions {
return &UnsetOptions{PushOptions: clicomponent.NewPushOptions()}
func NewUnsetOptions(prjClient project.Client) *UnsetOptions {
return &UnsetOptions{
PushOptions: clicomponent.NewPushOptions(prjClient),
}
}
// Complete completes UnsetOptions after they've been created
@@ -125,7 +129,9 @@ func (o *UnsetOptions) Run() error {
// NewCmdUnset implements the config unset odo command
func NewCmdUnset(name, fullName string) *cobra.Command {
o := NewUnsetOptions()
// The error is not handled at this point, it will be handled during Context creation
kubclient, _ := kclient.New()
o := NewUnsetOptions(project.NewClient(kubclient))
configurationUnsetCmd := &cobra.Command{
Use: name,
Short: "Unset a value in odo config file",

View File

@@ -3,6 +3,7 @@ package project
import (
"fmt"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/machineoutput"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
@@ -35,6 +36,9 @@ type ProjectCreateOptions struct {
// Context
*genericclioptions.Context
// Clients
prjClient project.Client
// Parameters
projectName string
@@ -43,8 +47,10 @@ type ProjectCreateOptions struct {
}
// NewProjectCreateOptions creates a ProjectCreateOptions instance
func NewProjectCreateOptions() *ProjectCreateOptions {
return &ProjectCreateOptions{}
func NewProjectCreateOptions(prjClient project.Client) *ProjectCreateOptions {
return &ProjectCreateOptions{
prjClient: prjClient,
}
}
// Complete completes ProjectCreateOptions after they've been created
@@ -77,7 +83,7 @@ func (pco *ProjectCreateOptions) Run() (err error) {
}
// Create the project & end the spinner (if there is any..)
err = project.Create(pco.Context.KClient, pco.projectName, pco.waitFlag)
err = pco.prjClient.Create(pco.projectName, pco.waitFlag)
if err != nil {
return err
}
@@ -87,7 +93,7 @@ func (pco *ProjectCreateOptions) Run() (err error) {
log.Successf(successMessage)
// Set the current project when created
err = project.SetCurrent(pco.Context.KClient, pco.projectName)
err = pco.prjClient.SetCurrent(pco.projectName)
if err != nil {
return err
}
@@ -105,7 +111,9 @@ func (pco *ProjectCreateOptions) Run() (err error) {
// NewCmdProjectCreate creates the project create command
func NewCmdProjectCreate(name, fullName string) *cobra.Command {
o := NewProjectCreateOptions()
// The error is not handled at this point, it will be handled during Context creation
kubclient, _ := kclient.New()
o := NewProjectCreateOptions(project.NewClient(kubclient))
projectCreateCmd := &cobra.Command{
Use: name,

View File

@@ -0,0 +1,119 @@
package project
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/golang/mock/gomock"
"github.com/redhat-developer/odo/pkg/envinfo"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
"github.com/redhat-developer/odo/pkg/project"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestCreate(t *testing.T) {
prefixDir, err := os.MkdirTemp(os.TempDir(), "unittests-")
if err != nil {
t.Errorf("Error creating temp directory for tests")
return
}
workingDir := filepath.Join(prefixDir, "myapp")
tests := []struct {
name string
populateWorkingDir func(fs filesystem.Filesystem)
args []string
// existingApps []string
wantProjectName string
//wantErrValidate string
}{
{
name: "project from args",
populateWorkingDir: func(fs filesystem.Filesystem) {
_ = fs.MkdirAll(filepath.Join(prefixDir, "myapp", ".odo", "env"), 0755)
env, er := envinfo.NewEnvSpecificInfo(filepath.Join(prefixDir, "myapp"))
if er != nil {
return
}
_ = env.SetComponentSettings(envinfo.ComponentSettings{
Name: "a-name",
Project: "a-project",
AppName: "an-app-name",
})
},
args: []string{"project-name-to-create"},
wantProjectName: "project-name-to-create",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// the first one is to cleanup the directory before execution (in case there are remaining files from a previous execution)
os.RemoveAll(prefixDir)
// the second one to cleanup after execution
defer os.RemoveAll(prefixDir)
// Fake Cobra
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmdline := cmdline.NewMockCmdline(ctrl)
cmdline.EXPECT().GetWorkingDirectory().Return(workingDir, nil).AnyTimes()
cmdline.EXPECT().FlagValueIfSet("project").Return("").AnyTimes()
cmdline.EXPECT().FlagValueIfSet("app").Return("").AnyTimes()
cmdline.EXPECT().FlagValueIfSet("component").Return("").AnyTimes()
cmdline.EXPECT().FlagValueIfSet("o").Return("").AnyTimes()
cmdline.EXPECT().CheckIfConfigurationNeeded().Return(false, nil).AnyTimes()
cmdline.EXPECT().Context().Return(context.Background()).AnyTimes()
// Fake odo Kube client
kclient := kclient.NewMockClientInterface(ctrl)
cmdline.EXPECT().GetKubeClient().Return(kclient, nil).AnyTimes()
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "a-project",
},
}
kclient.EXPECT().GetNamespaceNormal("a-project").Return(ns, nil).AnyTimes()
kclient.EXPECT().SetNamespace("a-project").AnyTimes()
tt.populateWorkingDir(filesystem.DefaultFs{})
/* Mocks for Complete */
prjClient := project.NewMockClient(ctrl)
opts := NewProjectCreateOptions(prjClient)
/* COMPLETE */
err = opts.Complete(cmdline, tt.args)
if err != nil {
t.Errorf("Expected nil error, got %s", err)
return
}
if opts.projectName != tt.wantProjectName {
t.Errorf("Got appName %q, expected %q", opts.projectName, tt.wantProjectName)
}
/* VALIDATE */
err = opts.Validate()
if err != nil {
return
}
/* Mocks for Run */
prjClient.EXPECT().Create(tt.wantProjectName, false).Times(1)
prjClient.EXPECT().SetCurrent(tt.wantProjectName).Times(1)
/* RUN */
err = opts.Run()
})
}
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
odoerrors "github.com/redhat-developer/odo/pkg/errors"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/machineoutput"
"github.com/redhat-developer/odo/pkg/odo/cli/ui"
@@ -36,6 +37,9 @@ type ProjectDeleteOptions struct {
// Context
*genericclioptions.Context
// Clients
prjClient project.Client
// Parameters
projectName string
@@ -45,8 +49,10 @@ type ProjectDeleteOptions struct {
}
// NewProjectDeleteOptions creates a ProjectDeleteOptions instance
func NewProjectDeleteOptions() *ProjectDeleteOptions {
return &ProjectDeleteOptions{}
func NewProjectDeleteOptions(prjClient project.Client) *ProjectDeleteOptions {
return &ProjectDeleteOptions{
prjClient: prjClient,
}
}
// Complete completes ProjectDeleteOptions after they've been created
@@ -59,7 +65,7 @@ func (pdo *ProjectDeleteOptions) Complete(cmdline cmdline.Cmdline, args []string
// Validate validates the parameters of the ProjectDeleteOptions
func (pdo *ProjectDeleteOptions) Validate() error {
// Validate existence of the project to be deleted
isValidProject, err := project.Exists(pdo.Context.KClient, pdo.projectName)
isValidProject, err := pdo.prjClient.Exists(pdo.projectName)
if kerrors.IsForbidden(err) {
return &odoerrors.Unauthorized{}
}
@@ -76,7 +82,7 @@ func (pdo *ProjectDeleteOptions) Run() (err error) {
s := &log.Status{}
// This to set the project in the file and runtime
err = project.SetCurrent(pdo.Context.KClient, pdo.projectName)
err = pdo.prjClient.SetCurrent(pdo.projectName)
if err != nil {
return err
}
@@ -97,7 +103,7 @@ func (pdo *ProjectDeleteOptions) Run() (err error) {
defer s.End(false)
}
err := project.Delete(pdo.Context.KClient, pdo.projectName, pdo.waitFlag)
err := pdo.prjClient.Delete(pdo.projectName, pdo.waitFlag)
if err != nil {
return err
}
@@ -118,7 +124,9 @@ func (pdo *ProjectDeleteOptions) Run() (err error) {
// NewCmdProjectDelete creates the project delete command
func NewCmdProjectDelete(name, fullName string) *cobra.Command {
o := NewProjectDeleteOptions()
// The error is not handled at this point, it will be handled during Context creation
kubclient, _ := kclient.New()
o := NewProjectDeleteOptions(project.NewClient(kubclient))
projectDeleteCmd := &cobra.Command{
Use: name,

View File

@@ -0,0 +1,155 @@
package project
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"github.com/golang/mock/gomock"
"github.com/redhat-developer/odo/pkg/envinfo"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
"github.com/redhat-developer/odo/pkg/project"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestDelete(t *testing.T) {
prefixDir, err := os.MkdirTemp(os.TempDir(), "unittests-")
if err != nil {
t.Errorf("Error creating temp directory for tests")
return
}
workingDir := filepath.Join(prefixDir, "myapp")
tests := []struct {
name string
populateWorkingDir func(fs filesystem.Filesystem)
args []string
projectExists bool
wantProjectName string
wantErrValidate string
}{
{
name: "project from args",
populateWorkingDir: func(fs filesystem.Filesystem) {
_ = fs.MkdirAll(filepath.Join(prefixDir, "myapp", ".odo", "env"), 0755)
env, er := envinfo.NewEnvSpecificInfo(filepath.Join(prefixDir, "myapp"))
if er != nil {
return
}
_ = env.SetComponentSettings(envinfo.ComponentSettings{
Name: "a-name",
Project: "a-project",
AppName: "an-app-name",
})
},
args: []string{"project-name-to-delete"},
projectExists: true,
wantProjectName: "project-name-to-delete",
}, {
name: "project from args not existing",
populateWorkingDir: func(fs filesystem.Filesystem) {
_ = fs.MkdirAll(filepath.Join(prefixDir, "myapp", ".odo", "env"), 0755)
env, er := envinfo.NewEnvSpecificInfo(filepath.Join(prefixDir, "myapp"))
if er != nil {
return
}
_ = env.SetComponentSettings(envinfo.ComponentSettings{
Name: "a-name",
Project: "a-project",
AppName: "an-app-name",
})
},
args: []string{"project-name-to-delete"},
projectExists: false,
wantProjectName: "project-name-to-delete",
wantErrValidate: `The project "project-name-to-delete" does not exist`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// the first one is to cleanup the directory before execution (in case there are remaining files from a previous execution)
os.RemoveAll(prefixDir)
// the second one to cleanup after execution
defer os.RemoveAll(prefixDir)
// Fake Cobra
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmdline := cmdline.NewMockCmdline(ctrl)
cmdline.EXPECT().GetWorkingDirectory().Return(workingDir, nil).AnyTimes()
cmdline.EXPECT().FlagValueIfSet("project").Return("").AnyTimes()
cmdline.EXPECT().FlagValueIfSet("app").Return("").AnyTimes()
cmdline.EXPECT().FlagValueIfSet("component").Return("").AnyTimes()
cmdline.EXPECT().FlagValueIfSet("o").Return("").AnyTimes()
cmdline.EXPECT().CheckIfConfigurationNeeded().Return(false, nil).AnyTimes()
cmdline.EXPECT().Context().Return(context.Background()).AnyTimes()
// Fake odo Kube client
kclient := kclient.NewMockClientInterface(ctrl)
cmdline.EXPECT().GetKubeClient().Return(kclient, nil).AnyTimes()
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "a-project",
},
}
kclient.EXPECT().GetNamespaceNormal("a-project").Return(ns, nil).AnyTimes()
kclient.EXPECT().SetNamespace("a-project").AnyTimes()
tt.populateWorkingDir(filesystem.DefaultFs{})
/* Mocks for Complete */
prjClient := project.NewMockClient(ctrl)
opts := NewProjectDeleteOptions(prjClient)
opts.forceFlag = true
/* COMPLETE */
err = opts.Complete(cmdline, tt.args)
if err != nil {
t.Errorf("Expected nil error, got %s", err)
return
}
if opts.projectName != tt.wantProjectName {
t.Errorf("Got appName %q, expected %q", opts.projectName, tt.wantProjectName)
}
/* Mocks for Validate */
prjClient.EXPECT().Exists(tt.wantProjectName).Return(tt.projectExists, nil).Times(1)
/* VALIDATE */
err = opts.Validate()
if err == nil && tt.wantErrValidate != "" {
t.Errorf("Expected %v, got no error", tt.wantErrValidate)
return
}
if err != nil && tt.wantErrValidate == "" {
t.Errorf("Expected no error, got %v", err.Error())
return
}
if err != nil && tt.wantErrValidate != "" && !strings.Contains(err.Error(), tt.wantErrValidate) {
t.Errorf("Expected error %v, got %v", tt.wantErrValidate, err.Error())
return
}
if err != nil {
return
}
/* Mocks for Run */
prjClient.EXPECT().SetCurrent(tt.wantProjectName).Times(1)
prjClient.EXPECT().Delete(tt.wantProjectName, false).Times(1)
/* RUN */
err = opts.Run()
})
}
}

View File

@@ -6,6 +6,7 @@ import (
"os"
"text/tabwriter"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/machineoutput"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
@@ -30,11 +31,16 @@ var (
type ProjectListOptions struct {
// Context
*genericclioptions.Context
// Clients
prjClient project.Client
}
// NewProjectListOptions creates a new ProjectListOptions instance
func NewProjectListOptions() *ProjectListOptions {
return &ProjectListOptions{}
func NewProjectListOptions(prjClient project.Client) *ProjectListOptions {
return &ProjectListOptions{
prjClient: prjClient,
}
}
// Complete completes ProjectListOptions after they've been created
@@ -50,7 +56,7 @@ func (plo *ProjectListOptions) Validate() (err error) {
// Run contains the logic for the odo project list command
func (plo *ProjectListOptions) Run() error {
projects, err := project.List(plo.Context.KClient)
projects, err := plo.prjClient.List()
if err != nil {
return err
}
@@ -68,7 +74,9 @@ func (plo *ProjectListOptions) Run() error {
// NewCmdProjectList implements the odo project list command.
func NewCmdProjectList(name, fullName string) *cobra.Command {
o := NewProjectListOptions()
// The error is not handled at this point, it will be handled during Context creation
kubclient, _ := kclient.New()
o := NewProjectListOptions(project.NewClient(kubclient))
projectListCmd := &cobra.Command{
Use: name,
Short: listLongDesc,

View File

@@ -4,6 +4,7 @@ import (
"fmt"
odoerrors "github.com/redhat-developer/odo/pkg/errors"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions"
@@ -36,6 +37,9 @@ type ProjectSetOptions struct {
// Context
*genericclioptions.Context
// Clients
prjClient project.Client
// Parameters
projectName string
@@ -44,8 +48,10 @@ type ProjectSetOptions struct {
}
// NewProjectSetOptions creates a ProjectSetOptions instance
func NewProjectSetOptions() *ProjectSetOptions {
return &ProjectSetOptions{}
func NewProjectSetOptions(prjClient project.Client) *ProjectSetOptions {
return &ProjectSetOptions{
prjClient: prjClient,
}
}
// Complete completes ProjectSetOptions after they've been created
@@ -64,7 +70,7 @@ func (pso *ProjectSetOptions) Complete(cmdline cmdline.Cmdline, args []string) (
// Validate validates the parameters of the ProjectSetOptions
func (pso *ProjectSetOptions) Validate() (err error) {
exists, err := project.Exists(pso.Context.KClient, pso.projectName)
exists, err := pso.prjClient.Exists(pso.projectName)
if kerrors.IsForbidden(err) {
return &odoerrors.Unauthorized{}
}
@@ -78,7 +84,7 @@ func (pso *ProjectSetOptions) Validate() (err error) {
// Run runs the project set command
func (pso *ProjectSetOptions) Run() (err error) {
current := pso.GetProject()
err = project.SetCurrent(pso.Context.KClient, pso.projectName)
err = pso.prjClient.SetCurrent(pso.projectName)
if err != nil {
return err
}
@@ -96,7 +102,9 @@ func (pso *ProjectSetOptions) Run() (err error) {
// NewCmdProjectSet creates the project set command
func NewCmdProjectSet(name, fullName string) *cobra.Command {
o := NewProjectSetOptions()
// The error is not handled at this point, it will be handled during Context creation
kubclient, _ := kclient.New()
o := NewProjectSetOptions(project.NewClient(kubclient))
projectSetCmd := &cobra.Command{
Use: name,

View File

@@ -6,6 +6,7 @@ import (
devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/pkg/errors"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/localConfigProvider"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/machineoutput"
@@ -14,6 +15,7 @@ import (
"github.com/redhat-developer/odo/pkg/odo/genericclioptions"
odoutil "github.com/redhat-developer/odo/pkg/odo/util"
"github.com/redhat-developer/odo/pkg/odo/util/completion"
"github.com/redhat-developer/odo/pkg/project"
"github.com/redhat-developer/odo/pkg/url"
"github.com/redhat-developer/odo/pkg/util"
@@ -75,8 +77,8 @@ type CreateOptions struct {
}
// NewURLCreateOptions creates a new CreateOptions instance
func NewURLCreateOptions() *CreateOptions {
return &CreateOptions{PushOptions: clicomponent.NewPushOptions()}
func NewURLCreateOptions(prjClient project.Client) *CreateOptions {
return &CreateOptions{PushOptions: clicomponent.NewPushOptions(prjClient)}
}
// Complete completes CreateOptions after they've been Created
@@ -193,7 +195,9 @@ func (o *CreateOptions) Run() (err error) {
// NewCmdURLCreate implements the odo url create command.
func NewCmdURLCreate(name, fullName string) *cobra.Command {
o := NewURLCreateOptions()
// The error is not handled at this point, it will be handled during Context creation
kubclient, _ := kclient.New()
o := NewURLCreateOptions(project.NewClient(kubclient))
urlCreateCmd := &cobra.Command{
Use: name + " [url name]",
Short: urlCreateShortDesc,

View File

@@ -3,6 +3,7 @@ package url
import (
"fmt"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/log"
clicomponent "github.com/redhat-developer/odo/pkg/odo/cli/component"
"github.com/redhat-developer/odo/pkg/odo/cli/ui"
@@ -10,6 +11,7 @@ import (
"github.com/redhat-developer/odo/pkg/odo/genericclioptions"
odoutil "github.com/redhat-developer/odo/pkg/odo/util"
"github.com/redhat-developer/odo/pkg/odo/util/completion"
"github.com/redhat-developer/odo/pkg/project"
"github.com/spf13/cobra"
ktemplates "k8s.io/kubectl/pkg/util/templates"
)
@@ -38,8 +40,8 @@ type DeleteOptions struct {
}
// NewURLDeleteOptions creates a new DeleteOptions instance
func NewURLDeleteOptions() *DeleteOptions {
return &DeleteOptions{PushOptions: clicomponent.NewPushOptions()}
func NewURLDeleteOptions(prjClient project.Client) *DeleteOptions {
return &DeleteOptions{PushOptions: clicomponent.NewPushOptions(prjClient)}
}
// Complete completes DeleteOptions after they've been Deleted
@@ -104,7 +106,9 @@ func (o *DeleteOptions) Run() (err error) {
// NewCmdURLDelete implements the odo url delete command.
func NewCmdURLDelete(name, fullName string) *cobra.Command {
o := NewURLDeleteOptions()
// The error is not handled at this point, it will be handled during Context creation
kubclient, _ := kclient.New()
o := NewURLDeleteOptions(project.NewClient(kubclient))
urlDeleteCmd := &cobra.Command{
Use: name + " [url name]",
Short: urlDeleteShortDesc,

View File

@@ -42,7 +42,7 @@ type internalCxt struct {
component string
// componentContext is the value passed with the `--context` flag
componentContext string
// outputFlag is the value passed with the `--output` flag
// outputFlag is the value passed with the `-o` flag
outputFlag string
// The path of the detected devfile
devfilePath string
@@ -202,6 +202,10 @@ func (o *Context) GetOutputFlag() string {
return o.outputFlag
}
func (o *Context) IsJSON() bool {
return o.outputFlag == "json"
}
func (o *Context) GetComponentContext() string {
return o.componentContext
}

View File

@@ -342,7 +342,7 @@ func TestNew(t *testing.T) {
cmdline.EXPECT().FlagValueIfSet("project").Return(tt.input.projectFlag).AnyTimes()
cmdline.EXPECT().FlagValueIfSet("app").Return(tt.input.appFlag).AnyTimes()
cmdline.EXPECT().FlagValueIfSet("component").Return(tt.input.componentFlag).AnyTimes()
cmdline.EXPECT().FlagValueIfSet("output").Return(tt.input.outputFlag).AnyTimes()
cmdline.EXPECT().FlagValueIfSet("o").Return(tt.input.outputFlag).AnyTimes()
cmdline.EXPECT().IsFlagSet("all").Return(tt.input.allFlagSet).AnyTimes()
cmdline.EXPECT().GetParentName().Return(tt.input.parentCommandName).AnyTimes()
cmdline.EXPECT().GetName().Return(tt.input.commandName).AnyTimes()

View File

@@ -17,7 +17,8 @@ import (
var AppCompletionHandler = func(cmd *cobra.Command, args parsedArgs, context *genericclioptions.Context) (completions []string) {
completions = make([]string, 0)
applications, err := application.List(context.KClient)
appClient := application.NewClient(context.KClient)
applications, err := appClient.List()
if err != nil {
return completions
}

View File

@@ -12,7 +12,7 @@ const (
// ComponentFlagName is the name of the flag allowing a user to specify which component to operate on
ComponentFlagName = "component"
// OutputFlagName is the name of the flag allowing user to specify output format
OutputFlagName = "output"
OutputFlagName = "o"
// ContextFlagName is the name of the flag allowing a user to specify the location of the component settings
ContextFlagName = "context"
)

133
pkg/project/kubernetes.go Normal file
View File

@@ -0,0 +1,133 @@
package project
import (
"github.com/pkg/errors"
"github.com/redhat-developer/odo/pkg/kclient"
)
type kubernetesClient struct {
client kclient.ClientInterface
}
func NewClient(client kclient.ClientInterface) Client {
return kubernetesClient{
client: client,
}
}
// SetCurrent sets projectName as the current project
func (o kubernetesClient) SetCurrent(projectName string) error {
err := o.client.SetCurrentNamespace(projectName)
if err != nil {
return errors.Wrap(err, "unable to set current project to"+projectName)
}
return nil
}
// Create a new project, either by creating a `project.openshift.io` resource if supported by the cluster
// (which will trigger the creation of a namespace),
// or by creating directly a `namespace` resource.
// With the `wait` flag, the function will wait for the `default` service account
// to be created in the namespace before returning
func (o kubernetesClient) Create(projectName string, wait bool) error {
if projectName == "" {
return errors.Errorf("no project name given")
}
projectSupport, err := o.client.IsProjectSupported()
if err != nil {
return errors.Wrap(err, "unable to detect project support")
}
if projectSupport {
err = o.client.CreateNewProject(projectName, wait)
} else {
_, err = o.client.CreateNamespace(projectName)
}
if err != nil {
return errors.Wrap(err, "unable to create new project")
}
if wait {
err = o.client.WaitForServiceAccountInNamespace(projectName, "default")
if err != nil {
return errors.Wrap(err, "unable to wait for service account")
}
}
return nil
}
// Delete deletes the project (the `project` resource if supported, or directly the `namespace`)
// with the name projectName and returns an error if any
func (o kubernetesClient) Delete(projectName string, wait bool) error {
if projectName == "" {
return errors.Errorf("no project name given")
}
projectSupport, err := o.client.IsProjectSupported()
if err != nil {
return errors.Wrap(err, "unable to detect project support")
}
if projectSupport {
err = o.client.DeleteProject(projectName, wait)
} else {
err = o.client.DeleteNamespace(projectName, wait)
}
if err != nil {
return errors.Wrapf(err, "unable to delete project %q", projectName)
}
return nil
}
// List all the projects on the cluster and returns an error if any
func (o kubernetesClient) List() (ProjectList, error) {
currentProject := o.client.GetCurrentNamespace()
projectSupport, err := o.client.IsProjectSupported()
if err != nil {
return ProjectList{}, errors.Wrap(err, "unable to detect project support")
}
var allProjects []string
if projectSupport {
allProjects, err = o.client.ListProjectNames()
} else {
allProjects, err = o.client.GetNamespaces()
}
if err != nil {
return ProjectList{}, errors.Wrap(err, "cannot get all the projects")
}
projects := make([]Project, len(allProjects))
for i, project := range allProjects {
isActive := project == currentProject
projects[i] = NewProject(project, isActive)
}
return NewProjectList(projects), nil
}
// Exists checks whether a project with the name `projectName` exists and returns an error if any
func (o kubernetesClient) Exists(projectName string) (bool, error) {
projectSupport, err := o.client.IsProjectSupported()
if err != nil {
return false, errors.Wrap(err, "unable to detect project support")
}
if projectSupport {
project, err := o.client.GetProject(projectName)
if err != nil || project == nil {
return false, err
}
} else {
namespace, err := o.client.GetNamespace(projectName)
if err != nil || namespace == nil {
return false, err
}
}
return true, nil
}

View File

@@ -62,6 +62,7 @@ func TestCreate(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
kc := kclient.NewMockClientInterface(ctrl)
appClient := NewClient(kc)
if tt.expectedErr == false {
kc.EXPECT().IsProjectSupported().Return(tt.isProjectSupported, tt.isProjectSupportedErr)
@@ -75,7 +76,7 @@ func TestCreate(t *testing.T) {
}
}
err := Create(kc, tt.projectName, tt.wait)
err := appClient.Create(tt.projectName, tt.wait)
if err != nil != tt.expectedErr {
t.Errorf("expected error %v, got %v", tt.expectedErr, err)
@@ -136,6 +137,7 @@ func TestDelete(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
kc := kclient.NewMockClientInterface(ctrl)
appClient := NewClient(kc)
if tt.expectedErr == false {
kc.EXPECT().IsProjectSupported().Return(tt.isProjectSupported, tt.isProjectSupportedErr)
@@ -146,7 +148,7 @@ func TestDelete(t *testing.T) {
}
}
err := Delete(kc, tt.projectName, tt.wait)
err := appClient.Delete(tt.projectName, tt.wait)
if err != nil != tt.expectedErr {
t.Errorf("expected error %v, got %v", tt.expectedErr, err)
@@ -214,6 +216,7 @@ func TestList(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
kc := kclient.NewMockClientInterface(ctrl)
appClient := NewClient(kc)
kc.EXPECT().GetCurrentNamespace().Times(1)
@@ -226,7 +229,7 @@ func TestList(t *testing.T) {
}
}
list, err := List(kc)
list, err := appClient.List()
if err != nil != tt.expectedErr {
t.Errorf("expected error %v, got %v", tt.expectedErr, err)
@@ -269,6 +272,7 @@ func TestExists(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
kc := kclient.NewMockClientInterface(ctrl)
appClient := NewClient(kc)
if tt.expectedErr == false {
kc.EXPECT().IsProjectSupported().Return(tt.isProjectSupported, tt.isProjectSupportedErr)
@@ -279,7 +283,7 @@ func TestExists(t *testing.T) {
}
}
_, err := Exists(kc, tt.projectName)
_, err := appClient.Exists(tt.projectName)
if err != nil != tt.expectedErr {
t.Errorf("expected error %v, got %v", tt.expectedErr, err)

106
pkg/project/mock.go Normal file
View File

@@ -0,0 +1,106 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: pkg/project/project.go
// Package project is a generated GoMock package.
package project
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// 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
}
// Create mocks base method.
func (m *MockClient) Create(projectName string, wait bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", projectName, wait)
ret0, _ := ret[0].(error)
return ret0
}
// Create indicates an expected call of Create.
func (mr *MockClientMockRecorder) Create(projectName, wait interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockClient)(nil).Create), projectName, wait)
}
// Delete mocks base method.
func (m *MockClient) Delete(projectName string, wait bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", projectName, wait)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockClientMockRecorder) Delete(projectName, wait interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockClient)(nil).Delete), projectName, wait)
}
// Exists mocks base method.
func (m *MockClient) Exists(projectName string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Exists", projectName)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Exists indicates an expected call of Exists.
func (mr *MockClientMockRecorder) Exists(projectName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockClient)(nil).Exists), projectName)
}
// List mocks base method.
func (m *MockClient) List() (ProjectList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List")
ret0, _ := ret[0].(ProjectList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockClientMockRecorder) List() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockClient)(nil).List))
}
// SetCurrent mocks base method.
func (m *MockClient) SetCurrent(projectName string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetCurrent", projectName)
ret0, _ := ret[0].(error)
return ret0
}
// SetCurrent indicates an expected call of SetCurrent.
func (mr *MockClientMockRecorder) SetCurrent(projectName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCurrent", reflect.TypeOf((*MockClient)(nil).SetCurrent), projectName)
}

View File

@@ -1,131 +1,9 @@
package project
import (
"github.com/pkg/errors"
"github.com/redhat-developer/odo/pkg/kclient"
)
// SetCurrent sets projectName as the current project
func SetCurrent(client kclient.ClientInterface, projectName string) error {
err := client.SetCurrentNamespace(projectName)
if err != nil {
return errors.Wrap(err, "unable to set current project to"+projectName)
}
return nil
}
// Create a new project, either by creating a `project.openshift.io` resource if supported by the cluster
// (which will trigger the creation of a namespace),
// or by creating directly a `namespace` resource.
// With the `wait` flag, the function will wait for the `default` service account
// to be created in the namespace before to return.
func Create(client kclient.ClientInterface, projectName string, wait bool) error {
if projectName == "" {
return errors.Errorf("no project name given")
}
projectSupport, err := client.IsProjectSupported()
if err != nil {
return errors.Wrap(err, "unable to detect project support")
}
if projectSupport {
err = client.CreateNewProject(projectName, wait)
if err != nil {
return errors.Wrap(err, "unable to create new project")
}
} else {
_, err = client.CreateNamespace(projectName)
if err != nil {
return errors.Wrap(err, "unable to create new project")
}
}
if wait {
err = client.WaitForServiceAccountInNamespace(projectName, "default")
if err != nil {
return errors.Wrap(err, "unable to wait for service account")
}
}
return nil
}
// Delete deletes the project (the `project` resource if supported, or directly the `namespace`)
// with the name projectName and returns an error if any
func Delete(client kclient.ClientInterface, projectName string, wait bool) error {
if projectName == "" {
return errors.Errorf("no project name given")
}
projectSupport, err := client.IsProjectSupported()
if err != nil {
return errors.Wrap(err, "unable to detect project support")
}
if projectSupport {
// Delete the requested project
err := client.DeleteProject(projectName, wait)
if err != nil {
return errors.Wrapf(err, "unable to delete project %s", projectName)
}
} else {
err := client.DeleteNamespace(projectName, wait)
if err != nil {
return errors.Wrapf(err, "unable to delete namespace %s", projectName)
}
}
return nil
}
// List all the projects on the cluster and returns an error if any
func List(client kclient.ClientInterface) (ProjectList, error) {
currentProject := client.GetCurrentNamespace()
projectSupport, err := client.IsProjectSupported()
if err != nil {
return ProjectList{}, errors.Wrap(err, "unable to detect project support")
}
var allProjects []string
if projectSupport {
allProjects, err = client.ListProjectNames()
if err != nil {
return ProjectList{}, errors.Wrap(err, "cannot get all the projects")
}
} else {
allProjects, err = client.GetNamespaces()
if err != nil {
return ProjectList{}, errors.Wrap(err, "cannot get all the namespaces")
}
}
projects := make([]Project, len(allProjects))
for i, project := range allProjects {
isActive := project == currentProject
projects[i] = NewProject(project, isActive)
}
return NewProjectList(projects), nil
}
// Exists checks whether a project with the name `projectName` exists and returns an error if any
func Exists(client kclient.ClientInterface, projectName string) (bool, error) {
projectSupport, err := client.IsProjectSupported()
if err != nil {
return false, errors.Wrap(err, "unable to detect project support")
}
if projectSupport {
project, err := client.GetProject(projectName)
if err != nil || project == nil {
return false, err
}
} else {
namespace, err := client.GetNamespace(projectName)
if err != nil || namespace == nil {
return false, err
}
}
return true, nil
type Client interface {
SetCurrent(projectName string) error
Create(projectName string, wait bool) error
Delete(projectName string, wait bool) error
List() (ProjectList, error)
Exists(projectName string) (bool, error)
}

View File

@@ -91,7 +91,13 @@ type DownloadParams struct {
func ConvertLabelsToSelector(labels map[string]string) string {
var selector string
isFirst := true
for k, v := range labels {
keys := make([]string, 0, len(labels))
for k := range labels {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v := labels[k]
if isFirst {
isFirst = false
if v == "" {
@@ -694,6 +700,7 @@ func RemoveDuplicates(s []string) []string {
for item := range m {
result = append(result, item)
}
sort.Strings(result)
return result
}

View File

@@ -12,7 +12,6 @@ import (
"reflect"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
"testing"
@@ -1057,7 +1056,7 @@ func TestRemoveDuplicate(t *testing.T) {
name: "Case 2 - Remove duplicates, none in array",
args: args{
input: []string{"bar", "foo"},
output: []string{"foo", "bar"},
output: []string{"bar", "foo"},
},
},
}
@@ -1067,10 +1066,6 @@ func TestRemoveDuplicate(t *testing.T) {
// Run function RemoveDuplicate
output := RemoveDuplicates(tt.args.input)
// sort the strings
sort.Strings(output)
sort.Strings(tt.args.output)
if !(reflect.DeepEqual(output, tt.args.output)) {
t.Errorf("expected %v, got %v", tt.args.output, output)
}
@@ -2686,7 +2681,7 @@ func TestGetGitOriginPath(t *testing.T) {
func TestConvertLabelsToSelector(t *testing.T) {
cases := []struct {
labels map[string]string
want []string
want string
}{
{
labels: map[string]string{
@@ -2694,7 +2689,7 @@ func TestConvertLabelsToSelector(t *testing.T) {
"app.kubernetes.io/managed-by": "odo",
"app.kubernetes.io/managed-by-version": "v2.1",
},
want: []string{"app=app", "app.kubernetes.io/managed-by=odo", "app.kubernetes.io/managed-by-version=v2.1"},
want: "app=app,app.kubernetes.io/managed-by=odo,app.kubernetes.io/managed-by-version=v2.1",
},
{
labels: map[string]string{
@@ -2702,28 +2697,26 @@ func TestConvertLabelsToSelector(t *testing.T) {
"app.kubernetes.io/managed-by": "!odo",
"app.kubernetes.io/managed-by-version": "4.8",
},
want: []string{"app=app", "app.kubernetes.io/managed-by!=odo", "app.kubernetes.io/managed-by-version=4.8"},
want: "app=app,app.kubernetes.io/managed-by!=odo,app.kubernetes.io/managed-by-version=4.8",
},
{
labels: map[string]string{
"app.kubernetes.io/managed-by": "odo",
},
want: []string{"app.kubernetes.io/managed-by=odo"},
want: "app.kubernetes.io/managed-by=odo",
},
{
labels: map[string]string{
"app.kubernetes.io/managed-by": "!odo",
},
want: []string{"app.kubernetes.io/managed-by!=odo"},
want: "app.kubernetes.io/managed-by!=odo",
},
}
for _, tt := range cases {
got := ConvertLabelsToSelector(tt.labels)
for _, want := range tt.want {
if !strings.Contains(got, want) {
t.Errorf("got: %q\nwant:%q", got, tt.want)
}
if got != tt.want {
t.Errorf("got: %q\nwant:%q", got, tt.want)
}
}
}

View File

@@ -26,3 +26,11 @@ mockgen -source=pkg/devfile/image/image.go \
mockgen -source=pkg/odo/cmdline/cmdline.go \
-package cmdline \
-destination pkg/odo/cmdline/mock.go
mockgen -source=pkg/application/application.go \
-package application \
-destination pkg/application/mock.go
mockgen -source=pkg/project/project.go \
-package project \
-destination pkg/project/mock.go