From 4116e695b0dbfe6b0df48184501e4aea46962c9d Mon Sep 17 00:00:00 2001 From: Igor Ignatyev Date: Wed, 1 May 2024 14:53:34 +0300 Subject: [PATCH] #43: web - add prebuild script --- .gitignore | 3 +- Makefile | 36 ++++- cmd/launchr/{launchr.go => launchr.dev.go} | 4 +- cmd/launchr/launchr.os.go | 19 +++ files.dev.go | 22 +++ files.embed.go | 30 ---- files.os.go | 27 +++- go.mod | 8 +- go.sum | 16 +- plugin.go | 15 +- scripts/prebuild.go | 172 +++++++++++++++++++++ 11 files changed, 287 insertions(+), 65 deletions(-) rename cmd/launchr/{launchr.go => launchr.dev.go} (71%) create mode 100644 cmd/launchr/launchr.os.go create mode 100644 files.dev.go delete mode 100644 files.embed.go create mode 100644 scripts/prebuild.go diff --git a/.gitignore b/.gitignore index 2ee6d97..20eeee3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.tar *.gz dist/ -/swagger-ui /.husky/_ .idea +assets/ +swagger-ui/ \ No newline at end of file diff --git a/Makefile b/Makefile index f3e1d43..2499025 100644 --- a/Makefile +++ b/Makefile @@ -19,9 +19,9 @@ else LDFLAGS_EXTRA=-s -w BUILD_OPTS=-trimpath endif -EMBED?=0 -ifeq ($(EMBED), 1) - BUILD_OPTS+=-tags embed +DEV?=0 +ifeq ($(DEV), 1) + BUILD_OPTS+=-tags dev endif BUILD_ENVPARMS:=CGO_ENABLED=0 @@ -33,10 +33,18 @@ LOCAL_BIN:=$(CURDIR)/bin GOLANGCI_BIN:=$(LOCAL_BIN)/golangci-lint GOLANGCI_TAG:=1.55.2 -SWAGGER_UI_DIR:=./swagger-ui +SWAGGER_UI_DIR:=cmd/launchr/assets/github.com/launchrctl/web/swagger-ui +DIST_DIR_SOURCE:=./client/dist +DIST_DIR_DEST:=cmd/launchr/assets/github.com/launchrctl/web .PHONY: all -all: deps test build +all: clean deps copy-front-build test build + +# clean assets folder +.PHONY: clean +clean: + $(info Cleaning assets folder...) + @sudo rm -rf cmd/launchr/assets # Install go dependencies .PHONY: deps @@ -47,7 +55,7 @@ deps: echo "Downloading Swagger UI..."; \ curl -Ss https://api.github.com/repos/swagger-api/swagger-ui/releases/latest | grep tarball_url | cut -d '"' -f 4 |\ xargs curl -LsS -o swagger-ui.tar.gz; \ - rm -rf $(SWAGGER_UI_DIR) $(SWAGGER_UI_DIR)-tmp && mkdir $(SWAGGER_UI_DIR)-tmp; \ + rm -rf $(SWAGGER_UI_DIR) $(SWAGGER_UI_DIR)-tmp && mkdir -p $(SWAGGER_UI_DIR)-tmp; \ tar xzf swagger-ui.tar.gz -C $(SWAGGER_UI_DIR)-tmp --strip=1; \ mv $(SWAGGER_UI_DIR)-tmp/dist $(SWAGGER_UI_DIR) && rm -rf $(SWAGGER_UI_DIR)-tmp && rm swagger-ui.tar.gz; \ sed -i.bkp "s|https://petstore.swagger.io/v2/swagger.json|/api/swagger.json|g" $(SWAGGER_UI_DIR)/swagger-initializer.js; \ @@ -63,6 +71,9 @@ test: .PHONY: build build: $(info Building launchr...) +ifeq ($(DEV),1) + @echo "development mode" +endif # Application related information available on build time. $(eval LDFLAGS:=-X '$(GOPKG).name=launchr' -X '$(GOPKG).version=$(APP_VERSION)' $(LDFLAGS_EXTRA)) $(eval BIN?=$(LOCAL_BIN)/launchr) @@ -92,13 +103,22 @@ endif $(info Running lint...) $(GOLANGCI_BIN) run --fix ./... - # Front tasks. front-install: docker run --rm -it -v $(PWD)/client:/usr/src/app -w /usr/src/app node:$(NODE_TAG) sh -c "corepack install && corepack enable && yarn install" front-build: - docker run --rm -it -v $(PWD)/client:/usr/src/app -w /usr/src/app node:$(NODE_TAG) sh -c "corepack install && corepack enable && yarn build" + docker run --rm -it -v $(PWD)/client:/usr/src/app -w /usr/src/app node:$(NODE_TAG) sh -c "corepack install && corepack enable && yarn build" \ + +copy-front-build: +ifeq ($(DEV),0) + $(info Copying front-build into assets...) + @if [ ! -d "$(DIST_DIR_DEST)" ]; then \ + echo "Creating assets folder"; \ + mkdir -p "$(DIST_DIR_DEST)"; \ + fi + @sudo cp -r "$(DIST_DIR_SOURCE)" "$(DIST_DIR_DEST)"; +endif front-dev: docker run --rm -it -v $(PWD)/client:/usr/src/app -p 5173:5173 -w /usr/src/app node:$(NODE_TAG) sh -c "corepack install && corepack enable && yarn dev -- --host" diff --git a/cmd/launchr/launchr.go b/cmd/launchr/launchr.dev.go similarity index 71% rename from cmd/launchr/launchr.go rename to cmd/launchr/launchr.dev.go index adf49cf..f98652b 100644 --- a/cmd/launchr/launchr.go +++ b/cmd/launchr/launchr.dev.go @@ -1,3 +1,5 @@ +//go:build dev + // Package executes Launchr application. package main @@ -9,5 +11,5 @@ import ( ) func main() { - os.Exit(launchr.Run()) + os.Exit(launchr.Run(&launchr.AppOptions{})) } diff --git a/cmd/launchr/launchr.os.go b/cmd/launchr/launchr.os.go new file mode 100644 index 0000000..010dcf0 --- /dev/null +++ b/cmd/launchr/launchr.os.go @@ -0,0 +1,19 @@ +//go:build !dev + +// Package executes Launchr application. +package main + +import ( + "embed" + "os" + + "github.com/launchrctl/launchr" + _ "github.com/launchrctl/web" +) + +//go:embed assets/* +var assets embed.FS + +func main() { + os.Exit(launchr.Run(&launchr.AppOptions{AssetsFs: assets})) +} diff --git a/files.dev.go b/files.dev.go new file mode 100644 index 0000000..5f63acb --- /dev/null +++ b/files.dev.go @@ -0,0 +1,22 @@ +//go:build dev + +package web + +import ( + "github.com/launchrctl/web/server" + "io/fs" + "os" +) + +func prepareRunOption(_ *Plugin, opts *server.RunOptions) { + opts.SwaggerUIFS = defaultSwaggerUIFS() + opts.ClientFS = defaultClientFS() +} + +func defaultSwaggerUIFS() fs.FS { + return os.DirFS("./cmd/launchr/assets/github.com/launchrctl/web/swagger-ui/swagger-ui") +} + +func defaultClientFS() fs.FS { + return os.DirFS("./client/dist") +} diff --git a/files.embed.go b/files.embed.go deleted file mode 100644 index 18cd142..0000000 --- a/files.embed.go +++ /dev/null @@ -1,30 +0,0 @@ -//go:build embed - -package web - -import ( - "embed" - "io/fs" -) - -//go:embed swagger-ui/* -var swaggerUIFS embed.FS - -//go:embed client/dist/* -var clientFS embed.FS - -func defaultSwaggerUIFS() fs.FS { - sub, err := fs.Sub(swaggerUIFS, "swagger-ui") - if err != nil { - panic(err) - } - return sub -} - -func defaultClientFS() fs.FS { - sub, err := fs.Sub(clientFS, "client/dist") - if err != nil { - panic(err) - } - return sub -} diff --git a/files.os.go b/files.os.go index 4d474c2..0398313 100644 --- a/files.os.go +++ b/files.os.go @@ -1,16 +1,31 @@ -//go:build !embed +//go:build !dev package web import ( "io/fs" - "os" + + "github.com/launchrctl/web/server" ) -func defaultSwaggerUIFS() fs.FS { - return os.DirFS("./swagger-ui") +func prepareRunOption(p *Plugin, opts *server.RunOptions) { + assetsFs := p.app.GetPluginAssets(p) + opts.SwaggerUIFS = defaultSwaggerUIFS(assetsFs) + opts.ClientFS = defaultClientFS(assetsFs) +} + +func defaultSwaggerUIFS(assets fs.FS) fs.FS { + sub, err := fs.Sub(assets, "swagger-ui") + if err != nil { + panic(err) + } + return sub } -func defaultClientFS() fs.FS { - return os.DirFS("./client/dist") +func defaultClientFS(assets fs.FS) fs.FS { + sub, err := fs.Sub(assets, "dist") + if err != nil { + panic(err) + } + return sub } diff --git a/go.mod b/go.mod index 7d494c0..049d8e6 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,11 @@ require ( github.com/go-chi/chi/v5 v5.0.11 github.com/go-chi/cors v1.2.1 github.com/go-chi/render v1.0.3 - github.com/launchrctl/launchr v0.5.7 + github.com/launchrctl/launchr v0.8.2-0.20240513162729-0023678afad2 github.com/oapi-codegen/nethttp-middleware v1.0.1 github.com/oapi-codegen/runtime v1.1.1 github.com/spf13/cobra v1.8.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -50,7 +51,7 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0-rc6 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect @@ -64,7 +65,6 @@ require ( golang.org/x/mod v0.15.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.17.0 // indirect + golang.org/x/tools v0.18.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 4c5efb1..bddd744 100644 --- a/go.sum +++ b/go.sum @@ -84,8 +84,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/launchrctl/launchr v0.5.7 h1:F02oAwxX/XM9oz6kw2Wvg1HXbLbYaN+znpgiS/1yt3c= -github.com/launchrctl/launchr v0.5.7/go.mod h1:mz7JSPdg/PqRX3iUZdln8n0kmq/B7vhC8sPeT9z5LHs= +github.com/launchrctl/launchr v0.8.2-0.20240513162729-0023678afad2 h1:FhZXR1ktpyBtZHHFbpAlYyersdS7twx+CQ4jJBIzz0A= +github.com/launchrctl/launchr v0.8.2-0.20240513162729-0023678afad2/go.mod h1:et+ykNbE3m7mMPydWKDV/6slFus1CD1vOaGH+j5uJ3M= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/moby/moby v25.0.3+incompatible h1:Uzxm7JQOHBY8kZY2fa95a9kg0aTOt1cBidSZ+LXCxC4= @@ -110,8 +110,8 @@ github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmt github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU= -github.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -170,8 +170,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -195,8 +195,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/plugin.go b/plugin.go index bf30465..2d8e1ae 100644 --- a/plugin.go +++ b/plugin.go @@ -4,11 +4,9 @@ package web import ( "fmt" - "github.com/spf13/cobra" - "github.com/launchrctl/launchr" - "github.com/launchrctl/web/server" + "github.com/spf13/cobra" ) // APIPrefix is a default api prefix on the server. @@ -46,15 +44,18 @@ func (p *Plugin) CobraAddCommands(rootCmd *cobra.Command) error { RunE: func(cmd *cobra.Command, args []string) error { // Don't show usage help on a runtime error. cmd.SilenceUsage = true - return server.Run(cmd.Context(), p.app, &server.RunOptions{ + + runOpts := &server.RunOptions{ Addr: fmt.Sprintf(":%s", port), // @todo use proper addr APIPrefix: APIPrefix, SwaggerJSON: useSwaggerUI, - SwaggerUIFS: defaultSwaggerUIFS(), - ClientFS: defaultClientFS(), ProxyClient: proxyClient, // @todo use embed fs for client or provide path ? - }) + } + + prepareRunOption(p, runOpts) + + return server.Run(cmd.Context(), p.app, runOpts) }, } cmd.Flags().StringVarP(&port, "port", "p", "8080", `Web server port`) diff --git a/scripts/prebuild.go b/scripts/prebuild.go new file mode 100644 index 0000000..82a7a64 --- /dev/null +++ b/scripts/prebuild.go @@ -0,0 +1,172 @@ +//go:build ignore + +package main + +import ( + "archive/tar" + "compress/gzip" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" +) + +func handleErr(err error) { + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } +} + +var ( + tplDistPath = "https://github.com/launchrctl/web/releases/download/%s/dist.tar.gz" +) + +func main() { + if len(os.Args) < 3 { + fmt.Println("not enough arguments provided") + os.Exit(2) + } + + release := os.Args[1] + folderPath := os.Args[2] + + archivePath := filepath.Clean(filepath.Join(folderPath, "dist.tar.gz")) + resultPath := filepath.Clean(filepath.Join(folderPath, ".")) + + fmt.Println("Trying to download dist archive...") + + downloadURL := fmt.Sprintf(tplDistPath, release) + err := downloadFile(downloadURL, archivePath) + handleErr(err) + + fmt.Println("Trying to unarchive dist archive...") + err = untar(archivePath, resultPath) + handleErr(err) + + fmt.Println("Removing tar file") + err = os.Remove(archivePath) + handleErr(err) + + fmt.Println("Success") +} + +func downloadFile(url string, filePath string) error { + // Create the file + out, err := os.Create(filepath.Clean(filePath)) + if err != nil { + return err + } + defer out.Close() + + // Download the body + client := &http.Client{} + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return err + } + + resp, err := client.Do(req) + if err != nil { + resp.Body.Close() //nolint + return err + } + defer resp.Body.Close() + + // Write the body to file + _, err = io.Copy(out, resp.Body) + return err +} + +func untar(fpath, tpath string) error { + r, errOp := os.Open(filepath.Clean(fpath)) + if errOp != nil { + return errOp + } + + gzr, errRead := gzip.NewReader(r) + if errRead != nil { + return errRead + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + + for { + header, err := tr.Next() + + switch { + + // if no more files are found return + case err == io.EOF: + + return nil + + // return any other error + case err != nil: + return err + + // if the header is nil, just skip it (not sure how this happens) + case header == nil: + continue + } + + // the target location where the dir/file should be created + target, err := sanitizeArchivePath(tpath, header.Name) + if err != nil { + return errors.New("invalid filepath") + } + + if !strings.HasPrefix(target, filepath.Clean(tpath)) { + return errors.New("invalid filepath") + } + + // check the file type + switch header.Typeflag { + + // if it's a dir, and it doesn't exist create it + case tar.TypeDir: + if _, err := os.Stat(target); err != nil { + if err := os.MkdirAll(target, 0750); err != nil { + return err + } + } + + // if it's a file create it + case tar.TypeReg: + f, err := os.OpenFile(filepath.Clean(target), os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + if err != nil { + return err + } + + for { + _, err = io.CopyN(f, tr, 1024) + if err != nil { + if err != io.EOF { + return err + } + break + } + } + + // manually close here after each file operation; deferring would cause each file close + // to wait until all operations have completed. + err = f.Close() + if err != nil { + return err + } + } + } +} + +func sanitizeArchivePath(d, t string) (v string, err error) { + v = filepath.Join(d, t) + if strings.HasPrefix(v, filepath.Clean(d)) { + return v, nil + } + + return "", fmt.Errorf("%s: %s", "content filepath is tainted", t) +}