瀏覽代碼

install chart template by filepaths

sunguroku 5 年之前
父節點
當前提交
dc46364b29

+ 1 - 1
dashboard/docker/dev.Dockerfile

@@ -5,7 +5,7 @@ WORKDIR /webpack
 
 COPY package*.json ./
 
-RUN npm i
+RUN npm install
 
 ENV NODE_ENV=development
 

+ 5 - 4
dashboard/package-lock.json

@@ -404,7 +404,8 @@
     "@types/js-base64": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/@types/js-base64/-/js-base64-3.0.0.tgz",
-      "integrity": "sha512-BnEyOcDE4H6bkg8m84xhdbkYoAoCg8sYERmAvE4Ff50U8jTfbmOinRdJpauBn1P9XsCCQgCLuSiyz3PM4WHYOA=="
+      "integrity": "sha512-BnEyOcDE4H6bkg8m84xhdbkYoAoCg8sYERmAvE4Ff50U8jTfbmOinRdJpauBn1P9XsCCQgCLuSiyz3PM4WHYOA==",
+      "dev": true
     },
     "@types/js-yaml": {
       "version": "3.12.5",
@@ -6861,9 +6862,9 @@
       "dev": true
     },
     "typescript": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz",
-      "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==",
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.2.tgz",
+      "integrity": "sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ==",
       "dev": true
     },
     "union-value": {

+ 2 - 2
dashboard/package.json

@@ -4,7 +4,6 @@
   "private": true,
   "dependencies": {
     "@fullstory/browser": "^1.4.5",
-    "@types/js-base64": "^3.0.0",
     "@types/js-yaml": "^3.12.5",
     "@types/markdown-to-jsx": "^6.11.3",
     "@types/qs": "^6.9.5",
@@ -33,6 +32,7 @@
     "@testing-library/react": "^9.3.2",
     "@testing-library/user-event": "^7.1.2",
     "@types/jest": "^24.0.0",
+    "@types/js-base64": "^3.0.0",
     "@types/node": "^12.12.62",
     "@types/qs": "^6.9.5",
     "@types/react": "^16.9.49",
@@ -46,7 +46,7 @@
     "qs": "^6.9.4",
     "source-map-loader": "^1.1.0",
     "ts-loader": "^8.0.4",
-    "typescript": "^4.0.3",
+    "typescript": "^4.1.2",
     "webpack": "^4.44.2",
     "webpack-cli": "^3.3.12",
     "webpack-dev-server": "^3.11.0"

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx

@@ -116,7 +116,7 @@ export default class ChartList extends Component<PropsType, StateType> {
       // don't retrieve controllers for chart that failed to even deploy.
       if (chart.info.status == 'failed') return;
 
-      await new Promise(next => {
+      await new Promise((next: (res?: any) => void) => {
         api.getChartControllers('<token>', {
           namespace: chart.namespace,
           cluster_id: currentCluster.id,
@@ -138,7 +138,7 @@ export default class ChartList extends Component<PropsType, StateType> {
           })
 
           res.data.forEach(async (c: any) => {
-            await new Promise(nextController => {
+            await new Promise((nextController: (res?: any) => void) => {
               this.setState({
                 chartLookupTable: {
                   ...this.state.chartLookupTable,

+ 8 - 9
dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx

@@ -4,7 +4,7 @@ import styled from 'styled-components';
 import { Context } from '../../../../shared/Context';
 import api from '../../../../shared/api';
 
-import { PorterChart, ChoiceType, Cluster } from '../../../../shared/types';
+import { PorterChart, ChoiceType, Cluster, StorageType } from '../../../../shared/types';
 import Selector from '../../../../components/Selector';
 import ImageSelector from '../../../../components/image-selector/ImageSelector';
 import TabRegion from '../../../../components/TabRegion';
@@ -35,28 +35,26 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
   };
 
   onSubmit = (formValues: any) => {
-    console.log(formValues);
-
     let { currentCluster, currentProject } = this.context;
-    console.log(formValues);
     api.deployTemplate('<token>', {
       templateName: this.props.currentTemplate.name,
-      clusterID: currentCluster.id,
       imageURL: "index.docker.io/bitnami/redis",
+      storage: StorageType.Secret,
       formValues,
     }, {
       id: currentProject.id,
+      cluster_id: currentCluster.id,
+      service_account_id: currentCluster.service_account_id,
     }, (err: any, res: any) => {
       if (err) {
-        // console.log(err)
+        console.log(err)
       } else {
-        // console.log(res.data)
+        console.log(res.data)
       }
     });
   }
 
   componentDidMount() {
-
     // Generate settings tabs from the provided form
     let tabOptions = [] as ChoiceType[];
     let tabContents = [] as any;
@@ -68,7 +66,8 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
             <ValuesForm 
               sections={tab.sections} 
               onSubmit={this.onSubmit}
-              disabled={this.state.selectedImageUrl === ''}
+              // disabled={this.state.selectedImageUrl === ''}
+              disabled={false}
             />
           </ValuesFormWrapper>
         ),

+ 5 - 4
dashboard/src/shared/api.tsx

@@ -164,11 +164,12 @@ const deleteProject = baseApi<{}, { id: number }>('DELETE', pathParams => {
 
 const deployTemplate = baseApi<{
   templateName: string,
-  clusterID: number,
   imageURL: string,
-  formValues: any
-}, { id: number }>('POST', pathParams => {
-  return `/api/projects/${pathParams.id}/deploy`;
+  formValues: any,
+  storage: StorageType,
+}, { id: number, cluster_id: number, service_account_id: number }>('POST', pathParams => {
+  let {id, cluster_id, service_account_id} = pathParams;
+  return `/api/projects/${id}/deploy?cluster_id=${cluster_id}&service_account_id=${service_account_id}`;
 });
 
 // Bundle export to allow default api import (api.<method> is more readable)

+ 2 - 0
go.sum

@@ -1875,8 +1875,10 @@ gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpA
 grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
 helm.sh/helm v2.16.12+incompatible h1:nQfifk10KcpAGD1RJaNZVW/fWiqluV0JMuuDwdba4rw=
 helm.sh/helm v2.16.12+incompatible/go.mod h1:0Xbc6ErzwWH9qC55X1+hE3ZwhM3atbhCm/NbFZw5i+4=
+helm.sh/helm v2.17.0+incompatible h1:cSe3FaQOpRWLDXvTObQNj0P7WI98IG5yloU6tQVls2k=
 helm.sh/helm/v3 v3.3.4 h1:tbad6WQVMxEw1HlVBvI2rQqOblmI5lgXOrWAMwJ198M=
 helm.sh/helm/v3 v3.3.4/go.mod h1:CyCGQa53/k1JFxXvXveGwtfJ4cuB9zkaBSGa5rnAiHU=
+helm.sh/helm/v3 v3.4.1 h1:NIdlBGKFRTAkhz0ooYKw1VBbmTldxNAZRY1nH6Glk6I=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

+ 13 - 0
internal/forms/release.go

@@ -123,3 +123,16 @@ type UpgradeReleaseForm struct {
 	Name   string `json:"name" form:"required"`
 	Values string `json:"values" form:"required"`
 }
+
+// ChartTemplateForm represents the accepted values for installing a new chart from a template.
+type ChartTemplateForm struct {
+	TemplateName string                 `json:"templateName" form:"required"`
+	ImageURL     string                 `json:"imageURL" form:"required"`
+	FormValues   map[string]interface{} `json:"formValues"`
+}
+
+// InstallChartTemplateForm represents the accepted values for installing a new chart from a template.
+type InstallChartTemplateForm struct {
+	*ReleaseForm
+	*ChartTemplateForm
+}

+ 56 - 0
internal/helm/agent.go

@@ -3,7 +3,10 @@ package helm
 import (
 	"fmt"
 
+	"github.com/pkg/errors"
 	"helm.sh/helm/v3/pkg/action"
+	"helm.sh/helm/v3/pkg/chart"
+	"helm.sh/helm/v3/pkg/chart/loader"
 	"helm.sh/helm/v3/pkg/release"
 	"k8s.io/helm/pkg/chartutil"
 )
@@ -77,6 +80,47 @@ func (a *Agent) UpgradeRelease(
 	return res, nil
 }
 
+// InstallChart installs a new chart by URL, absolute or relative filepaths.
+// Equivalent to `helm install [CHART_NAME] [cp]` where cp is one of the following:
+//  1) Absolute URL: https://example.com/charts/nginx-1.2.3.tgz
+//  2) path to packaged chart ./nginx-1.2.3.tgz
+//  3) path to unpacked chart ./nginx
+func (a *Agent) InstallChart(
+	cp string,
+	values []byte,
+) (*release.Release, error) {
+	cmd := action.NewInstall(a.ActionConfig)
+	valuesYaml, err := chartutil.ReadValues(values)
+
+	if err != nil {
+		return nil, fmt.Errorf("Values could not be parsed: %v", err)
+	}
+
+	// Only supports filepaths for now, URL option WIP.
+	// Check chart dependencies to make sure all are present in /charts
+	chartRequested, err := loader.Load(cp)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := checkIfInstallable(chartRequested); err != nil {
+		return nil, err
+	}
+
+	if chartRequested.Metadata.Deprecated {
+		return nil, fmt.Errorf("This chart is deprecated")
+	}
+
+	if req := chartRequested.Metadata.Dependencies; req != nil {
+		if err := action.CheckDependencies(chartRequested, req); err != nil {
+			// TODO: Handle dependency updates.
+			return nil, err
+		}
+	}
+
+	return cmd.Run(chartRequested, valuesYaml)
+}
+
 // RollbackRelease rolls a release back to a specified revision/version
 func (a *Agent) RollbackRelease(
 	name string,
@@ -86,3 +130,15 @@ func (a *Agent) RollbackRelease(
 	cmd.Version = version
 	return cmd.Run(name)
 }
+
+// ------------------------ Helm agent helper functions ------------------------ //
+
+// checkIfInstallable validates if a chart can be installed
+// Application chart type is only installable
+func checkIfInstallable(ch *chart.Chart) error {
+	switch ch.Metadata.Type {
+	case "", "application":
+		return nil
+	}
+	return errors.Errorf("%s charts are not installable", ch.Metadata.Type)
+}

+ 21 - 0
node_modules/@types/js-base64/LICENSE

@@ -0,0 +1,21 @@
+    MIT License
+
+    Copyright (c) Microsoft Corporation.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in all
+    copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+    SOFTWARE

+ 16 - 0
node_modules/@types/js-base64/README.md

@@ -0,0 +1,16 @@
+# Installation
+> `npm install --save @types/js-base64`
+
+# Summary
+This package contains type definitions for js-base64 (https://github.com/dankogai/js-base64).
+
+# Details
+Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/js-base64.
+
+### Additional Details
+ * Last updated: Sat, 18 Jul 2020 15:47:12 GMT
+ * Dependencies: none
+ * Global values: `Base64`
+
+# Credits
+These definitions were written by [Denis Carriere](https://github.com/DenisCarriere), [Tommy Lent](https://github.com/tlent), and [JounQin](https://github.com/JounQin).

+ 82 - 0
node_modules/@types/js-base64/index.d.ts

@@ -0,0 +1,82 @@
+// Type definitions for js-base64 3.0
+// Project: https://github.com/dankogai/js-base64
+// Definitions by: Denis Carriere <https://github.com/DenisCarriere>
+//                 Tommy Lent <https://github.com/tlent>
+//                 JounQin <https://github.com/JounQin>
+// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
+
+export interface Base64 {
+    VERSION: string;
+    encode(s: string, uriSafe?: boolean): string;
+    encodeURI(s: string): string;
+    encodeURL: Base64['encodeURI'];
+    decode(base64: string): string;
+    atob(base64: string): string;
+    btoa(s: string): string;
+    fromBase64(base64: string): string;
+    toBase64(s: string, uriSafe?: boolean): string;
+    btou(s: string): string;
+    utob(s: string): string;
+    fromUint8Array(uint8Array: Uint8Array, uriSafe?: boolean): string;
+    toUint8Array(s: string): Uint8Array;
+    extendString(): void;
+    extendUint8Array(): void;
+    extendBuiltins(): void;
+}
+
+export const Base64: Base64;
+
+export const VERSION: string;
+
+export const encode: Base64['encode'];
+
+export const encodeURI: Base64['encodeURI'];
+
+export const encodeURL: Base64['encodeURL'];
+
+export const decode: Base64['decode'];
+
+export const atob: Base64['atob'];
+
+export const btoa: Base64['btoa'];
+
+export const fromBase64: Base64['fromBase64'];
+
+export const toBase64: Base64['toBase64'];
+
+export const btou: Base64['btou'];
+
+export const utob: Base64['utob'];
+
+export const fromUint8Array: Base64['fromUint8Array'];
+
+export const toUint8Array: Base64['toUint8Array'];
+
+export const extendString: Base64['extendString'];
+
+export const extendUint8Array: Base64['extendUint8Array'];
+
+export const extendBuiltins: Base64['extendBuiltins'];
+
+/**
+ * only for global usage, not available in esm actually
+ */
+export function noConflict(): Base64;
+
+export as namespace Base64;
+
+declare global {
+    interface String {
+        fromBase64(): string;
+        toBase64(uriSafe?: boolean): string;
+        toBase64URI(): string;
+        toBase64URL(): string;
+        toUint8Array(): Uint8Array;
+    }
+
+    interface Uint8Array {
+        toBase64(uriSafe?: boolean): string;
+        toBase64URI(): string;
+        toBase64URL(): string;
+    }
+}

+ 62 - 0
node_modules/@types/js-base64/package.json

@@ -0,0 +1,62 @@
+{
+  "_from": "@types/js-base64",
+  "_id": "@types/js-base64@3.0.0",
+  "_inBundle": false,
+  "_integrity": "sha512-BnEyOcDE4H6bkg8m84xhdbkYoAoCg8sYERmAvE4Ff50U8jTfbmOinRdJpauBn1P9XsCCQgCLuSiyz3PM4WHYOA==",
+  "_location": "/@types/js-base64",
+  "_phantomChildren": {},
+  "_requested": {
+    "type": "tag",
+    "registry": true,
+    "raw": "@types/js-base64",
+    "name": "@types/js-base64",
+    "escapedName": "@types%2fjs-base64",
+    "scope": "@types",
+    "rawSpec": "",
+    "saveSpec": null,
+    "fetchSpec": "latest"
+  },
+  "_requiredBy": [
+    "#USER",
+    "/"
+  ],
+  "_resolved": "https://registry.npmjs.org/@types/js-base64/-/js-base64-3.0.0.tgz",
+  "_shasum": "b7b4c130facefefd5c57ba82664c41e2995f91be",
+  "_spec": "@types/js-base64",
+  "_where": "/Users/trevorshim/Development/porter-dev/porter",
+  "bugs": {
+    "url": "https://github.com/DefinitelyTyped/DefinitelyTyped/issues"
+  },
+  "bundleDependencies": false,
+  "contributors": [
+    {
+      "name": "Denis Carriere",
+      "url": "https://github.com/DenisCarriere"
+    },
+    {
+      "name": "Tommy Lent",
+      "url": "https://github.com/tlent"
+    },
+    {
+      "name": "JounQin",
+      "url": "https://github.com/JounQin"
+    }
+  ],
+  "dependencies": {},
+  "deprecated": false,
+  "description": "TypeScript definitions for js-base64",
+  "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped#readme",
+  "license": "MIT",
+  "main": "",
+  "name": "@types/js-base64",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/DefinitelyTyped/DefinitelyTyped.git",
+    "directory": "types/js-base64"
+  },
+  "scripts": {},
+  "typeScriptVersion": "3.0",
+  "types": "index.d.ts",
+  "typesPublisherContentHash": "4b5afb34917caed330bdb1d07cae9ec4f28c8f27affcb5551a4412b3f9d082eb",
+  "version": "3.0.0"
+}

+ 53 - 26
server/api/deploy_handler.go

@@ -10,63 +10,76 @@ import (
 	"io"
 	"io/ioutil"
 	"net/http"
+	"net/url"
 	"strings"
 
+	"github.com/porter-dev/porter/internal/forms"
+	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/models"
 	"gopkg.in/yaml.v2"
 )
 
-// DeployTemplateForm describes the parameters of a deploy template request
-type DeployTemplateForm struct {
-	TemplateName string
-	ClusterID    int
-	ImageURL     string
-	FormValues   map[string]interface{}
-}
-
 // HandleDeployTemplate triggers a chart deployment from a template
 func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
+	vals, err := url.ParseQuery(r.URL.RawQuery)
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
 
-	// TODO: use create form
-	requestForm := make(map[string]interface{})
+	form := &forms.InstallChartTemplateForm{
+		ReleaseForm: &forms.ReleaseForm{
+			Form: &helm.Form{
+				UpdateTokenCache: app.updateTokenCache,
+			},
+		},
+		ChartTemplateForm: &forms.ChartTemplateForm{},
+	}
+
+	form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
+		vals,
+		app.repo.ServiceAccount,
+	)
 
-	// decode from JSON to form value
-	if err := json.NewDecoder(r.Body).Decode(&requestForm); err != nil {
-		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrUserDecode, w)
 		return
 	}
 
-	// TODO: use create form
-	params := DeployTemplateForm{}
-	params.TemplateName = requestForm["templateName"].(string)
-	params.ClusterID = int(requestForm["clusterID"].(float64))
-	params.ImageURL = requestForm["imageURL"].(string)
-	params.FormValues = requestForm["formValues"].(map[string]interface{})
+	agent, err := app.getAgentFromReleaseForm(
+		w,
+		r,
+		form.ReleaseForm,
+	)
+
+	if err != nil {
+		return
+	}
 
 	baseURL := "https://porter-dev.github.io/chart-repo/"
-	defaultValues, err := getDefaultValues(params.TemplateName, baseURL)
+	values, err := getDefaultValues(form.ChartTemplateForm.TemplateName, baseURL)
 	if err != nil {
 		return
 	}
 
 	// Set image URL
-	(*defaultValues)["image"].(map[interface{}]interface{})["repository"] = params.ImageURL
+	(*values)["image"].(map[interface{}]interface{})["repository"] = form.ChartTemplateForm.ImageURL
 
 	// Loop through form params to override
-	for k := range params.FormValues {
+	for k := range form.ChartTemplateForm.FormValues {
 		switch v := interface{}(k).(type) {
 		case string:
 			splits := strings.Split(v, ".")
 
 			// Validate that the field to override exists
-			currentLoc := *defaultValues
+			currentLoc := *values
 			for s := range splits {
 				key := splits[s]
 				val := currentLoc[key]
 				if val == nil {
 					fmt.Printf("No such field: %v\n", key)
 				} else if s == len(splits)-1 {
-					newValue := params.FormValues[v]
+					newValue := form.ChartTemplateForm.FormValues[v]
 					fmt.Printf("Overriding default %v with %v\n", val, newValue)
 					currentLoc[key] = newValue
 				} else {
@@ -79,15 +92,29 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	d, err := yaml.Marshal(defaultValues)
+	v, err := yaml.Marshal(values)
+
 	if err != nil {
 		return
 	}
 
 	// Output values.yaml string
-	fmt.Println(string(d))
+	_, err = agent.InstallChart(baseURL+"react-0.1.5.tgz", v)
+
+	if err != nil {
+		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
+			Code:   ErrReleaseDeploy,
+			Errors: []string{"error installing a new chart" + err.Error()},
+		}, w)
+
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
 }
 
+// ------------------------ Deploy handler helper functions ------------------------ //
+
 func getDefaultValues(templateName string, baseURL string) (*map[interface{}]interface{}, error) {
 	resp, err := http.Get(baseURL + "index.yaml")
 	if err != nil {

+ 2 - 1
server/api/release_handler.go

@@ -159,7 +159,8 @@ func (app *App) HandleGetReleaseComponents(w http.ResponseWriter, r *http.Reques
 	}
 }
 
-// HandleGetReleaseControllers retrieves a single release based on a name and revision
+// HandleGetReleaseControllers retrieves controllers that belong to a release.
+// Used to display status of charts.
 func (app *App) HandleGetReleaseControllers(w http.ResponseWriter, r *http.Request) {
 	name := chi.URLParam(r, "name")
 	revision, err := strconv.ParseUint(chi.URLParam(r, "revision"), 0, 64)

+ 9 - 1
server/router/router.go

@@ -275,7 +275,15 @@ func New(
 		r.Method(
 			"POST",
 			"/projects/{project_id}/deploy",
-			auth.BasicAuthenticate(requestlog.NewHandler(a.HandleDeployTemplate, l)),
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveServiceAccountAccess(
+					requestlog.NewHandler(a.HandleDeployTemplate, l),
+					mw.URLParam,
+					mw.QueryParam,
+				),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
 		)
 
 		// /api/templates routes