diff --git a/CHANGELOG.md b/CHANGELOG.md index d4621af8e..95beb6963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan - [#160](https://github.com/kobsio/kobs/pull/160): Allow users to sort the returned logs within the documents table in the ClickHouse plugin. - [#161](https://github.com/kobsio/kobs/pull/161): Add support for materialized columns, to improve query performance for most frequently queried field. - [#162](https://github.com/kobsio/kobs/pull/162): Add support to visualize logs in the ClickHouse plugin. +- [#170](https://github.com/kobsio/kobs/pull/170): Add Custom Resource Definition for Users. ### Fixed diff --git a/Makefile b/Makefile index 46cea4f30..c379ff038 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ REPO ?= github.com/kobsio/kobs REVISION ?= $(shell git rev-parse HEAD) VERSION ?= $(shell git describe --tags) -CRDS ?= team application dashboard +CRDS ?= team application dashboard user .PHONY: build build: @@ -27,6 +27,7 @@ generate-crds: rm -rf ./pkg/api/clients/$$crd/clientset; \ rm -rf ./pkg/api/clients/$$crd/informers; \ rm -rf ./pkg/api/clients/$$crd/listers; \ + mkdir -p ./pkg/api/clients/$$crd; \ mv ./tmp/github.com/kobsio/kobs/pkg/api/apis/$$crd/v1beta1/zz_generated.deepcopy.go ./pkg/api/apis/$$crd/v1beta1; \ mv ./tmp/github.com/kobsio/kobs/pkg/api/clients/$$crd/clientset ./pkg/api/clients/$$crd/clientset; \ mv ./tmp/github.com/kobsio/kobs/pkg/api/clients/$$crd/informers ./pkg/api/clients/$$crd/informers; \ @@ -34,7 +35,7 @@ generate-crds: rm -rf ./tmp; \ done - controller-gen "crd:crdVersions={v1},trivialVersions=true" paths="./..." output:crd:artifacts:config=deploy/kustomize/crds + controller-gen "crd:crdVersions={v1},trivialVersions=true" paths="./pkg/..." output:crd:artifacts:config=deploy/kustomize/crds for crd in $(CRDS); do \ cp ./deploy/kustomize/crds/kobs.io_$$crd\s.yaml ./deploy/helm/kobs/crds/kobs.io_$$crd\s.yaml; \ diff --git a/app/package.json b/app/package.json index 62bc2561d..c89749052 100644 --- a/app/package.json +++ b/app/package.json @@ -18,6 +18,7 @@ "@kobsio/plugin-resources": "*", "@kobsio/plugin-rss": "*", "@kobsio/plugin-teams": "*", + "@kobsio/plugin-users": "*", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "react": "^17.0.2", diff --git a/app/src/index.tsx b/app/src/index.tsx index 7e3b82c06..8476e7110 100644 --- a/app/src/index.tsx +++ b/app/src/index.tsx @@ -8,6 +8,7 @@ import './index.css'; import { App } from '@kobsio/plugin-core'; import resourcesPlugin from '@kobsio/plugin-resources'; import teamsPlugin from '@kobsio/plugin-teams'; +import usersPlugin from '@kobsio/plugin-users'; import applicationsPlugin from '@kobsio/plugin-applications'; import dashboardsPlugin from '@kobsio/plugin-dashboards'; import prometheusPlugin from '@kobsio/plugin-prometheus'; @@ -26,6 +27,7 @@ ReactDOM.render( 0 { + if configShallowCopy.Burst <= 0 { + return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") + } + configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) + } + var cs Clientset + var err error + cs.kobsV1beta1, err = kobsv1beta1.NewForConfig(&configShallowCopy) + if err != nil { + return nil, err + } + + cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy) + if err != nil { + return nil, err + } + return &cs, nil +} + +// NewForConfigOrDie creates a new Clientset for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *Clientset { + var cs Clientset + cs.kobsV1beta1 = kobsv1beta1.NewForConfigOrDie(c) + + cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c) + return &cs +} + +// New creates a new Clientset for the given RESTClient. +func New(c rest.Interface) *Clientset { + var cs Clientset + cs.kobsV1beta1 = kobsv1beta1.New(c) + + cs.DiscoveryClient = discovery.NewDiscoveryClient(c) + return &cs +} diff --git a/pkg/api/clients/user/clientset/versioned/doc.go b/pkg/api/clients/user/clientset/versioned/doc.go new file mode 100644 index 000000000..41721ca52 --- /dev/null +++ b/pkg/api/clients/user/clientset/versioned/doc.go @@ -0,0 +1,20 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated clientset. +package versioned diff --git a/pkg/api/clients/user/clientset/versioned/fake/clientset_generated.go b/pkg/api/clients/user/clientset/versioned/fake/clientset_generated.go new file mode 100644 index 000000000..e5d3b31a6 --- /dev/null +++ b/pkg/api/clients/user/clientset/versioned/fake/clientset_generated.go @@ -0,0 +1,82 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + clientset "github.com/kobsio/kobs/pkg/api/clients/user/clientset/versioned" + kobsv1beta1 "github.com/kobsio/kobs/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1" + fakekobsv1beta1 "github.com/kobsio/kobs/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/fake" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/discovery" + fakediscovery "k8s.io/client-go/discovery/fake" + "k8s.io/client-go/testing" +) + +// NewSimpleClientset returns a clientset that will respond with the provided objects. +// It's backed by a very simple object tracker that processes creates, updates and deletions as-is, +// without applying any validations and/or defaults. It shouldn't be considered a replacement +// for a real clientset and is mostly useful in simple unit tests. +func NewSimpleClientset(objects ...runtime.Object) *Clientset { + o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) + for _, obj := range objects { + if err := o.Add(obj); err != nil { + panic(err) + } + } + + cs := &Clientset{tracker: o} + cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} + cs.AddReactor("*", "*", testing.ObjectReaction(o)) + cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { + gvr := action.GetResource() + ns := action.GetNamespace() + watch, err := o.Watch(gvr, ns) + if err != nil { + return false, nil, err + } + return true, watch, nil + }) + + return cs +} + +// Clientset implements clientset.Interface. Meant to be embedded into a +// struct to get a default implementation. This makes faking out just the method +// you want to test easier. +type Clientset struct { + testing.Fake + discovery *fakediscovery.FakeDiscovery + tracker testing.ObjectTracker +} + +func (c *Clientset) Discovery() discovery.DiscoveryInterface { + return c.discovery +} + +func (c *Clientset) Tracker() testing.ObjectTracker { + return c.tracker +} + +var _ clientset.Interface = &Clientset{} + +// KobsV1beta1 retrieves the KobsV1beta1Client +func (c *Clientset) KobsV1beta1() kobsv1beta1.KobsV1beta1Interface { + return &fakekobsv1beta1.FakeKobsV1beta1{Fake: &c.Fake} +} diff --git a/pkg/api/clients/user/clientset/versioned/fake/doc.go b/pkg/api/clients/user/clientset/versioned/fake/doc.go new file mode 100644 index 000000000..9b99e7167 --- /dev/null +++ b/pkg/api/clients/user/clientset/versioned/fake/doc.go @@ -0,0 +1,20 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated fake clientset. +package fake diff --git a/pkg/api/clients/user/clientset/versioned/fake/register.go b/pkg/api/clients/user/clientset/versioned/fake/register.go new file mode 100644 index 000000000..6a51b92ba --- /dev/null +++ b/pkg/api/clients/user/clientset/versioned/fake/register.go @@ -0,0 +1,56 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + kobsv1beta1 "github.com/kobsio/kobs/pkg/api/apis/user/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var scheme = runtime.NewScheme() +var codecs = serializer.NewCodecFactory(scheme) + +var localSchemeBuilder = runtime.SchemeBuilder{ + kobsv1beta1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(scheme)) +} diff --git a/pkg/api/clients/user/clientset/versioned/scheme/doc.go b/pkg/api/clients/user/clientset/versioned/scheme/doc.go new file mode 100644 index 000000000..7dc375616 --- /dev/null +++ b/pkg/api/clients/user/clientset/versioned/scheme/doc.go @@ -0,0 +1,20 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package contains the scheme of the automatically generated clientset. +package scheme diff --git a/pkg/api/clients/user/clientset/versioned/scheme/register.go b/pkg/api/clients/user/clientset/versioned/scheme/register.go new file mode 100644 index 000000000..2444e3a05 --- /dev/null +++ b/pkg/api/clients/user/clientset/versioned/scheme/register.go @@ -0,0 +1,56 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package scheme + +import ( + kobsv1beta1 "github.com/kobsio/kobs/pkg/api/apis/user/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var Scheme = runtime.NewScheme() +var Codecs = serializer.NewCodecFactory(Scheme) +var ParameterCodec = runtime.NewParameterCodec(Scheme) +var localSchemeBuilder = runtime.SchemeBuilder{ + kobsv1beta1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(Scheme)) +} diff --git a/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/doc.go b/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/doc.go new file mode 100644 index 000000000..771101956 --- /dev/null +++ b/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/doc.go @@ -0,0 +1,20 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1beta1 diff --git a/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/fake/doc.go b/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/fake/doc.go new file mode 100644 index 000000000..16f443990 --- /dev/null +++ b/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/fake/doc.go @@ -0,0 +1,20 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/fake/fake_user.go b/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/fake/fake_user.go new file mode 100644 index 000000000..c1064904b --- /dev/null +++ b/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/fake/fake_user.go @@ -0,0 +1,130 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1beta1 "github.com/kobsio/kobs/pkg/api/apis/user/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeUsers implements UserInterface +type FakeUsers struct { + Fake *FakeKobsV1beta1 + ns string +} + +var usersResource = schema.GroupVersionResource{Group: "kobs.io", Version: "v1beta1", Resource: "users"} + +var usersKind = schema.GroupVersionKind{Group: "kobs.io", Version: "v1beta1", Kind: "User"} + +// Get takes name of the user, and returns the corresponding user object, and an error if there is any. +func (c *FakeUsers) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.User, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(usersResource, c.ns, name), &v1beta1.User{}) + + if obj == nil { + return nil, err + } + return obj.(*v1beta1.User), err +} + +// List takes label and field selectors, and returns the list of Users that match those selectors. +func (c *FakeUsers) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.UserList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(usersResource, usersKind, c.ns, opts), &v1beta1.UserList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1beta1.UserList{ListMeta: obj.(*v1beta1.UserList).ListMeta} + for _, item := range obj.(*v1beta1.UserList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested users. +func (c *FakeUsers) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(usersResource, c.ns, opts)) + +} + +// Create takes the representation of a user and creates it. Returns the server's representation of the user, and an error, if there is any. +func (c *FakeUsers) Create(ctx context.Context, user *v1beta1.User, opts v1.CreateOptions) (result *v1beta1.User, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(usersResource, c.ns, user), &v1beta1.User{}) + + if obj == nil { + return nil, err + } + return obj.(*v1beta1.User), err +} + +// Update takes the representation of a user and updates it. Returns the server's representation of the user, and an error, if there is any. +func (c *FakeUsers) Update(ctx context.Context, user *v1beta1.User, opts v1.UpdateOptions) (result *v1beta1.User, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(usersResource, c.ns, user), &v1beta1.User{}) + + if obj == nil { + return nil, err + } + return obj.(*v1beta1.User), err +} + +// Delete takes name of the user and deletes it. Returns an error if one occurs. +func (c *FakeUsers) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(usersResource, c.ns, name), &v1beta1.User{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeUsers) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(usersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1beta1.UserList{}) + return err +} + +// Patch applies the patch and returns the patched user. +func (c *FakeUsers) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.User, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(usersResource, c.ns, name, pt, data, subresources...), &v1beta1.User{}) + + if obj == nil { + return nil, err + } + return obj.(*v1beta1.User), err +} diff --git a/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/fake/fake_user_client.go b/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/fake/fake_user_client.go new file mode 100644 index 000000000..3e1e741e0 --- /dev/null +++ b/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/fake/fake_user_client.go @@ -0,0 +1,40 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1beta1 "github.com/kobsio/kobs/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeKobsV1beta1 struct { + *testing.Fake +} + +func (c *FakeKobsV1beta1) Users(namespace string) v1beta1.UserInterface { + return &FakeUsers{c, namespace} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeKobsV1beta1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/generated_expansion.go b/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/generated_expansion.go new file mode 100644 index 000000000..0a76c5b82 --- /dev/null +++ b/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/generated_expansion.go @@ -0,0 +1,21 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1beta1 + +type UserExpansion interface{} diff --git a/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/user.go b/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/user.go new file mode 100644 index 000000000..a890a74a8 --- /dev/null +++ b/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/user.go @@ -0,0 +1,178 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1beta1 + +import ( + "context" + "time" + + v1beta1 "github.com/kobsio/kobs/pkg/api/apis/user/v1beta1" + scheme "github.com/kobsio/kobs/pkg/api/clients/user/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// UsersGetter has a method to return a UserInterface. +// A group's client should implement this interface. +type UsersGetter interface { + Users(namespace string) UserInterface +} + +// UserInterface has methods to work with User resources. +type UserInterface interface { + Create(ctx context.Context, user *v1beta1.User, opts v1.CreateOptions) (*v1beta1.User, error) + Update(ctx context.Context, user *v1beta1.User, opts v1.UpdateOptions) (*v1beta1.User, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1beta1.User, error) + List(ctx context.Context, opts v1.ListOptions) (*v1beta1.UserList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.User, err error) + UserExpansion +} + +// users implements UserInterface +type users struct { + client rest.Interface + ns string +} + +// newUsers returns a Users +func newUsers(c *KobsV1beta1Client, namespace string) *users { + return &users{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the user, and returns the corresponding user object, and an error if there is any. +func (c *users) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.User, err error) { + result = &v1beta1.User{} + err = c.client.Get(). + Namespace(c.ns). + Resource("users"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of Users that match those selectors. +func (c *users) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.UserList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1beta1.UserList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("users"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested users. +func (c *users) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("users"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a user and creates it. Returns the server's representation of the user, and an error, if there is any. +func (c *users) Create(ctx context.Context, user *v1beta1.User, opts v1.CreateOptions) (result *v1beta1.User, err error) { + result = &v1beta1.User{} + err = c.client.Post(). + Namespace(c.ns). + Resource("users"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(user). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a user and updates it. Returns the server's representation of the user, and an error, if there is any. +func (c *users) Update(ctx context.Context, user *v1beta1.User, opts v1.UpdateOptions) (result *v1beta1.User, err error) { + result = &v1beta1.User{} + err = c.client.Put(). + Namespace(c.ns). + Resource("users"). + Name(user.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(user). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the user and deletes it. Returns an error if one occurs. +func (c *users) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("users"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *users) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("users"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched user. +func (c *users) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.User, err error) { + result = &v1beta1.User{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("users"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/user_client.go b/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/user_client.go new file mode 100644 index 000000000..eb47ccf39 --- /dev/null +++ b/pkg/api/clients/user/clientset/versioned/typed/user/v1beta1/user_client.go @@ -0,0 +1,89 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1beta1 + +import ( + v1beta1 "github.com/kobsio/kobs/pkg/api/apis/user/v1beta1" + "github.com/kobsio/kobs/pkg/api/clients/user/clientset/versioned/scheme" + rest "k8s.io/client-go/rest" +) + +type KobsV1beta1Interface interface { + RESTClient() rest.Interface + UsersGetter +} + +// KobsV1beta1Client is used to interact with features provided by the kobs.io group. +type KobsV1beta1Client struct { + restClient rest.Interface +} + +func (c *KobsV1beta1Client) Users(namespace string) UserInterface { + return newUsers(c, namespace) +} + +// NewForConfig creates a new KobsV1beta1Client for the given config. +func NewForConfig(c *rest.Config) (*KobsV1beta1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientFor(&config) + if err != nil { + return nil, err + } + return &KobsV1beta1Client{client}, nil +} + +// NewForConfigOrDie creates a new KobsV1beta1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *KobsV1beta1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new KobsV1beta1Client for the given RESTClient. +func New(c rest.Interface) *KobsV1beta1Client { + return &KobsV1beta1Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1beta1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *KobsV1beta1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/pkg/api/clients/user/informers/externalversions/factory.go b/pkg/api/clients/user/informers/externalversions/factory.go new file mode 100644 index 000000000..cdf91ed68 --- /dev/null +++ b/pkg/api/clients/user/informers/externalversions/factory.go @@ -0,0 +1,180 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package externalversions + +import ( + reflect "reflect" + sync "sync" + time "time" + + versioned "github.com/kobsio/kobs/pkg/api/clients/user/clientset/versioned" + internalinterfaces "github.com/kobsio/kobs/pkg/api/clients/user/informers/externalversions/internalinterfaces" + user "github.com/kobsio/kobs/pkg/api/clients/user/informers/externalversions/user" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// SharedInformerOption defines the functional option type for SharedInformerFactory. +type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory + +type sharedInformerFactory struct { + client versioned.Interface + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc + lock sync.Mutex + defaultResync time.Duration + customResync map[reflect.Type]time.Duration + + informers map[reflect.Type]cache.SharedIndexInformer + // startedInformers is used for tracking which informers have been started. + // This allows Start() to be called multiple times safely. + startedInformers map[reflect.Type]bool +} + +// WithCustomResyncConfig sets a custom resync period for the specified informer types. +func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + for k, v := range resyncConfig { + factory.customResync[reflect.TypeOf(k)] = v + } + return factory + } +} + +// WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. +func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.tweakListOptions = tweakListOptions + return factory + } +} + +// WithNamespace limits the SharedInformerFactory to the specified namespace. +func WithNamespace(namespace string) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.namespace = namespace + return factory + } +} + +// NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. +func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync) +} + +// NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. +// Listers obtained via this SharedInformerFactory will be subject to the same filters +// as specified here. +// Deprecated: Please use NewSharedInformerFactoryWithOptions instead +func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) +} + +// NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. +func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { + factory := &sharedInformerFactory{ + client: client, + namespace: v1.NamespaceAll, + defaultResync: defaultResync, + informers: make(map[reflect.Type]cache.SharedIndexInformer), + startedInformers: make(map[reflect.Type]bool), + customResync: make(map[reflect.Type]time.Duration), + } + + // Apply all options + for _, opt := range options { + factory = opt(factory) + } + + return factory +} + +// Start initializes all requested informers. +func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { + f.lock.Lock() + defer f.lock.Unlock() + + for informerType, informer := range f.informers { + if !f.startedInformers[informerType] { + go informer.Run(stopCh) + f.startedInformers[informerType] = true + } + } +} + +// WaitForCacheSync waits for all started informers' cache were synced. +func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { + informers := func() map[reflect.Type]cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informers := map[reflect.Type]cache.SharedIndexInformer{} + for informerType, informer := range f.informers { + if f.startedInformers[informerType] { + informers[informerType] = informer + } + } + return informers + }() + + res := map[reflect.Type]bool{} + for informType, informer := range informers { + res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) + } + return res +} + +// InternalInformerFor returns the SharedIndexInformer for obj using an internal +// client. +func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informerType := reflect.TypeOf(obj) + informer, exists := f.informers[informerType] + if exists { + return informer + } + + resyncPeriod, exists := f.customResync[informerType] + if !exists { + resyncPeriod = f.defaultResync + } + + informer = newFunc(f.client, resyncPeriod) + f.informers[informerType] = informer + + return informer +} + +// SharedInformerFactory provides shared informers for resources in all known +// API group versions. +type SharedInformerFactory interface { + internalinterfaces.SharedInformerFactory + ForResource(resource schema.GroupVersionResource) (GenericInformer, error) + WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool + + Kobs() user.Interface +} + +func (f *sharedInformerFactory) Kobs() user.Interface { + return user.New(f, f.namespace, f.tweakListOptions) +} diff --git a/pkg/api/clients/user/informers/externalversions/generic.go b/pkg/api/clients/user/informers/externalversions/generic.go new file mode 100644 index 000000000..ff1498496 --- /dev/null +++ b/pkg/api/clients/user/informers/externalversions/generic.go @@ -0,0 +1,62 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package externalversions + +import ( + "fmt" + + v1beta1 "github.com/kobsio/kobs/pkg/api/apis/user/v1beta1" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// GenericInformer is type of SharedIndexInformer which will locate and delegate to other +// sharedInformers based on type +type GenericInformer interface { + Informer() cache.SharedIndexInformer + Lister() cache.GenericLister +} + +type genericInformer struct { + informer cache.SharedIndexInformer + resource schema.GroupResource +} + +// Informer returns the SharedIndexInformer. +func (f *genericInformer) Informer() cache.SharedIndexInformer { + return f.informer +} + +// Lister returns the GenericLister. +func (f *genericInformer) Lister() cache.GenericLister { + return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) +} + +// ForResource gives generic access to a shared informer of the matching type +// TODO extend this to unknown resources with a client pool +func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { + switch resource { + // Group=kobs.io, Version=v1beta1 + case v1beta1.SchemeGroupVersion.WithResource("users"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Kobs().V1beta1().Users().Informer()}, nil + + } + + return nil, fmt.Errorf("no informer found for %v", resource) +} diff --git a/pkg/api/clients/user/informers/externalversions/internalinterfaces/factory_interfaces.go b/pkg/api/clients/user/informers/externalversions/internalinterfaces/factory_interfaces.go new file mode 100644 index 000000000..7cd28e0cf --- /dev/null +++ b/pkg/api/clients/user/informers/externalversions/internalinterfaces/factory_interfaces.go @@ -0,0 +1,40 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package internalinterfaces + +import ( + time "time" + + versioned "github.com/kobsio/kobs/pkg/api/clients/user/clientset/versioned" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + cache "k8s.io/client-go/tools/cache" +) + +// NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. +type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer + +// SharedInformerFactory a small interface to allow for adding an informer without an import cycle +type SharedInformerFactory interface { + Start(stopCh <-chan struct{}) + InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer +} + +// TweakListOptionsFunc is a function that transforms a v1.ListOptions. +type TweakListOptionsFunc func(*v1.ListOptions) diff --git a/pkg/api/clients/user/informers/externalversions/user/interface.go b/pkg/api/clients/user/informers/externalversions/user/interface.go new file mode 100644 index 000000000..116664ad5 --- /dev/null +++ b/pkg/api/clients/user/informers/externalversions/user/interface.go @@ -0,0 +1,46 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package user + +import ( + internalinterfaces "github.com/kobsio/kobs/pkg/api/clients/user/informers/externalversions/internalinterfaces" + v1beta1 "github.com/kobsio/kobs/pkg/api/clients/user/informers/externalversions/user/v1beta1" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1beta1 provides access to shared informers for resources in V1beta1. + V1beta1() v1beta1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1beta1 returns a new v1beta1.Interface. +func (g *group) V1beta1() v1beta1.Interface { + return v1beta1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/pkg/api/clients/user/informers/externalversions/user/v1beta1/interface.go b/pkg/api/clients/user/informers/externalversions/user/v1beta1/interface.go new file mode 100644 index 000000000..4794a3db4 --- /dev/null +++ b/pkg/api/clients/user/informers/externalversions/user/v1beta1/interface.go @@ -0,0 +1,45 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1beta1 + +import ( + internalinterfaces "github.com/kobsio/kobs/pkg/api/clients/user/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // Users returns a UserInformer. + Users() UserInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// Users returns a UserInformer. +func (v *version) Users() UserInformer { + return &userInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/api/clients/user/informers/externalversions/user/v1beta1/user.go b/pkg/api/clients/user/informers/externalversions/user/v1beta1/user.go new file mode 100644 index 000000000..8daa27049 --- /dev/null +++ b/pkg/api/clients/user/informers/externalversions/user/v1beta1/user.go @@ -0,0 +1,90 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1beta1 + +import ( + "context" + time "time" + + userv1beta1 "github.com/kobsio/kobs/pkg/api/apis/user/v1beta1" + versioned "github.com/kobsio/kobs/pkg/api/clients/user/clientset/versioned" + internalinterfaces "github.com/kobsio/kobs/pkg/api/clients/user/informers/externalversions/internalinterfaces" + v1beta1 "github.com/kobsio/kobs/pkg/api/clients/user/listers/user/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// UserInformer provides access to a shared informer and lister for +// Users. +type UserInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1beta1.UserLister +} + +type userInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewUserInformer constructs a new informer for User type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewUserInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredUserInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredUserInformer constructs a new informer for User type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredUserInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KobsV1beta1().Users(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KobsV1beta1().Users(namespace).Watch(context.TODO(), options) + }, + }, + &userv1beta1.User{}, + resyncPeriod, + indexers, + ) +} + +func (f *userInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredUserInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *userInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&userv1beta1.User{}, f.defaultInformer) +} + +func (f *userInformer) Lister() v1beta1.UserLister { + return v1beta1.NewUserLister(f.Informer().GetIndexer()) +} diff --git a/pkg/api/clients/user/listers/user/v1beta1/expansion_generated.go b/pkg/api/clients/user/listers/user/v1beta1/expansion_generated.go new file mode 100644 index 000000000..1c90ff792 --- /dev/null +++ b/pkg/api/clients/user/listers/user/v1beta1/expansion_generated.go @@ -0,0 +1,27 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1beta1 + +// UserListerExpansion allows custom methods to be added to +// UserLister. +type UserListerExpansion interface{} + +// UserNamespaceListerExpansion allows custom methods to be added to +// UserNamespaceLister. +type UserNamespaceListerExpansion interface{} diff --git a/pkg/api/clients/user/listers/user/v1beta1/user.go b/pkg/api/clients/user/listers/user/v1beta1/user.go new file mode 100644 index 000000000..073733977 --- /dev/null +++ b/pkg/api/clients/user/listers/user/v1beta1/user.go @@ -0,0 +1,99 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1beta1 + +import ( + v1beta1 "github.com/kobsio/kobs/pkg/api/apis/user/v1beta1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// UserLister helps list Users. +// All objects returned here must be treated as read-only. +type UserLister interface { + // List lists all Users in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1beta1.User, err error) + // Users returns an object that can list and get Users. + Users(namespace string) UserNamespaceLister + UserListerExpansion +} + +// userLister implements the UserLister interface. +type userLister struct { + indexer cache.Indexer +} + +// NewUserLister returns a new UserLister. +func NewUserLister(indexer cache.Indexer) UserLister { + return &userLister{indexer: indexer} +} + +// List lists all Users in the indexer. +func (s *userLister) List(selector labels.Selector) (ret []*v1beta1.User, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1beta1.User)) + }) + return ret, err +} + +// Users returns an object that can list and get Users. +func (s *userLister) Users(namespace string) UserNamespaceLister { + return userNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// UserNamespaceLister helps list and get Users. +// All objects returned here must be treated as read-only. +type UserNamespaceLister interface { + // List lists all Users in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1beta1.User, err error) + // Get retrieves the User from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1beta1.User, error) + UserNamespaceListerExpansion +} + +// userNamespaceLister implements the UserNamespaceLister +// interface. +type userNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all Users in the indexer for a given namespace. +func (s userNamespaceLister) List(selector labels.Selector) (ret []*v1beta1.User, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1beta1.User)) + }) + return ret, err +} + +// Get retrieves the User from the indexer for a given namespace and name. +func (s userNamespaceLister) Get(name string) (*v1beta1.User, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1beta1.Resource("user"), name) + } + return obj.(*v1beta1.User), nil +} diff --git a/pkg/api/clusters/cluster/cluster.go b/pkg/api/clusters/cluster/cluster.go index 283575c16..76d99d2c9 100644 --- a/pkg/api/clusters/cluster/cluster.go +++ b/pkg/api/clusters/cluster/cluster.go @@ -14,9 +14,11 @@ import ( application "github.com/kobsio/kobs/pkg/api/apis/application/v1beta1" dashboard "github.com/kobsio/kobs/pkg/api/apis/dashboard/v1beta1" team "github.com/kobsio/kobs/pkg/api/apis/team/v1beta1" + user "github.com/kobsio/kobs/pkg/api/apis/user/v1beta1" applicationClientsetVersioned "github.com/kobsio/kobs/pkg/api/clients/application/clientset/versioned" dashboardClientsetVersioned "github.com/kobsio/kobs/pkg/api/clients/dashboard/clientset/versioned" teamClientsetVersioned "github.com/kobsio/kobs/pkg/api/clients/team/clientset/versioned" + userClientsetVersioned "github.com/kobsio/kobs/pkg/api/clients/user/clientset/versioned" "github.com/kobsio/kobs/pkg/api/clusters/cluster/terminal" "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" @@ -43,6 +45,7 @@ type Cluster struct { applicationClientset *applicationClientsetVersioned.Clientset teamClientset *teamClientsetVersioned.Clientset dashboardClientset *dashboardClientsetVersioned.Clientset + userClientset *userClientsetVersioned.Clientset name string crds []CRD } @@ -434,6 +437,45 @@ func (c *Cluster) GetDashboard(ctx context.Context, namespace, name string) (*da return &dashboard, nil } +// GetUsers returns a list of users for the given namespace. It also adds the cluster, namespace and user name to the +// User CR, so that this information must not be specified by the user in the CR. +func (c *Cluster) GetUsers(ctx context.Context, namespace string) ([]user.UserSpec, error) { + usersList, err := c.userClientset.KobsV1beta1().Users(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + + var users []user.UserSpec + + for _, userItem := range usersList.Items { + user := userItem.Spec + user.Cluster = c.name + user.Namespace = userItem.Namespace + user.Name = userItem.Name + + users = append(users, user) + } + + return users, nil +} + +// GetUser returns a user for the given namespace and name. After the user is retrieved we replace, the cluster, +// namespace and name in the spec of the User CR. This is needed, so that the user doesn't have to, provide these +// fields. +func (c *Cluster) GetUser(ctx context.Context, namespace, name string) (*user.UserSpec, error) { + userCR, err := c.userClientset.KobsV1beta1().Users(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + + user := userCR.Spec + user.Cluster = c.name + user.Namespace = namespace + user.Name = name + + return &user, nil +} + // loadCRDs retrieves all CRDs from the Kubernetes API of this cluster. Then the CRDs are transformed into our internal // CRD format and saved within the cluster. Since this function is only called once after a cluster was loaded, we call // it in a endless loop until it succeeds. @@ -521,7 +563,13 @@ func NewCluster(name string, restConfig *rest.Config) (*Cluster, error) { dashboardClientset, err := dashboardClientsetVersioned.NewForConfig(restConfig) if err != nil { - log.WithError(err).Debugf("Could not create team clientset.") + log.WithError(err).Debugf("Could not create dashboard clientset.") + return nil, err + } + + userClientset, err := userClientsetVersioned.NewForConfig(restConfig) + if err != nil { + log.WithError(err).Debugf("Could not create user clientset.") return nil, err } @@ -533,6 +581,7 @@ func NewCluster(name string, restConfig *rest.Config) (*Cluster, error) { applicationClientset: applicationClientset, teamClientset: teamClientset, dashboardClientset: dashboardClientset, + userClientset: userClientset, name: name, } diff --git a/plugins/teams/src/components/home/Home.tsx b/plugins/teams/src/components/home/Home.tsx index acf8b4292..6a88b22fb 100644 --- a/plugins/teams/src/components/home/Home.tsx +++ b/plugins/teams/src/components/home/Home.tsx @@ -75,7 +75,7 @@ const Home: React.FunctionComponent = () => { return ( - + diff --git a/plugins/teams/src/components/page/TeamsItem.tsx b/plugins/teams/src/components/page/TeamsItem.tsx index 13671418d..ce241b88e 100644 --- a/plugins/teams/src/components/page/TeamsItem.tsx +++ b/plugins/teams/src/components/page/TeamsItem.tsx @@ -1,4 +1,4 @@ -import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core'; +import { Avatar, Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core'; import React from 'react'; import { LinkWrapper } from '@kobsio/plugin-core'; @@ -31,7 +31,7 @@ const TeamsItem: React.FunctionComponent = ({ - {name} + {name} {description} diff --git a/plugins/teams/src/index.ts b/plugins/teams/src/index.ts index da02f1947..300c363ab 100644 --- a/plugins/teams/src/index.ts +++ b/plugins/teams/src/index.ts @@ -6,6 +6,9 @@ import Home from './components/home/Home'; import Page from './components/page/Page'; import Panel from './components/panel/Panel'; +import { ITeam } from './utils/interfaces'; +import TeamsItem from './components/page/TeamsItem'; + const teamsPlugin: IPluginComponents = { teams: { home: Home, @@ -16,3 +19,6 @@ const teamsPlugin: IPluginComponents = { }; export default teamsPlugin; + +export { TeamsItem }; +export type ITeamTeam = ITeam; diff --git a/plugins/users/package.json b/plugins/users/package.json new file mode 100644 index 000000000..2ffaa1ed3 --- /dev/null +++ b/plugins/users/package.json @@ -0,0 +1,27 @@ +{ + "name": "@kobsio/plugin-users", + "version": "0.0.0", + "license": "MIT", + "private": false, + "main": "./lib/index.js", + "module": "./lib-esm/index.js", + "types": "./lib/index.d.ts", + "scripts": { + "plugin": "tsc && tsc --build tsconfig.esm.json && cp -r src/assets lib && cp -r src/assets lib-esm" + }, + "dependencies": { + "@kobsio/plugin-core": "*", + "@kobsio/plugin-teams": "*", + "@patternfly/react-core": "^4.128.2", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", + "@types/react-router-dom": "^5.1.7", + "md5": "^2.3.0", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-markdown": "^7.0.1", + "react-query": "^3.17.2", + "react-router-dom": "^5.2.0", + "typescript": "^4.3.4" + } +} diff --git a/plugins/users/src/assets/icon.png b/plugins/users/src/assets/icon.png new file mode 100644 index 000000000..9d606eae8 Binary files /dev/null and b/plugins/users/src/assets/icon.png differ diff --git a/plugins/users/src/components/home/Home.tsx b/plugins/users/src/components/home/Home.tsx new file mode 100644 index 000000000..c788dd66f --- /dev/null +++ b/plugins/users/src/components/home/Home.tsx @@ -0,0 +1,126 @@ +import { + Alert, + AlertActionLink, + AlertVariant, + Card, + Gallery, + GalleryItem, + Spinner, + TextInput, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, +} from '@patternfly/react-core'; +import { QueryObserverResult, useQuery } from 'react-query'; +import React, { useState } from 'react'; + +import { IPluginPageProps, useDebounce } from '@kobsio/plugin-core'; +import { IUser } from '../../utils/interfaces'; +import UsersItem from '../page/UsersItem'; + +const Home: React.FunctionComponent = () => { + const [searchTerm, setSearchTerm] = useState(''); + const debouncedSearchTerm = useDebounce(searchTerm, 500); + + const { isError, isLoading, error, data, refetch } = useQuery(['users/users'], async () => { + try { + const response = await fetch(`/api/plugins/users/users`, { method: 'get' }); + const json = await response.json(); + + if (response.status >= 200 && response.status < 300) { + return json; + } else { + if (json.error) { + throw new Error(json.error); + } else { + throw new Error('An unknown error occured'); + } + } + } catch (err) { + throw err; + } + }); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( + + > => refetch()}> + Retry + +
+ } + > +

{error?.message}

+ + ); + } + + if (!data) { + return null; + } + + return ( + + + + + + + + + + + + + +

 

+ + + {data + .filter((user) => + !debouncedSearchTerm + ? true + : user.cluster.includes(debouncedSearchTerm) || + user.namespace.includes(debouncedSearchTerm) || + user.name.includes(debouncedSearchTerm) || + user.fullName.includes(debouncedSearchTerm) || + user.email.includes(debouncedSearchTerm) || + user.position?.includes(debouncedSearchTerm), + ) + .map((user, index) => ( + + + + ))} + +
+ ); +}; + +export default Home; diff --git a/plugins/users/src/components/page/Page.tsx b/plugins/users/src/components/page/Page.tsx new file mode 100644 index 000000000..41c448e05 --- /dev/null +++ b/plugins/users/src/components/page/Page.tsx @@ -0,0 +1,23 @@ +import { Route, Switch } from 'react-router-dom'; +import React from 'react'; + +import { IPluginPageProps } from '@kobsio/plugin-core'; +import User from './User'; +import Users from './Users'; + +// The page implementation for the users plugin supports two different routes. The first page shows a list of all users, +// while the latter one is used to show a single user. +const Page: React.FunctionComponent = ({ name, displayName, description }: IPluginPageProps) => { + return ( + + + + + + + + + ); +}; + +export default Page; diff --git a/plugins/users/src/components/page/Teams.tsx b/plugins/users/src/components/page/Teams.tsx new file mode 100644 index 000000000..adffc3b6f --- /dev/null +++ b/plugins/users/src/components/page/Teams.tsx @@ -0,0 +1,60 @@ +import { Gallery, GalleryItem, PageSection, PageSectionVariants } from '@patternfly/react-core'; +import React from 'react'; +import { useQuery } from 'react-query'; + +import { ITeamTeam, TeamsItem } from '@kobsio/plugin-teams'; +import { IUser } from '../../utils/interfaces'; + +export interface ITeamsProps { + user: IUser; +} + +const Teams: React.FunctionComponent = ({ user }: ITeamsProps) => { + const { isError, isLoading, data } = useQuery(['users/teams', user], async () => { + try { + const response = await fetch(`/api/plugins/users/teams?cluster=${user.cluster}&namespace=${user.namespace}`, { + body: JSON.stringify({ + teams: user.teams, + }), + method: 'post', + }); + const json = await response.json(); + + if (response.status >= 200 && response.status < 300) { + return json; + } else { + if (json.error) { + throw new Error(json.error); + } else { + throw new Error('An unknown error occured'); + } + } + } catch (err) { + throw err; + } + }); + + if (isLoading || isError || !data) { + return null; + } + + return ( + + + {data.map((team, index) => ( + + + + ))} + + + ); +}; + +export default Teams; diff --git a/plugins/users/src/components/page/User.tsx b/plugins/users/src/components/page/User.tsx new file mode 100644 index 000000000..cb2fb9286 --- /dev/null +++ b/plugins/users/src/components/page/User.tsx @@ -0,0 +1,128 @@ +import { + Alert, + AlertActionLink, + AlertVariant, + Avatar, + Card, + CardBody, + PageSection, + PageSectionVariants, + Spinner, + TextContent, +} from '@patternfly/react-core'; +import { QueryObserverResult, useQuery } from 'react-query'; +import { useHistory, useParams } from 'react-router-dom'; +import React from 'react'; +import ReactMarkdown from 'react-markdown'; + +import { IUser } from '../../utils/interfaces'; +import Teams from './Teams'; +import { getGravatarImageUrl } from '../../utils/helpers'; + +interface IUserParams { + cluster: string; + namespace: string; + name: string; +} + +// User is the component for the users page. It loads a user by the specified cluster, namespace and name URL +// parameter. Everytime the cluster, namespace or name parameter is changed we make an API call to get the user. +const User: React.FunctionComponent = () => { + const history = useHistory(); + const params = useParams(); + + const { isError, isLoading, error, data, refetch } = useQuery( + ['users/user', params.cluster, params.namespace, params.name], + async () => { + try { + const response = await fetch( + `/api/plugins/users/user?cluster=${params.cluster}&namespace=${params.namespace}&name=${params.name}`, + { method: 'get' }, + ); + const json = await response.json(); + + if (response.status >= 200 && response.status < 300) { + return json; + } else { + if (json.error) { + throw new Error(json.error); + } else { + throw new Error('An unknown error occured'); + } + } + } catch (err) { + throw err; + } + }, + ); + + // During the API call we are showing a spinner, to show the user that the user is currently loading. + if (isLoading) { + return ; + } + + // When an error occures during the API call, we show the user this error. The user can then go back to the home page, + // to the users page or he can retry the API call. + if (isError) { + return ( + + history.push('/')}>Home + history.push('/users')}>Users + > => refetch()}> + Retry + + + } + > +

{error?.message}

+
+ ); + } + + // When the data is undefined and the isLoading and isError variables are not true we show nothing. This happens while + // the component is rendered the first time. + if (!data) { + return null; + } + + return ( + + + + +
+ +
{data.fullName}
+
{data.position}
+
+
+
+
+ + {data.teams && } + + {data.bio && ( + + + + + {data.bio} + + + + + )} +
+ ); +}; + +export default User; diff --git a/plugins/users/src/components/page/Users.tsx b/plugins/users/src/components/page/Users.tsx new file mode 100644 index 000000000..84afe6727 --- /dev/null +++ b/plugins/users/src/components/page/Users.tsx @@ -0,0 +1,107 @@ +import { + Alert, + AlertActionLink, + AlertVariant, + Drawer, + DrawerContent, + DrawerContentBody, + Gallery, + GalleryItem, + PageSection, + PageSectionVariants, + Spinner, + Title, +} from '@patternfly/react-core'; +import { QueryObserverResult, useQuery } from 'react-query'; +import React from 'react'; +import { useHistory } from 'react-router-dom'; + +import { IUser } from '../../utils/interfaces'; +import UsersItem from './UsersItem'; + +export interface IUsersProps { + displayName: string; + description: string; +} + +// Users is the page which is used to show all user. The component will display the configured name and description of +// the users plugin. Below this header it will display all the loaded users. +const Users: React.FunctionComponent = ({ displayName, description }: IUsersProps) => { + const history = useHistory(); + + const { isError, isLoading, error, data, refetch } = useQuery(['users/users'], async () => { + try { + const response = await fetch(`/api/plugins/users/users`, { method: 'get' }); + const json = await response.json(); + + if (response.status >= 200 && response.status < 300) { + return json; + } else { + if (json.error) { + throw new Error(json.error); + } else { + throw new Error('An unknown error occured'); + } + } + } catch (err) { + throw err; + } + }); + + return ( + + + + {displayName} + +

{description}

+
+ + + + + + {isLoading ? ( +
+ +
+ ) : isError ? ( + + history.push('/')}>Home + > => refetch()}> + Retry + +
+ } + > +

{error?.message}

+ + ) : data ? ( + + {data.map((user, index) => ( + + + + ))} + + ) : null} + + + + + + ); +}; + +export default Users; diff --git a/plugins/users/src/components/page/UsersItem.tsx b/plugins/users/src/components/page/UsersItem.tsx new file mode 100644 index 000000000..b7148e80f --- /dev/null +++ b/plugins/users/src/components/page/UsersItem.tsx @@ -0,0 +1,43 @@ +import { Avatar, Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core'; +import React from 'react'; + +import { LinkWrapper } from '@kobsio/plugin-core'; +import { getGravatarImageUrl } from '../../utils/helpers'; + +interface IUsersItemProps { + cluster: string; + namespace: string; + name: string; + fullName: string; + email: string; + position?: string; +} + +// UsersItem renders a single user in a Card component. The Card is wrapped by our LinkWrapper so that the user is +// redirected to the page of the user, when he clicks on the card. +const UsersItem: React.FunctionComponent = ({ + cluster, + namespace, + name, + fullName, + email, + position, +}: IUsersItemProps) => { + return ( + + + + + {fullName} + + {position ?

{position}

:

 

}
+
+
+ ); +}; + +export default UsersItem; diff --git a/plugins/users/src/components/panel/Panel.tsx b/plugins/users/src/components/panel/Panel.tsx new file mode 100644 index 000000000..75c9ff830 --- /dev/null +++ b/plugins/users/src/components/panel/Panel.tsx @@ -0,0 +1,40 @@ +import React, { memo } from 'react'; + +import { IPluginPanelProps, PluginCard, PluginOptionsMissing } from '@kobsio/plugin-core'; +import { IPanelOptions } from '../../utils/interfaces'; +import Users from './Users'; + +interface IPanelProps extends IPluginPanelProps { + options?: IPanelOptions; +} + +export const Panel: React.FunctionComponent = ({ title, description, defaults, options }: IPanelProps) => { + if (!options || !options.name) { + return ( + + ); + } + + return ( + + + + ); +}; + +export default memo(Panel, (prevProps, nextProps) => { + if (JSON.stringify(prevProps) === JSON.stringify(nextProps)) { + return true; + } + + return false; +}); diff --git a/plugins/users/src/components/panel/Users.tsx b/plugins/users/src/components/panel/Users.tsx new file mode 100644 index 000000000..ebdf3fc01 --- /dev/null +++ b/plugins/users/src/components/panel/Users.tsx @@ -0,0 +1,88 @@ +import { Alert, AlertActionLink, AlertVariant, Gallery, GalleryItem, Spinner } from '@patternfly/react-core'; +import { QueryObserverResult, useQuery } from 'react-query'; +import React from 'react'; + +import { IUser } from '../../utils/interfaces'; +import UsersItem from '../page/UsersItem'; + +interface IUsersProps { + cluster: string; + namespace: string; + name: string; +} + +// The Users component is used to load all users for the specified team. +const Users: React.FunctionComponent = ({ cluster, namespace, name }: IUsersProps) => { + const { isError, isLoading, error, data, refetch } = useQuery( + ['users/team', cluster, namespace, name], + async () => { + try { + const response = await fetch(`/api/plugins/users/team?cluster=${cluster}&namespace=${namespace}&name=${name}`, { + method: 'get', + }); + const json = await response.json(); + + if (response.status >= 200 && response.status < 300) { + return json; + } else { + if (json.error) { + throw new Error(json.error); + } else { + throw new Error('An unknown error occured'); + } + } + } catch (err) { + throw err; + } + }, + ); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( + + > => refetch()}> + Retry + + + } + > +

{error?.message}

+
+ ); + } + + if (!data) { + return null; + } + + return ( + + {data.map((user, index) => ( + + + + ))} + + ); +}; + +export default Users; diff --git a/plugins/users/src/index.ts b/plugins/users/src/index.ts new file mode 100644 index 000000000..8f4435650 --- /dev/null +++ b/plugins/users/src/index.ts @@ -0,0 +1,18 @@ +import { IPluginComponents } from '@kobsio/plugin-core'; + +import icon from './assets/icon.png'; + +import Home from './components/home/Home'; +import Page from './components/page/Page'; +import Panel from './components/panel/Panel'; + +const usersPlugin: IPluginComponents = { + users: { + home: Home, + icon: icon, + page: Page, + panel: Panel, + }, +}; + +export default usersPlugin; diff --git a/plugins/users/src/utils/helpers.ts b/plugins/users/src/utils/helpers.ts new file mode 100644 index 000000000..dc0dbe5c3 --- /dev/null +++ b/plugins/users/src/utils/helpers.ts @@ -0,0 +1,5 @@ +import md5 from 'md5'; + +export const getGravatarImageUrl = (email: string, size: number): string => { + return 'https://secure.gravatar.com/avatar/' + md5(email.toLowerCase().trim()) + '?size=' + size + '&default=mm'; +}; diff --git a/plugins/users/src/utils/interfaces.ts b/plugins/users/src/utils/interfaces.ts new file mode 100644 index 000000000..794909e77 --- /dev/null +++ b/plugins/users/src/utils/interfaces.ts @@ -0,0 +1,26 @@ +// IUser is the interface for a User CR. The interface must implement the same fields as the Users CRD. the only +// different is that we can be sure that the cluster, namespace and name of a user is always present in the frontend, +// because it will be set when a user is retrieved from the Kubernetes API. +export interface IUser { + cluster: string; + namespace: string; + name: string; + id: string; + fullName: string; + email: string; + position?: string; + bio?: string; + teams?: ITeam[]; +} + +export interface ITeam { + cluster?: string; + namespace?: string; + name: string; +} + +export interface IPanelOptions { + cluster?: string; + namespace?: string; + name?: string; +} diff --git a/plugins/users/tsconfig.esm.json b/plugins/users/tsconfig.esm.json new file mode 100644 index 000000000..acbc1eff8 --- /dev/null +++ b/plugins/users/tsconfig.esm.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "compilerOptions": { + "outDir": "lib-esm", + "module": "esnext", + "target": "esnext", + "moduleResolution": "node", + "lib": ["dom", "esnext"], + "declaration": false + } +} diff --git a/plugins/users/tsconfig.json b/plugins/users/tsconfig.json new file mode 100644 index 000000000..09365d619 --- /dev/null +++ b/plugins/users/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.json", + "exclude": ["node_modules", "lib-esm", "lib"], + "include": ["src"], + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "outDir": "lib", + "declaration": true + } +} diff --git a/plugins/users/users.go b/plugins/users/users.go new file mode 100644 index 000000000..3c32f3114 --- /dev/null +++ b/plugins/users/users.go @@ -0,0 +1,201 @@ +package users + +import ( + "encoding/json" + "net/http" + + team "github.com/kobsio/kobs/pkg/api/apis/team/v1beta1" + user "github.com/kobsio/kobs/pkg/api/apis/user/v1beta1" + "github.com/kobsio/kobs/pkg/api/clusters" + "github.com/kobsio/kobs/pkg/api/middleware/errresponse" + "github.com/kobsio/kobs/pkg/api/plugins/plugin" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "github.com/sirupsen/logrus" +) + +// Route is the route under which the plugin should be registered in our router for the rest api. +const Route = "/users" + +var ( + log = logrus.WithFields(logrus.Fields{"package": "users"}) +) + +// Config is the structure of the configuration for the users plugin. +type Config struct{} + +// Router implements the router for the resources plugin, which can be registered in the router for our rest api. +type Router struct { + *chi.Mux + clusters *clusters.Clusters + config Config +} + +type getTeamsData struct { + Teams []user.TeamReference `json:"teams"` +} + +func isMember(teams []user.TeamReference, defaultCluster, defaultNamespace, cluster, namespace, name string) bool { + for _, team := range teams { + c := defaultCluster + if team.Cluster != "" { + c = team.Cluster + } + + n := defaultNamespace + if team.Namespace != "" { + n = team.Namespace + } + + if c == cluster && n == namespace && team.Name == name { + return true + } + } + + return false +} + +// getUsers returns a list of users for all clusters and namespaces. We always return all users for all clusters and +// namespaces. For this we are looping though the loaded clusters and callend the GetUsers function for each one. +func (router *Router) getUsers(w http.ResponseWriter, r *http.Request) { + log.Tracef("getUsers") + + var users []user.UserSpec + + for _, cluster := range router.clusters.Clusters { + user, err := cluster.GetUsers(r.Context(), "") + if err != nil { + errresponse.Render(w, r, err, http.StatusBadRequest, "Could not get users") + return + } + + users = append(users, user...) + } + + log.WithFields(logrus.Fields{"count": len(users)}).Tracef("getUsers") + render.JSON(w, r, users) +} + +// getUser returns a a single user for the given cluster and namespace and name. The cluster, namespace and name is +// defined via a corresponding query parameter. Then we are using the cluster object to get the user via the GetUser +// function. +func (router *Router) getUser(w http.ResponseWriter, r *http.Request) { + clusterName := r.URL.Query().Get("cluster") + namespace := r.URL.Query().Get("namespace") + name := r.URL.Query().Get("name") + + log.WithFields(logrus.Fields{"cluster": clusterName, "namespace": namespace, "name": name}).Tracef("getUser") + + cluster := router.clusters.GetCluster(clusterName) + if cluster == nil { + errresponse.Render(w, r, nil, http.StatusBadRequest, "Invalid cluster name") + return + } + + user, err := cluster.GetUser(r.Context(), namespace, name) + if err != nil { + errresponse.Render(w, r, err, http.StatusBadRequest, "Could not get user") + return + } + + render.JSON(w, r, user) +} + +// getTeams returns all teams, where a users is a member of. +func (router *Router) getTeams(w http.ResponseWriter, r *http.Request) { + defaultClusterName := r.URL.Query().Get("cluster") + defaultNamespace := r.URL.Query().Get("namespace") + + var data getTeamsData + + err := json.NewDecoder(r.Body).Decode(&data) + if err != nil { + errresponse.Render(w, r, err, http.StatusBadRequest, "Could not decode request body") + return + } + + var teams []*team.TeamSpec + + for _, team := range data.Teams { + c := defaultClusterName + if team.Cluster != "" { + c = team.Cluster + } + + n := defaultNamespace + if team.Namespace != "" { + c = team.Namespace + } + + cluster := router.clusters.GetCluster(c) + if cluster == nil { + errresponse.Render(w, r, nil, http.StatusBadRequest, "Invalid cluster name") + return + } + + t, err := cluster.GetTeam(r.Context(), n, team.Name) + if err != nil { + errresponse.Render(w, r, err, http.StatusBadRequest, "Could not get team") + return + } + + teams = append(teams, t) + } + + render.JSON(w, r, teams) +} + +// getTeam returns all users for the given team. +func (router *Router) getTeam(w http.ResponseWriter, r *http.Request) { + teamCluster := r.URL.Query().Get("cluster") + teamNamespace := r.URL.Query().Get("namespace") + teamName := r.URL.Query().Get("name") + + log.WithFields(logrus.Fields{"cluster": teamCluster, "namespace": teamNamespace, "name": teamName}).Tracef("getTeam") + + var users []user.UserSpec + var filteredUsers []user.UserSpec + + for _, cluster := range router.clusters.Clusters { + user, err := cluster.GetUsers(r.Context(), "") + if err != nil { + errresponse.Render(w, r, err, http.StatusBadRequest, "Could not get users") + return + } + + users = append(users, user...) + } + + for _, user := range users { + if isMember(user.Teams, user.Cluster, user.Namespace, teamCluster, teamNamespace, teamName) { + filteredUsers = append(filteredUsers, user) + } + } + + render.JSON(w, r, filteredUsers) +} + +// Register returns a new router which can be used in the router for the kobs rest api. +func Register(clusters *clusters.Clusters, plugins *plugin.Plugins, config Config) chi.Router { + plugins.Append(plugin.Plugin{ + Name: "users", + DisplayName: "Users", + Description: "Define the members of your Teams.", + Home: true, + Type: "users", + }) + + router := Router{ + chi.NewRouter(), + clusters, + config, + } + + router.Get("/users", router.getUsers) + router.Get("/user", router.getUser) + router.Post("/teams", router.getTeams) + router.Get("/team", router.getTeam) + + return router +} diff --git a/typings.d.ts b/typings.d.ts index 48b51ad0e..1cc546e20 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -21,3 +21,8 @@ declare module '*.css' { declare module 'cytoscape-dagre'; declare module 'cytoscape-node-html-label'; + +declare module 'md5' { + function md5(data: string, options?: { encoding: string; asBytes: boolean; asString: boolean }): string; + export = md5; +} diff --git a/yarn.lock b/yarn.lock index 6880593c5..630612398 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4718,6 +4718,11 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +charenc@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + check-types@^11.1.1: version "11.1.2" resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f" @@ -5345,6 +5350,11 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" +crypt@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -8356,7 +8366,7 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-buffer@^1.1.5: +is-buffer@^1.1.5, is-buffer@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -9836,6 +9846,15 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" +md5@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" + integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== + dependencies: + charenc "0.0.2" + crypt "0.0.2" + is-buffer "~1.1.6" + mdast-util-definitions@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.0.tgz#b6d10ef00a3c4cf191e8d9a5fa58d7f4a366f817" @@ -12770,7 +12789,7 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== -react-markdown@7.0.1: +react-markdown@7.0.1, react-markdown@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-7.0.1.tgz#c7365fcd7d1813b3ae68f2200e8f92d47d865627" integrity sha512-pthNPaoiwg0q7hukoE04F2ENwSzijIlWHJ4UMs/96LUe/G/P3FnbP4qHzx3FoNqae+2SqDG8vzniTLnJDeWneg==