Skip to content

Commit

Permalink
Update proxy to support websockets
Browse files Browse the repository at this point in the history
Our current proxy does not support connection upgrades, which means we do not
have a clean way to access the built-in support for websockets provided by the
Kubernetes API server. Instead, the Dashboard provides a single websocket
connection which provides updates for all supported resources.

This has a number of drawbacks such as:
- not being able to selectively register to receive updates for:
  - resources in a specific namespace
  - a particular kind of resource
  - a single specific resource
- not supporting real time updates for resource-based extensions

This means that all pages in the Dashboard incur an unnecessary overhead by
processing updates for resources they may never require.

Add a new proxy implementation which handles the connection upgrades, while
also providing the ability to support more fine-grained permissions and
user-based auth which we want to add in future.

Migrate the existing endpoints to the new server/proxy setup, including the
existing websocket connection as it's still in use but will be replaced in the
near future.

Remove the service-based extension support as the webhooks-extension was
deprecated after v0.7.0 and has since been deleted. Similar functionality could
be added via a small modification to the resource-based extensions and using
the new proxy to access the services.
  • Loading branch information
AlanGreene authored and tekton-robot committed Jul 6, 2021
1 parent d62cce7 commit 32c360c
Show file tree
Hide file tree
Showing 23 changed files with 274 additions and 1,547 deletions.
38 changes: 14 additions & 24 deletions cmd/dashboard/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ limitations under the License.
package main

import (
"context"
"flag"
"fmt"
"net/http"
Expand All @@ -23,7 +22,6 @@ import (

dashboardclientset "github.com/tektoncd/dashboard/pkg/client/clientset/versioned"
"github.com/tektoncd/dashboard/pkg/controllers"
"github.com/tektoncd/dashboard/pkg/csrf"
"github.com/tektoncd/dashboard/pkg/endpoints"
"github.com/tektoncd/dashboard/pkg/logging"
"github.com/tektoncd/dashboard/pkg/router"
Expand Down Expand Up @@ -126,37 +124,29 @@ func main() {

ctx := signals.NewContext()

routerHandler := router.Register(resource)
server, err := router.Register(resource, cfg)

if err != nil {
logging.Log.Errorf("Error creating proxy: %s", err.Error())
return
}

logging.Log.Info("Creating controllers")
resyncDur := time.Second * 30
controllers.StartTektonControllers(resource.DynamicClient, resyncDur, *tenantNamespace, ctx.Done())
controllers.StartKubeControllers(resource.K8sClient, resyncDur, *tenantNamespace, *readOnly, routerHandler, ctx.Done())
controllers.StartKubeControllers(resource.K8sClient, resyncDur, *tenantNamespace, *readOnly, ctx.Done())
controllers.StartDashboardControllers(resource.DashboardClient, resyncDur, *tenantNamespace, ctx.Done())

if isTriggersInstalled {
controllers.StartTriggersControllers(resource.DynamicClient, resyncDur, *tenantNamespace, ctx.Done())
}

logging.Log.Infof("Creating server and entering wait loop")
CSRF := csrf.Protect()
server := &http.Server{Addr: fmt.Sprintf(":%d", *portNumber), Handler: CSRF(routerHandler)}

errCh := make(chan error, 1)
defer close(errCh)
go func() {
// Don't forward ErrServerClosed as that indicates we're already shutting down.
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errCh <- fmt.Errorf("dashboard server failed: %w", err)
}
}()

select {
case err := <-errCh:
logging.Log.Fatal(err)
case <-ctx.Done():
if err := server.Shutdown(context.Background()); err != nil {
logging.Log.Fatal(err)
}
l, err := server.Listen("", *portNumber)
if err != nil {
logging.Log.Errorf("Error listening: %s", err.Error())
return
}

logging.Log.Infof("Starting to serve on %s", l.Addr().String())
server.ServeOnListener(l)
}
10 changes: 0 additions & 10 deletions docs/dev/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,6 @@ The backend API provides the following endpoints:

__GET endpoints__

__Extensions__
```
GET /v1/extensions
```

- Get all extensions in the given namespace
- Returns HTTP code 500 if an error occurred getting the extensions
- Returns HTTP code 200 and the given extensions in the given namespace if found,
otherwise an empty list is returned

__Dashboard Properties__
```
GET /v1/properties
Expand Down
207 changes: 1 addition & 206 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,14 @@ This guide explains what Tekton Dashboard extensions are and how to manage them.
- [Extension CRD (apiVersion: dashboard.tekton.dev/v1alpha1)](#extension-crd-apiversion-dashboardtektondevv1alpha1)
- [Example: Register a CronJob extension](#example-register-a-cronjob-extension)
- [Example: Extend Tekton Dashboard service account permissions](#example-extend-tekton-dashboard-service-account-permissions)
- [Service based extensions](#service-based-extensions)
- [Example: Create a simple nodejs backend](#example-create-a-simple-nodejs-backend)
- [Example: Add and serve frontend code](#example-add-and-serve-frontend-code)

## Before you begin

Tekton Dashboard Extensions are currently alpha and are considered experimental. This means things could change at any time.

There are two types of extension supported by the Tekton Dashboard:
- [Resource based extensions](#resource-based-extensions) are deployed as `Extension` resources,
the Dashboard will generate UI to display the chosen resources
- [Service based extensions](#service-based-extensions) are deployed as independent services in the cluster, and can provide
custom UI and APIs to be surfaced by the Dashboard

## Resource based extensions

Resource based extensions provide a simple and easy way to list and view resources inside a cluster.
Resource based extensions provide a simple and easy way to list and view resources inside a cluster. They are deployed as `Extension` resources and the Dashboard will generate UI to display the chosen resources.

Using them requires two steps (see an example below):
1. create an [Extension resource](#extension-crd-apiversion-dashboardtektondevv1alpha1) in your cluster
Expand Down Expand Up @@ -100,202 +91,6 @@ Now the Tekton Dashboard will show `CronJob`s in your cluster.

![Resource based extension RBAC](./extensions-resource-based-rbac.png)

## Service based extensions

Service based extensions are more powerful tools, they let you write your custom backend and frontend code.

The frontend code will be dynamically loaded by the Tekton Dashboard at runtime, the backend code can expose an API that will be proxied by the Dashboard backend.

This takes more work to develop but allows for feature rich extensions.

To create such an extension you will need to deploy a `Service` in your cluster, this service will serve frontend code and host the backend API.
You will also need to add a well-known label `tekton-dashboard-extension: "true"` to let the Dashboard know about it.

Well-known annotations are used to describe inner workings of the extension:
- `tekton-dashboard-display-name` is the display name of your extension to appear in the side nav
- `tekton-dashboard-endpoints` registers the list of endpoints exposed by the backend API
- `tekton-dashboard-bundle-location` tells the Tekton Dashboard where to load the frontend code

Additionally, the Tekton Dashboard host will globally expose a few objects to let the extension's frontend code connect to these shared components (you can view the list of shared objects [here](../src/containers/Extension/globals.js)).

The next section provides an example of developing a simple service based extension.

### Example: Create a simple nodejs backend

To begin, you will create a simple Node.js backend, deploy it in a pod and expose it through a service that will be detected by the Tekton Dashboard.

```bash
kubectl apply -n tekton-pipelines -f - <<EOF
kind: Service
apiVersion: v1
metadata:
name: sample-extension
labels:
app: sample-extension
tekton-dashboard-extension: "true"
annotations:
tekton-dashboard-display-name: Hello
tekton-dashboard-endpoints: sample
spec:
ports:
- port: 3000
targetPort: 3000
selector:
app: sample-extension
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-extension
labels:
app: sample-extension
spec:
replicas: 1
selector:
matchLabels:
app: sample-extension
template:
metadata:
labels:
app: sample-extension
spec:
containers:
- name: node
image: node:latest
ports:
- containerPort: 3000
command:
- bash
args:
- -c
- |
cat <<EOF > server.js
const express = require('express');
const app = express();
app.get('/sample', (req, res) => res.send('Hello Tekton Dashboard !'));
app.listen(3000, '0.0.0.0');
EOF
npm install express
node ./server.js
EOF
```

The command above does the following:
- create a deployment where the `express` web server is installed in a `nodejs` container
- start the web server and serve the `/sample` endpoint returning the `'Hello Tekton Dashboard !'` message when hit
- create a service exposing the deployment
- the `tekton-dashboard-extension: "true"` label lets the Tekton Dashboard know about the service based extension
- the `tekton-dashboard-display-name: Hello` annotation make the extension appear in the side nav under the `Hello` name
- the `tekton-dashboard-endpoints: sample` annotation allows proxying requests to the extension by the Tekton Dashboard

You can verify that the extension backend is working by hitting the path `/v1/extensions/sample-extension/sample` and checking for the `Hello Tekton Dashboard !` message.

![Service based extension backend](./extensions-service-based-backend.png)

The error message happens because the extension doesn't have frontend code yet, [next step](#example-add-and-serve-frontend-code) will guide you through adding frontend code to your extension.

### Example: Add and serve frontend code

The Tekton Dashboard is developed using [React](https://reactjs.org/), therefore the extension frontend code will need to use React too.

All you need to do is provide an ES module exporting your `Component`, this component will be loaded at runtime by the Tekton Dashboard host and injected into the page.

For this to work, you will need to create your component using the shared objects provided by the host, namely you won't `import React, { Component } from 'react';` but use `window.React` instead.
This can be done manually or using an ES module bundler such as [Rollup](https://rollupjs.org/) to transform your imports.

To deploy the service based extension with frontend code run the following command:

```bash
kubectl apply -n tekton-pipelines -f - <<EOF
kind: Service
apiVersion: v1
metadata:
name: sample-extension
labels:
app: sample-extension
tekton-dashboard-extension: "true"
annotations:
tekton-dashboard-display-name: Hello
tekton-dashboard-endpoints: sample.bundle
tekton-dashboard-bundle-location: bundle
spec:
ports:
- port: 3000
targetPort: 3000
selector:
app: sample-extension
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-extension
labels:
app: sample-extension
spec:
replicas: 1
selector:
matchLabels:
app: sample-extension
template:
metadata:
labels:
app: sample-extension
spec:
containers:
- name: node
image: node:latest
ports:
- containerPort: 3000
command:
- bash
args:
- -c
- |
cat <<EOF > frontend.js
const React = window.React;
class Extension extends React.Component {
state = {
message: 'Loading ...'
};
componentDidMount() {
fetch('/v1/extensions/sample-extension/sample')
.then(response => response.text())
.then(message => this.setState({ message }));
};
render() {
const { message } = this.state;
return React.createElement("h1", null, message);
}
}
export default Extension;
EOF
cat <<EOF > server.js
const express = require('express');
const path = require('path');
const app = express();
app.get('/sample', (req, res) => res.send('Hello Tekton Dashboard !'));
app.get('/bundle', (req, res) => res.sendFile(path.resolve(__dirname, './frontend.js')));
app.listen(3000, '0.0.0.0');
EOF
npm install express
node ./server.js
EOF
```

You can see from the code above that the `Service` annotations changed:
- `tekton-dashboard-endpoints: sample.bundle` means that both `sample` and `bundle` endpoints exist on the extension
- `tekton-dashboard-bundle-location: bundle` means that the frontend code will be loaded at the `/bundle` path

In the extension `Pod`, a new `frontend.js` file is generated containing the frontend code and the `/bundle` route is registered in the `express` server to serve the frontend js file.

**NOTE:** In a real extension the frontend code would be written and bundled separately, devs aren't expected to inline an ES module into the extension resource and are free to use additional libraries if they wish. The extension API only requires that they provide an ES module that exposes a React component for the Tekton Dashboard to load.

Once the extension frontend code is injected in the page, it will call `/sample` backend endpoint to fetch a message and will render the obtained message in the Dashboard UI.

The complete extension looks something like this:

![Service based extension frontend](./extensions-service-based-frontend.png)

---

Except as otherwise noted, the content of this page is licensed under the [Creative Commons Attribution 4.0 License](https://creativecommons.org/licenses/by/4.0/).
Expand Down
17 changes: 7 additions & 10 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ module github.com/tektoncd/dashboard

go 1.15

// Pin k8s deps to v0.19.7
// Pin k8s deps to v0.20.7
replace (
k8s.io/api => k8s.io/api v0.19.7
k8s.io/apimachinery => k8s.io/apimachinery v0.19.7
k8s.io/client-go => k8s.io/client-go v0.19.7
k8s.io/code-generator => k8s.io/code-generator v0.19.7
k8s.io/api => k8s.io/api v0.20.7
k8s.io/apimachinery => k8s.io/apimachinery v0.20.7
k8s.io/client-go => k8s.io/client-go v0.20.7
k8s.io/code-generator => k8s.io/code-generator v0.20.7
k8s.io/gengo => k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027 // indirect
)

require (
github.com/emicklei/go-restful v2.12.0+incompatible
github.com/gorilla/websocket v1.4.2
github.com/imdario/mergo v0.3.9 // indirect
github.com/kr/text v0.2.0 // indirect
Expand All @@ -21,14 +20,12 @@ require (
github.com/onsi/gomega v1.9.0 // indirect
github.com/tektoncd/plumbing v0.0.0-20210514044347-f8a9689d5bd5
go.uber.org/zap v1.15.0
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect
google.golang.org/appengine v1.6.6 // indirect
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
honnef.co/go/tools v0.0.1-2020.1.4 // indirect
k8s.io/api v0.19.7
k8s.io/apimachinery v0.19.7
k8s.io/api v0.20.7
k8s.io/apimachinery v0.20.7
k8s.io/client-go v11.0.1-0.20190805182717-6502b5e7b1b5+incompatible
k8s.io/code-generator v0.19.7
knative.dev/pkg v0.0.0-20200702222342-ea4d6e985ba0
Expand Down
Loading

0 comments on commit 32c360c

Please sign in to comment.