Skip to content

Commit

Permalink
Add frontend support for single namespace visibility support
Browse files Browse the repository at this point in the history
  • Loading branch information
charles-edouard.breteche authored and tekton-robot committed Jun 5, 2020
1 parent 4ceb116 commit d7ed18a
Show file tree
Hide file tree
Showing 31 changed files with 839 additions and 391 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,13 @@ These options are documented below:
| `--read-only` | Enable or disable read only mode | `bool` | `false` |
| `--web-dir` | Dashboard web resources directory | `string` | `""` |
| `--logout-url` | If set, enables logout on the frontend and binds the logout button to this url | `string` | `""` |
| `--namespace` | If set, limits the scope of resources watched to this namespace only | `string` | `""` |

Run `dashboard --help` to show the supported command line arguments and their default value directly from the `dashboard` binary.

**Important note:** using `--namespace` ensures that the dashboard is watching resources in the namespace specified (and drives the frontend).
It doesn't limit actions that can be performed to this namespace only though. It's important that this flag is used AND that rbac rules are setup accordingly.

### Optionally set up the Ingress endpoint

An Ingress definition is provided in the `ingress` directory, and this can optionally be installed and configured. If you wish to access the Tekton Dashboard, for example on your laptop that has a visible IP address, you can use the freely available [`nip.io`](https://nip.io/) service. A worked example follows.
Expand Down
2 changes: 1 addition & 1 deletion cmd/dashboard/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func main() {
logging.Log.Info("Creating controllers")
resyncDur := time.Second * 30
controllers.StartTektonControllers(resource.PipelineClient, resource.PipelineResourceClient, *tenantNamespace, resyncDur, ctx.Done())
controllers.StartKubeControllers(resource.K8sClient, resyncDur, installNamespace, *tenantNamespace, *readOnly, routerHandler, ctx.Done())
controllers.StartKubeControllers(resource.K8sClient, resyncDur, *tenantNamespace, *readOnly, routerHandler, ctx.Done())

logging.Log.Infof("Creating server and entering wait loop")
CSRF := csrf.Protect(
Expand Down
5 changes: 5 additions & 0 deletions overlays/dev-single-namespace/deployment-patch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
- op: add
path: /spec/template/spec/containers/0/args/-
value:
--namespace=tekton-tenant
63 changes: 63 additions & 0 deletions overlays/dev-single-namespace/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copyright 2020 The Tekton 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.

---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base/200-clusterrole-backend.yaml
- ../../base/200-clusterrole-extensions.yaml
- ../../base/200-clusterrole-pipelines.yaml
- ../../base/200-clusterrole-tenant.yaml
- ../../base/200-clusterrole-triggers.yaml
- ../../base/201-clusterrolebinding-backend.yaml
- ../../base/201-rolebinding-extensions.yaml
- ../../base/201-rolebinding-pipelines.yaml
- ../../base/201-rolebinding-tenant.yaml
- ../../base/201-rolebinding-triggers.yaml
- ../../base/202-extension-crd.yaml
- ../../base/203-serviceaccount.yaml
- ../../base/300-deployment.yaml
- ../../base/300-service.yaml
images:
- name: dashboardImage
newName: github.com/tektoncd/dashboard/cmd/dashboard
newTag:
patchesJson6902:
- target:
group: rbac.authorization.k8s.io
version: v1
kind: ClusterRole
name: tekton-dashboard-backend
path: ../full-fat/clusterrole-backend-patch.yaml
- target:
group: rbac.authorization.k8s.io
version: v1
kind: ClusterRole
name: tekton-dashboard-tenant
path: ../full-fat/clusterrole-tenant-patch.yaml
- target:
group: apps
version: v1
kind: Deployment
name: tekton-dashboard
namespace: tekton-pipelines
path: ../dev/csrf-secure-cookie-patch.yaml
- target:
group: apps
version: v1
kind: Deployment
name: tekton-dashboard
namespace: tekton-pipelines
path: ./deployment-patch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class LogoutButton extends Component {
const logoutURL = await this.props.getLogoutURL();
this.setState({ logoutURL });
} catch (error) {
console.log(error); // eslint-disable-line
console.log(error); // eslint-disable-line no-console
}
}

Expand Down
6 changes: 4 additions & 2 deletions pkg/controllers/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,16 @@ func StartTektonControllers(clientset tektonclientset.Interface, clientresources
}

// StartKubeControllers creates and starts Kube controllers
func StartKubeControllers(clientset k8sclientset.Interface, resyncDur time.Duration, installNamespace, tenantNamespace string, readOnly bool, handler *router.Handler, stopCh <-chan struct{}) {
func StartKubeControllers(clientset k8sclientset.Interface, resyncDur time.Duration, tenantNamespace string, readOnly bool, handler *router.Handler, stopCh <-chan struct{}) {
logging.Log.Info("Creating Kube controllers")
clusterInformerFactory := k8sinformers.NewSharedInformerFactory(clientset, resyncDur)
tenantInformerFactory := k8sinformers.NewSharedInformerFactoryWithOptions(clientset, resyncDur, k8sinformers.WithNamespace(tenantNamespace))
// Add all kube controllers
kubecontroller.NewExtensionController(clusterInformerFactory, installNamespace, handler)
if tenantNamespace == "" {
kubecontroller.NewExtensionController(clusterInformerFactory, handler)
kubecontroller.NewNamespaceController(clusterInformerFactory)
} else {
kubecontroller.NewExtensionController(tenantInformerFactory, handler)
}
if !readOnly {
kubecontroller.NewSecretController(tenantInformerFactory)
Expand Down
106 changes: 49 additions & 57 deletions pkg/controllers/kubernetes/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,14 @@ import (
// used within informer handler functions
type extensionHandler struct {
*router.Handler
installNamespace string
}

// NewExtensionController registers the K8s shared informer that reacts to
// extension service updates
func NewExtensionController(sharedK8sInformerFactory k8sinformer.SharedInformerFactory, dashboardNamespace string, handler *router.Handler) {
func NewExtensionController(sharedK8sInformerFactory k8sinformer.SharedInformerFactory, handler *router.Handler) {
logging.Log.Debug("In NewExtensionController")
h := extensionHandler{
Handler: handler,
installNamespace: dashboardNamespace,
Handler: handler,
}
extensionServiceInformer := sharedK8sInformerFactory.Core().V1().Services().Informer()
// ResourceEventHandler interface functions only pass object interfaces
Expand All @@ -38,16 +36,14 @@ func NewExtensionController(sharedK8sInformerFactory k8sinformer.SharedInformerF

func (e extensionHandler) serviceCreated(obj interface{}) {
service := obj.(*v1.Service)
if service.Namespace == e.installNamespace {
if value := service.Labels[router.ExtensionLabelKey]; value == router.ExtensionLabelValue && service.Spec.ClusterIP != "" {
logging.Log.Debugf("Extension Controller detected extension '%s' created", service.Name)
e.RegisterExtension(service)
data := broadcaster.SocketData{
MessageType: broadcaster.ExtensionCreated,
Payload: obj,
}
endpoints.ResourcesChannel <- data
if value := service.Labels[router.ExtensionLabelKey]; value == router.ExtensionLabelValue && service.Spec.ClusterIP != "" {
logging.Log.Debugf("Extension Controller detected extension '%s' created", service.Name)
e.RegisterExtension(service)
data := broadcaster.SocketData{
MessageType: broadcaster.ExtensionCreated,
Payload: obj,
}
endpoints.ResourcesChannel <- data
}
}

Expand All @@ -56,58 +52,54 @@ func (e extensionHandler) serviceUpdated(oldObj, newObj interface{}) {
// If resourceVersion differs between old and new, an actual update event was observed
versionUpdated := oldService.ResourceVersion != newService.ResourceVersion
// Updated services will still be in the same namespace
if oldService.Namespace == e.installNamespace {
var event string
if value := oldService.Labels[router.ExtensionLabelKey]; versionUpdated && value == router.ExtensionLabelValue && oldService.Spec.ClusterIP != "" {
logging.Log.Debugf("Extension Controller Update: Removing old extension '%s'", oldService.Name)
e.UnregisterExtension(oldService)
event = "delete"
var event string
if value := oldService.Labels[router.ExtensionLabelKey]; versionUpdated && value == router.ExtensionLabelValue && oldService.Spec.ClusterIP != "" {
logging.Log.Debugf("Extension Controller Update: Removing old extension '%s'", oldService.Name)
e.UnregisterExtension(oldService)
event = "delete"
}
if value := newService.Labels[router.ExtensionLabelKey]; versionUpdated && value == router.ExtensionLabelValue && newService.Spec.ClusterIP != "" {
logging.Log.Debugf("Extension Controller Update: Add new extension '%s'", newService.Name)
e.RegisterExtension(newService)
if len(event) != 0 {
event = "update"
} else {
event = "create"
}
}
switch event {
case "delete": // Service has removed the extension label
data := broadcaster.SocketData{
MessageType: broadcaster.ExtensionDeleted,
Payload: newObj,
}
if value := newService.Labels[router.ExtensionLabelKey]; versionUpdated && value == router.ExtensionLabelValue && newService.Spec.ClusterIP != "" {
logging.Log.Debugf("Extension Controller Update: Add new extension '%s'", newService.Name)
e.RegisterExtension(newService)
if len(event) != 0 {
event = "update"
} else {
event = "create"
}
endpoints.ResourcesChannel <- data
case "create": // Service has added the extension label
data := broadcaster.SocketData{
MessageType: broadcaster.ExtensionCreated,
Payload: newObj,
}
switch event {
case "delete": // Service has removed the extension label
data := broadcaster.SocketData{
MessageType: broadcaster.ExtensionDeleted,
Payload: newObj,
}
endpoints.ResourcesChannel <- data
case "create": // Service has added the extension label
data := broadcaster.SocketData{
MessageType: broadcaster.ExtensionCreated,
Payload: newObj,
}
endpoints.ResourcesChannel <- data
case "update": // Extension service was modified
data := broadcaster.SocketData{
MessageType: broadcaster.ExtensionUpdated,
Payload: newObj,
}
endpoints.ResourcesChannel <- data
endpoints.ResourcesChannel <- data
case "update": // Extension service was modified
data := broadcaster.SocketData{
MessageType: broadcaster.ExtensionUpdated,
Payload: newObj,
}
endpoints.ResourcesChannel <- data
}
}

func (e extensionHandler) serviceDeleted(obj interface{}) {
serviceMeta := utils.GetDeletedObjectMeta(obj)
if serviceMeta.GetNamespace() == e.installNamespace {
if value := serviceMeta.GetLabels()[router.ExtensionLabelKey]; value == router.ExtensionLabelValue {
logging.Log.Debugf("Extension Controller detected extension '%s' deleted", serviceMeta.GetName())
if serviceMeta.GetUID() != "" {
e.UnregisterExtensionByMeta(serviceMeta)
}
data := broadcaster.SocketData{
MessageType: broadcaster.ExtensionDeleted,
Payload: obj,
}
endpoints.ResourcesChannel <- data
if value := serviceMeta.GetLabels()[router.ExtensionLabelKey]; value == router.ExtensionLabelValue {
logging.Log.Debugf("Extension Controller detected extension '%s' deleted", serviceMeta.GetName())
if serviceMeta.GetUID() != "" {
e.UnregisterExtensionByMeta(serviceMeta)
}
data := broadcaster.SocketData{
MessageType: broadcaster.ExtensionDeleted,
Payload: obj,
}
endpoints.ResourcesChannel <- data
}
}
2 changes: 1 addition & 1 deletion pkg/testutils/testutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func DummyServer() (*httptest.Server, *endpoints.Resource, string) {
stopCh := make(<-chan struct{})
resyncDur := time.Second * 30
controllers.StartTektonControllers(resource.PipelineClient, resource.PipelineResourceClient, "", resyncDur, stopCh)
controllers.StartKubeControllers(resource.K8sClient, resyncDur, dashboardNamespace, "", false, routerHandler, stopCh)
controllers.StartKubeControllers(resource.K8sClient, resyncDur, "", false, routerHandler, stopCh)
// Wait until namespace is detected by informer and functionally "dropped" since the informer will be eventually consistent
timeout := time.After(5 * time.Second)
for {
Expand Down
8 changes: 5 additions & 3 deletions src/actions/extensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ limitations under the License.
*/

import { getExtensions } from '../api';
import { fetchCollection } from './actionCreators';
import { fetchNamespacedCollection } from './actionCreators';

export function fetchExtensions() {
return fetchCollection('Extension', getExtensions);
export function fetchExtensions({ namespace } = {}) {
return fetchNamespacedCollection('Extension', getExtensions, {
namespace
});
}
7 changes: 4 additions & 3 deletions src/actions/extensions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ import { fetchExtensions } from './extensions';
import * as creators from './actionCreators';

it('fetchExtensions', async () => {
jest.spyOn(creators, 'fetchCollection');
jest.spyOn(creators, 'fetchNamespacedCollection');
fetchExtensions();
expect(creators.fetchCollection).toHaveBeenCalledWith(
expect(creators.fetchNamespacedCollection).toHaveBeenCalledWith(
'Extension',
API.getExtensions
API.getExtensions,
{}
);
});
5 changes: 3 additions & 2 deletions src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,12 +403,13 @@ export function getCustomResource(...args) {
return get(uri);
}

export async function getExtensions() {
export async function getExtensions({ namespace } = {}) {
const uri = `${apiRoot}/v1/extensions`;
const resourceExtensionsUri = getResourcesAPI({
group: 'dashboard.tekton.dev',
version: 'v1alpha1',
type: 'extensions'
type: 'extensions',
namespace
});
let extensions = await get(uri);
const resourceExtensions = await get(resourceExtensionsUri);
Expand Down
3 changes: 2 additions & 1 deletion src/components/CreateSecret/Form/UniversalFields.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ const namespaces = {
};

const store = mockStore({
namespaces
namespaces,
properties: {}
});

it('UniversalFields renders with blank inputs', () => {
Expand Down
3 changes: 2 additions & 1 deletion src/components/CreateSecret/Form/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ const namespaces = {
};

const store = mockStore({
namespaces
namespaces,
properties: {}
});

const props = {
Expand Down
Loading

0 comments on commit d7ed18a

Please sign in to comment.