diff --git a/internal/modules/clusteroverview/clusteroverview.go b/internal/modules/clusteroverview/clusteroverview.go index 8ffe77b2b0..496ccbfd96 100644 --- a/internal/modules/clusteroverview/clusteroverview.go +++ b/internal/modules/clusteroverview/clusteroverview.go @@ -201,7 +201,7 @@ func (co *ClusterOverview) Navigation(ctx context.Context, namespace string, roo nf := octant.NewNavigationFactory("", root, objectStore, navigationEntries) - entries, err := nf.Generate(ctx, "Cluster Overview", icon.ClusterOverview, "") + entries, err := nf.Generate(ctx, "Cluster Overview", icon.ClusterOverview, "", true) if err != nil { return nil, err } @@ -227,8 +227,8 @@ func (co *ClusterOverview) Generators() []octant.Generator { return []octant.Generator{} } -func rbacEntries(_ context.Context, prefix, _ string, _ store.Store) ([]navigation.Navigation, error) { - neh := navigation.NavigationEntriesHelper{} +func rbacEntries(_ context.Context, prefix, _ string, _ store.Store, _ bool) ([]navigation.Navigation, error) { + neh := navigation.EntriesHelper{} neh.Add("Cluster Roles", "cluster-roles", icon.ClusterOverviewClusterRole) neh.Add("Cluster Role Bindings", "cluster-role-bindings", icon.ClusterOverviewClusterRoleBinding) diff --git a/internal/modules/overview/navigation.go b/internal/modules/overview/navigation.go index f16fb8faa6..394376673c 100644 --- a/internal/modules/overview/navigation.go +++ b/internal/modules/overview/navigation.go @@ -24,8 +24,8 @@ var ( } ) -func workloadEntries(_ context.Context, prefix, _ string, _ store.Store) ([]navigation.Navigation, error) { - neh := navigation.NavigationEntriesHelper{} +func workloadEntries(_ context.Context, prefix, _ string, _ store.Store, _ bool) ([]navigation.Navigation, error) { + neh := navigation.EntriesHelper{} neh.Add("Cron Jobs", "cron-jobs", icon.OverviewCronJob) neh.Add("Daemon Sets", "daemon-sets", icon.OverviewDaemonSet) neh.Add("Deployments", "deployments", icon.OverviewDeployment) @@ -38,16 +38,16 @@ func workloadEntries(_ context.Context, prefix, _ string, _ store.Store) ([]navi return neh.Generate(prefix) } -func discoAndLBEntries(_ context.Context, prefix, _ string, _ store.Store) ([]navigation.Navigation, error) { - neh := navigation.NavigationEntriesHelper{} +func discoAndLBEntries(_ context.Context, prefix, _ string, _ store.Store, _ bool) ([]navigation.Navigation, error) { + neh := navigation.EntriesHelper{} neh.Add("Ingresses", "ingresses", icon.OverviewIngress) neh.Add("Services", "services", icon.OverviewService) return neh.Generate(prefix) } -func configAndStorageEntries(_ context.Context, prefix, _ string, _ store.Store) ([]navigation.Navigation, error) { - neh := navigation.NavigationEntriesHelper{} +func configAndStorageEntries(_ context.Context, prefix, _ string, _ store.Store, _ bool) ([]navigation.Navigation, error) { + neh := navigation.EntriesHelper{} neh.Add("Config Maps", "config-maps", icon.OverviewConfigMap) neh.Add("Persistent Volume Claims", "persistent-volume-claims", icon.OverviewPersistentVolumeClaim) neh.Add("Secrets", "secrets", icon.OverviewSecret) @@ -56,8 +56,8 @@ func configAndStorageEntries(_ context.Context, prefix, _ string, _ store.Store) return neh.Generate(prefix) } -func rbacEntries(_ context.Context, prefix, _ string, _ store.Store) ([]navigation.Navigation, error) { - neh := navigation.NavigationEntriesHelper{} +func rbacEntries(_ context.Context, prefix, _ string, _ store.Store, _ bool) ([]navigation.Navigation, error) { + neh := navigation.EntriesHelper{} neh.Add("Roles", "roles", icon.OverviewRole) neh.Add("Role Bindings", "role-bindings", icon.OverviewRoleBinding) diff --git a/internal/modules/overview/overview.go b/internal/modules/overview/overview.go index 2aa02c20dd..8c83349a4a 100644 --- a/internal/modules/overview/overview.go +++ b/internal/modules/overview/overview.go @@ -185,7 +185,7 @@ func (co *Overview) Navigation(ctx context.Context, namespace, root string) ([]n nf := octant.NewNavigationFactory(namespace, root, objectStore, navigationEntries) - entries, err := nf.Generate(ctx, "Overview", icon.Overview, "") + entries, err := nf.Generate(ctx, "Overview", icon.Overview, "", false) if err != nil { return nil, err } diff --git a/internal/octant/factory.go b/internal/octant/factory.go index ee6f3a1d79..1e502468cb 100644 --- a/internal/octant/factory.go +++ b/internal/octant/factory.go @@ -19,7 +19,7 @@ import ( ) // EntriesFunc is a function that can create navigation entries. -type EntriesFunc func(ctx context.Context, prefix, namespace string, objectStore store.Store) ([]navigation.Navigation, error) +type EntriesFunc func(ctx context.Context, prefix, namespace string, objectStore store.Store, wantsClusterScoped bool) ([]navigation.Navigation, error) // NavigationEntries help construct navigation entries. type NavigationEntries struct { @@ -60,7 +60,7 @@ func (nf *NavigationFactory) Root() string { } // Generate returns navigation entries. -func (nf *NavigationFactory) Generate(ctx context.Context, title string, iconName, iconSource string) (*navigation.Navigation, error) { +func (nf *NavigationFactory) Generate(ctx context.Context, title string, iconName, iconSource string, wantsClusterScoped bool) (*navigation.Navigation, error) { n := &navigation.Navigation{ Title: title, Path: nf.rootPath, @@ -80,7 +80,7 @@ func (nf *NavigationFactory) Generate(ctx context.Context, title string, iconNam for _, name := range nf.entries.Order { g.Go(func() error { - children, err := nf.genNode(ctx, name, nf.entries.EntriesFuncs[name]) + children, err := nf.genNode(ctx, name, nf.entries.EntriesFuncs[name], wantsClusterScoped) if err != nil { return errors.Wrapf(err, "generate entries for %s", name) } @@ -105,14 +105,14 @@ func (nf *NavigationFactory) pathFor(elements ...string) string { return path.Join(append([]string{nf.rootPath}, elements...)...) } -func (nf *NavigationFactory) genNode(ctx context.Context, name string, childFn EntriesFunc) (*navigation.Navigation, error) { +func (nf *NavigationFactory) genNode(ctx context.Context, name string, childFn EntriesFunc, wantsClusterScoped bool) (*navigation.Navigation, error) { node, err := navigation.New(name, nf.pathFor(nf.entries.Lookup[name])) if err != nil { return nil, err } if childFn != nil { - children, err := childFn(ctx, node.Path, nf.namespace, nf.objectStore) + children, err := childFn(ctx, node.Path, nf.namespace, nf.objectStore, wantsClusterScoped) if err != nil { return nil, err } diff --git a/pkg/navigation/navigation.go b/pkg/navigation/navigation.go index 6e8cd57891..952ca3c9c9 100644 --- a/pkg/navigation/navigation.go +++ b/pkg/navigation/navigation.go @@ -55,8 +55,8 @@ type Navigation struct { } // New creates a Navigation. -func New(title, path string, options ...Option) (*Navigation, error) { - navigation := &Navigation{Title: title, Path: path} +func New(title, navigationPath string, options ...Option) (*Navigation, error) { + navigation := &Navigation{Title: title, Path: navigationPath} for _, option := range options { if err := option(navigation); err != nil { @@ -68,7 +68,7 @@ func New(title, path string, options ...Option) (*Navigation, error) { } // CRDEntries generates navigation entries for CRDs. -func CRDEntries(ctx context.Context, prefix, namespace string, objectStore store.Store) ([]Navigation, error) { +func CRDEntries(ctx context.Context, prefix, namespace string, objectStore store.Store, wantsClusterScoped bool) ([]Navigation, error) { var list []Navigation crds, err := CustomResourceDefinitions(ctx, objectStore) @@ -81,6 +81,11 @@ func CRDEntries(ctx context.Context, prefix, namespace string, objectStore store }) for i := range crds { + if wantsClusterScoped && crds[i].Spec.Scope != apiextv1beta1.ClusterScoped { + continue + } else if !wantsClusterScoped && crds[i].Spec.Scope != apiextv1beta1.NamespaceScoped { + continue + } objects, err := ListCustomResources(ctx, crds[i], namespace, objectStore, nil) if err != nil { @@ -106,10 +111,6 @@ func CustomResourceDefinitions(ctx context.Context, o store.Store) ([]*apiextv1b Kind: "CustomResourceDefinition", } - if err := o.HasAccess(ctx, key, "list"); err != nil { - return nil, nil - } - rawList, err := o.List(ctx, key) if err != nil { return nil, errors.Wrap(err, "listing CRDs") @@ -169,14 +170,13 @@ func ListCustomResources( apiVersion, kind := gvk.ToAPIVersionAndKind() key := store.Key{ - Namespace: namespace, APIVersion: apiVersion, Kind: kind, Selector: selector, } - if err := o.HasAccess(ctx, key, "list"); err != nil { - return nil, nil + if crd.Spec.Scope == apiextv1beta1.NamespaceScoped { + key.Namespace = namespace } objects, err := o.List(ctx, key) @@ -193,20 +193,20 @@ type navConfig struct { iconName string } -// NavigationEntriesHelper generates navigation entries. -type NavigationEntriesHelper struct { +// EntriesHelper generates navigation entries. +type EntriesHelper struct { navConfigs []navConfig } // Add adds an entry. -func (neh *NavigationEntriesHelper) Add(title, suffix, iconName string) { +func (neh *EntriesHelper) Add(title, suffix, iconName string) { neh.navConfigs = append(neh.navConfigs, navConfig{ title: title, suffix: suffix, iconName: iconName, }) } // Generate generates navigation entries. -func (neh *NavigationEntriesHelper) Generate(prefix string) ([]Navigation, error) { +func (neh *EntriesHelper) Generate(prefix string) ([]Navigation, error) { var navigations []Navigation for _, nc := range neh.navConfigs { diff --git a/pkg/navigation/navigation_test.go b/pkg/navigation/navigation_test.go index 539c5dffaf..764cf77577 100644 --- a/pkg/navigation/navigation_test.go +++ b/pkg/navigation/navigation_test.go @@ -6,14 +6,22 @@ SPDX-License-Identifier: Apache-2.0 package navigation import ( + "context" "fmt" "path" "testing" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "github.com/vmware/octant/internal/testutil" "github.com/vmware/octant/pkg/icon" + "github.com/vmware/octant/pkg/store" + "github.com/vmware/octant/pkg/store/fake" ) func Test_NewNavigation(t *testing.T) { @@ -27,8 +35,8 @@ func Test_NewNavigation(t *testing.T) { assert.Equal(t, title, nav.Title) } -func TestNavigationEntriesHelper(t *testing.T) { - neh := NavigationEntriesHelper{} +func TestEntriesHelper(t *testing.T) { + neh := EntriesHelper{} neh.Add("title", "suffix", icon.OverviewService) @@ -47,3 +55,111 @@ func TestNavigationEntriesHelper(t *testing.T) { assert.Equal(t, expected.IconName, list[0].IconName) assert.NotEmpty(t, list[0].IconSource) } + +func TestCRDEntries_namespace_scoped(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + + objectStore := fake.NewMockStore(controller) + clusterScopedCRD := createCRD("cluster-scoped", "ClusterScoped", true) + namespaceScopedCRD := createCRD("namespace-scoped", "NamespaceScoped", false) + + crds := testutil.ToUnstructuredList(t, clusterScopedCRD, namespaceScopedCRD) + crdKey := store.Key{ + APIVersion: "apiextensions.k8s.io/v1beta1", + Kind: "CustomResourceDefinition", + } + objectStore.EXPECT(). + List(gomock.Any(), crdKey). + Return(crds, nil). + AnyTimes() + + clusterCR := createCR("testing", "v1", "ClusterScoped", "cluster-scoped") + clusterCRs := testutil.ToUnstructuredList(t, clusterCR) + namespaceCR := createCR("testing", "v1", "NamespaceScoped", "namespace-scoped") + namespaceCRs := testutil.ToUnstructuredList(t, namespaceCR) + + crNamespaceKey := store.Key{ + Namespace: "default", + APIVersion: "testing/v1", + Kind: "NamespaceScoped", + } + objectStore.EXPECT(). + List(gomock.Any(), crNamespaceKey). + Return(namespaceCRs, nil). + AnyTimes() + crClusterKey := store.Key{ + APIVersion: "testing/v1", + Kind: "ClusterScoped", + } + objectStore.EXPECT(). + List(gomock.Any(), crClusterKey). + Return(clusterCRs, nil). + AnyTimes() + + ctx := context.Background() + + namespaceGot, err := CRDEntries(ctx, "/prefix", "default", objectStore, false) + require.NoError(t, err) + + namespaceExpected := []Navigation{ + createNavForCR(t, namespaceCR.GetName()), + } + + assert.Equal(t, namespaceExpected, namespaceGot) + + clusterGot, err := CRDEntries(ctx, "/prefix", "default", objectStore, true) + require.NoError(t, err) + + clusterExpected := []Navigation{ + createNavForCR(t, clusterCR.GetName()), + } + + assert.Equal(t, clusterExpected, clusterGot) +} + +func createNavForCR(t *testing.T, name string) Navigation { + nav, err := New(name, path.Join("/prefix", name), SetNavigationIcon(icon.CustomResourceDefinition)) + require.NoError(t, err) + + return *nav +} + +func createCRD(name, kind string, isClusterScoped bool) *apiextv1beta1.CustomResourceDefinition { + scope := apiextv1beta1.ClusterScoped + if !isClusterScoped { + scope = apiextv1beta1.NamespaceScoped + } + + crd := testutil.CreateCRD(name) + crd.Spec.Scope = scope + crd.Spec.Group = "testing" + crd.Spec.Names = apiextv1beta1.CustomResourceDefinitionNames{ + Kind: kind, + } + // TODO fix this because Version is deprecated + crd.Spec.Version = "v1" + crd.Spec.Versions = []apiextv1beta1.CustomResourceDefinitionVersion{ + { + Name: "v1", + Served: true, + Storage: true, + }, + } + + return crd +} + +func createCR(group, version, kind, name string) *unstructured.Unstructured { + m := make(map[string]interface{}) + u := &unstructured.Unstructured{Object: m} + + u.SetName(name) + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: group, + Version: version, + Kind: kind, + }) + + return u +} diff --git a/web/src/app/services/namespace/namespace.service.spec.ts b/web/src/app/services/namespace/namespace.service.spec.ts index 054a04ceff..95a33137ea 100644 --- a/web/src/app/services/namespace/namespace.service.spec.ts +++ b/web/src/app/services/namespace/namespace.service.spec.ts @@ -33,7 +33,7 @@ describe('NamespaceService', () => { namespaces: new BehaviorSubject([]), registerStreamer: (name: string, handler: Streamer) => {}, streamer: () => new BehaviorSubject(emptyNavigation), - navigation: new BehaviorSubject(emptyNavigation) + navigation: new BehaviorSubject(emptyNavigation), }; const notifierServiceStub = {