Repository: google/exposure-notifications-server
Pull request #1407 (2021-02-28 17:15:26)

Migrate admin-console assets to embedded fs
Gin doesn't support reloading with an embedded fs, but I don't think it matters much given the use case for admin-console.

Fixes https://github.com/google/exposure-notifications-server/issues/1386

**Release Note**

<!-- Enter your extended release note in the below block. If the PR requires
additional action from users switching to the new release, include the string
"action required". If no release note is required, write "NONE". -->

```release-note
Migrate admin-console assets to embedded fs.
```

google-oss-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/google/exposure-notifications-server/pull/1407#" title="Author self-approved">sethvargo</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=google%2Fexposure-notifications-server).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/google/exposure-notifications-server/blob/main/OWNERS)~~ [sethvargo]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
sethvargo: /assign @mikehelmick 

Patch:
diff --git a/Makefile b/Makefile
index 899b74c..1e014ca 100644
--- a/Makefile
+++ b/Makefile
@@ -21,7 +21,7 @@ MD_FILES = $(shell find . -name \*.md)
 # locally. There is a chance that CI detects linter errors that are not found
 # locally, but it should be rare.
 lint:
-	@command -v golangci-lint > /dev/null 2>&1 || GO111MODULE=off go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.37.1
+	@command -v golangci-lint > /dev/null 2>&1 || (cd $${TMPDIR} && go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.37.1)
 	golangci-lint run --config .golangci.yaml
 .PHONY: lint
 
@@ -55,6 +55,6 @@ test-coverage:
 .PHONY: test-coverage
 
 zapcheck:
-	@command -v zapw > /dev/null 2>&1 || GO111MODULE=off go get github.com/sethvargo/zapw/cmd/zapw
+	@command -v zapw > /dev/null 2>&1 || (cd $${TMPDIR} && go get github.com/sethvargo/zapw/cmd/zapw)
 	@zapw ./...
 .PHONY: zapcheck
diff --git a/builders/admin-console.dockerfile b/builders/admin-console.dockerfile
deleted file mode 100644
index f1e00a5..0000000
--- a/builders/admin-console.dockerfile
+++ /dev/null
@@ -1,31 +0,0 @@
-# Copyright 2021 Google LLC
-#
-# 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.
-
-FROM alpine AS builder
-RUN mkdir -p /var/run/secrets && \
-  chmod 0700 /var/run/secrets && \
-  chown 65534:65534 /var/run/secrets
-
-FROM scratch
-COPY ./builders/passwd /etc/passwd
-COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
-
-USER nobody
-COPY ./bin/admin-console /admin-console
-COPY --chown=65534:65534 ./cmd/admin-console/templates /templates
-COPY --from=builder /var/run /var/run
-COPY --from=builder /var/run/secrets /var/run/secrets
-
-ENV PORT 8080
-ENTRYPOINT ["/admin-console"]
diff --git a/builders/build.yaml b/builders/build.yaml
index e25ec71..a11dbf0 100644
--- a/builders/build.yaml
+++ b/builders/build.yaml
@@ -98,7 +98,7 @@ steps:
   name: 'docker:19'
   args:
   - 'build'
-  - '--file=builders/admin-console.dockerfile'
+  - '--file=builders/service.dockerfile'
   - '--tag=gcr.io/${PROJECT_ID}/${_REPO}/admin-console:${_TAG}'
   - '--build-arg=SERVICE=admin-console'
   - '.'
diff --git a/internal/admin/config.go b/internal/admin/config.go
index 9377d62..a9fc6ac 100644
--- a/internal/admin/config.go
+++ b/internal/admin/config.go
@@ -17,9 +17,9 @@
 package admin
 
 import (
+	"embed"
 	"fmt"
 	"html/template"
-	"log"
 	"net/http"
 
 	"github.com/google/exposure-notifications-server/internal/database"
@@ -29,6 +29,9 @@ import (
 	"github.com/google/exposure-notifications-server/pkg/secrets"
 )
 
+//go:embed templates/*
+var assetsFS embed.FS
+
 var (
 	_ setup.BlobstoreConfigProvider     = (*Config)(nil)
 	_ setup.DatabaseConfigProvider      = (*Config)(nil)
@@ -42,10 +45,7 @@ type Config struct {
 	SecretManager secrets.Config
 	Storage       storage.Config
 
-	Port         string `env:"PORT, default=8080"`
-	TemplatePath string `env:"TEMPLATE_DIR, default=./cmd/admin-console/templates"`
-	TopFile      string `env:"TOP_FILE, default=top"`
-	BotFile      string `env:"BOTTOM_FILE, default=bottom"`
+	Port string `env:"PORT, default=8080"`
 }
 
 func (c *Config) DatabaseConfig() *database.Config {
@@ -64,30 +64,30 @@ func (c *Config) BlobstoreConfig() *storage.Config {
 	return &c.Storage
 }
 
-func (c *Config) RenderTemplate(w http.ResponseWriter, tmpl string, p TemplateMap) error {
-	files := []string{
-		fmt.Sprintf("%s/%s.html", c.TemplatePath, c.TopFile),
-		fmt.Sprintf("%s/%s.html", c.TemplatePath, tmpl),
-		fmt.Sprintf("%s/%s.html", c.TemplatePath, c.BotFile),
+func (c *Config) TemplateRenderer() (*template.Template, error) {
+	tmpl, err := template.New("").
+		Option("missingkey=zero").
+		Funcs(TemplateFuncMap).
+		ParseFS(assetsFS, "templates/*.html")
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse templates from fs: %w", err)
 	}
+	return tmpl, nil
+}
 
-	t, err := template.
-		New(tmpl).
-		Funcs(TemplateFuncMap).
-		ParseFiles(files...)
+func (c *Config) RenderTemplate(w http.ResponseWriter, name string, p TemplateMap) error {
+	tmpl, err := c.TemplateRenderer()
 	if err != nil {
-		w.WriteHeader(http.StatusInternalServerError)
-		fmt.Fprint(w, err)
-		log.Printf("ERROR: %v", err)
+		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return err
 	}
-	if err := t.ExecuteTemplate(w, tmpl, p); err != nil {
-		message := fmt.Sprintf("error rendering template: %v", err)
-		w.WriteHeader(http.StatusInternalServerError)
-		fmt.Fprint(w, message)
-		log.Printf("ERROR: %v", err)
-		return fmt.Errorf("error rendering template: %w", err)
+
+	if err := tmpl.ExecuteTemplate(w, name, p); err != nil {
+		err = fmt.Errorf("failed to render template %q: %w", name, err)
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return err
 	}
 
+	w.WriteHeader(http.StatusOK)
 	return nil
 }
diff --git a/internal/admin/server.go b/internal/admin/server.go
index fa17f4e..68fbfc5 100644
--- a/internal/admin/server.go
+++ b/internal/admin/server.go
@@ -42,9 +42,14 @@ func NewServer(config *Config, env *serverenv.ServerEnv) (*Server, error) {
 }
 
 func (s *Server) Routes(ctx context.Context) http.Handler {
+	tmpl, err := s.config.TemplateRenderer()
+	if err != nil {
+		panic(fmt.Errorf("failed to load templates: %w", err))
+	}
+
 	mux := gin.Default()
 	mux.SetFuncMap(TemplateFuncMap)
-	mux.LoadHTMLGlob(s.config.TemplatePath + "/*")
+	mux.SetHTMLTemplate(tmpl)
 
 	// Landing page.
 	mux.GET("/", s.HandleIndex())
diff --git a/cmd/admin-console/templates/authorizedapp.html b/internal/admin/templates/authorizedapp.html
similarity index 100%
rename from cmd/admin-console/templates/authorizedapp.html
rename to internal/admin/templates/authorizedapp.html
diff --git a/cmd/admin-console/templates/bottom.html b/internal/admin/templates/bottom.html
similarity index 100%
rename from cmd/admin-console/templates/bottom.html
rename to internal/admin/templates/bottom.html
diff --git a/cmd/admin-console/templates/error.html b/internal/admin/templates/error.html
similarity index 100%
rename from cmd/admin-console/templates/error.html
rename to internal/admin/templates/error.html
diff --git a/cmd/admin-console/templates/export-importer.html b/internal/admin/templates/export-importer.html
similarity index 100%
rename from cmd/admin-console/templates/export-importer.html
rename to internal/admin/templates/export-importer.html
diff --git a/cmd/admin-console/templates/export.html b/internal/admin/templates/export.html
similarity index 100%
rename from cmd/admin-console/templates/export.html
rename to internal/admin/templates/export.html
diff --git a/cmd/admin-console/templates/healthauthority.html b/internal/admin/templates/healthauthority.html
similarity index 100%
rename from cmd/admin-console/templates/healthauthority.html
rename to internal/admin/templates/healthauthority.html
diff --git a/cmd/admin-console/templates/index.html b/internal/admin/templates/index.html
similarity index 100%
rename from cmd/admin-console/templates/index.html
rename to internal/admin/templates/index.html
diff --git a/cmd/admin-console/templates/mirror.html b/internal/admin/templates/mirror.html
similarity index 100%
rename from cmd/admin-console/templates/mirror.html
rename to internal/admin/templates/mirror.html
diff --git a/cmd/admin-console/templates/siginfo.html b/internal/admin/templates/siginfo.html
similarity index 100%
rename from cmd/admin-console/templates/siginfo.html
rename to internal/admin/templates/siginfo.html
diff --git a/cmd/admin-console/templates/top.html b/internal/admin/templates/top.html
similarity index 100%
rename from cmd/admin-console/templates/top.html
rename to internal/admin/templates/top.html
diff --git a/internal/project/util.go b/internal/project/util.go
new file mode 100644
index 0000000..52a7c82
--- /dev/null
+++ b/internal/project/util.go
@@ -0,0 +1,48 @@
+// Copyright 2021 Google LLC
+//
+// 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 project
+
+import (
+	"os"
+	"path/filepath"
+	"runtime"
+	"strconv"
+)
+
+// devMode indicates whether the project is running in development mode.
+var devMode, _ = strconv.ParseBool(os.Getenv("DEV_MODE"))
+
+// DevMode indicates whether the project is running in development mode.
+func DevMode() bool {
+	return devMode
+}
+
+// root is the path to this file parent module.
+var root = func() string {
+	_, self, _, _ := runtime.Caller(0)
+	return filepath.Join(filepath.Dir(self), "..", "..")
+}()
+
+// Root returns the filepath to the root of this project.
+func Root(more ...string) string {
+	if len(more) == 0 {
+		return root
+	}
+
+	parts := make([]string, 0, len(more)+1)
+	parts = append(parts, root)
+	parts = append(parts, more...)
+	return filepath.Join(parts...)
+}


Test patch:
diff --git a/internal/admin/handle_authorized_apps_test.go b/internal/admin/handle_authorized_apps_test.go
index 014c065..0ff8e09 100644
--- a/internal/admin/handle_authorized_apps_test.go
+++ b/internal/admin/handle_authorized_apps_test.go
@@ -29,11 +29,7 @@ func TestRenderAuthorizedApps(t *testing.T) {
 	m["app"] = authorizedApp
 
 	recorder := httptest.NewRecorder()
-	config := Config{
-		TemplatePath: "../../cmd/admin-console/templates",
-		TopFile:      "top",
-		BotFile:      "bottom",
-	}
+	config := Config{}
 	err := config.RenderTemplate(recorder, "authorizedapp", m)
 	if err != nil {
 		t.Fatalf("error rendering template: %v", err)
diff --git a/internal/admin/handle_export_importers_test.go b/internal/admin/handle_export_importers_test.go
index c71d912..080c114 100644
--- a/internal/admin/handle_export_importers_test.go
+++ b/internal/admin/handle_export_importers_test.go
@@ -29,11 +29,7 @@ func TestRenderExportImporters(t *testing.T) {
 	m["model"] = model
 
 	recorder := httptest.NewRecorder()
-	config := Config{
-		TemplatePath: "../../cmd/admin-console/templates",
-		TopFile:      "top",
-		BotFile:      "bottom",
-	}
+	config := Config{}
 	err := config.RenderTemplate(recorder, "export-importer", m)
 	if err != nil {
 		t.Fatalf("error rendering template: %v", err)
diff --git a/internal/admin/handle_exports_test.go b/internal/admin/handle_exports_test.go
index 944c016..262af17 100644
--- a/internal/admin/handle_exports_test.go
+++ b/internal/admin/handle_exports_test.go
@@ -37,11 +37,7 @@ func TestRenderExports(t *testing.T) {
 	m["siginfos"] = sigInfos
 
 	recorder := httptest.NewRecorder()
-	config := Config{
-		TemplatePath: "../../cmd/admin-console/templates",
-		TopFile:      "top",
-		BotFile:      "bottom",
-	}
+	config := Config{}
 	err := config.RenderTemplate(recorder, "export", m)
 	if err != nil {
 		t.Fatalf("error rendering template: %v", err)
diff --git a/internal/admin/handle_mirrors_test.go b/internal/admin/handle_mirrors_test.go
index 8caf7fb..3982362 100644
--- a/internal/admin/handle_mirrors_test.go
+++ b/internal/admin/handle_mirrors_test.go
@@ -29,11 +29,7 @@ func TestRenderMirrors(t *testing.T) {
 	m["mirror"] = mirror
 
 	recorder := httptest.NewRecorder()
-	config := Config{
-		TemplatePath: "../../cmd/admin-console/templates",
-		TopFile:      "top",
-		BotFile:      "bottom",
-	}
+	config := Config{}
 	err := config.RenderTemplate(recorder, "mirror", m)
 	if err != nil {
 		t.Fatalf("error rendering template: %v", err)
diff --git a/internal/admin/handle_signature_infos_test.go b/internal/admin/handle_signature_infos_test.go
index 45c1e67..f6a3958 100644
--- a/internal/admin/handle_signature_infos_test.go
+++ b/internal/admin/handle_signature_infos_test.go
@@ -29,11 +29,7 @@ func TestRenderSignatureInfo(t *testing.T) {
 	m["siginfo"] = sigInfo
 
 	recorder := httptest.NewRecorder()
-	config := Config{
-		TemplatePath: "../../cmd/admin-console/templates",
-		TopFile:      "top",
-		BotFile:      "bottom",
-	}
+	config := Config{}
 	err := config.RenderTemplate(recorder, "siginfo", m)
 	if err != nil {
 		t.Fatalf("error rendering template: %v", err)