add Kubernetes namespace support (#3647)

* add Kubernetes namespace support

* fix project unit tests

* add project unit tests for Kubernetes

* fix race in unit test

* use Context as it encasulates both client and kclient
This commit is contained in:
Tomas Kral
2020-08-05 06:38:48 +02:00
committed by GitHub
parent f59106082b
commit 3ca50987d0
19 changed files with 779 additions and 295 deletions

View File

@@ -2,40 +2,52 @@ package project
import (
"github.com/openshift/odo/pkg/machineoutput"
"github.com/openshift/odo/pkg/odo/genericclioptions"
"github.com/pkg/errors"
"github.com/openshift/odo/pkg/occlient"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const apiVersion = "odo.dev/v1alpha1"
// GetCurrent return current project
func GetCurrent(client *occlient.Client) string {
project := client.GetCurrentProjectName()
return project
func GetCurrent(context *genericclioptions.Context) string {
return context.KClient.GetCurrentNamespace()
}
// SetCurrent sets the projectName as current project
func SetCurrent(client *occlient.Client, projectName string) error {
err := client.SetCurrentProject(projectName)
func SetCurrent(context *genericclioptions.Context, projectName string) error {
err := context.KClient.SetCurrentNamespace(projectName)
if err != nil {
return errors.Wrap(err, "unable to set current project to"+projectName)
}
return nil
}
func Create(client *occlient.Client, projectName string, wait bool) error {
func Create(context *genericclioptions.Context, projectName string, wait bool) error {
if projectName == "" {
return errors.Errorf("no project name given")
}
err := client.CreateNewProject(projectName, wait)
projectSupport, err := context.Client.IsProjectSupported()
if err != nil {
return errors.Wrap(err, "unable to create new project")
return errors.Wrap(err, "unable to detect project support")
}
if projectSupport {
err := context.Client.CreateNewProject(projectName, wait)
if err != nil {
return errors.Wrap(err, "unable to create new project")
}
} else {
_, err := context.KClient.CreateNamespace(projectName)
if err != nil {
return errors.Wrap(err, "unable to create new project")
}
}
if wait {
err = client.WaitForServiceAccountInNamespace(projectName, "default")
err = context.KClient.WaitForServiceAccountInNamespace(projectName, "default")
if err != nil {
return errors.Wrap(err, "unable to wait for service account")
}
@@ -44,26 +56,52 @@ func Create(client *occlient.Client, projectName string, wait bool) error {
}
// Delete deletes the project with name projectName and returns errors if any
func Delete(client *occlient.Client, projectName string, wait bool) error {
func Delete(context *genericclioptions.Context, projectName string, wait bool) error {
if projectName == "" {
return errors.Errorf("no project name given")
}
// Delete the requested project
err := client.DeleteProject(projectName, wait)
projectSupport, err := context.Client.IsProjectSupported()
if err != nil {
return errors.Wrap(err, "unable to delete project")
return errors.Wrap(err, "unable to detect project support")
}
if projectSupport {
// Delete the requested project
err := context.Client.DeleteProject(projectName, wait)
if err != nil {
return errors.Wrapf(err, "unable to delete project %s", projectName)
}
} else {
err := context.KClient.DeleteNamespace(projectName, wait)
if err != nil {
return errors.Wrapf(err, "unable to delete namespace %s", projectName)
}
}
return nil
}
// List lists all the projects on the cluster
// returns a list of the projects or the error if any
func List(client *occlient.Client) (ProjectList, error) {
currentProject := client.GetCurrentProjectName()
allProjects, err := client.GetProjectNames()
func List(context *genericclioptions.Context) (ProjectList, error) {
currentProject := context.KClient.GetCurrentNamespace()
projectSupport, err := context.Client.IsProjectSupported()
if err != nil {
return ProjectList{}, errors.Wrap(err, "cannot get all the projects")
return ProjectList{}, errors.Wrap(err, "unable to detect project support")
}
var allProjects []string
if projectSupport {
allProjects, err = context.Client.GetProjectNames()
if err != nil {
return ProjectList{}, errors.Wrap(err, "cannot get all the projects")
}
} else {
allProjects, err = context.KClient.GetNamespaces()
if err != nil {
return ProjectList{}, errors.Wrap(err, "cannot get all the namespaces")
}
}
// Get apps from project
var projects []Project
@@ -82,10 +120,22 @@ func List(client *occlient.Client) (ProjectList, error) {
// projectName is the project name to perform check for
// The first returned parameter is a bool indicating if a project with the given name already exists or not
// The second returned parameter is the error that might occurs while execution
func Exists(client *occlient.Client, projectName string) (bool, error) {
project, err := client.GetProject(projectName)
if err != nil || project == nil {
return false, err
func Exists(context *genericclioptions.Context, projectName string) (bool, error) {
projectSupport, err := context.Client.IsProjectSupported()
if err != nil {
return false, errors.Wrap(err, "unable to detect project support")
}
if projectSupport {
project, err := context.Client.GetProject(projectName)
if err != nil || project == nil {
return false, err
}
} else {
namespace, err := context.KClient.GetNamespace(projectName)
if err != nil || namespace == nil {
return false, err
}
}
return true, nil

View File

@@ -1,23 +1,232 @@
package project
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"fmt"
"os"
"reflect"
"sync"
"testing"
projectv1 "github.com/openshift/api/project/v1"
v1 "github.com/openshift/api/project/v1"
"github.com/openshift/odo/pkg/kclient"
"github.com/openshift/odo/pkg/occlient"
"github.com/openshift/odo/pkg/odo/genericclioptions"
"github.com/openshift/odo/pkg/testingutil"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/discovery/fake"
ktesting "k8s.io/client-go/testing"
"k8s.io/client-go/tools/clientcmd"
)
type resourceMapEntry struct {
list *metav1.APIResourceList
err error
}
type fakeDiscovery struct {
*fake.FakeDiscovery
lock sync.Mutex
resourceMap map[string]*resourceMapEntry
}
var fakeDiscoveryWithProject = &fakeDiscovery{
resourceMap: map[string]*resourceMapEntry{
"project.openshift.io/v1": {
list: &metav1.APIResourceList{
GroupVersion: "project.openshift.io/v1",
APIResources: []metav1.APIResource{{
Name: "projects",
SingularName: "project",
Namespaced: false,
Kind: "Project",
ShortNames: []string{"proj"},
}},
},
},
},
}
var fakeDiscoveryWithNamespace = &fakeDiscovery{
resourceMap: map[string]*resourceMapEntry{
"v1": {
list: &metav1.APIResourceList{
GroupVersion: "v1",
APIResources: []metav1.APIResource{{
Name: "namespaces",
SingularName: "namespace",
Namespaced: false,
Kind: "Namespace",
ShortNames: []string{"ns"},
}},
},
},
},
}
func (c *fakeDiscovery) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
c.lock.Lock()
defer c.lock.Unlock()
if rl, ok := c.resourceMap[groupVersion]; ok {
return rl.list, rl.err
}
return nil, kerrors.NewNotFound(schema.GroupResource{}, "")
}
func fakeDeleteKClient(namespace string, deleteLast bool) (*kclient.Client, *kclient.FakeClientset) {
// Fake the client with the appropriate arguments
client, fakeClientSet := kclient.FakeNew()
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
configOverrides := &clientcmd.ConfigOverrides{}
client.KubeConfig = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
client.Namespace = "testing"
fkWatch := watch.NewFake()
fakeClientSet.Kubernetes.PrependReactor("list", "namespaces", func(action ktesting.Action) (bool, runtime.Object, error) {
if deleteLast {
return true, testingutil.FakeOnlyOneExistingNamespace(), nil
}
return true, testingutil.FakeProjects(), nil
})
fakeClientSet.Kubernetes.PrependReactor("delete", "namespaces", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, nil, nil
})
// We pass in the fakeNamespace in order to avoid race conditions with multiple go routines
fakeNamespace := testingutil.FakeNamespaceStatus(corev1.NamespacePhase(""), namespace)
go func(namespace *corev1.Namespace) {
fkWatch.Delete(namespace)
}(fakeNamespace)
fakeClientSet.Kubernetes.PrependWatchReactor("namespaces", func(action ktesting.Action) (handled bool, ret watch.Interface, err error) {
return true, fkWatch, nil
})
return client, fakeClientSet
}
func fakeDeleteClient(namespace string, deleteLast bool) (*occlient.Client, *occlient.FakeClientset) {
// Fake the client with the appropriate arguments
client, fakeClientSet := occlient.FakeNew()
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
configOverrides := &clientcmd.ConfigOverrides{}
client.KubeConfig = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
client.Namespace = "testing"
fkWatch := watch.NewFake()
fakeClientSet.ProjClientset.PrependReactor("list", "projects", func(action ktesting.Action) (bool, runtime.Object, error) {
if deleteLast {
return true, testingutil.FakeOnlyOneExistingProjects(), nil
}
return true, testingutil.FakeProjects(), nil
})
fakeClientSet.ProjClientset.PrependReactor("delete", "projects", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, nil, nil
})
// We pass in the fakeProject in order to avoid race conditions with multiple go routines
fakeProject := testingutil.FakeProjectStatus(corev1.NamespacePhase(""), namespace)
go func(project *projectv1.Project) {
fkWatch.Delete(project)
}(fakeProject)
fakeClientSet.ProjClientset.PrependWatchReactor("projects", func(action ktesting.Action) (handled bool, ret watch.Interface, err error) {
return true, fkWatch, nil
})
return client, fakeClientSet
}
func fakeCreateKClient(namespace string) (*kclient.Client, *kclient.FakeClientset) {
// Fake the client with the appropriate arguments
client, fakeClientSet := kclient.FakeNew()
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
configOverrides := &clientcmd.ConfigOverrides{}
client.KubeConfig = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
fkWatch := watch.NewFake()
fakeClientSet.Kubernetes.PrependReactor("create", "namespace", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, nil, nil
})
fakeNamespace := testingutil.FakeNamespaceStatus(corev1.NamespacePhase("Active"), namespace)
go func(project *corev1.Namespace) {
fkWatch.Add(project)
}(fakeNamespace)
fakeClientSet.Kubernetes.PrependWatchReactor("namespaces", func(action ktesting.Action) (handled bool, ret watch.Interface, err error) {
return true, fkWatch, nil
})
kFkWatch2 := watch.NewFake()
go func() {
kFkWatch2.Add(&corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
},
})
}()
fakeClientSet.Kubernetes.PrependWatchReactor("serviceaccounts", func(action ktesting.Action) (handled bool, ret watch.Interface, err error) {
return true, kFkWatch2, nil
})
return client, fakeClientSet
}
func fakeCreateClient(namespace string) (*occlient.Client, *occlient.FakeClientset) {
// Fake the client with the appropriate arguments
client, fakeClientSet := occlient.FakeNew()
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
configOverrides := &clientcmd.ConfigOverrides{}
client.KubeConfig = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
fkWatch := watch.NewFake()
fakeClientSet.ProjClientset.PrependReactor("create", "projectrequests", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, nil, nil
})
fakeProject := testingutil.FakeProjectStatus(corev1.NamespacePhase("Active"), namespace)
go func(project *projectv1.Project) {
fkWatch.Add(project)
}(fakeProject)
fakeClientSet.ProjClientset.PrependWatchReactor("projects", func(action ktesting.Action) (handled bool, ret watch.Interface, err error) {
return true, fkWatch, nil
})
fkWatch2 := watch.NewFake()
go func() {
fkWatch2.Add(&corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
},
})
}()
fakeClientSet.Kubernetes.PrependWatchReactor("serviceaccounts", func(action ktesting.Action) (handled bool, ret watch.Interface, err error) {
return true, fkWatch2, nil
})
return client, fakeClientSet
}
func TestCreate(t *testing.T) {
tests := []struct {
name string
@@ -52,43 +261,18 @@ func TestCreate(t *testing.T) {
t.Errorf("failed to create mock odo and kube config files. Error %v", err)
}
// run tests for OpenShift (Project)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Run(fmt.Sprintf(" %s with Project", tt.name), func(t *testing.T) {
// Fake the client with the appropriate arguments
client, fakeClientSet := occlient.FakeNew()
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
configOverrides := &clientcmd.ConfigOverrides{}
client.KubeConfig = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
fkWatch := watch.NewFake()
fakeClientSet.ProjClientset.PrependReactor("create", "projectrequests", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, nil, nil
})
go func() {
fkWatch.Add(testingutil.FakeProjectStatus(corev1.NamespacePhase("Active"), tt.projectName))
}()
fakeClientSet.ProjClientset.PrependWatchReactor("projects", func(action ktesting.Action) (handled bool, ret watch.Interface, err error) {
return true, fkWatch, nil
})
fkWatch2 := watch.NewFake()
go func() {
fkWatch2.Add(&corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
},
})
}()
fakeClientSet.Kubernetes.PrependWatchReactor("serviceaccounts", func(action ktesting.Action) (handled bool, ret watch.Interface, err error) {
return true, fkWatch2, nil
})
client, fakeClientSet := fakeCreateClient(tt.projectName)
kubeClient, _ := fakeCreateKClient(tt.projectName)
// The function we are testing
err := Create(client, tt.projectName, true)
context := genericclioptions.NewFakeContext(tt.projectName, "app", "cmp", client, kubeClient)
context.Client.SetDiscoveryInterface(fakeDiscoveryWithProject)
err := Create(context, tt.projectName, true)
if err == nil && !tt.wantErr {
if len(fakeClientSet.ProjClientset.Actions()) != 2 {
@@ -103,6 +287,32 @@ func TestCreate(t *testing.T) {
})
}
// run tests for Kubernetes (Namespace)
for _, tt := range tests {
t.Run(fmt.Sprintf(" %s with Namespace", tt.name), func(t *testing.T) {
client, _ := fakeCreateClient(tt.projectName)
kclient, fakeKubeClientSet := fakeCreateKClient(tt.projectName)
// The function we are testing
context := genericclioptions.NewFakeContext(tt.projectName, "app", "cmp", client, kclient)
context.Client.SetDiscoveryInterface(fakeDiscoveryWithNamespace)
err := Create(context, tt.projectName, true)
// Checks for error in positive cases
if !tt.wantErr == (err != nil) {
t.Errorf("project Create() unexpected error %v, wantErr %v", err, tt.wantErr)
}
if err == nil && !tt.wantErr {
if len(fakeKubeClientSet.Kubernetes.Actions()) != 2 {
t.Errorf("expected 2 ProjClientSet.Actions() in Project Create, got: %v", len(fakeKubeClientSet.Kubernetes.Actions()))
}
}
})
}
}
func TestDelete(t *testing.T) {
@@ -111,18 +321,21 @@ func TestDelete(t *testing.T) {
wantErr bool
wait bool
projectName string
deleteLast bool
}{
{
name: "Case 1: Test project delete for multiple projects",
wantErr: false,
wait: false,
projectName: "prj2",
deleteLast: false,
},
{
name: "Case 2: Test delete the only remaining project",
wantErr: false,
wait: false,
projectName: "testing",
deleteLast: true,
},
}
@@ -142,42 +355,18 @@ func TestDelete(t *testing.T) {
t.Errorf("failed to create mock odo and kube config files. Error %v", err)
}
// run as on OpenShift (with Project)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Run(fmt.Sprintf(" %s with Project", tt.name), func(t *testing.T) {
// Fake the client with the appropriate arguments
client, fakeClientSet := occlient.FakeNew()
client, fakeClientSet := fakeDeleteClient(tt.projectName, tt.deleteLast)
kclient, _ := fakeDeleteKClient(tt.projectName, tt.deleteLast)
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
configOverrides := &clientcmd.ConfigOverrides{}
client.KubeConfig = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
client.Namespace = "testing"
fkWatch := watch.NewFake()
fakeClientSet.ProjClientset.PrependReactor("list", "projects", func(action ktesting.Action) (bool, runtime.Object, error) {
if tt.name == "Test delete the only remaining project" {
return true, testingutil.FakeOnlyOneExistingProjects(), nil
}
return true, testingutil.FakeProjects(), nil
})
fakeClientSet.ProjClientset.PrependReactor("delete", "projects", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, nil, nil
})
// We pass in the fakeProject in order to avoid race conditions with multiple go routines
fakeProject := testingutil.FakeProjectStatus(corev1.NamespacePhase(""), tt.projectName)
go func(project *projectv1.Project) {
fkWatch.Delete(project)
}(fakeProject)
fakeClientSet.ProjClientset.PrependWatchReactor("projects", func(action ktesting.Action) (handled bool, ret watch.Interface, err error) {
return true, fkWatch, nil
})
context := genericclioptions.NewFakeContext(tt.projectName, "app", "cmp", client, kclient)
context.Client.SetDiscoveryInterface(fakeDiscoveryWithProject)
// The function we are testing
err := Delete(client, tt.projectName, tt.wait)
err := Delete(context, tt.projectName, tt.wait)
if err == nil && !tt.wantErr {
if len(fakeClientSet.ProjClientset.Actions()) != 1 {
@@ -191,6 +380,32 @@ func TestDelete(t *testing.T) {
}
})
}
// run as on Kubernetes (with Namespace)
for _, tt := range tests {
t.Run(fmt.Sprintf(" %s with Namespace", tt.name), func(t *testing.T) {
client, _ := fakeDeleteClient(tt.projectName, tt.deleteLast)
kubeClient, fakeKClientSet := fakeDeleteKClient(tt.projectName, tt.deleteLast)
context := genericclioptions.NewFakeContext(tt.projectName, "app", "cmp", client, kubeClient)
context.Client.SetDiscoveryInterface(fakeDiscoveryWithNamespace)
// The function we are testing
err := Delete(context, tt.projectName, tt.wait)
if err == nil && !tt.wantErr {
if len(fakeKClientSet.Kubernetes.Actions()) != 1 {
t.Errorf("expected 1 ProjClientSet.Actions() in Project Delete, got: %v", len(fakeKClientSet.Kubernetes.Actions()))
}
}
// Checks for error in positive cases
if !tt.wantErr == (err != nil) {
t.Errorf("project Delete() unexpected error %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestList(t *testing.T) {
@@ -262,8 +477,14 @@ func TestList(t *testing.T) {
return true, tt.returnedProjects, nil
})
kubeClient, _ := kclient.FakeNew()
client.SetDiscoveryInterface(fakeDiscoveryWithProject)
context := genericclioptions.NewFakeContext("test", "app", "cmp", client, kubeClient)
// The function we are testing
projects, err := List(client)
projects, err := List(context)
if !reflect.DeepEqual(projects, tt.expectedProjects) {
t.Errorf("Expected project output is not equal, expected: %v, actual: %v", tt.expectedProjects, projects)
@@ -281,4 +502,5 @@ func TestList(t *testing.T) {
}
})
}
}