*: add theme based frontend configuration
This PR reworks the web layout so static files can be provided and a "themes" directory to allow a certain degree of control over logos, styles, etc. This PR does NOT add general support for frontend customization, only enough to allow us to start exploring theming internally. The dex binary also must now be run from the root directory since templates are no longer "compiled into" the binary. The docker image has been updated with frontend assets.
This commit is contained in:
parent
e267dbd236
commit
391dc51c13
16 changed files with 175 additions and 556 deletions
|
@ -13,6 +13,11 @@ RUN apk add --update ca-certificates openssl
|
||||||
|
|
||||||
COPY _output/bin/dex /usr/local/bin/dex
|
COPY _output/bin/dex /usr/local/bin/dex
|
||||||
|
|
||||||
|
# Import frontend assets and set the correct CWD directory so the assets
|
||||||
|
# are in the default path.
|
||||||
|
COPY web /web
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
ENTRYPOINT ["dex"]
|
ENTRYPOINT ["dex"]
|
||||||
|
|
||||||
CMD ["version"]
|
CMD ["version"]
|
||||||
|
|
8
Makefile
8
Makefile
|
@ -25,7 +25,7 @@ LD_FLAGS="-w -X $(REPO_PATH)/version.Version=$(VERSION)"
|
||||||
|
|
||||||
build: bin/dex bin/example-app
|
build: bin/dex bin/example-app
|
||||||
|
|
||||||
bin/dex: FORCE generated
|
bin/dex: FORCE
|
||||||
@go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex
|
@go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex
|
||||||
|
|
||||||
bin/example-app: FORCE
|
bin/example-app: FORCE
|
||||||
|
@ -35,9 +35,6 @@ bin/example-app: FORCE
|
||||||
release-binary:
|
release-binary:
|
||||||
@go build -o _output/bin/dex -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex
|
@go build -o _output/bin/dex -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex
|
||||||
|
|
||||||
.PHONY: generated
|
|
||||||
generated: server/templates_default.go
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@go test -v -i $(shell go list ./... | grep -v '/vendor/')
|
@go test -v -i $(shell go list ./... | grep -v '/vendor/')
|
||||||
@go test -v $(shell go list ./... | grep -v '/vendor/')
|
@go test -v $(shell go list ./... | grep -v '/vendor/')
|
||||||
|
@ -57,9 +54,6 @@ lint:
|
||||||
golint -set_exit_status $$package $$i || exit 1; \
|
golint -set_exit_status $$package $$i || exit 1; \
|
||||||
done
|
done
|
||||||
|
|
||||||
server/templates_default.go: $(wildcard web/templates/**)
|
|
||||||
@go run server/templates_default_gen.go
|
|
||||||
|
|
||||||
_output/bin/dex:
|
_output/bin/dex:
|
||||||
# Using rkt to build the dex binary.
|
# Using rkt to build the dex binary.
|
||||||
@./scripts/rkt-build
|
@./scripts/rkt-build
|
||||||
|
|
|
@ -30,7 +30,7 @@ type Config struct {
|
||||||
GRPC GRPC `json:"grpc"`
|
GRPC GRPC `json:"grpc"`
|
||||||
Expiry Expiry `json:"expiry"`
|
Expiry Expiry `json:"expiry"`
|
||||||
|
|
||||||
Templates server.TemplateConfig `json:"templates"`
|
Frontend server.WebConfig `json:"frontend"`
|
||||||
|
|
||||||
// StaticClients cause the server to use this list of clients rather than
|
// StaticClients cause the server to use this list of clients rather than
|
||||||
// querying the storage. Write operations, like creating a client, will fail.
|
// querying the storage. Write operations, like creating a client, will fail.
|
||||||
|
|
|
@ -151,7 +151,7 @@ func serve(cmd *cobra.Command, args []string) error {
|
||||||
Issuer: c.Issuer,
|
Issuer: c.Issuer,
|
||||||
Connectors: connectors,
|
Connectors: connectors,
|
||||||
Storage: s,
|
Storage: s,
|
||||||
TemplateConfig: c.Templates,
|
Web: c.Frontend,
|
||||||
EnablePasswordDB: c.EnablePasswordDB,
|
EnablePasswordDB: c.EnablePasswordDB,
|
||||||
}
|
}
|
||||||
if c.Expiry.SigningKeys != "" {
|
if c.Expiry.SigningKeys != "" {
|
||||||
|
|
|
@ -14,7 +14,7 @@ storage:
|
||||||
|
|
||||||
# Configuration for the HTTP endpoints.
|
# Configuration for the HTTP endpoints.
|
||||||
web:
|
web:
|
||||||
http: 127.0.0.1:5556
|
http: 0.0.0.0:5556
|
||||||
# Uncomment for HTTPS options.
|
# Uncomment for HTTPS options.
|
||||||
# https: 127.0.0.1:5554
|
# https: 127.0.0.1:5554
|
||||||
# tlsCert: /etc/dex/tls.crt
|
# tlsCert: /etc/dex/tls.crt
|
||||||
|
|
|
@ -56,7 +56,32 @@ type Config struct {
|
||||||
|
|
||||||
EnablePasswordDB bool
|
EnablePasswordDB bool
|
||||||
|
|
||||||
TemplateConfig TemplateConfig
|
Web WebConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebConfig holds the server's frontend templates and asset configuration.
|
||||||
|
//
|
||||||
|
// These are currently very custom to CoreOS and it's not recommended that
|
||||||
|
// outside users attempt to customize these.
|
||||||
|
type WebConfig struct {
|
||||||
|
// A filepath to web static.
|
||||||
|
//
|
||||||
|
// It is expected to contain the following directories:
|
||||||
|
//
|
||||||
|
// * static - Static static served at "( issuer URL )/static".
|
||||||
|
// * templates - HTML templates controlled by dex.
|
||||||
|
// * themes/(theme) - Static static served at "( issuer URL )/theme".
|
||||||
|
//
|
||||||
|
Dir string
|
||||||
|
|
||||||
|
// Defaults to "( issuer URL )/theme/logo.png"
|
||||||
|
LogoURL string
|
||||||
|
|
||||||
|
// Defaults to "dex"
|
||||||
|
Issuer string
|
||||||
|
|
||||||
|
// Defaults to "coreos"
|
||||||
|
Theme string
|
||||||
}
|
}
|
||||||
|
|
||||||
func value(val, defaultValue time.Duration) time.Duration {
|
func value(val, defaultValue time.Duration) time.Duration {
|
||||||
|
@ -130,9 +155,17 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy)
|
||||||
supported[respType] = true
|
supported[respType] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpls, err := loadTemplates(c.TemplateConfig)
|
web := webConfig{
|
||||||
|
dir: c.Web.Dir,
|
||||||
|
logoURL: c.Web.LogoURL,
|
||||||
|
issuerURL: c.Issuer,
|
||||||
|
issuer: c.Web.Issuer,
|
||||||
|
theme: c.Web.Theme,
|
||||||
|
}
|
||||||
|
|
||||||
|
static, theme, tmpls, err := loadWebConfig(web)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("server: failed to load templates: %v", err)
|
return nil, fmt.Errorf("server: failed to load web static: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
now := c.Now
|
now := c.Now
|
||||||
|
@ -159,6 +192,10 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy)
|
||||||
handleFunc := func(p string, h http.HandlerFunc) {
|
handleFunc := func(p string, h http.HandlerFunc) {
|
||||||
r.HandleFunc(path.Join(issuerURL.Path, p), h)
|
r.HandleFunc(path.Join(issuerURL.Path, p), h)
|
||||||
}
|
}
|
||||||
|
handlePrefix := func(p string, h http.Handler) {
|
||||||
|
prefix := path.Join(issuerURL.Path, p)
|
||||||
|
r.PathPrefix(prefix).Handler(http.StripPrefix(prefix, h))
|
||||||
|
}
|
||||||
r.NotFoundHandler = http.HandlerFunc(s.notFound)
|
r.NotFoundHandler = http.HandlerFunc(s.notFound)
|
||||||
|
|
||||||
discoveryHandler, err := s.discoveryHandler()
|
discoveryHandler, err := s.discoveryHandler()
|
||||||
|
@ -175,6 +212,8 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy)
|
||||||
handleFunc("/callback", s.handleConnectorCallback)
|
handleFunc("/callback", s.handleConnectorCallback)
|
||||||
handleFunc("/approval", s.handleApproval)
|
handleFunc("/approval", s.handleApproval)
|
||||||
handleFunc("/healthz", s.handleHealth)
|
handleFunc("/healthz", s.handleHealth)
|
||||||
|
handlePrefix("/static", static)
|
||||||
|
handlePrefix("/theme", theme)
|
||||||
s.mux = r
|
s.mux = r
|
||||||
|
|
||||||
startKeyRotation(ctx, c.Storage, rotationStrategy, now)
|
startKeyRotation(ctx, c.Storage, rotationStrategy, now)
|
||||||
|
|
|
@ -11,6 +11,8 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -85,6 +87,9 @@ func newTestServer(ctx context.Context, t *testing.T, updateConfig func(c *Confi
|
||||||
Connector: mock.NewCallbackConnector(),
|
Connector: mock.NewCallbackConnector(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Web: WebConfig{
|
||||||
|
Dir: filepath.Join(os.Getenv("GOPATH"), "src/github.com/coreos/dex/web"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
if updateConfig != nil {
|
if updateConfig != nil {
|
||||||
updateConfig(&config)
|
updateConfig(&config)
|
||||||
|
|
|
@ -6,8 +6,10 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -18,8 +20,6 @@ const (
|
||||||
tmplOOB = "oob.html"
|
tmplOOB = "oob.html"
|
||||||
)
|
)
|
||||||
|
|
||||||
const coreOSLogoURL = "https://coreos.com/assets/images/brand/coreos-wordmark-135x40px.png"
|
|
||||||
|
|
||||||
var requiredTmpls = []string{
|
var requiredTmpls = []string{
|
||||||
tmplApproval,
|
tmplApproval,
|
||||||
tmplLogin,
|
tmplLogin,
|
||||||
|
@ -27,65 +27,122 @@ var requiredTmpls = []string{
|
||||||
tmplOOB,
|
tmplOOB,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TemplateConfig describes.
|
type templates struct {
|
||||||
type TemplateConfig struct {
|
loginTmpl *template.Template
|
||||||
// TODO(ericchiang): Asking for a directory with a set of templates doesn't indicate
|
approvalTmpl *template.Template
|
||||||
// what the templates should look like and doesn't allow consumers of this package to
|
passwordTmpl *template.Template
|
||||||
// provide their own templates in memory. In the future clean this up.
|
oobTmpl *template.Template
|
||||||
|
|
||||||
// Directory of the templates. If empty, these will be loaded from memory.
|
|
||||||
Dir string `yaml:"dir"`
|
|
||||||
|
|
||||||
// Defaults to the CoreOS logo and "dex".
|
|
||||||
LogoURL string `yaml:"logoURL"`
|
|
||||||
Issuer string `yaml:"issuerName"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type globalData struct {
|
type webConfig struct {
|
||||||
LogoURL string
|
dir string
|
||||||
Issuer string
|
logoURL string
|
||||||
|
issuer string
|
||||||
|
theme string
|
||||||
|
issuerURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadTemplates(config TemplateConfig) (*templates, error) {
|
func join(base, path string) string {
|
||||||
var tmpls *template.Template
|
b := strings.HasSuffix(base, "/")
|
||||||
if config.Dir != "" {
|
p := strings.HasPrefix(path, "/")
|
||||||
files, err := ioutil.ReadDir(config.Dir)
|
switch {
|
||||||
if err != nil {
|
case b && p:
|
||||||
return nil, fmt.Errorf("read dir: %v", err)
|
return base + path[1:]
|
||||||
|
case b || p:
|
||||||
|
return base + path
|
||||||
|
default:
|
||||||
|
return base + "/" + path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dirExists(dir string) error {
|
||||||
|
stat, err := os.Stat(dir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("directory %q does not exist", dir)
|
||||||
}
|
}
|
||||||
filenames := []string{}
|
return fmt.Errorf("stat directory %q: %v", dir, err)
|
||||||
for _, file := range files {
|
}
|
||||||
if file.IsDir() {
|
if !stat.IsDir() {
|
||||||
continue
|
return fmt.Errorf("path %q is a file not a directory", dir)
|
||||||
}
|
}
|
||||||
filenames = append(filenames, filepath.Join(config.Dir, file.Name()))
|
return nil
|
||||||
}
|
}
|
||||||
if len(filenames) == 0 {
|
|
||||||
return nil, fmt.Errorf("no files in template dir %s", config.Dir)
|
// loadWebConfig returns static assets, theme assets, and templates used by the frontend by
|
||||||
}
|
// reading the directory specified in the webConfig.
|
||||||
if tmpls, err = template.ParseFiles(filenames...); err != nil {
|
//
|
||||||
return nil, fmt.Errorf("parse files: %v", err)
|
// The directory layout is expected to be:
|
||||||
}
|
//
|
||||||
} else {
|
// ( web directory )
|
||||||
// Load templates from memory. This code is largely copied from the standard library's
|
// |- static
|
||||||
// ParseFiles source code.
|
// |- themes
|
||||||
// See: https://goo.gl/6Wm4mN
|
// | |- (theme name)
|
||||||
for name, data := range defaultTemplates {
|
// |- templates
|
||||||
var t *template.Template
|
//
|
||||||
if tmpls == nil {
|
func loadWebConfig(c webConfig) (static, theme http.Handler, templates *templates, err error) {
|
||||||
tmpls = template.New(name)
|
if c.theme == "" {
|
||||||
}
|
c.theme = "coreos"
|
||||||
if name == tmpls.Name() {
|
}
|
||||||
t = tmpls
|
if c.issuer == "" {
|
||||||
} else {
|
c.issuer = "dex"
|
||||||
t = tmpls.New(name)
|
}
|
||||||
}
|
if c.dir == "" {
|
||||||
if _, err := t.Parse(data); err != nil {
|
c.dir = "./web"
|
||||||
return nil, fmt.Errorf("parsing %s: %v", name, err)
|
}
|
||||||
}
|
if c.logoURL == "" {
|
||||||
|
c.logoURL = join(c.issuerURL, "theme/logo.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dirExists(c.dir); err != nil {
|
||||||
|
return nil, nil, nil, fmt.Errorf("load web dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
staticDir := filepath.Join(c.dir, "static")
|
||||||
|
templatesDir := filepath.Join(c.dir, "templates")
|
||||||
|
themeDir := filepath.Join(c.dir, "themes", c.theme)
|
||||||
|
|
||||||
|
for _, dir := range []string{staticDir, templatesDir, themeDir} {
|
||||||
|
if err := dirExists(dir); err != nil {
|
||||||
|
return nil, nil, nil, fmt.Errorf("load dir: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static = http.FileServer(http.Dir(staticDir))
|
||||||
|
theme = http.FileServer(http.Dir(themeDir))
|
||||||
|
|
||||||
|
templates, err = loadTemplates(c, templatesDir)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadTemplates parses the expected templates from the provided directory.
|
||||||
|
func loadTemplates(c webConfig, templatesDir string) (*templates, error) {
|
||||||
|
files, err := ioutil.ReadDir(templatesDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filenames := []string{}
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filenames = append(filenames, filepath.Join(templatesDir, file.Name()))
|
||||||
|
}
|
||||||
|
if len(filenames) == 0 {
|
||||||
|
return nil, fmt.Errorf("no files in template dir %q", templatesDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
funcs := map[string]interface{}{
|
||||||
|
"issuer": func() string { return c.issuer },
|
||||||
|
"logo": func() string { return c.logoURL },
|
||||||
|
"url": func(s string) string { return join(c.issuerURL, s) },
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpls, err := template.New("").Funcs(funcs).ParseFiles(filenames...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse files: %v", err)
|
||||||
|
}
|
||||||
missingTmpls := []string{}
|
missingTmpls := []string{}
|
||||||
for _, tmplName := range requiredTmpls {
|
for _, tmplName := range requiredTmpls {
|
||||||
if tmpls.Lookup(tmplName) == nil {
|
if tmpls.Lookup(tmplName) == nil {
|
||||||
|
@ -95,16 +152,7 @@ func loadTemplates(config TemplateConfig) (*templates, error) {
|
||||||
if len(missingTmpls) > 0 {
|
if len(missingTmpls) > 0 {
|
||||||
return nil, fmt.Errorf("missing template(s): %s", missingTmpls)
|
return nil, fmt.Errorf("missing template(s): %s", missingTmpls)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.LogoURL == "" {
|
|
||||||
config.LogoURL = coreOSLogoURL
|
|
||||||
}
|
|
||||||
if config.Issuer == "" {
|
|
||||||
config.Issuer = "dex"
|
|
||||||
}
|
|
||||||
|
|
||||||
return &templates{
|
return &templates{
|
||||||
globalData: config,
|
|
||||||
loginTmpl: tmpls.Lookup(tmplLogin),
|
loginTmpl: tmpls.Lookup(tmplLogin),
|
||||||
approvalTmpl: tmpls.Lookup(tmplApproval),
|
approvalTmpl: tmpls.Lookup(tmplApproval),
|
||||||
passwordTmpl: tmpls.Lookup(tmplPassword),
|
passwordTmpl: tmpls.Lookup(tmplPassword),
|
||||||
|
@ -118,14 +166,6 @@ var scopeDescriptions = map[string]string{
|
||||||
"email": "View your email",
|
"email": "View your email",
|
||||||
}
|
}
|
||||||
|
|
||||||
type templates struct {
|
|
||||||
globalData TemplateConfig
|
|
||||||
loginTmpl *template.Template
|
|
||||||
approvalTmpl *template.Template
|
|
||||||
passwordTmpl *template.Template
|
|
||||||
oobTmpl *template.Template
|
|
||||||
}
|
|
||||||
|
|
||||||
type connectorInfo struct {
|
type connectorInfo struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
|
@ -142,21 +182,19 @@ func (t *templates) login(w http.ResponseWriter, connectors []connectorInfo, aut
|
||||||
sort.Sort(byName(connectors))
|
sort.Sort(byName(connectors))
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
TemplateConfig
|
|
||||||
Connectors []connectorInfo
|
Connectors []connectorInfo
|
||||||
AuthReqID string
|
AuthReqID string
|
||||||
}{t.globalData, connectors, authReqID}
|
}{connectors, authReqID}
|
||||||
renderTemplate(w, t.loginTmpl, data)
|
renderTemplate(w, t.loginTmpl, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *templates) password(w http.ResponseWriter, authReqID, callback, lastUsername string, lastWasInvalid bool) {
|
func (t *templates) password(w http.ResponseWriter, authReqID, callback, lastUsername string, lastWasInvalid bool) {
|
||||||
data := struct {
|
data := struct {
|
||||||
TemplateConfig
|
|
||||||
AuthReqID string
|
AuthReqID string
|
||||||
PostURL string
|
PostURL string
|
||||||
Username string
|
Username string
|
||||||
Invalid bool
|
Invalid bool
|
||||||
}{t.globalData, authReqID, callback, lastUsername, lastWasInvalid}
|
}{authReqID, string(callback), lastUsername, lastWasInvalid}
|
||||||
renderTemplate(w, t.passwordTmpl, data)
|
renderTemplate(w, t.passwordTmpl, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,20 +208,18 @@ func (t *templates) approval(w http.ResponseWriter, authReqID, username, clientN
|
||||||
}
|
}
|
||||||
sort.Strings(accesses)
|
sort.Strings(accesses)
|
||||||
data := struct {
|
data := struct {
|
||||||
TemplateConfig
|
|
||||||
User string
|
User string
|
||||||
Client string
|
Client string
|
||||||
AuthReqID string
|
AuthReqID string
|
||||||
Scopes []string
|
Scopes []string
|
||||||
}{t.globalData, username, clientName, authReqID, accesses}
|
}{username, clientName, authReqID, accesses}
|
||||||
renderTemplate(w, t.approvalTmpl, data)
|
renderTemplate(w, t.approvalTmpl, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *templates) oob(w http.ResponseWriter, code string) {
|
func (t *templates) oob(w http.ResponseWriter, code string) {
|
||||||
data := struct {
|
data := struct {
|
||||||
TemplateConfig
|
|
||||||
Code string
|
Code string
|
||||||
}{t.globalData, code}
|
}{code}
|
||||||
renderTemplate(w, t.oobTmpl, data)
|
renderTemplate(w, t.oobTmpl, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,85 +0,0 @@
|
||||||
// +build ignore
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ignoreFile uses "git check-ignore" to determine if we should ignore a file.
|
|
||||||
func ignoreFile(p string) (ok bool, err error) {
|
|
||||||
err = exec.Command("git", "check-ignore", p).Run()
|
|
||||||
if err == nil {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
exitErr, ok := err.(*exec.ExitError)
|
|
||||||
if ok {
|
|
||||||
if sys := exitErr.Sys(); sys != nil {
|
|
||||||
e, ok := sys.(interface {
|
|
||||||
// Is the returned value something that returns an exit status?
|
|
||||||
ExitStatus() int
|
|
||||||
})
|
|
||||||
if ok && e.ExitStatus() == 1 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maps aren't deterministic, use a struct instead.
|
|
||||||
|
|
||||||
type fileData struct {
|
|
||||||
name string
|
|
||||||
data string
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// ReadDir guarentees result in sorted order.
|
|
||||||
dir, err := ioutil.ReadDir("web/templates")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
files := []fileData{}
|
|
||||||
for _, file := range dir {
|
|
||||||
p := filepath.Join("web/templates", file.Name())
|
|
||||||
ignore, err := ignoreFile(p)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
if ignore {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := ioutil.ReadFile(p)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
if bytes.Contains(data, []byte{'`'}) {
|
|
||||||
log.Fatalf("file %s contains escape character '`' and cannot be compiled into go source", p)
|
|
||||||
}
|
|
||||||
files = append(files, fileData{file.Name(), string(data)})
|
|
||||||
}
|
|
||||||
|
|
||||||
f := new(bytes.Buffer)
|
|
||||||
|
|
||||||
fmt.Fprintln(f, "// This file was generated by the makefile. Do not edit.")
|
|
||||||
fmt.Fprintln(f)
|
|
||||||
fmt.Fprintln(f, "package server")
|
|
||||||
fmt.Fprintln(f)
|
|
||||||
fmt.Fprintln(f, "// defaultTemplates is a key for file name to file data of the files in web/templates.")
|
|
||||||
fmt.Fprintln(f, "var defaultTemplates = map[string]string{")
|
|
||||||
for _, file := range files {
|
|
||||||
fmt.Fprintf(f, "\t%q: `%s`,\n", file.name, file.data)
|
|
||||||
}
|
|
||||||
fmt.Fprintln(f, "}")
|
|
||||||
|
|
||||||
if err := ioutil.WriteFile("server/templates_default.go", f.Bytes(), 0644); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +1 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestNewTemplates(t *testing.T) {
|
|
||||||
var config TemplateConfig
|
|
||||||
if _, err := loadTemplates(config); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoadTemplates(t *testing.T) {
|
|
||||||
var config TemplateConfig
|
|
||||||
|
|
||||||
config.Dir = "../web/templates"
|
|
||||||
}
|
|
||||||
|
|
0
web/static/main.css
Normal file
0
web/static/main.css
Normal file
|
@ -3,8 +3,10 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||||
<title>{{ .Issuer }}</title>
|
<title>{{ issuer }}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link href="{{ url "static/main.css" }}" rel="stylesheet">
|
||||||
|
<link href="{{ url "theme/style.css" }}" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
-webkit-box-sizing: border-box;
|
-webkit-box-sizing: border-box;
|
||||||
|
@ -232,7 +234,7 @@
|
||||||
<body>
|
<body>
|
||||||
<div id="navbar">
|
<div id="navbar">
|
||||||
<div id="navbar-logo-wrap">
|
<div id="navbar-logo-wrap">
|
||||||
<img id="navbar-logo" src="{{ .LogoURL }}">
|
<img id="navbar-logo" src="{{ logo }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{{ template "header.html" . }}
|
{{ template "header.html" . }}
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2 class="heading">Log in to {{ .Issuer }} </h2>
|
<h2 class="heading">Log in to {{ issuer }} </h2>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{{ range $c := .Connectors }}
|
{{ range $c := .Connectors }}
|
||||||
|
|
BIN
web/themes/coreos/logo.png
Normal file
BIN
web/themes/coreos/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
0
web/themes/coreos/style.css
Normal file
0
web/themes/coreos/style.css
Normal file
Reference in a new issue