multinode,web/multinode: use go:embed for assets

Go can now directly embed files without relying on external tools.
This makes code use go:embed and avoid the external tooling.

go:embed requires files to be present in the embedded directory,
hence we need to add .keep to "dist" folder. We also add one to
public/.keep, such that it won't be deleted when building multinode.

Change-Id: I53ac3d5ac76e44f740d95221acf0da99fc256d42
This commit is contained in:
Egon Elbre 2022-03-10 17:28:10 +02:00
parent fb7454ed9c
commit 28c9403702
8 changed files with 46 additions and 70 deletions

View File

@ -175,11 +175,6 @@ multinode-console:
-u $(shell id -u):$(shell id -g) \ -u $(shell id -u):$(shell id -g) \
node:${NODE_VERSION} \ node:${NODE_VERSION} \
/bin/bash -c "npm ci && npm run build" /bin/bash -c "npm ci && npm run build"
# embed web assets into go
go-bindata -prefix web/multinode/ -fs -o multinode/console/consoleassets/bindata.resource.go -pkg consoleassets web/multinode/dist/... web/multinode/static/...
# configure existing go code to know about the new assets
/usr/bin/env echo -e '\nfunc init() { FileSystem = AssetFile() }' >> multinode/console/consoleassets/bindata.resource.go
gofmt -w -s multinode/console/consoleassets/bindata.resource.go
.PHONY: satellite-admin-ui .PHONY: satellite-admin-ui
satellite-admin-ui: satellite-admin-ui:

View File

@ -1,13 +0,0 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package consoleassets
import (
"net/http"
)
// FileSystem is nil by default, but when our Makefile generates
// embedded resources via go-bindata, it will also drop an init method
// that sets this to not nil.
var FileSystem http.FileSystem

View File

@ -6,8 +6,8 @@ package server
import ( import (
"context" "context"
"errors" "errors"
"html/template" "io"
"io/ioutil" "io/fs"
"net" "net"
"net/http" "net/http"
@ -54,7 +54,7 @@ type Server struct {
log *zap.Logger log *zap.Logger
listener net.Listener listener net.Listener
http http.Server http http.Server
assets http.FileSystem assets fs.FS
nodes *nodes.Service nodes *nodes.Service
payouts *payouts.Service payouts *payouts.Service
@ -62,12 +62,10 @@ type Server struct {
bandwidth *bandwidth.Service bandwidth *bandwidth.Service
storage *storage.Service storage *storage.Service
reputation *reputation.Service reputation *reputation.Service
index *template.Template
} }
// NewServer returns new instance of Multinode Dashboard http server. // NewServer returns new instance of Multinode Dashboard http server.
func NewServer(log *zap.Logger, listener net.Listener, assets http.FileSystem, services Services) (*Server, error) { func NewServer(log *zap.Logger, listener net.Listener, assets fs.FS, services Services) (*Server, error) {
server := Server{ server := Server{
log: log, log: log,
listener: listener, listener: listener,
@ -134,11 +132,8 @@ func NewServer(log *zap.Logger, listener net.Listener, assets http.FileSystem, s
reputationRouter := apiRouter.PathPrefix("/reputation").Subrouter() reputationRouter := apiRouter.PathPrefix("/reputation").Subrouter()
reputationRouter.HandleFunc("/satellites/{satelliteID}", reputationController.Stats) reputationRouter.HandleFunc("/satellites/{satelliteID}", reputationController.Stats)
if server.assets != nil { router.PathPrefix("/static").Handler(http.FileServer(http.FS(server.assets)))
fs := http.FileServer(server.assets) router.PathPrefix("/").HandlerFunc(server.appHandler)
router.PathPrefix("/static/").Handler(http.StripPrefix("/static", fs))
router.PathPrefix("/").HandlerFunc(server.appHandler)
}
server.http = http.Server{ server.http = http.Server{
Handler: router, Handler: router,
@ -155,24 +150,18 @@ func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) {
header.Set("X-Content-Type-Options", "nosniff") header.Set("X-Content-Type-Options", "nosniff")
header.Set("Referrer-Policy", "same-origin") header.Set("Referrer-Policy", "same-origin")
if server.index == nil { f, err := server.assets.Open("index.html")
server.log.Error("index template is not set") if err != nil {
http.Error(w, `web/multinode unbuilt, run "npm install && npm run build" in web/multinode.`, http.StatusNotFound)
return return
} }
defer func() { _ = f.Close() }()
if err := server.index.Execute(w, nil); err != nil { _, _ = io.Copy(w, f)
server.log.Error("index template could not be executed", zap.Error(Error.Wrap(err)))
return
}
} }
// Run starts the server that host webapp and api endpoints. // Run starts the server that host webapp and api endpoints.
func (server *Server) Run(ctx context.Context) (err error) { func (server *Server) Run(ctx context.Context) (err error) {
err = server.initializeTemplates()
if err != nil {
return Error.Wrap(err)
}
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
var group errgroup.Group var group errgroup.Group
@ -196,23 +185,3 @@ func (server *Server) Run(ctx context.Context) (err error) {
func (server *Server) Close() error { func (server *Server) Close() error {
return Error.Wrap(server.http.Close()) return Error.Wrap(server.http.Close())
} }
// initializeTemplates is used to initialize all templates.
func (server *Server) initializeTemplates() (err error) {
f, err := server.assets.Open("/dist/index.html")
if err != nil {
server.log.Error("dist folder is not generated. use 'npm run build' command", zap.Error(err))
return nil
}
b, err := ioutil.ReadAll(f)
if err != nil {
return Error.Wrap(err)
}
server.index, err = template.New("index.html").Parse(string(b))
if err != nil {
return Error.Wrap(err)
}
return nil
}

View File

@ -5,8 +5,10 @@ package multinode
import ( import (
"context" "context"
"io/fs"
"net" "net"
"net/http" "os"
"path/filepath"
"github.com/spacemonkeygo/monkit/v3" "github.com/spacemonkeygo/monkit/v3"
"go.uber.org/zap" "go.uber.org/zap"
@ -17,7 +19,6 @@ import (
"storj.io/common/rpc" "storj.io/common/rpc"
"storj.io/private/debug" "storj.io/private/debug"
"storj.io/storj/multinode/bandwidth" "storj.io/storj/multinode/bandwidth"
"storj.io/storj/multinode/console/consoleassets"
"storj.io/storj/multinode/console/server" "storj.io/storj/multinode/console/server"
"storj.io/storj/multinode/nodes" "storj.io/storj/multinode/nodes"
"storj.io/storj/multinode/operators" "storj.io/storj/multinode/operators"
@ -25,6 +26,7 @@ import (
"storj.io/storj/multinode/reputation" "storj.io/storj/multinode/reputation"
"storj.io/storj/multinode/storage" "storj.io/storj/multinode/storage"
"storj.io/storj/private/lifecycle" "storj.io/storj/private/lifecycle"
multinodeweb "storj.io/storj/web/multinode"
) )
var ( var (
@ -175,10 +177,14 @@ func New(log *zap.Logger, full *identity.FullIdentity, config Config, db DB) (_
return nil, err return nil, err
} }
assets := consoleassets.FileSystem var assets fs.FS
assets = multinodeweb.Assets
// Use a custom directory instead of the embedded assets.
if config.Console.StaticDir != "" { if config.Console.StaticDir != "" {
// a specific directory has been configured. use it // HACKFIX: Previous setups specify the directory for web/multinode,
assets = http.Dir(config.Console.StaticDir) // instead of the actual built data. This is for backwards compatibility.
distDir := filepath.Join(config.Console.StaticDir, "dist")
assets = os.DirFS(distDir)
} }
peer.Console.Endpoint, err = server.NewServer( peer.Console.Endpoint, err = server.NewServer(

23
web/multinode/assets.go Normal file
View File

@ -0,0 +1,23 @@
// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
package multinodeweb
import (
"embed"
"fmt"
"io/fs"
)
//go:embed dist/*
var assets embed.FS
// Assets contains either the built multinode from web/multinode/dist directory
// or it is empty.
var Assets = func() fs.FS {
dist, err := fs.Sub(assets, "dist")
if err != nil {
panic(fmt.Errorf("invalid embedding: %w", err))
}
return dist
}()

0
web/multinode/dist/.keep vendored Normal file
View File

View File

View File

@ -4,17 +4,14 @@
const path = require('path'); const path = require('path');
module.exports = { module.exports = {
publicPath: '/static/dist',
productionSourceMap: false, productionSourceMap: false,
parallel: true, parallel: true,
lintOnSave: false, // disables eslint for builds lintOnSave: false, // disables eslint for builds
assetsDir: "static",
configureWebpack: { configureWebpack: {
plugins: [], plugins: [],
}, },
chainWebpack: config => { chainWebpack: config => {
config.output.chunkFilename('js/vendors_[hash].js');
config.output.filename('js/app_[hash].js');
config.resolve.alias config.resolve.alias
.set('@', path.resolve('src')); .set('@', path.resolve('src'));
@ -22,7 +19,6 @@ module.exports = {
.plugin('html') .plugin('html')
.tap(args => { .tap(args => {
args[0].template = './index.html'; args[0].template = './index.html';
return args; return args;
}); });