From 32c360ce20ec3ce96a6534c51b5d387635f2a204 Mon Sep 17 00:00:00 2001 From: Alan Greene Date: Sat, 3 Jul 2021 01:26:29 +0100 Subject: [PATCH] Update proxy to support websockets 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. --- cmd/dashboard/main.go | 38 +-- docs/dev/api.md | 10 - docs/extensions.md | 207 +----------- go.mod | 17 +- go.sum | 85 +++-- pkg/controllers/controller.go | 6 +- pkg/controllers/kubernetes/extension.go | 125 -------- pkg/endpoints/cluster.go | 28 +- pkg/endpoints/externallogs.go | 23 +- pkg/endpoints/health.go | 6 +- pkg/endpoints/health_test.go | 32 -- pkg/endpoints/websocket.go | 7 +- pkg/endpoints/websocket_test.go | 135 -------- pkg/router/router.go | 409 ++++++++---------------- pkg/router/routes_test.go | 352 -------------------- pkg/testutils/testutils.go | 144 --------- pkg/utils/utils.go | 50 +-- pkg/websocket/websocket.go | 8 +- src/api/extensions.js | 45 +-- src/api/extensions.test.js | 14 +- src/reducers/extensions.js | 21 +- src/reducers/extensions.test.js | 47 +-- webpack.dev.js | 12 - 23 files changed, 274 insertions(+), 1547 deletions(-) delete mode 100644 pkg/controllers/kubernetes/extension.go delete mode 100644 pkg/endpoints/health_test.go delete mode 100644 pkg/router/routes_test.go diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index 1184d1e98..39a6775b1 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -14,7 +14,6 @@ limitations under the License. package main import ( - "context" "flag" "fmt" "net/http" @@ -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" @@ -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) } diff --git a/docs/dev/api.md b/docs/dev/api.md index 1947283c1..a9bcf55b3 100644 --- a/docs/dev/api.md +++ b/docs/dev/api.md @@ -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 diff --git a/docs/extensions.md b/docs/extensions.md index e437e878d..5ea3a768e 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -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 @@ -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 - < 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 - < 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 < 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/). diff --git a/go.mod b/go.mod index ad13a1709..04aa0caf9 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 diff --git a/go.sum b/go.sum index 03b36cf29..c8f87b8af 100644 --- a/go.sum +++ b/go.sum @@ -13,9 +13,9 @@ cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTj cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.47.0/go.mod h1:5p3Ky/7f3N10VBkhuR5LFtddroTiMyjZV/Kj5qOQFxU= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.55.0/go.mod h1:ZHmoY+/lIMNkN2+fBmuTiqZ4inFhvQad8ft7MT8IV5Y= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= @@ -54,26 +54,35 @@ github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxw github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v12.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.2.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0= github.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= +github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= github.com/Azure/go-autorest/autorest/adal v0.1.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E= github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= +github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= +github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc= github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA= github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8= github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -231,8 +240,6 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful v2.12.0+incompatible h1:SIvoTSbsMEwuM3dzFirLwKc4BH6VXP5CNf+G1FfJVr4= -github.com/emicklei/go-restful v2.12.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -249,6 +256,7 @@ github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwo github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.8.1/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= @@ -340,6 +348,8 @@ github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5 github.com/gogo/protobuf v1.2.2-0.20190730201129-28a6bbf47e48/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -354,6 +364,7 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -368,8 +379,8 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho= @@ -384,6 +395,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-containerregistry v0.0.0-20200115214256-379933c9c22b/go.mod h1:Wtl/v6YdQxv397EREtzwgd9+Ud7Q5D8XMbi3Zazgkrs= github.com/google/go-containerregistry v0.0.0-20200123184029-53ce695e4179/go.mod h1:Wtl/v6YdQxv397EREtzwgd9+Ud7Q5D8XMbi3Zazgkrs= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= @@ -413,6 +426,8 @@ github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s= github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -506,6 +521,7 @@ github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= @@ -572,6 +588,7 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= @@ -734,6 +751,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tektoncd/pipeline v0.11.0/go.mod h1:hlkH32S92+/UODROH0dmxzyuMxfRFp/Nc3e29MewLn8= @@ -768,6 +787,7 @@ github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSf github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= @@ -830,6 +850,8 @@ golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -902,6 +924,7 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -918,6 +941,7 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -979,12 +1003,14 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1036,15 +1062,19 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200214144324-88be01311a71/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200303214625-2b0b585e22fe/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200317043434-63da46f3035e/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200616133436-c1934b75d054 h1:HHeAlu5H9b71C+Fx0K+1dGgVFN1DM1/wz4aoGOA5qS8= -golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.0.1/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= gomodules.xyz/jsonpatch/v2 v2.1.0/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= @@ -1098,6 +1128,7 @@ google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200317114155-1f3552e48f24/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200326112834-f447254575fd/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= @@ -1127,7 +1158,6 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= @@ -1175,6 +1205,8 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= helm.sh/helm/v3 v3.1.1/go.mod h1:WYsFJuMASa/4XUqLyv54s0U/f3mlAaRErGmyy4z921g= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1187,22 +1219,22 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.19.7 h1:MpHhls03C2pyzoYcpbe4QqYiiZjdvW+tuWq6TbjV14Y= -k8s.io/api v0.19.7/go.mod h1:KTryDUT3l6Mtv7K2J2486PNL9DBns3wOYTkGR+iz63Y= +k8s.io/api v0.20.7 h1:wOEPJ3NoimUfR9v9sAO2JosPiEP9IGFNplf7zZvYzPU= +k8s.io/api v0.20.7/go.mod h1:4x0yErUkcEWYG+O0S4QdrYa2+PLEeY2M7aeQe++2nmk= k8s.io/apiextensions-apiserver v0.17.2/go.mod h1:4KdMpjkEjjDI2pPfBA15OscyNldHWdBCfsWMDWAmSTs= k8s.io/apiextensions-apiserver v0.17.6/go.mod h1:Z3CHLP3Tha+Rbav7JR3S+ye427UaJkHBomK2c4XtZ3A= -k8s.io/apimachinery v0.19.7 h1:nTaEnYVH+i//aPgMA0zTEV2lfVLCV9LextqVd67mulc= -k8s.io/apimachinery v0.19.7/go.mod h1:6sRbGRAVY5DOCuZwB5XkqguBqpqLU6q/kOaOdk29z6Q= +k8s.io/apimachinery v0.20.7 h1:tBfhql7OggSCahvASeEpLRzvxc7FK77wNivi1uXCQWM= +k8s.io/apimachinery v0.20.7/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= k8s.io/apiserver v0.17.0/go.mod h1:ABM+9x/prjINN6iiffRVNCBR2Wk7uY4z+EtEGZD48cg= k8s.io/apiserver v0.17.2/go.mod h1:lBmw/TtQdtxvrTk0e2cgtOxHizXI+d0mmGQURIHQZlo= k8s.io/apiserver v0.17.6/go.mod h1:sAYqm8hUDNA9aj/TzqwsJoExWrxprKv0tqs/z88qym0= k8s.io/cli-runtime v0.17.2/go.mod h1:aa8t9ziyQdbkuizkNLAw3qe3srSyWh9zlSB7zTqRNPI= k8s.io/cli-runtime v0.17.3/go.mod h1:X7idckYphH4SZflgNpOOViSxetiMj6xI0viMAjM81TA= -k8s.io/client-go v0.19.7 h1:SoJ4mzZ9LyXBGDe8MmpMznw0CwQ1ITWgsmG7GixvhUU= -k8s.io/client-go v0.19.7/go.mod h1:iytGI7S3kmv6bWnn+bSQUE4VlrEi4YFssvVB7J7Hvqg= +k8s.io/client-go v0.20.7 h1:Ot22456XfYAWrCWddw/quevMrFHqP7s1qT499FoumVU= +k8s.io/client-go v0.20.7/go.mod h1:uGl3qh/Jy3cTF1nDoIKBqUZlRWnj/EM+/leAXETKRuA= k8s.io/cloud-provider v0.17.0/go.mod h1:Ze4c3w2C0bRsjkBUoHpFi+qWe3ob1wI2/7cUn+YQIDE= -k8s.io/code-generator v0.19.7 h1:kM/68Y26Z/u//TFc1ggVVcg62te8A2yQh57jBfD0FWQ= -k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0= +k8s.io/code-generator v0.20.7 h1:iXz1ME6EQqoCkLefa7bcniKHu0SzgbxsFV1RlBcfypc= +k8s.io/code-generator v0.20.7/go.mod h1:i6FmG+QxaLxvJsezvZp0q/gAEzzOz3U53KFibghWToU= k8s.io/component-base v0.17.0/go.mod h1:rKuRAokNMY2nn2A6LP/MiwpoaMRHpfRnrPaUJJj1Yoc= k8s.io/component-base v0.17.2/go.mod h1:zMPW3g5aH7cHJpKYQ/ZsGMcgbsA/VyhEugF3QT1awLs= k8s.io/component-base v0.17.6/go.mod h1:jgRLWl0B0rOzFNtxQ9E4BphPmDqoMafujdau6AdG2Xo= @@ -1216,19 +1248,23 @@ k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ= +k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= k8s.io/kube-openapi v0.0.0-20200410145947-bcb3869e6f29/go.mod h1:F+5wygcW0wmRTnM3cOgIqGivxkwSWIWT5YdsDbeAOaU= -k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6 h1:+WnxoVtG8TMiudHBSEtrVL1egv36TkkJm+bA8AxicmQ= -k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= +k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd h1:sOHNzJIkytDF6qadMNKhhDRpc6ODik8lVC6nOur7B2c= +k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= +k8s.io/kubectl v0.17.2 h1:QZR8Q6lWiVRjwKslekdbN5WPMp53dS/17j5e+oi5XVU= k8s.io/kubectl v0.17.2/go.mod h1:y4rfLV0n6aPmvbRCqZQjvOp3ezxsFgpqL+zF5jH/lxk= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/legacy-cloud-providers v0.17.0/go.mod h1:DdzaepJ3RtRy+e5YhNtrCYwlgyK87j/5+Yfp0L9Syp8= k8s.io/metrics v0.17.2/go.mod h1:3TkNHET4ROd+NfzNxkjoVfQ0Ob4iZnaHmSEA4vYpwLw= k8s.io/test-infra v0.0.0-20200514184223-ba32c8aae783/go.mod h1:bW6thaPZfL2hW7ecjx2WYwlP9KQLM47/xIJyttkVk5s= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20200124190032-861946025e34 h1:HjlUD6M0K3P8nRXmr2B9o4F9dUy9TCj/aEpReeyi6+k= k8s.io/utils v0.0.0-20200124190032-861946025e34/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= -k8s.io/utils v0.0.0-20200729134348-d5654de09c73 h1:uJmqzgNWG7XyClnU/mLPBWwfKKF1K8Hf8whTseBgJcg= -k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920 h1:CbnUZsM497iRC5QMVkHwyl8s2tB3g7yaSHkYPkpgelw= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= knative.dev/caching v0.0.0-20200116200605-67bca2c83dfa/go.mod h1:dHXFU6CGlLlbzaWc32g80cR92iuBSpsslDNBWI8C7eg= knative.dev/eventing-contrib v0.11.2/go.mod h1:SnXZgSGgMSMLNFTwTnpaOH7hXDzTFtw0J8OmHflNx3g= knative.dev/pkg v0.0.0-20200207155214-fef852970f43/go.mod h1:pgODObA1dTyhNoFxPZTTjNWfx6F0aKsKzn+vaT9XO/Q= @@ -1249,8 +1285,9 @@ sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:w sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06 h1:zD2IemQ4LmOcAumeiyDWXKUI2SO0NYDe3H6QGvPOVgU= sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18= sigs.k8s.io/structured-merge-diff/v2 v2.0.1/go.mod h1:Wb7vfKAodbKgf6tn1Kl0VvGj7mRH6DGaRcixXEJXTsE= -sigs.k8s.io/structured-merge-diff/v4 v4.0.1 h1:YXTMot5Qz/X1iBRJhAt+vI+HVttY0WkSqqhKxQ0xVbA= -sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.3 h1:4oyYo8NREp49LBBhKxEqCulFjg26rawYKrnCmg+Sr6c= +sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/pkg/controllers/controller.go b/pkg/controllers/controller.go index 50f6dd122..1aeba48eb 100644 --- a/pkg/controllers/controller.go +++ b/pkg/controllers/controller.go @@ -23,7 +23,6 @@ import ( tektoncontroller "github.com/tektoncd/dashboard/pkg/controllers/tekton" triggerscontroller "github.com/tektoncd/dashboard/pkg/controllers/triggers" "github.com/tektoncd/dashboard/pkg/logging" - "github.com/tektoncd/dashboard/pkg/router" "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/dynamicinformer" k8sinformers "k8s.io/client-go/informers" @@ -48,16 +47,13 @@ func StartTektonControllers(clientset dynamic.Interface, resyncDur time.Duration tenantInformerFactory.Start(stopCh) } -func StartKubeControllers(clientset k8sclientset.Interface, resyncDur time.Duration, tenantNamespace string, readOnly bool, handler *router.Handler, stopCh <-chan struct{}) { +func StartKubeControllers(clientset k8sclientset.Interface, resyncDur time.Duration, tenantNamespace string, readOnly bool, stopCh <-chan struct{}) { logging.Log.Info("Creating Kube controllers") clusterInformerFactory := k8sinformers.NewSharedInformerFactory(clientset, resyncDur) tenantInformerFactory := k8sinformers.NewSharedInformerFactoryWithOptions(clientset, resyncDur, k8sinformers.WithNamespace(tenantNamespace)) if tenantNamespace == "" { - kubecontroller.NewExtensionController(clusterInformerFactory, handler) kubecontroller.NewNamespaceController(clusterInformerFactory) - } else { - kubecontroller.NewExtensionController(tenantInformerFactory, handler) } if !readOnly { kubecontroller.NewServiceAccountController(tenantInformerFactory) diff --git a/pkg/controllers/kubernetes/extension.go b/pkg/controllers/kubernetes/extension.go deleted file mode 100644 index d40073564..000000000 --- a/pkg/controllers/kubernetes/extension.go +++ /dev/null @@ -1,125 +0,0 @@ -/* -Copyright 2019-2021 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. -*/ -package kubernetes - -import ( - "github.com/tektoncd/dashboard/pkg/broadcaster" - "github.com/tektoncd/dashboard/pkg/endpoints" - "github.com/tektoncd/dashboard/pkg/logging" - "github.com/tektoncd/dashboard/pkg/router" - "github.com/tektoncd/dashboard/pkg/utils" - v1 "k8s.io/api/core/v1" - k8sinformer "k8s.io/client-go/informers" - "k8s.io/client-go/tools/cache" -) - -// extensionHandler is a wrapper around the router Handler so that it can be -// used within informer handler functions -type extensionHandler struct { - *router.Handler -} - -// NewExtensionController registers the K8s shared informer that reacts to -// extension service updates -func NewExtensionController(sharedK8sInformerFactory k8sinformer.SharedInformerFactory, handler *router.Handler) { - logging.Log.Debug("In NewExtensionController") - h := extensionHandler{ - Handler: handler, - } - extensionServiceInformer := sharedK8sInformerFactory.Core().V1().Services().Informer() - // ResourceEventHandler interface functions only pass object interfaces - // Inlined functions to keep mux in scope rather than reconciler - extensionServiceInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ - AddFunc: h.serviceCreated, - UpdateFunc: h.serviceUpdated, - DeleteFunc: h.serviceDeleted, - }) -} - -func (e extensionHandler) serviceCreated(obj interface{}) { - service := obj.(*v1.Service) - if value := service.Labels[router.ExtensionLabelKey]; value == router.ExtensionLabelValue && service.Spec.ClusterIP != "" { - logging.Log.Debugf("Extension Controller detected extension '%s' created", service.Name) - ext := e.RegisterExtension(service) - data := broadcaster.SocketData{ - Kind: "ServiceExtension", - Operation: broadcaster.Created, - Payload: ext, - } - endpoints.ResourcesChannel <- data - } -} - -func (e extensionHandler) serviceUpdated(oldObj, newObj interface{}) { - oldService, newService := oldObj.(*v1.Service), newObj.(*v1.Service) - // 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 - var event string - var ext *router.Extension - 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) - ext = 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) - ext = e.RegisterExtension(newService) - if len(event) != 0 { - event = "update" - } else { - event = "create" - } - } - if ext != nil { - switch event { - case "delete": // Service has removed the extension label - data := broadcaster.SocketData{ - Kind: "ServiceExtension", - Operation: broadcaster.Deleted, - Payload: ext, - } - endpoints.ResourcesChannel <- data - case "create": // Service has added the extension label - data := broadcaster.SocketData{ - Kind: "ServiceExtension", - Operation: broadcaster.Created, - Payload: ext, - } - endpoints.ResourcesChannel <- data - case "update": // Extension service was modified - data := broadcaster.SocketData{ - Kind: "ServiceExtension", - Operation: broadcaster.Updated, - Payload: ext, - } - endpoints.ResourcesChannel <- data - } - } -} - -func (e extensionHandler) serviceDeleted(obj interface{}) { - serviceMeta := utils.GetDeletedObjectMeta(obj) - if value := serviceMeta.GetLabels()[router.ExtensionLabelKey]; value == router.ExtensionLabelValue { - logging.Log.Debugf("Extension Controller detected extension '%s' deleted", serviceMeta.GetName()) - if serviceMeta.GetUID() != "" { - ext := e.UnregisterExtensionByMeta(serviceMeta) - data := broadcaster.SocketData{ - Kind: "ServiceExtension", - Operation: broadcaster.Deleted, - Payload: ext, - } - endpoints.ResourcesChannel <- data - } - } -} diff --git a/pkg/endpoints/cluster.go b/pkg/endpoints/cluster.go index e322c3f63..867a239b1 100644 --- a/pkg/endpoints/cluster.go +++ b/pkg/endpoints/cluster.go @@ -14,11 +14,8 @@ limitations under the License. package endpoints import ( + "encoding/json" "net/http" - "net/url" - - restful "github.com/emicklei/go-restful" - "github.com/tektoncd/dashboard/pkg/utils" ) // Properties : properties we want to be able to retrieve via REST @@ -36,25 +33,10 @@ type Properties struct { TriggersVersion string `json:"triggersVersion,omitempty"` } -// ProxyRequest does as the name suggests: proxies requests and logs what's going on -func (r Resource) ProxyRequest(request *restful.Request, response *restful.Response) { - parsedURL, err := url.Parse(request.Request.URL.String()) - if err != nil { - utils.RespondError(response, err, http.StatusNotFound) - return - } - - uri := request.PathParameter("subpath") + "?" + parsedURL.RawQuery - - if statusCode, err := utils.Proxy(request.Request, response, r.Config.Host+"/"+uri, r.HttpClient); err != nil { - utils.RespondError(response, err, statusCode) - } -} - // GetProperties is used to get the installed namespace for the Dashboard, // the version of the Tekton Dashboard, the version of Tekton Pipelines, // when one's in read-only mode and Tekton Triggers version (if Installed) -func (r Resource) GetProperties(request *restful.Request, response *restful.Response) { +func (r Resource) GetProperties(response http.ResponseWriter, request *http.Request) { pipelineNamespace := r.Options.GetPipelinesNamespace() triggersNamespace := r.Options.GetTriggersNamespace() dashboardVersion := getDashboardVersion(r, r.Options.InstallNamespace) @@ -83,5 +65,9 @@ func (r Resource) GetProperties(request *restful.Request, response *restful.Resp properties.TriggersVersion = triggersVersion } - response.WriteEntity(properties) + response.Header().Set("Content-Type", "application/json") + response.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + response.Header().Set("Pragma", "no-cache") + response.Header().Set("Expires", "0") + json.NewEncoder(response).Encode(properties) } diff --git a/pkg/endpoints/externallogs.go b/pkg/endpoints/externallogs.go index 8ad2d4e0b..c8f4e5429 100644 --- a/pkg/endpoints/externallogs.go +++ b/pkg/endpoints/externallogs.go @@ -1,23 +1,36 @@ +/* +Copyright 2019-2021 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. +*/ + package endpoints import ( "net/http" "net/url" + "strings" - restful "github.com/emicklei/go-restful" "github.com/tektoncd/dashboard/pkg/utils" ) -func (r Resource) LogsProxy(request *restful.Request, response *restful.Response) { - parsedURL, err := url.Parse(request.Request.URL.String()) +func (r Resource) LogsProxy(response http.ResponseWriter, request *http.Request) { + parsedURL, err := url.Parse(request.URL.String()) if err != nil { utils.RespondError(response, err, http.StatusNotFound) return } - uri := request.PathParameter("subpath") + "?" + parsedURL.RawQuery + uri := strings.TrimPrefix(request.URL.Path, "/v1/logs-proxy") + "?" + parsedURL.RawQuery - if statusCode, err := utils.Proxy(request.Request, response, r.Options.ExternalLogsURL+"/"+uri, http.DefaultClient); err != nil { + if statusCode, err := utils.Proxy(request, response, r.Options.ExternalLogsURL+"/"+uri, http.DefaultClient); err != nil { utils.RespondError(response, err, statusCode) } } diff --git a/pkg/endpoints/health.go b/pkg/endpoints/health.go index cdae2ee04..499778a40 100644 --- a/pkg/endpoints/health.go +++ b/pkg/endpoints/health.go @@ -1,5 +1,5 @@ /* -Copyright 2019 The Tekton Authors +Copyright 2019-2021 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 @@ -15,11 +15,9 @@ package endpoints import ( "net/http" - - restful "github.com/emicklei/go-restful" ) -func (r Resource) CheckHealth(request *restful.Request, response *restful.Response) { +func (r Resource) CheckHealth(response http.ResponseWriter, request *http.Request) { // A method here so there's scope for doing anything fancy e.g. checking anything else response.WriteHeader(http.StatusOK) } diff --git a/pkg/endpoints/health_test.go b/pkg/endpoints/health_test.go deleted file mode 100644 index 9792af845..000000000 --- a/pkg/endpoints/health_test.go +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright 2019 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. -*/ -package endpoints_test - -import ( - "fmt" - "github.com/tektoncd/dashboard/pkg/testutils" - "net/http" - "testing" -) - -// GET health + readiness -func TestGETHealthEndpoint(t *testing.T) { - server, _, _ := testutils.DummyServer() - defer server.Close() - httpReq := testutils.DummyHTTPRequest("GET", fmt.Sprintf("%s/health", server.URL), nil) - response, _ := http.DefaultClient.Do(httpReq) - expectedStatus := http.StatusOK - if response.StatusCode != expectedStatus { - t.Fatalf("Health check failed: expected statusCode %d, actual %d", expectedStatus, response.StatusCode) - } -} diff --git a/pkg/endpoints/websocket.go b/pkg/endpoints/websocket.go index 0985b316f..31d19d5c9 100644 --- a/pkg/endpoints/websocket.go +++ b/pkg/endpoints/websocket.go @@ -1,5 +1,5 @@ /* -Copyright 2019 The Tekton Authors +Copyright 2019-2021 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 @@ -14,7 +14,8 @@ limitations under the License. package endpoints import ( - restful "github.com/emicklei/go-restful" + "net/http" + broadcaster "github.com/tektoncd/dashboard/pkg/broadcaster" logging "github.com/tektoncd/dashboard/pkg/logging" "github.com/tektoncd/dashboard/pkg/websocket" @@ -27,7 +28,7 @@ var ResourcesChannel = make(chan broadcaster.SocketData) var ResourcesBroadcaster = broadcaster.NewBroadcaster(ResourcesChannel) // Establish websocket and subscribe to pipelinerun events -func (r Resource) EstablishResourcesWebsocket(request *restful.Request, response *restful.Response) { +func (r Resource) EstablishResourcesWebsocket(response http.ResponseWriter, request *http.Request) { connection, err := websocket.UpgradeToWebsocket(request, response) if err != nil { logging.Log.Errorf("Could not upgrade to websocket connection: %s", err) diff --git a/pkg/endpoints/websocket_test.go b/pkg/endpoints/websocket_test.go index 86ee40a79..32bac7e70 100644 --- a/pkg/endpoints/websocket_test.go +++ b/pkg/endpoints/websocket_test.go @@ -16,26 +16,19 @@ import ( "context" "crypto/tls" "encoding/json" - "fmt" - "net/url" "strings" - "sync" "sync/atomic" "testing" "time" - "strconv" - gorillaSocket "github.com/gorilla/websocket" "github.com/tektoncd/dashboard/pkg/broadcaster" . "github.com/tektoncd/dashboard/pkg/endpoints" - "github.com/tektoncd/dashboard/pkg/router" "github.com/tektoncd/dashboard/pkg/testutils" "github.com/tektoncd/dashboard/pkg/websocket" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" ) type informerRecord struct { @@ -82,91 +75,6 @@ func (i *informerRecord) Delete() int32 { return atomic.LoadInt32(&i.delete) } -// Ensures all resource types sent over websocket are received as intended -func TestWebsocketResources(t *testing.T) { - t.Log("Enter TestLogWebsocket...") - server, r, installNamespace := testutils.DummyServer() - defer server.Close() - - devopsServer := strings.TrimPrefix(server.URL, "http://") - websocketURL := url.URL{Scheme: "ws", Host: devopsServer, Path: "/v1/websockets/resources"} - websocketEndpoint := websocketURL.String() - const clients int = 5 - connectionDur := time.Second * 5 - var wg sync.WaitGroup - - // CUD records - taskRecord := NewInformerRecord("Task", true) - clusterTaskRecord := NewInformerRecord("ClusterTask", true) - extensionRecord := NewInformerRecord("ServiceExtension", true) - // CD records - namespaceRecord := NewInformerRecord("Namespace", false) - - // Route incoming socket data to correct informer - recordMap := map[string]*informerRecord{ - taskRecord.CRD: &taskRecord, - clusterTaskRecord.CRD: &clusterTaskRecord, - namespaceRecord.CRD: &namespaceRecord, - extensionRecord.CRD: &extensionRecord, - } - - for i := 1; i <= clients; i++ { - websocketChan := clientWebsocket(websocketEndpoint, connectionDur, t) - // Wait until connection timeout - go func() { - defer wg.Done() - for { - socketData, open := <-websocketChan - if !open { - return - } - informerRecord := recordMap[socketData.Kind] - informerRecord.Handle(socketData.Operation) - } - }() - wg.Add(1) - } - awaitAllClients := func() bool { - return ResourcesBroadcaster.PoolSize() == clients - } - // Wait until all broadcaster has registered all clients - awaitFatal(awaitAllClients, t, fmt.Sprintf("Expected %d clients within pool", clients)) - - // CUD/CD methods should create a single informer event for each type (Create|Update|Delete) - // Create, Update, and Delete records - CUDTasks(r, t, installNamespace) - CUDClusterTasks(r, t) - CUDExtensions(r, t, installNamespace) - // Create and Delete records - CDNamespaces(r, t) - // Wait until connections terminate and all subscribers have been removed from pool - // This is our synchronization point to compare against each informerRecord - t.Log("Waiting for clients to terminate...") - wg.Wait() - awaitNoClients := func() bool { - return ResourcesBroadcaster.PoolSize() == 0 - } - awaitFatal(awaitNoClients, t, "Pool should be empty") - - // Check that all fields have been populated - for _, informerRecord := range recordMap { - t.Log(informerRecord) - creates := int(informerRecord.Create()) - updates := int(informerRecord.Update()) - deletes := int(informerRecord.Delete()) - // records without an update hook/informer - if updates == -1 { - if creates != clients || creates != deletes { - t.Fatalf("CD informer %s creates[%d] and deletes[%d] not equal expected to value: %d\n", informerRecord.CRD, creates, deletes, clients) - } - } else { - if creates != clients || creates != deletes || creates != updates { - t.Fatalf("CUD informer %s creates[%d], updates[%d] and deletes[%d] not equal to expected value: %d\n", informerRecord.CRD, creates, updates, deletes, clients) - } - } - } -} - // Abstract connection into a channel of broadcaster.SocketData // Closed channel = closed connection func clientWebsocket(websocketEndpoint string, readDeadline time.Duration, t *testing.T) <-chan broadcaster.SocketData { @@ -288,49 +196,6 @@ func CUDClusterTasks(r *Resource, t *testing.T) { } } -func CUDExtensions(r *Resource, t *testing.T, namespace string) { - resourceVersion := "1" - - extensionService := corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: "extension", - ResourceVersion: resourceVersion, - UID: types.UID(strconv.FormatInt(time.Now().UnixNano(), 10)), - Labels: map[string]string{ - router.ExtensionLabelKey: router.ExtensionLabelValue, - }, - }, - Spec: corev1.ServiceSpec{ - ClusterIP: "127.0.0.1", - Ports: []corev1.ServicePort{ - { - Port: int32(1234), - }, - }, - }, - } - - t.Log("Creating extensionService") - _, err := r.K8sClient.CoreV1().Services(namespace).Create(context.TODO(), &extensionService, metav1.CreateOptions{}) - if err != nil { - t.Fatalf("Error creating extensionService: %s: %s\n", extensionService.Name, err.Error()) - } - - newVersion := "2" - extensionService.ResourceVersion = newVersion - t.Log("Updating extensionService") - _, err = r.K8sClient.CoreV1().Services(namespace).Update(context.TODO(), &extensionService, metav1.UpdateOptions{}) - if err != nil { - t.Fatalf("Error updating extensionService: %s: %s\n", extensionService.Name, err.Error()) - } - - t.Log("Deleting extensionService") - err = r.K8sClient.CoreV1().Services(namespace).Delete(context.TODO(), extensionService.Name, metav1.DeleteOptions{}) - if err != nil { - t.Fatalf("Error deleting extensionService: %s: %s\n", extensionService.Name, err.Error()) - } -} - // CD functions func CDNamespaces(r *Resource, t *testing.T) { diff --git a/pkg/router/router.go b/pkg/router/router.go index 22336d7cf..45f992f84 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -15,208 +15,32 @@ package router import ( "fmt" + "net" "net/http" - "net/http/httputil" "net/url" "regexp" - "strconv" "strings" - "sync" + "time" - restful "github.com/emicklei/go-restful" + "github.com/tektoncd/dashboard/pkg/csrf" "github.com/tektoncd/dashboard/pkg/endpoints" logging "github.com/tektoncd/dashboard/pkg/logging" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilnet "k8s.io/apimachinery/pkg/util/net" + "k8s.io/apimachinery/pkg/util/proxy" + "k8s.io/client-go/rest" + "k8s.io/client-go/transport" ) -// ExtensionLabelKey is the label key required by services to be registered as a -// dashboard extension -const ExtensionLabelKey = "tekton-dashboard-extension" - -// ExtensionLabelValue is the label value required by services to be registered -// as a dashboard extension -const ExtensionLabelValue = "true" - -// ExtensionURLKey specifies the valid extension paths, defaults to "/" -const ExtensionURLKey = "tekton-dashboard-endpoints" - -// ExtensionEndpointDelimiter is the Delimiter to be used between the extension -// endpoints annotation value -const ExtensionEndpointDelimiter = "." - -// ExtensionBundleLocationKey IS the UI bundle location annotation key -const ExtensionBundleLocationKey = "tekton-dashboard-bundle-location" - -// ExtensionDisplayNameKey is the display name annotation key -const ExtensionDisplayNameKey = "tekton-dashboard-display-name" - -// ExtensionRoot is the URL root when accessing extensions -const ExtensionRoot = "/v1/extensions" - const webResourcesDir = "/var/run/ko" var webResourcesStaticPattern = regexp.MustCompile("^/([[:alnum:]]+\\.)?[[:alnum:]]+\\.(js)|(css)|(png)$") var webResourcesStaticExcludePattern = regexp.MustCompile("^/favicon.png$") -// Register returns an HTTP handler that has the Dashboard REST API registered -func Register(resource endpoints.Resource) *Handler { - logging.Log.Info("Registering all endpoints") - h := &Handler{ - Container: restful.NewContainer(), - uidExtensionMap: make(map[string]*Extension), - } - - registerWeb(resource, h.Container) - registerPropertiesEndpoint(resource, h.Container) - registerWebsocket(resource, h.Container) - registerHealthProbe(resource, h.Container) - registerReadinessProbe(resource, h.Container) - registerKubeAPIProxy(resource, h.Container) - registerLogsProxy(resource, h.Container) - h.registerExtensions() - return h -} - -// Handler is an HTTP handler with internal configuration to avoid global state -type Handler struct { - *restful.Container - // extensionWebService is the exposed dynamic route webservice that - // extensions are added to - extensionWebService *restful.WebService - uidExtensionMap map[string]*Extension - sync.RWMutex -} - -// RegisterExtension registers a discovered extension service as a webservice -// to the container/mux. The extension should have a unique name -func (h *Handler) RegisterExtension(extensionService *corev1.Service) *Extension { - logging.Log.Infof("Adding Extension %s", extensionService.Name) - - ext := newExtension(extensionService) - h.Lock() - defer h.Unlock() - // Add routes for extension service - for _, path := range ext.endpoints { - extensionPath := extensionPath(ext.Name, path) - // Routes - h.extensionWebService.Route(h.extensionWebService.GET(extensionPath).To(ext.handleExtension)) - h.extensionWebService.Route(h.extensionWebService.POST(extensionPath).To(ext.handleExtension)) - h.extensionWebService.Route(h.extensionWebService.PUT(extensionPath).To(ext.handleExtension)) - h.extensionWebService.Route(h.extensionWebService.DELETE(extensionPath).To(ext.handleExtension)) - // Subroutes - h.extensionWebService.Route(h.extensionWebService.GET(extensionPath + "/{var:*}").To(ext.handleExtension)) - h.extensionWebService.Route(h.extensionWebService.POST(extensionPath + "/{var:*}").To(ext.handleExtension)) - h.extensionWebService.Route(h.extensionWebService.PUT(extensionPath + "/{var:*}").To(ext.handleExtension)) - h.extensionWebService.Route(h.extensionWebService.DELETE(extensionPath + "/{var:*}").To(ext.handleExtension)) - } - h.uidExtensionMap[string(extensionService.UID)] = ext - return ext -} - -// UnregisterExtension unregisters an extension. This should be called BEFORE -// registration of extensionService on informer update -func (h *Handler) UnregisterExtension(extensionService *corev1.Service) *Extension { - return h.UnregisterExtensionByMeta(&extensionService.ObjectMeta) -} - -// UnregisterExtensionByMeta unregisters an extension by its metadata. This -// should be called BEFORE registration of extensionService on informer update -func (h *Handler) UnregisterExtensionByMeta(extensionService metav1.Object) *Extension { - logging.Log.Infof("Removing extension %s", extensionService.GetName()) - h.Lock() - defer h.Unlock() - - // Grab endpoints to remove from service - uid := extensionService.GetUID() - ext := h.uidExtensionMap[string(uid)] - defer delete(h.uidExtensionMap, string(uid)) - for _, path := range ext.endpoints { - extensionPath := extensionPath(ext.Name, path) - fullPath := fmt.Sprintf("%s/%s", h.extensionWebService.RootPath(), extensionPath) - // Routes must be removed individually and should correspond to the above registration - h.extensionWebService.RemoveRoute(fullPath, "GET") - h.extensionWebService.RemoveRoute(fullPath, "POST") - h.extensionWebService.RemoveRoute(fullPath, "PUT") - h.extensionWebService.RemoveRoute(fullPath, "DELETE") - h.extensionWebService.RemoveRoute(fullPath+"/{var:*}", "GET") - h.extensionWebService.RemoveRoute(fullPath+"/{var:*}", "POST") - h.extensionWebService.RemoveRoute(fullPath+"/{var:*}", "PUT") - h.extensionWebService.RemoveRoute(fullPath+"/{var:*}", "DELETE") - } - return ext -} - -// registerExtensions registers the WebService responsible for -// proxying to all extensions and also the endpoint to get all extensions -func (h *Handler) registerExtensions() { - logging.Log.Info("Adding API for Extensions") - extensionWebService := new(restful.WebService) - extensionWebService.SetDynamicRoutes(true) - extensionWebService. - Path(ExtensionRoot). - Consumes(restful.MIME_JSON). - Produces(restful.MIME_JSON) - extensionWebService.Route(extensionWebService.GET("").To(h.getAllExtensions)) - h.Add(extensionWebService) - h.extensionWebService = extensionWebService -} - -type RedactedExtension struct { - Name string `json:"name"` - DisplayName string `json:"displayname"` - BundleLocation string `json:"bundlelocation"` - endpoints []string -} - -// getExtensions gets all of the registered extensions on the handler -func (h *Handler) getExtensions() []RedactedExtension { - h.RLock() - defer h.RUnlock() - - extensions := []RedactedExtension{} - for _, e := range h.uidExtensionMap { - redactedExtension := RedactedExtension{ - Name: e.Name, - DisplayName: e.DisplayName, - BundleLocation: e.BundleLocation, - endpoints: e.endpoints, - } - extensions = append(extensions, redactedExtension) - } - return extensions -} - -// getAllExtensions returns all of the extensions within the install namespace -func (h *Handler) getAllExtensions(request *restful.Request, response *restful.Response) { - logging.Log.Debugf("In getAllExtensions") - extensions := h.getExtensions() - logging.Log.Debugf("Extensions: %+v", extensions) - response.WriteEntity(extensions) -} - -func registerKubeAPIProxy(r endpoints.Resource, container *restful.Container) { - proxy := new(restful.WebService) - proxy.Filter(restful.NoBrowserCacheFilter) - proxy.Consumes(restful.MIME_JSON, "text/plain", "application/json-patch+json"). - Produces(restful.MIME_JSON, "text/plain", "application/json-patch+json"). - Path("/proxy") - - logging.Log.Info("Adding Kube API Proxy") - - proxy.Route(proxy.GET("/{subpath:*}").To(r.ProxyRequest)) - proxy.Route(proxy.POST("/{subpath:*}").To(r.ProxyRequest)) - proxy.Route(proxy.PUT("/{subpath:*}").To(r.ProxyRequest)) - proxy.Route(proxy.DELETE("/{subpath:*}").To(r.ProxyRequest)) - proxy.Route(proxy.PATCH("/{subpath:*}").To(r.ProxyRequest)) - container.Add(proxy) -} - -func registerWeb(resource endpoints.Resource, container *restful.Container) { +func registerWeb(resource endpoints.Resource, mux *http.ServeMux) { logging.Log.Info("Adding Web API") fs := http.FileServer(http.Dir(webResourcesDir)) - container.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if webResourcesStaticPattern.Match([]byte(r.URL.Path)) && !webResourcesStaticExcludePattern.Match([]byte(r.URL.Path)) { // Static resources are immutable and have a content hash in their URL w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") @@ -233,131 +57,156 @@ func registerWeb(resource endpoints.Resource, container *restful.Container) { })) } -// registerWebsocket registers a websocket with which we can send log -// information -func registerWebsocket(r endpoints.Resource, container *restful.Container) { - logging.Log.Info("Adding API for websocket") - wsv2 := new(restful.WebService) - wsv2. - Path("/v1/websockets"). - Consumes(restful.MIME_JSON). - Produces(restful.MIME_JSON) - wsv2.Route(wsv2.GET("/resources").To(r.EstablishResourcesWebsocket)) - container.Add(wsv2) -} - -// registerHealthProbes registers the /health endpoint -func registerHealthProbe(r endpoints.Resource, container *restful.Container) { +// registerHealthProbe registers the /health endpoint +func registerHealthProbe(r endpoints.Resource, mux *http.ServeMux) { logging.Log.Info("Adding API for health") - wsv3 := new(restful.WebService) - wsv3. - Path("/health") + mux.HandleFunc("/health", r.CheckHealth) - wsv3.Route(wsv3.GET("").To(r.CheckHealth)) - - container.Add(wsv3) } -// registerReadinessProbes registers the /readiness endpoint -func registerReadinessProbe(r endpoints.Resource, container *restful.Container) { +// registerReadinessProbe registers the /readiness endpoint +func registerReadinessProbe(r endpoints.Resource, mux *http.ServeMux) { logging.Log.Info("Adding API for readiness") - wsv4 := new(restful.WebService) - wsv4. - Path("/readiness") - - wsv4.Route(wsv4.GET("").To(r.CheckHealth)) + mux.HandleFunc("/readiness", r.CheckHealth) +} - container.Add(wsv4) +// registerWebsocket registers a websocket with which we can send log +// information +func registerWebsocket(r endpoints.Resource, mux *http.ServeMux) { + logging.Log.Info("Adding API for websocket") + mux.HandleFunc("/v1/websockets/resources", r.EstablishResourcesWebsocket) } // registerPropertiesEndpoint adds the endpoint for obtaining any properties we // want to serve. -func registerPropertiesEndpoint(r endpoints.Resource, container *restful.Container) { +func registerPropertiesEndpoint(r endpoints.Resource, mux *http.ServeMux) { logging.Log.Info("Adding API for properties") - wsDefaults := new(restful.WebService) - wsDefaults.Filter(restful.NoBrowserCacheFilter) - wsDefaults. - Path("/v1/properties"). - Consumes(restful.MIME_JSON, "text/plain"). - Produces(restful.MIME_JSON, "text/plain") - - wsDefaults.Route(wsDefaults.GET("/").To(r.GetProperties)) - container.Add(wsDefaults) + mux.HandleFunc("/v1/properties", r.GetProperties) } -func registerLogsProxy(r endpoints.Resource, container *restful.Container) { +func registerLogsProxy(r endpoints.Resource, mux *http.ServeMux) { if r.Options.ExternalLogsURL != "" { - ws := new(restful.WebService) - ws.Path("/v1/logs-proxy").Produces("text/plain") - ws.Route(ws.GET("/{subpath:*}").To(r.LogsProxy)) - container.Add(ws) + logging.Log.Info("Adding API for logs proxy") + mux.HandleFunc("/v1/logs-proxy", r.LogsProxy) } } -// Extension is the back-end representation of an extension. A service is an -// extension when it is in the dashboard namespace with the dashboard label -// key/value pair. Endpoints are specified with the extension URL annotation -type Extension struct { - Name string `json:"name"` - URL *url.URL `json:"url"` - Port string `json:"port"` - DisplayName string `json:"displayname"` - BundleLocation string `json:"bundlelocation"` - endpoints []string +// Server is a http.Handler which proxies Kubernetes APIs to the API server. +type Server struct { + handler http.Handler } -// newExtension returns a new extension -func newExtension(extService *corev1.Service) *Extension { - port := getServicePort(extService) - url, _ := url.ParseRequestURI(fmt.Sprintf("http://%s:%s", extService.Spec.ClusterIP, port)) - return &Extension{ - Name: extService.ObjectMeta.Name, - URL: url, - Port: port, - DisplayName: extService.ObjectMeta.Annotations[ExtensionDisplayNameKey], - BundleLocation: extService.ObjectMeta.Annotations[ExtensionBundleLocationKey], - endpoints: getExtensionEndpoints(extService.ObjectMeta.Annotations[ExtensionURLKey]), +type responder struct{} + +func (r *responder) Error(w http.ResponseWriter, req *http.Request, err error) { + logging.Log.Errorf("Error while proxying request: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) +} + +func makeUpgradeTransport(config *rest.Config, keepalive time.Duration) (proxy.UpgradeRequestRoundTripper, error) { + transportConfig, err := config.TransportConfig() + if err != nil { + return nil, err + } + tlsConfig, err := transport.TLSConfigFor(transportConfig) + if err != nil { + return nil, err + } + rt := utilnet.SetOldTransportDefaults(&http.Transport{ + TLSClientConfig: tlsConfig, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: keepalive, + }).DialContext, + }) + + upgrader, err := transport.HTTPWrappersForConfig(transportConfig, proxy.MirrorRequest) + if err != nil { + return nil, err } + return proxy.NewUpgradeRequestRoundTripper(rt, upgrader), nil } -// handleExtension handles requests to the extension service by stripping the -// extension root prefix from the request URL and reverse proxying -func (e Extension) handleExtension(request *restful.Request, response *restful.Response) { - logging.Log.Debugf("Request Path: %s %+v", request.Request.Method, request.Request.URL.Path) - request.Request.URL.Path = strings.TrimPrefix(request.Request.URL.Path, fmt.Sprintf("%s/%s", ExtensionRoot, e.Name)) - // Explicitly route to root, better visibility in logs - if request.Request.URL.Path == "" { - request.Request.URL.Path = "/" +// Register returns a HTTP handler with the Dashboard and Kubernetes APIs registered +func Register(r endpoints.Resource, cfg *rest.Config) (*Server, error) { + logging.Log.Info("Adding Kube API") + apiProxyPrefix := "/proxy/" + proxyHandler, err := NewProxyHandler(apiProxyPrefix, cfg, 30*time.Second) + if err != nil { + return nil, err } - logging.Log.Debugf("Proxy Path: %s %+v", request.Request.Method, request.Request.URL.Path) - proxy := httputil.NewSingleHostReverseProxy(e.URL) - proxy.ServeHTTP(response, request.Request) + mux := http.NewServeMux() + mux.Handle(apiProxyPrefix, proxyHandler) + + logging.Log.Info("Adding Dashboard APIs") + registerWeb(r, mux) + registerPropertiesEndpoint(r, mux) + registerWebsocket(r, mux) + registerHealthProbe(r, mux) + registerReadinessProbe(r, mux) + registerLogsProxy(r, mux) + + return &Server{handler: mux}, nil } -// getExtensionEndpoints sanitizes the delimited endpoints -func getExtensionEndpoints(delimited string) []string { - endpoints := strings.Split(delimited, ExtensionEndpointDelimiter) - if endpoints == nil { - return []string{""} +// NewProxyHandler creates an API proxy handler for the cluster +func NewProxyHandler(apiProxyPrefix string, cfg *rest.Config, keepalive time.Duration) (http.Handler, error) { + host := cfg.Host + if !strings.HasSuffix(host, "/") { + host = host + "/" + } + target, err := url.Parse(host) + if err != nil { + return nil, err + } + + responder := &responder{} + transport, err := rest.TransportFor(cfg) + if err != nil { + return nil, err } - for i := range endpoints { - // Remove trailing/leading slashes - endpoints[i] = strings.TrimSuffix(endpoints[i], "/") - endpoints[i] = strings.TrimPrefix(endpoints[i], "/") + upgradeTransport, err := makeUpgradeTransport(cfg, keepalive) + if err != nil { + return nil, err } - return endpoints + proxy := proxy.NewUpgradeAwareHandler(target, transport, false, false, responder) + proxy.UpgradeTransport = upgradeTransport + proxy.UseRequestLocation = true + // TODO: enable after update to k8s apimachinery 0.21 + // proxy.UseLocationHost = true + + proxyServer := http.Handler(proxy) + proxyServer = stripLeaveSlash(apiProxyPrefix, proxyServer) + + return proxyServer, nil } -// extensionPath constructs the extension path (excluding the root) used by -// restful.Route -func extensionPath(extName, path string) string { - return strings.TrimSuffix(fmt.Sprintf("%s/%s", extName, path), "/") +// Listen is a simple wrapper around net.Listen. +func (s *Server) Listen(address string, port int) (net.Listener, error) { + return net.Listen("tcp", fmt.Sprintf("%s:%d", address, port)) } -// getServicePort returns the target port if exists or the source port otherwise -func getServicePort(svc *corev1.Service) string { - if svc.Spec.Ports[0].TargetPort.StrVal != "" { - return svc.Spec.Ports[0].TargetPort.String() +// ServeOnListener starts the server using given listener, loops forever. +func (s *Server) ServeOnListener(l net.Listener) error { + CSRF := csrf.Protect() + + server := http.Server{ + Handler: CSRF(s.handler), } - return strconv.Itoa(int(svc.Spec.Ports[0].Port)) + return server.Serve(l) +} + +func stripLeaveSlash(prefix string, h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + p := strings.TrimPrefix(req.URL.Path, prefix) + if len(p) >= len(req.URL.Path) { + http.NotFound(w, req) + return + } + if len(p) > 0 && p[:1] != "/" { + p = "/" + p + } + req.URL.Path = p + h.ServeHTTP(w, req) + }) } diff --git a/pkg/router/routes_test.go b/pkg/router/routes_test.go deleted file mode 100644 index a8599b546..000000000 --- a/pkg/router/routes_test.go +++ /dev/null @@ -1,352 +0,0 @@ -/* -Copyright 2019-2021 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. -*/ - -package router_test - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" - "net/url" - "reflect" - "strings" - "testing" - - "github.com/tektoncd/dashboard/pkg/endpoints" - "github.com/tektoncd/dashboard/pkg/router" - . "github.com/tektoncd/dashboard/pkg/router" - "github.com/tektoncd/dashboard/pkg/testutils" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/intstr" -) - -// Router successful response contract -// Does not apply to excludeRoutes -// GET: 200 -// POST: 201 -// PUT: 204 -// Delete: 204 - -// Exclude testing of routes that contain any of these substring values -var excludeRoutes []string = []string{ - "/v1/websockets", // No response code - ExtensionRoot, // Response codes dictated by extension logic - "health", // Returns 204 - "readiness", // Returns 204 - "proxy", // Kube API server has its own standard - "properties", // Pods and namespace will not exist -} - -var methodRouteMap = make(map[string][]string) // k, v := HTTP_METHOD, []route -// Stores names of resources created using POST endpoints in case id/name is modified from payload (for example, pipelineRun) -var routeNameMap = make(map[string]string) - -// Populate methodMap -// Router parameters are NOT replaced -func init() { - server, _, _ := testutils.DummyServer() - mux, _ := server.Config.Handler.(*Handler) - for _, registeredWebservices := range mux.Container.RegisteredWebServices() { - for _, route := range registeredWebservices.Routes() { - for _, excludeRoute := range excludeRoutes { - if strings.Contains(route.Path, excludeRoute) { - goto SkipRoute - } - } - methodRouteMap[route.Method] = append(methodRouteMap[route.Method], route.Path) - SkipRoute: - } - } -} - -// Validates router contract for CRUD endpoints (status codes, headers, etc.) -func TestRouterContract(t *testing.T) { - server, r, namespace := testutils.DummyServer() - defer server.Close() - - // map traversal is random - // GET/PUT/DELETE /{name} requires object exists (POST) - orderedHttpKeys := []string{ - "POST", - "GET", - "PUT", - "DELETE", - } - methodStatusMap := map[string]int{ - "POST": 201, - "GET": 200, - "PUT": 204, - "DELETE": 204, - } - - for _, httpMethod := range orderedHttpKeys { - statusCode := methodStatusMap[httpMethod] - t.Logf("Checking %s endpoints for %d\n", httpMethod, statusCode) - for _, route := range methodRouteMap[httpMethod] { - validateRoute(t, r, httpMethod, statusCode, server, route, namespace) - } - } -} - -func validateRoute(t *testing.T, r *endpoints.Resource, httpMethod string, expectedStatusCode int, server *httptest.Server, route, namespace string) { - var httpRequestBody io.Reader - t.Logf("Validating route: %s", route) - namelessRoute := strings.TrimSuffix(route, "/{name}") - resource := namelessRoute[strings.LastIndex(namelessRoute, "/")+1:] - // Remove plural - resource = strings.TrimSuffix(resource, "s") - resourceName := fmt.Sprintf("fake%s", resource) - - // Grab stored name, if any - storedResourceName, ok := routeNameMap[namelessRoute] - if ok { - resourceName = storedResourceName - } - - // Make request body - if httpMethod == "POST" || httpMethod == "PUT" { - resourceObject := fakeStub(t, r, httpMethod, resource, namespace, resourceName) - if resourceObject == nil { - t.Fatalf("Fake stub does not exist for %s\n", resource) - } - jsonBytes, err := json.Marshal(resourceObject) - if err != nil { - t.Fatalf("Error marshalling %s resource\n", resource) - } - httpRequestBody = bytes.NewReader(jsonBytes) - } else if httpMethod == "GET" { - // GET/{name} route - if strings.Contains(route, "{name}") { - // This should return nil if there is no POST fakeStub implementation - resourceObject := fakeStub(t, r, "POST", resource, namespace, resourceName) - if resourceObject == nil { - makeFake(t, r, resource, namespace, resourceName) - } - } - } - // Replace path params - serverRoute := fmt.Sprintf("%s%s", server.URL, route) - serverRoute = strings.Replace(serverRoute, "{namespace}", namespace, 1) - serverRoute = strings.Replace(serverRoute, "{name}", resourceName, 1) - - httpReq := testutils.DummyHTTPRequest(httpMethod, serverRoute, httpRequestBody) - response, err := http.DefaultClient.Do(httpReq) - if err != nil { - t.Fatalf("Error getting response from %s: %v\n", serverRoute, err) - } - if response.StatusCode != expectedStatusCode { - t.Logf("%s route %s failure\n", httpMethod, route) - t.Fatalf("Response code %d did not equal expected %d\n", response.StatusCode, expectedStatusCode) - } - // Ensure "Content-Location" header routes to resource properly - if httpMethod == "POST" { - contentLocationSlice := response.Header["Content-Location"] - if len(contentLocationSlice) == 0 { - t.Fatalf("POST route %s did not set 'Content-Location' header", route) - } - // "Content-Location" header should be of the form /some/path//resource-name - postedResourceName := contentLocationSlice[0][strings.LastIndex(contentLocationSlice[0], "/")+1:] - t.Log("POSTed resource name:", postedResourceName) - routeNameMap[route] = postedResourceName - httpReq := testutils.DummyHTTPRequest("GET", fmt.Sprintf("%s%s", server.URL, contentLocationSlice[0]), httpRequestBody) - _, err := http.DefaultClient.Do(httpReq) - if err != nil { - t.Fatalf("Unable to locate resource as specified by 'Content-Location' header: %s", contentLocationSlice[0]) - } - } -} - -// ALL and ONLY resourceTypes with POST/PUT routes must implement -// Returns stub to be marshalled for dashboard route validation -func fakeStub(t *testing.T, r *endpoints.Resource, httpMethod, resourceType, namespace, resourceName string) interface{} { - t.Logf("Getting fake resource type: %s\n", resourceType) - return nil -} - -// Creates resources for GET/{name} routes without corresponding POST to prevent lookup failures -func makeFake(t *testing.T, r *endpoints.Resource, resourceType, namespace, resourceName string) { - t.Logf("Making fake resource %s with name %s\n", resourceType, resourceName) - switch resourceType { - case "task": - task := testutils.GetObject("v1beta1", "Task", namespace, resourceName, "1") - gvr := schema.GroupVersionResource{ - Group: "tekton.dev", - Version: "v1beta1", - Resource: "tasks", - } - _, err := r.DynamicClient.Resource(gvr).Namespace(namespace).Create(context.TODO(), task, metav1.CreateOptions{}) - if err != nil { - t.Fatalf("Error creating task: %v\n", err) - } - case "taskrun": - taskRun := testutils.GetObject("v1beta1", "TaskRun", namespace, resourceName, "1") - gvr := schema.GroupVersionResource{ - Group: "tekton.dev", - Version: "v1beta1", - Resource: "taskruns", - } - _, err := r.DynamicClient.Resource(gvr).Namespace(namespace).Create(context.TODO(), taskRun, metav1.CreateOptions{}) - if err != nil { - t.Fatalf("Error creating taskRun: %v\n", err) - } - case "pipeline": - pipeline := testutils.GetObject("v1beta1", "Pipeline", namespace, resourceName, "1") - gvr := schema.GroupVersionResource{ - Group: "tekton.dev", - Version: "v1beta1", - Resource: "pipelines", - } - _, err := r.DynamicClient.Resource(gvr).Namespace(namespace).Create(context.TODO(), pipeline, metav1.CreateOptions{}) - if err != nil { - t.Fatalf("Error creating pipeline: %v\n", err) - } - case "log": - pod := corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: resourceName, - Namespace: namespace, - }, - } - _, err := r.K8sClient.CoreV1().Pods(namespace).Create(context.TODO(), &pod, metav1.CreateOptions{}) - if err != nil { - t.Fatalf("Error creating pod: %v\n", err) - } - case "pipelineresource": - pipelineResource := testutils.GetObject("v1alpha1", "PipelineResource", namespace, resourceName, "1") - gvr := schema.GroupVersionResource{ - Group: "tekton.dev", - Version: "v1alpha1", - Resource: "pipelineresources", - } - _, err := r.DynamicClient.Resource(gvr).Namespace(namespace).Create(context.TODO(), pipelineResource, metav1.CreateOptions{}) - if err != nil { - t.Fatalf("Error creating pipelineResource: %v\n", err) - } - } -} - -func TestGetAllExtensions(t *testing.T) { - server, r, _ := testutils.DummyServer() - defer server.Close() - - annotations := make(map[string]string) - annotations["tekton-dashboard-bundle-location"] = "web/extension.86386c2c.js" - annotations["tekton-dashboard-display-name"] = "Webhooks" - annotations["tekton-dashboard-endpoints"] = "webhooks.web" - - labels := make(map[string]string) - labels["app"] = "webhooks-extension" - labels["tekton-dashboard-extension"] = "true" - - servicePort := corev1.ServicePort{ - NodePort: 30810, Port: 8080, TargetPort: intstr.FromInt(8080), - } - servicePorts := []corev1.ServicePort{servicePort} - - service := corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testExtension", - Namespace: "tekton-pipelines", - Annotations: annotations, - Labels: labels, - UID: "65e6ab11-939b-486a-b2f1-323675676c84", - }, - Spec: corev1.ServiceSpec{ - ClusterIP: "172.30.155.248", - Ports: servicePorts, - }, - } - h := router.Register(*r) - h.RegisterExtension(&service) - server.Config.Handler = h - - httpReq := testutils.DummyHTTPRequest("GET", fmt.Sprintf("%s/v1/extensions", server.URL), nil) - response, err := http.DefaultClient.Do(httpReq) - if err != nil { - t.Fatalf("Error getting extensions: %s", err.Error()) - } - - var extensions []RedactedExtension - if err := json.NewDecoder(response.Body).Decode(&extensions); err != nil { - t.Fatalf("Error decoding getAllExtensions response: %v\n", err) - } - - url, _ := url.ParseRequestURI(fmt.Sprintf("http://%s:%d", service.Spec.ClusterIP, servicePort.TargetPort.IntVal)) - - structValue := reflect.ValueOf((extensions[0])) - structType := structValue.Type() - - // This test is roughly to check that the Extension struct is not returned - // as it contains a URL element that exposes the IP address. - for i := 0; i < structValue.NumField(); i++ { - if structType.Field(i).Name == "URL" { - t.Error("URL was returned and would expose sensitive data") - } - if structValue.Field(i).CanInterface() { - if structValue.Field(i).Interface() == url { - t.Error("URL was found exposing sensitive data") - } - } - } -} - -func TestXframeOptions(t *testing.T) { - tests := []struct { - options endpoints.Options - expected []string - }{ - { - options: endpoints.Options{XFrameOptions: "DENY"}, - expected: []string{"DENY"}, - }, - { - options: endpoints.Options{XFrameOptions: "SAMEORIGIN"}, - expected: []string{"SAMEORIGIN"}, - }, - { - options: endpoints.Options{XFrameOptions: ""}, - expected: nil, - }, - { - options: endpoints.Options{XFrameOptions: "FOO"}, - expected: []string{"DENY"}, - }, - } - - server, _, _ := testutils.DummyServer() - defer server.Close() - - for _, tt := range tests { - r := testutils.DummyResource() - r.Options = tt.options - h := router.Register(*r) - server.Config.Handler = h - httpReq := testutils.DummyHTTPRequest("GET", server.URL, nil) - response, err := http.DefaultClient.Do(httpReq) - if err != nil { - t.Fatalf("Error getting server response: %s", err.Error()) - } - if tt.expected == nil && response.Header["X-Frame-Options"] != nil { - t.Fatalf("response xframe header: %v, expected: %v ", response.Header["X-Frame-Options"], tt.expected) - } - if tt.expected != nil && !reflect.DeepEqual(response.Header["X-Frame-Options"], tt.expected) { - t.Fatalf("response xframe header: %v, expected: %v ", response.Header["X-Frame-Options"], tt.expected) - } - } -} diff --git a/pkg/testutils/testutils.go b/pkg/testutils/testutils.go index a36aa94a1..6b3b3083b 100644 --- a/pkg/testutils/testutils.go +++ b/pkg/testutils/testutils.go @@ -15,153 +15,9 @@ limitations under the License. package testutils import ( - "context" - "fmt" - "io" - "net/http" - "net/http/httptest" - "reflect" - "time" - - broadcaster "github.com/tektoncd/dashboard/pkg/broadcaster" - "github.com/tektoncd/dashboard/pkg/controllers" - "github.com/tektoncd/dashboard/pkg/endpoints" - logging "github.com/tektoncd/dashboard/pkg/logging" - "github.com/tektoncd/dashboard/pkg/router" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - fakedynamicclientset "k8s.io/client-go/dynamic/fake" - fakek8sclientset "k8s.io/client-go/kubernetes/fake" ) -// DummyK8sClientset returns a fake K8s clientset -func DummyK8sClientset() *fakek8sclientset.Clientset { - result := fakek8sclientset.NewSimpleClientset() - return result -} - -// DummyK8sClientset returns a fake K8s clientset -func DummyDynamicClientset() *fakedynamicclientset.FakeDynamicClient { - result := fakedynamicclientset.NewSimpleDynamicClient(runtime.NewScheme()) - return result -} - -// DummyResource returns a Resource populated by fake clientsets -func DummyResource() *endpoints.Resource { - resource := endpoints.Resource{ - DynamicClient: DummyDynamicClientset(), - K8sClient: DummyK8sClientset(), - } - return &resource -} - -// DummyHTTPRequest returns a HTTP request with a JSON content type header as -// well as the configured with the parameters -func DummyHTTPRequest(method string, url string, body io.Reader) *http.Request { - httpReq, _ := http.NewRequest(method, url, body) - httpReq.Header.Set("Content-Type", "application/json") - return httpReq -} - -// DummyServer returns a httptest server of the Tekton Dashboard -func DummyServer() (*httptest.Server, *endpoints.Resource, string) { - resource := DummyResource() - // Create subscriber to wait for controller to detect created namespace - subscriber, _ := endpoints.ResourcesBroadcaster.Subscribe() - - // Create namespace that is referenced by extensionController - dashboardNamespace := "tekton-pipelines" - _, err := resource.K8sClient.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: dashboardNamespace}}, metav1.CreateOptions{}) - if err != nil { - logging.Log.Fatalf("error creating namespace '%s': %v", dashboardNamespace, err) - } - - // K8s signals only allows for a single channel, which will panic when executed twice - // There should be no os signals for testing purposes - routerHandler := router.Register(*resource) - logging.Log.Info("Creating controllers") - stopCh := make(<-chan struct{}) - resyncDur := time.Second * 30 - controllers.StartTektonControllers(resource.DynamicClient, resyncDur, "", 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 { - select { - case <-timeout: - logging.Log.Fatalf("namespace informer did not detect installNamespace by deadline") - case event := <-subscriber.SubChan(): - if event.Kind == "Namespace" && event.Operation == broadcaster.Created { - goto NamespaceDetected - } - } - } -NamespaceDetected: - // Remove subscriber from pool - endpoints.ResourcesBroadcaster.Unsubscribe(subscriber) - server := httptest.NewServer(routerHandler) - return server, resource, dashboardNamespace -} - -// ObjectListDeepEqual errors if the two object lists are not equal -func ObjectListDeepEqual(expectedListPointer interface{}, actualListPointer interface{}) error { - createNamespaceToNameObjectMap := func(slicePointer interface{}) (map[string]map[string]interface{}, error) { - // slicePointer should refer to a slice of struct (likely a CRD) - slice := reflect.ValueOf(slicePointer) - if slice.Kind() != reflect.Slice { - return nil, fmt.Errorf("interface passed was a non-slice type") - } - - // Convert *[]someType (interface{}) to []interface{} - interfaceList := make([]interface{}, 0, slice.Len()) - for i := 0; i < slice.Len(); i++ { - object := slice.Index(i).Interface() - interfaceList = append(interfaceList, object) - } - // Separate objects/CRD by assumed `Namespace` and `Name` fields - namespaceNameObjectMap := make(map[string]map[string]interface{}) - for _, object := range interfaceList { - name := reflect.ValueOf(object).FieldByName("Name").String() - namespace := reflect.ValueOf(object).FieldByName("Namespace").String() - // Create map[string]interface{} value for namespace key, if nil - if namespaceNameObjectMap[namespace] == nil { - namespaceNameObjectMap[namespace] = make(map[string]interface{}) - } - // Add object - namespaceNameObjectMap[namespace][name] = object - } - return namespaceNameObjectMap, nil - } - // actual/response - actualMap, err := createNamespaceToNameObjectMap(actualListPointer) - if err != nil { - return fmt.Errorf("unable to create map for actual list: %v", err) - } - expectedMap, err := createNamespaceToNameObjectMap(expectedListPointer) - if err != nil { - return fmt.Errorf("unable to create map for expected list: %v", err) - } - for namespace, actualNameObjectMap := range actualMap { - for name, actualObject := range actualNameObjectMap { - expectedNameObjectMap, found := expectedMap[namespace] - if !found { - return fmt.Errorf("actual object list has references to namespace %s that expected list does not", namespace) - } - expectedObject, found := expectedNameObjectMap[name] - if !found { - return fmt.Errorf("did not find object in %s namespace with name %s in expectedList", namespace, name) - } - if !reflect.DeepEqual(expectedObject, actualObject) { - return fmt.Errorf("actual object %v did not equal expected %v", actualObject, expectedObject) - } - } - } - // The two lists are equal - return nil -} - func GetObject(version, kind, namespace, name, resourceVersion string) *unstructured.Unstructured { return &unstructured.Unstructured{ Object: map[string]interface{}{ diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 8ff5a28f9..26f5b9751 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -14,11 +14,9 @@ limitations under the License. package utils import ( - "encoding/json" "net/http" "strings" - restful "github.com/emicklei/go-restful" logging "github.com/tektoncd/dashboard/pkg/logging" "k8s.io/apimachinery/pkg/api/meta" @@ -27,51 +25,11 @@ import ( ) // RespondError - logs and writes an error response with a desired status code -func RespondError(response *restful.Response, err error, statusCode int) { +func RespondError(response http.ResponseWriter, err error, statusCode int) { logging.Log.Error("Error: ", strings.Replace(err.Error(), "/", "", -1)) - response.AddHeader("Content-Type", "text/plain") - response.WriteError(statusCode, err) -} - -// RespondErrorMessage - logs and writes an error message with a desired status code -func RespondErrorMessage(response *restful.Response, message string, statusCode int) { - logging.Log.Debugf("Error message: %s", message) - response.AddHeader("Content-Type", "text/plain") - response.WriteErrorString(statusCode, message) -} - -// RespondMessageAndLogError - logs and writes an error message with a desired status code and logs the error -func RespondMessageAndLogError(response *restful.Response, err error, message string, statusCode int) { - logging.Log.Error("Error: ", strings.Replace(err.Error(), "/", "", -1)) - logging.Log.Debugf("Message: %s", message) - response.AddHeader("Content-Type", "text/plain") - response.WriteErrorString(statusCode, message) -} - -// Write Content-Location header within POST methods and set StatusCode to 201 -// Headers MUST be set before writing to body (if any) to succeed -func WriteResponseLocation(request *restful.Request, response *restful.Response, identifier string) { - location := request.Request.URL.Path - if request.Request.Method == http.MethodPost { - location = location + "/" + identifier - } - response.AddHeader("Content-Location", location) - response.WriteHeader(201) -} - -func GetNamespace(request *restful.Request) string { - namespace := request.PathParameter("namespace") - if namespace == "*" { - namespace = "" - } - return namespace -} - -func GetContentType(content []byte) string { - if json.Valid(content) { - return "application/json" - } - return "text/plain" + response.Header().Set("Content-Type", "text/plain") + response.WriteHeader(statusCode) + response.Write([]byte(err.Error())) } // Adapted from https://github.com/kubernetes-sigs/controller-runtime/blob/v0.5.2/pkg/source/internal/eventsource.go#L131-L149 diff --git a/pkg/websocket/websocket.go b/pkg/websocket/websocket.go index c4ddd4957..44c09f294 100644 --- a/pkg/websocket/websocket.go +++ b/pkg/websocket/websocket.go @@ -1,5 +1,5 @@ /* -Copyright 2019 The Tekton Authors +Copyright 2019-2021 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 @@ -18,22 +18,20 @@ import ( "net/http" "time" - restful "github.com/emicklei/go-restful" "github.com/gorilla/websocket" broadcaster "github.com/tektoncd/dashboard/pkg/broadcaster" logging "github.com/tektoncd/dashboard/pkg/logging" ) // UpgradeToWebsocket attempts to upgrade connection from HTTP(S) to WS(S) -func UpgradeToWebsocket(request *restful.Request, response *restful.Response) (*websocket.Conn, error) { - var writer http.ResponseWriter = response +func UpgradeToWebsocket(request *http.Request, response http.ResponseWriter) (*websocket.Conn, error) { logging.Log.Debug("Upgrading connection to websocket...") // Handles writing error to response upgrader := websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 4096, } - connection, err := upgrader.Upgrade(writer, request.Request, nil) + connection, err := upgrader.Upgrade(response, request, nil) return connection, err } diff --git a/src/api/extensions.js b/src/api/extensions.js index f7b3cb49b..1c75b338c 100644 --- a/src/api/extensions.js +++ b/src/api/extensions.js @@ -1,5 +1,5 @@ /* -Copyright 2019-2020 The Tekton Authors +Copyright 2019-2021 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 @@ -12,45 +12,26 @@ limitations under the License. */ import { get } from './comms'; -import { apiRoot, dashboardAPIGroup, getResourcesAPI } from './utils'; - -export function getExtensionBaseURL(name) { - return `${apiRoot}/v1/extensions/${name}`; -} - -export function getExtensionBundleURL(name, bundlelocation) { - return `${getExtensionBaseURL(name)}/${bundlelocation}`; -} +import { dashboardAPIGroup, getResourcesAPI } from './utils'; export async function getExtensions({ namespace } = {}) { - const uri = `${apiRoot}/v1/extensions`; const resourceExtensionsUri = getResourcesAPI({ group: dashboardAPIGroup, version: 'v1alpha1', type: 'extensions', namespace }); - let extensions = await get(uri); const resourceExtensions = await get(resourceExtensionsUri); - extensions = (extensions || []).map( - ({ bundlelocation, displayname, name }) => ({ - displayName: displayname, + return (resourceExtensions?.items || []).map(({ spec }) => { + const { displayname: displayName, name, namespaced } = spec; + const [apiGroup, apiVersion] = spec.apiVersion.split('/'); + return { + apiGroup, + apiVersion, + displayName, + extensionType: 'kubernetes-resource', name, - source: getExtensionBundleURL(name, bundlelocation) - }) - ); - return extensions.concat( - ((resourceExtensions && resourceExtensions.items) || []).map(({ spec }) => { - const { displayname: displayName, name, namespaced } = spec; - const [apiGroup, apiVersion] = spec.apiVersion.split('/'); - return { - apiGroup, - apiVersion, - displayName, - extensionType: 'kubernetes-resource', - name, - namespaced - }; - }) - ); + namespaced + }; + }); } diff --git a/src/api/extensions.test.js b/src/api/extensions.test.js index 3330f592f..b6318165b 100644 --- a/src/api/extensions.test.js +++ b/src/api/extensions.test.js @@ -1,5 +1,5 @@ /* -Copyright 2019-2020 The Tekton Authors +Copyright 2019-2021 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 @@ -17,11 +17,13 @@ import * as API from './extensions'; it('getExtensions', () => { const displayName = 'displayName'; const name = 'name'; - const bundlelocation = 'bundlelocation'; - const source = API.getExtensionBundleURL(name, bundlelocation); - const extensions = [{ displayname: displayName, name, bundlelocation }]; - const transformedExtensions = [{ displayName, name, source }]; - fetchMock.get(/extensions/, extensions); + const extensions = [ + { spec: { apiVersion: 'v1alpha1', displayname: displayName, name } } + ]; + const transformedExtensions = [ + expect.objectContaining({ displayName, name }) + ]; + fetchMock.get(/extensions/, { items: extensions }); return API.getExtensions().then(response => { expect(response).toEqual(transformedExtensions); fetchMock.restore(); diff --git a/src/reducers/extensions.js b/src/reducers/extensions.js index f843542fc..f70e1e292 100644 --- a/src/reducers/extensions.js +++ b/src/reducers/extensions.js @@ -1,5 +1,5 @@ /* -Copyright 2019 The Tekton Authors +Copyright 2019-2021 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 @@ -18,7 +18,6 @@ import { createErrorMessageReducer, createIsFetchingReducer } from './reducerCreators'; -import { getExtensionBundleURL } from '../api'; const type = 'Extension'; @@ -34,14 +33,6 @@ export function mapResourceExtension(extension) { }; } -export function mapServiceExtension(extension) { - return { - name: extension.name, - displayName: extension.displayname, - source: getExtensionBundleURL(extension.name, extension.bundlelocation) - }; -} - function byName(state = {}, action) { switch (action.type) { case 'ResourceExtensionCreated': @@ -49,21 +40,11 @@ function byName(state = {}, action) { const extension = mapResourceExtension(action.payload); return { ...state, [extension.name]: extension }; } - case 'ServiceExtensionCreated': - case 'ServiceExtensionUpdated': { - const extension = mapServiceExtension(action.payload); - return { ...state, [extension.name]: extension }; - } case 'ResourceExtensionDeleted': { const newState = { ...state }; delete newState[action.payload.spec.name]; return newState; } - case 'ServiceExtensionDeleted': { - const newState = { ...state }; - delete newState[action.payload.name]; - return newState; - } case 'EXTENSIONS_FETCH_SUCCESS': return keyBy(action.data, 'name'); default: diff --git a/src/reducers/extensions.test.js b/src/reducers/extensions.test.js index 6b5cad8ab..99a56147a 100644 --- a/src/reducers/extensions.test.js +++ b/src/reducers/extensions.test.js @@ -1,5 +1,5 @@ /* -Copyright 2019 The Tekton Authors +Copyright 2019-2021 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 @@ -104,48 +104,3 @@ it('ResourceExtension Events', () => { expect(selectors.getExtensions(deletedState)).toEqual([]); expect(selectors.isFetchingExtensions(deletedState)).toBe(false); }); - -it('ServiceExtension Events', () => { - const extension = { - bundlelocation: 'bundle', - displayname: 'before', - name: 'sample-extension' - }; - - const action = { - type: 'ServiceExtensionCreated', - payload: extension - }; - - const state = extensionsReducer({}, action); - expect(selectors.getExtensions(state)).toEqual([ - selectors.mapServiceExtension(extension) - ]); - expect(selectors.isFetchingExtensions(state)).toBe(false); - - const updatedExtension = { - bundlelocation: 'bundle', - displayname: 'after', - name: 'sample-extension' - }; - - const updateAction = { - type: 'ServiceExtensionUpdated', - payload: updatedExtension - }; - - const updatedState = extensionsReducer(state, updateAction); - expect(selectors.getExtensions(updatedState)).toEqual([ - selectors.mapServiceExtension(updatedExtension) - ]); - expect(selectors.isFetchingExtensions(updatedState)).toBe(false); - - const deleteAction = { - type: 'ServiceExtensionDeleted', - payload: updatedExtension - }; - - const deletedState = extensionsReducer(state, deleteAction); - expect(selectors.getExtensions(deletedState)).toEqual([]); - expect(selectors.isFetchingExtensions(deletedState)).toBe(false); -}); diff --git a/webpack.dev.js b/webpack.dev.js index abb559920..140e3aa72 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -15,17 +15,6 @@ const { merge } = require('webpack-merge'); const common = require('./webpack.common.js'); const { API_DOMAIN, PORT } = require('./config_frontend/config.json'); -const extensionConfig = { - '/v1/extensions': { - target: 'http://localhost:9999', - pathRewrite: { '^/v1/extensions': '' } - }, - '/v1/extensions/dev-extension': { - target: 'http://localhost:9999', - pathRewrite: { '^/v1/extensions/dev-extension': '' } - } -}; - const mode = 'development'; const customAPIDomain = process.env.API_DOMAIN; @@ -52,7 +41,6 @@ module.exports = merge(common({ mode }), { overlay: true, port: process.env.PORT || PORT, proxy: { - ...(process.env.EXTENSIONS_LOCAL_DEV ? extensionConfig : {}), '/v1': { ...proxyOptions },