cmd/relui: use go:embed for templates and content

This changes relui to use embed from go1.16 for managing content. This
drastically simplifies the deployment steps, in preparation for
containerizing.

For golang/go#47401

Change-Id: I1eb9f6f63fa490ef73ed454ddecd1fd99db4f960
Reviewed-on: https://go-review.googlesource.com/c/build/+/340429
Trust: Alexander Rakoczy <alex@golang.org>
Run-TryBot: Alexander Rakoczy <alex@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/cmd/relui/content.go b/cmd/relui/content.go
new file mode 100644
index 0000000..ce02062
--- /dev/null
+++ b/cmd/relui/content.go
@@ -0,0 +1,18 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build go1.16
+// +build go1.16
+
+package main
+
+import "embed"
+
+// static is our static web server content.
+//go:embed static
+var static embed.FS
+
+// templates are our template files.
+//go:embed templates
+var templates embed.FS
diff --git a/cmd/relui/main.go b/cmd/relui/main.go
index b04013b..945f96a 100644
--- a/cmd/relui/main.go
+++ b/cmd/relui/main.go
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.16
+// +build go1.16
+
 // relui is a web interface for managing the release process of Go.
 package main
 
@@ -44,7 +47,7 @@
 	http.Handle("/workflows/create", http.HandlerFunc(s.createWorkflowHandler))
 	http.Handle("/workflows/new", http.HandlerFunc(s.newWorkflowHandler))
 	http.Handle("/tasks/start", http.HandlerFunc(s.startTaskHandler))
-	http.Handle("/", fileServerHandler(relativeFile("./static"), http.HandlerFunc(s.homeHandler)))
+	http.Handle("/", fileServerHandler(static, http.HandlerFunc(s.homeHandler)))
 	port := os.Getenv("PORT")
 	if port == "" {
 		port = "8080"
diff --git a/cmd/relui/store.go b/cmd/relui/store.go
index 26edf32..84db0af 100644
--- a/cmd/relui/store.go
+++ b/cmd/relui/store.go
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.16
+// +build go1.16
+
 package main
 
 import (
diff --git a/cmd/relui/templates/layout.html b/cmd/relui/templates/layout.html
index 03b892b..bf0a4aa 100644
--- a/cmd/relui/templates/layout.html
+++ b/cmd/relui/templates/layout.html
@@ -7,7 +7,7 @@
 <html lang="en">
   <title>Go Releases</title>
   <meta name="viewport" content="width=device-width, initial-scale=1" />
-  <link rel="stylesheet" href="/styles.css" />
+  <link rel="stylesheet" href="/static/styles.css" />
   <body class="Site">
     <header class="Site-header">
       <div class="Header">
diff --git a/cmd/relui/web.go b/cmd/relui/web.go
index ae32970..2ff9ad1 100644
--- a/cmd/relui/web.go
+++ b/cmd/relui/web.go
@@ -2,12 +2,16 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.16
+// +build go1.16
+
 package main
 
 import (
 	"bytes"
 	"html/template"
 	"io"
+	"io/fs"
 	"log"
 	"mime"
 	"net/http"
@@ -21,33 +25,28 @@
 	reluipb "golang.org/x/build/cmd/relui/protos"
 )
 
-// fileServerHandler returns a http.Handler rooted at root. It will call the next handler provided for requests to "/".
+// fileServerHandler returns a http.Handler rooted at root. It will
+// call the next handler provided for requests to "/".
 //
-// The returned handler sets the appropriate Content-Type and Cache-Control headers for the returned file.
-func fileServerHandler(root string, next http.Handler) http.Handler {
+// The returned handler sets the appropriate Content-Type and
+// Cache-Control headers for the returned file.
+func fileServerHandler(fs fs.FS, next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		if r.URL.Path == "/" {
 			next.ServeHTTP(w, r)
 			return
 		}
-		// http.FileServer would correctly return a 404, but we need to check that the file exists
-		// before calculating the Content-Type header.
-		if _, err := os.Stat(path.Join(root, r.URL.Path)); os.IsNotExist(err) {
-			http.NotFound(w, r)
-			return
-		}
 		w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(r.URL.Path)))
 		w.Header().Set("Cache-Control", "no-cache, private, max-age=0")
-
-		fs := http.FileServer(http.Dir(root))
-		fs.ServeHTTP(w, r)
+		s := http.FileServer(http.FS(fs))
+		s.ServeHTTP(w, r)
 	})
 }
 
 var (
-	homeTmpl        = template.Must(template.Must(layoutTmpl.Clone()).ParseFiles(relativeFile("templates/home.html")))
-	layoutTmpl      = template.Must(template.ParseFiles(relativeFile("templates/layout.html")))
-	newWorkflowTmpl = template.Must(template.Must(layoutTmpl.Clone()).ParseFiles(relativeFile("templates/new_workflow.html")))
+	homeTmpl        = template.Must(template.Must(layoutTmpl.Clone()).ParseFS(templates, "templates/home.html"))
+	layoutTmpl      = template.Must(template.ParseFS(templates, "templates/layout.html"))
+	newWorkflowTmpl = template.Must(template.Must(layoutTmpl.Clone()).ParseFS(templates, "templates/new_workflow.html"))
 )
 
 // server implements the http handlers for relui.
diff --git a/cmd/relui/web_test.go b/cmd/relui/web_test.go
index 145ad5b..dcd18e6 100644
--- a/cmd/relui/web_test.go
+++ b/cmd/relui/web_test.go
@@ -2,10 +2,14 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.16
+// +build go1.16
+
 package main
 
 import (
 	"context"
+	"embed"
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
@@ -21,8 +25,12 @@
 	"google.golang.org/grpc"
 )
 
+// testStatic is our static web server content.
+//go:embed testing
+var testStatic embed.FS
+
 func TestFileServerHandler(t *testing.T) {
-	h := fileServerHandler("./testing", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+	h := fileServerHandler(testStatic, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.Write([]byte("Home"))
 	}))
 
@@ -41,7 +49,7 @@
 		},
 		{
 			desc:     "sets headers and returns file",
-			path:     "/test.css",
+			path:     "/testing/test.css",
 			wantCode: http.StatusOK,
 			wantBody: ".Header { font-size: 10rem; }\n",
 			wantHeaders: map[string]string{
@@ -54,6 +62,9 @@
 			path:     "/foo.js",
 			wantCode: http.StatusNotFound,
 			wantBody: "404 page not found\n",
+			wantHeaders: map[string]string{
+				"Content-Type": "text/plain; charset=utf-8",
+			},
 		},
 	}
 	for _, c := range cases {