From 9f4403f43b5b1b4757c7c43e2ddbd9ba7af98645 Mon Sep 17 00:00:00 2001 From: Robert Kopaczewski Date: Wed, 4 Aug 2021 16:33:35 +0200 Subject: [PATCH] feat: wizard improvements, handle recreate steps --- .golangci.yaml | 1 + cmd/apps.go | 5 +- cmd/deploy.go | 2 +- cmd/executor_config.go | 4 +- cmd/force_unlock.go | 2 +- cmd/plugins.go | 4 +- cmd/run.go | 11 +- go.mod | 3 +- go.sum | 19 ++- internal/fileutil/file.go | 11 ++ pkg/actions/app_add.go | 240 ++++++++++++++++++++++++------- pkg/actions/app_list.go | 12 +- pkg/actions/deploy.go | 30 ++-- pkg/actions/force_unlock.go | 8 +- pkg/actions/init.go | 49 ++++--- pkg/actions/plugin_list.go | 10 +- pkg/actions/plugin_update.go | 14 +- pkg/actions/run.go | 136 ++++++++++++++++++ pkg/actions/util.go | 11 +- pkg/config/app.go | 28 +++- pkg/config/app_static.go | 6 + pkg/config/project.go | 4 +- pkg/config/state.go | 3 +- pkg/plugins/client/client.go | 20 ++- pkg/plugins/client/run.go | 54 +++++++ pkg/plugins/downloader_github.go | 4 +- schema/schema-app.json | 16 ++- templates/app-static.yaml.tpl | 7 + 28 files changed, 570 insertions(+), 144 deletions(-) create mode 100644 pkg/actions/run.go create mode 100644 pkg/plugins/client/run.go diff --git a/.golangci.yaml b/.golangci.yaml index 926de76..6c15871 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -40,6 +40,7 @@ linters-settings: disabled-checks: - whyNoLint - commentedOutCode + - octalLiteral linters: disable-all: true diff --git a/cmd/apps.go b/cmd/apps.go index be0afc6..13f31a8 100644 --- a/cmd/apps.go +++ b/cmd/apps.go @@ -32,7 +32,7 @@ func (e *Executor) newAppsCmd() *cobra.Command { return config.ErrProjectConfigNotFound } - return actions.NewAppList(e.Log(), listOpts).Run(cmd.Context(), e.cfg) + return actions.NewAppList(e.Log(), e.cfg, listOpts).Run(cmd.Context()) }, } @@ -71,7 +71,7 @@ func (e *Executor) newAppsCmd() *cobra.Command { return config.ErrProjectConfigNotFound } - return actions.NewAppAdd(e.Log(), addOpts).Run(cmd.Context(), e.cfg) + return actions.NewAppAdd(e.Log(), e.cfg, addOpts).Run(cmd.Context()) }, } @@ -82,6 +82,7 @@ func (e *Executor) newAppsCmd() *cobra.Command { f.StringVar(&addOpts.Type, "type", "", "application type (options: static, function, service)") f.StringVar(&addOpts.Static.BuildCommand, "static-build-command", "", "static app build command") f.StringVar(&addOpts.Static.BuildDir, "static-build-dir", "", "static app build dir") + f.StringVar(&addOpts.Static.DevCommand, "static-dev-command", "", "static app dev command") f.StringVar(&addOpts.Static.Routing, "static-routing", "", "static app routing (options: react, disabled)") f.StringVarP(&addOpts.OutputPath, "output-path", "o", "", "output path, defaults to: /") diff --git a/cmd/deploy.go b/cmd/deploy.go index d897728..779665a 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -22,7 +22,7 @@ func (e *Executor) newDeployCmd() *cobra.Command { return config.ErrProjectConfigNotFound } - return actions.NewDeploy(e.Log(), opts).Run(cmd.Context(), e.cfg) + return actions.NewDeploy(e.Log(), e.cfg, opts).Run(cmd.Context()) }, } diff --git a/cmd/executor_config.go b/cmd/executor_config.go index 32ba4c4..47a019f 100644 --- a/cmd/executor_config.go +++ b/cmd/executor_config.go @@ -3,12 +3,12 @@ package cmd import ( "context" "fmt" - "io/ioutil" "path/filepath" "github.com/goccy/go-yaml" "github.com/outblocks/outblocks-cli/pkg/config" "github.com/outblocks/outblocks-cli/pkg/plugins" + plugin_util "github.com/outblocks/outblocks-plugin-go/util" ) func (e *Executor) loadProjectConfig(ctx context.Context, cfgPath string, vals map[string]interface{}, skipLoadPlugins, skipCheck bool) error { @@ -69,7 +69,7 @@ func (e *Executor) saveLockfile() error { return fmt.Errorf("marshaling lockfile error: %w", err) } - if err := ioutil.WriteFile(filepath.Join(e.cfg.Path, config.LockfileName), data, 0755); err != nil { + if err := plugin_util.WriteFile(filepath.Join(e.cfg.Path, config.LockfileName), data, 0755); err != nil { return fmt.Errorf("writing lockfile error: %w", err) } diff --git a/cmd/force_unlock.go b/cmd/force_unlock.go index 3cbdd90..79248e0 100644 --- a/cmd/force_unlock.go +++ b/cmd/force_unlock.go @@ -21,7 +21,7 @@ func (e *Executor) newForceUnlockCmd() *cobra.Command { return config.ErrProjectConfigNotFound } - return actions.NewForceUnlock(e.Log()).Run(cmd.Context(), e.cfg, args[0]) + return actions.NewForceUnlock(e.Log(), e.cfg).Run(cmd.Context(), args[0]) }, } diff --git a/cmd/plugins.go b/cmd/plugins.go index c3d8947..1d7c662 100644 --- a/cmd/plugins.go +++ b/cmd/plugins.go @@ -30,7 +30,7 @@ func (e *Executor) newPluginsCmd() *cobra.Command { return config.ErrProjectConfigNotFound } - return actions.NewPluginList(e.Log(), e.loader).Run(cmd.Context(), e.cfg) + return actions.NewPluginList(e.Log(), e.cfg, e.loader).Run(cmd.Context()) }, } @@ -48,7 +48,7 @@ func (e *Executor) newPluginsCmd() *cobra.Command { return config.ErrProjectConfigNotFound } - return actions.NewPluginUpdate(e.Log(), e.loader).Run(cmd.Context(), e.cfg) + return actions.NewPluginUpdate(e.Log(), e.cfg, e.loader).Run(cmd.Context()) }, } diff --git a/cmd/run.go b/cmd/run.go index 73f7c93..a105c21 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -1,13 +1,14 @@ package cmd import ( - "fmt" - + "github.com/outblocks/outblocks-cli/pkg/actions" "github.com/outblocks/outblocks-cli/pkg/config" "github.com/spf13/cobra" ) func (e *Executor) newRunCmd() *cobra.Command { + opts := &actions.RunOptions{} + cmd := &cobra.Command{ Use: "run", Short: "Runs stack locally", @@ -21,11 +22,7 @@ func (e *Executor) newRunCmd() *cobra.Command { return config.ErrProjectConfigNotFound } - fmt.Println("RUN E") - - // spew.Dump(e.opts.cfg) - - return nil + return actions.NewRun(e.log, e.cfg, opts).Run(cmd.Context()) }, } diff --git a/go.mod b/go.mod index 0154aed..abf656b 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/otiai10/copy v1.6.0 - github.com/outblocks/outblocks-plugin-go v0.0.0-20210722172243-99b1cdb63052 + github.com/outblocks/outblocks-plugin-go v0.0.0-20210804143053-52d781bc2e48 github.com/pelletier/go-toml v1.9.1 // indirect github.com/pterm/pterm v0.12.22 github.com/spf13/afero v1.6.0 // indirect @@ -33,6 +33,7 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.7.1 + github.com/txn2/txeh v1.3.0 golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c golang.org/x/sys v0.0.0-20210603125802-9665404d3644 // indirect golang.org/x/text v0.3.6 // indirect diff --git a/go.sum b/go.sum index 1ef80d4..b7d7174 100644 --- a/go.sum +++ b/go.sum @@ -54,6 +54,7 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= @@ -73,10 +74,14 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -241,6 +246,7 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= @@ -288,8 +294,8 @@ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6 github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.2 h1:VYWnrP5fXmz1MXvjuUvcBrXSjGE6xjON+axB/UrpO3E= github.com/otiai10/mint v1.3.2/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= -github.com/outblocks/outblocks-plugin-go v0.0.0-20210722172243-99b1cdb63052 h1:zhUfN0jWTn+Avl/TidZ53KsrniQZXPPNc99bwqY9jEo= -github.com/outblocks/outblocks-plugin-go v0.0.0-20210722172243-99b1cdb63052/go.mod h1:vAn4Vv7fXTyrjNEvAVcKtKJ2Bwaqk3Oy63lqnBRIct4= +github.com/outblocks/outblocks-plugin-go v0.0.0-20210804143053-52d781bc2e48 h1:KK4S2dEXs0voTdisjI7myMetKLiE5GqslOy63XlA1WU= +github.com/outblocks/outblocks-plugin-go v0.0.0-20210804143053-52d781bc2e48/go.mod h1:vAn4Vv7fXTyrjNEvAVcKtKJ2Bwaqk3Oy63lqnBRIct4= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.1 h1:a6qW1EVNZWH9WGI6CsYdD8WAylkoXBS5yv0XHlh17Tc= @@ -318,6 +324,7 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= @@ -335,6 +342,7 @@ github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= @@ -343,6 +351,7 @@ github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= @@ -358,6 +367,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/txn2/txeh v1.3.0 h1:vnbv63htVMZCaQgLqVBxKvj2+HHHFUzNW7I183zjg3E= +github.com/txn2/txeh v1.3.0/go.mod h1:O7M6gUTPeMF+vsa4c4Ipx3JDkOYrruB1Wry8QRsMcw8= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/ulikunitz/xz v0.5.7 h1:YvTNdFzX6+W5m9msiYg/zpkSURPPtOlzbqYjrFn7Yt4= github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= @@ -366,6 +378,7 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMx github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -380,6 +393,7 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -472,6 +486,7 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/fileutil/file.go b/internal/fileutil/file.go index b2cd491..b85c1fb 100644 --- a/internal/fileutil/file.go +++ b/internal/fileutil/file.go @@ -154,3 +154,14 @@ func CheckDir(path string) (string, bool) { return path, true } + +func IsRelativeSubdir(parent, dir string) bool { + parent, _ = filepath.Abs(parent) + if !filepath.IsAbs(dir) { + dir = filepath.Join(parent, dir) + } + + rel, err := filepath.Rel(parent, dir) + + return err == nil && !strings.HasPrefix(rel, "..") +} diff --git a/pkg/actions/app_add.go b/pkg/actions/app_add.go index 9256ec3..e2b4a2c 100644 --- a/pkg/actions/app_add.go +++ b/pkg/actions/app_add.go @@ -5,7 +5,7 @@ import ( "context" "errors" "fmt" - "io/ioutil" + "io/fs" "os" "path/filepath" "regexp" @@ -20,16 +20,18 @@ import ( "github.com/outblocks/outblocks-cli/pkg/config" "github.com/outblocks/outblocks-cli/pkg/logger" "github.com/outblocks/outblocks-cli/templates" + plugin_util "github.com/outblocks/outblocks-plugin-go/util" "github.com/pterm/pterm" ) var ( errAppAddCanceled = errors.New("adding app canceled") - validValueRegex = regexp.MustCompile(`^[a-zA-Z0-9{}\-_.]+$`) + validURLRegex = regexp.MustCompile(`^(https?://)?[a-zA-Z0-9{}\-_.]+$`) ) type AppAdd struct { log logger.Logger + cfg *config.Project opts *AppAddOptions } @@ -42,6 +44,7 @@ type staticAppInfo struct { type AppStaticOptions struct { BuildCommand string BuildDir string + DevCommand string Routing string } @@ -64,22 +67,26 @@ type AppAddOptions struct { func (o *AppAddOptions) Validate() error { return validation.ValidateStruct(o, - validation.Field(&o.Name, validation.Required, validation.Match(config.ValidNameRegex)), validation.Field(&o.Type, validation.Required, validation.In(util.InterfaceSlice(config.ValidAppTypes)...)), - validation.Field(&o.URL, validation.Required, validation.Match(validValueRegex)), validation.Field(&o.Static), ) } -func NewAppAdd(log logger.Logger, opts *AppAddOptions) *AppAdd { +func NewAppAdd(log logger.Logger, cfg *config.Project, opts *AppAddOptions) *AppAdd { return &AppAdd{ log: log, + cfg: cfg, opts: opts, } } -func (d *AppAdd) Run(ctx context.Context, cfg *config.Project) error { - appInfo, err := d.prompt(ctx, cfg) +func (d *AppAdd) Run(ctx context.Context) error { + curDir, err := os.Getwd() + if err != nil { + return fmt.Errorf("can't get current working dir: %w", err) + } + + appInfo, err := d.prompt(curDir) if errors.Is(err, errAppAddCanceled) { d.log.Println("Adding application canceled.") return nil @@ -110,7 +117,9 @@ func (d *AppAdd) Run(ctx context.Context, cfg *config.Project) error { return err } - err = ioutil.WriteFile(filepath.Join(path, config.AppYAMLName+".yaml"), appYAML.Bytes(), 0644) + destFile := filepath.Join(path, config.AppYAMLName+".yaml") + + err = plugin_util.WriteFile(destFile, appYAML.Bytes(), 0644) if err != nil { return err } @@ -118,16 +127,39 @@ func (d *AppAdd) Run(ctx context.Context, cfg *config.Project) error { return nil } -func (d *AppAdd) prompt(_ context.Context, cfg *config.Project) (interface{}, error) { +func validateAppAddName(val interface{}) error { + return util.RegexValidator(config.ValidNameRegex, "must start with a letter and consist only of letters, numbers, underscore or hyphens")(val) +} + +func validateAppAddURL(val interface{}) error { + return util.RegexValidator(validURLRegex, "invalid URL, example example.com/some_path/run or using vars: ${var.base_url}/some_path/run")(val) +} + +func validateAppAddOutputPath(cfg *config.Project) func(val interface{}) error { + return func(val interface{}) error { + if s, ok := val.(string); ok && !fileutil.IsRelativeSubdir(cfg.Path, s) { + return fmt.Errorf("output path must be somewhere in current project config location tree") + } + + return nil + } +} + +func (d *AppAdd) promptBasic() error { var qs []*survey.Question if d.opts.Name == "" { qs = append(qs, &survey.Question{ Name: "name", Prompt: &survey.Input{Message: "Name of application:"}, - Validate: util.RegexValidator(config.ValidNameRegex, "must start with a letter and consist only of letters, numbers, underscore or hyphens"), + Validate: validateAppAddName, }) } else { + err := validateAppAddName(d.opts.Name) + if err != nil { + return err + } + d.log.Printf("%s %s\n", pterm.Bold.Sprint("Name of application:"), pterm.Cyan(d.opts.Name)) } @@ -148,42 +180,79 @@ func (d *AppAdd) prompt(_ context.Context, cfg *config.Project) (interface{}, er if d.opts.URL == "" { defaultURL := "" - if len(cfg.DNS) > 0 { - defaultURL = cfg.DNS[0].Domain + if len(d.cfg.DNS) > 0 { + defaultURL = d.cfg.DNS[0].Domain } qs = append(qs, &survey.Question{ Name: "url", Prompt: &survey.Input{Message: "URL of application:", Default: defaultURL}, - Validate: util.RegexValidator(validValueRegex, "invalid URL, example example.com/some_path/run or using vars: ${var.base_url}/some_path/run"), + Validate: validateAppAddURL, }) } else { - d.opts.Type = strings.ToLower(d.opts.URL) + err := validateAppAddURL(d.opts.URL) + if err != nil { + return err + } + + d.opts.URL = strings.ToLower(d.opts.URL) d.log.Printf("%s %s\n", pterm.Bold.Sprint("URL of application:"), pterm.Cyan(d.opts.URL)) } - answers := *d.opts - // Get basic info about app. if len(qs) != 0 { - err := survey.Ask(qs, &answers) + err := survey.Ask(qs, d.opts) if err != nil { - return nil, errAppAddCanceled + return errAppAddCanceled } } - err := answers.Validate() + err := d.opts.Validate() if err != nil { - return nil, err + return err } - if answers.OutputPath == "" { - answers.OutputPath = filepath.Join(cfg.Path, answers.Type, answers.Name) + qs = []*survey.Question{} + + // Get output path. + validateOutputPath := validateAppAddOutputPath(d.cfg) + + if d.opts.OutputPath == "" { + defaultOutputPath := filepath.Join(d.cfg.Path, d.opts.Type, d.opts.Name) + + qs = append(qs, &survey.Question{ + Name: "outputpath", + Prompt: &survey.Input{Message: "Path to save application YAML:", Default: defaultOutputPath}, + Validate: validateOutputPath, + }) + } else { + d.opts.OutputPath, _ = filepath.Abs(d.opts.OutputPath) + + err := validateOutputPath(d.opts.OutputPath) + if err != nil { + return err + } + + d.log.Printf("%s %s\n", pterm.Bold.Sprint("Path to save application YAML:"), pterm.Cyan(d.opts.OutputPath)) } - stat, err := os.Stat(answers.OutputPath) + err = survey.Ask(qs, d.opts) + if err != nil { + return errAppAddCanceled + } + + return nil +} + +func (d *AppAdd) prompt(curDir string) (interface{}, error) { + err := d.promptBasic() + if err != nil { + return nil, err + } + + stat, err := os.Stat(d.opts.OutputPath) if os.IsNotExist(err) { - err = os.MkdirAll(answers.OutputPath, 0755) + err = os.MkdirAll(d.opts.OutputPath, 0755) if err != nil { return nil, err } @@ -194,10 +263,10 @@ func (d *AppAdd) prompt(_ context.Context, cfg *config.Project) (interface{}, er } if stat != nil && !stat.IsDir() { - return nil, fmt.Errorf("output path '%s' is not a directory", answers.OutputPath) + return nil, fmt.Errorf("output path '%s' is not a directory", d.opts.OutputPath) } - if !d.opts.Overwrite && fileutil.FindYAML(filepath.Join(answers.OutputPath, config.AppYAMLName)) != "" { + if !d.opts.Overwrite && fileutil.FindYAML(filepath.Join(d.opts.OutputPath, config.AppYAMLName)) != "" { proceed := false prompt := &survey.Confirm{ Message: "Application config already exists! Do you want to overwrite it?", @@ -210,36 +279,103 @@ func (d *AppAdd) prompt(_ context.Context, cfg *config.Project) (interface{}, er } } - switch answers.Type { + switch d.opts.Type { case config.TypeStatic: - return d.promptStatic(&answers) + return d.promptStatic(curDir, d.opts) default: return nil, fmt.Errorf("unsupported app type (WIP)") } } -func (d *AppAdd) promptStatic(answers *AppAddOptions) (*staticAppInfo, error) { +func validateAppStaticBuildDir(cfg *config.Project, opts *AppAddOptions) func(val interface{}) error { + return func(val interface{}) error { + str, ok := val.(string) + if !ok { + return nil + } + + if !fileutil.IsRelativeSubdir(cfg.Path, str) { + return fmt.Errorf("build dir path must be somewhere in current project config location tree") + } + + if fileutil.IsRelativeSubdir(str, opts.OutputPath) { + return fmt.Errorf("build dir path cannot be a parent of output path") + } + + return nil + } +} + +func suggestAppStaticBuildDir(cfg *config.Project, opts *AppAddOptions) func(toComplete string) []string { + return func(toComplete string) []string { + var dirs []string + + _ = filepath.WalkDir(cfg.Path, func(path string, entry fs.DirEntry, err error) error { + if !entry.IsDir() { + return nil + } + + if strings.HasPrefix(entry.Name(), ".") { + return fs.SkipDir + } + + if !fileutil.IsRelativeSubdir(cfg.Path, path) || fileutil.IsRelativeSubdir(path, opts.OutputPath) { + return nil + } + + dirs = append(dirs, path) + return nil + }) + + return dirs + } +} + +func (d *AppAdd) promptStatic(curDir string, opts *AppAddOptions) (*staticAppInfo, error) { var qs []*survey.Question - if answers.Static.BuildDir == "" { + buildDirValidator := validateAppStaticBuildDir(d.cfg, opts) + + if opts.Static.BuildDir == "" { + def, _ := filepath.Rel(curDir, filepath.Join(opts.OutputPath, config.DefaultStaticAppBuildDir)) + qs = append(qs, &survey.Question{ - Name: "builddir", - Prompt: &survey.Input{Message: "Build directory of application:", Default: config.DefaultStaticAppBuildDir}, + Name: "builddir", + Prompt: &survey.Input{ + Message: "Build directory of application:", + Default: "./" + def, + Suggest: suggestAppStaticBuildDir(d.cfg, opts), + }, + Validate: buildDirValidator, }) } else { - d.log.Printf("%s %s\n", pterm.Bold.Sprint("Build directory of application:"), pterm.Cyan(answers.Static.BuildDir)) + err := buildDirValidator(opts.Static.BuildDir) + if err != nil { + return nil, err + } + + d.log.Printf("%s %s\n", pterm.Bold.Sprint("Build directory of application:"), pterm.Cyan(opts.Static.BuildDir)) } - if answers.Static.BuildCommand == "" { + if opts.Static.BuildCommand == "" { qs = append(qs, &survey.Question{ Name: "buildcommand", - Prompt: &survey.Input{Message: "Build command of application (optional):"}, + Prompt: &survey.Input{Message: "Build command of application (optional, e.g. yarn build):"}, }) } else { - d.log.Printf("%s %s\n", pterm.Bold.Sprint("Build command of application:"), pterm.Cyan(answers.Static.BuildCommand)) + d.log.Printf("%s %s\n", pterm.Bold.Sprint("Build command of application:"), pterm.Cyan(opts.Static.BuildCommand)) } - if answers.Static.Routing == "" { + if opts.Static.DevCommand == "" { + qs = append(qs, &survey.Question{ + Name: "devcommand", + Prompt: &survey.Input{Message: "Dev command of application (optional, e.g. yarn dev):"}, + }) + } else { + d.log.Printf("%s %s\n", pterm.Bold.Sprint("Dev command of application:"), pterm.Cyan(opts.Static.DevCommand)) + } + + if opts.Static.Routing == "" { qs = append(qs, &survey.Question{ Name: "routing", Prompt: &survey.Select{ @@ -249,37 +385,39 @@ func (d *AppAdd) promptStatic(answers *AppAddOptions) (*staticAppInfo, error) { }, }) } else { - d.log.Printf("%s %s\n", pterm.Bold.Sprint("Routing of application:"), pterm.Cyan(answers.Static.BuildCommand)) + d.log.Printf("%s %s\n", pterm.Bold.Sprint("Routing of application:"), pterm.Cyan(opts.Static.BuildCommand)) } // Get info about static app. if len(qs) != 0 { - err := survey.Ask(qs, &answers.Static) + err := survey.Ask(qs, &opts.Static) if err != nil { return nil, errAppAddCanceled } } - // Skip "type" if it can be deduced from path. - if config.KnownType(config.DetectAppType(answers.OutputPath)) != "" { - answers.Type = "" - } + // Cleanup. + opts.Static.BuildDir, _ = filepath.Rel(opts.OutputPath, opts.Static.BuildDir) + opts.Static.BuildDir = "./" + opts.Static.BuildDir return &staticAppInfo{ App: config.StaticApp{ BasicApp: config.BasicApp{ - AppName: answers.Name, - AppURL: answers.URL, - AppPath: answers.OutputPath, + AppName: opts.Name, + AppURL: opts.URL, + AppPath: opts.OutputPath, }, Build: &config.StaticAppBuild{ - Command: answers.Static.BuildCommand, - Dir: answers.Static.BuildDir, + Command: opts.Static.BuildCommand, + Dir: opts.Static.BuildDir, + }, + Dev: &config.StaticAppDev{ + Command: opts.Static.DevCommand, }, - Routing: answers.Static.Routing, + Routing: opts.Static.Routing, }, - URL: answers.URL, - Type: answers.Type, + URL: opts.URL, + Type: opts.Type, }, nil } diff --git a/pkg/actions/app_list.go b/pkg/actions/app_list.go index 3dd129f..923f635 100644 --- a/pkg/actions/app_list.go +++ b/pkg/actions/app_list.go @@ -10,24 +10,26 @@ import ( type AppList struct { log logger.Logger + cfg *config.Project opts *AppListOptions } type AppListOptions struct{} -func NewAppList(log logger.Logger, opts *AppListOptions) *AppList { +func NewAppList(log logger.Logger, cfg *config.Project, opts *AppListOptions) *AppList { return &AppList{ log: log, + cfg: cfg, opts: opts, } } -func (d *AppList) appsList(cfg *config.Project) [][]string { +func (d *AppList) appsList() [][]string { data := [][]string{ {"Name", "Type", "Deployment", "Path"}, } - for _, a := range cfg.Apps { + for _, a := range d.cfg.Apps { data = append(data, []string{ pterm.Yellow(a.Name()), pterm.Magenta(a.Type()), @@ -43,8 +45,8 @@ func (d *AppList) appsList(cfg *config.Project) [][]string { return nil } -func (d *AppList) Run(ctx context.Context, cfg *config.Project) error { - appList := d.appsList(cfg) +func (d *AppList) Run(ctx context.Context) error { + appList := d.appsList() if len(appList) != 0 { err := d.log.Table().WithHasHeader().WithData(pterm.TableData(appList)).Render() diff --git a/pkg/actions/deploy.go b/pkg/actions/deploy.go index 6ab4a8d..8332120 100644 --- a/pkg/actions/deploy.go +++ b/pkg/actions/deploy.go @@ -24,6 +24,7 @@ type planParams struct { type Deploy struct { log logger.Logger + cfg *config.Project opts *DeployOptions } @@ -32,18 +33,19 @@ type DeployOptions struct { Destroy bool } -func NewDeploy(log logger.Logger, opts *DeployOptions) *Deploy { +func NewDeploy(log logger.Logger, cfg *config.Project, opts *DeployOptions) *Deploy { return &Deploy{ log: log, + cfg: cfg, opts: opts, } } -func (d *Deploy) Run(ctx context.Context, cfg *config.Project) error { +func (d *Deploy) Run(ctx context.Context) error { verify := d.opts.Verify spinner, _ := d.log.Spinner().WithRemoveWhenDone(true).Start("Getting state...") - stateRes, err := getState(ctx, cfg) + stateRes, err := getState(ctx, d.cfg) if err != nil { _ = spinner.Stop() return err @@ -59,12 +61,12 @@ func (d *Deploy) Run(ctx context.Context, cfg *config.Project) error { spinner, _ = spinner.Start("Planning...") - planMap := calculatePlanMap(cfg.Apps, cfg.Dependencies) + planMap := calculatePlanMap(d.cfg.Apps, d.cfg.Dependencies) planRetMap, err := plan(ctx, stateRes.State, planMap, verify, d.opts.Destroy) if err != nil { _ = spinner.Stop() - _ = releaseLock(cfg, stateRes.LockInfo) + _ = releaseLock(d.cfg, stateRes.LockInfo) return err } @@ -76,13 +78,13 @@ func (d *Deploy) Run(ctx context.Context, cfg *config.Project) error { empty, canceled := planPrompt(d.log, deployChanges, dnsChanges) if canceled || empty { - releaseErr := releaseLock(cfg, stateRes.LockInfo) + releaseErr := releaseLock(d.cfg, stateRes.LockInfo) if releaseErr != nil { return releaseErr } - return d.showStateStatus(cfg, stateRes.State) + return d.showStateStatus(stateRes.State) } start := time.Now() @@ -90,10 +92,10 @@ func (d *Deploy) Run(ctx context.Context, cfg *config.Project) error { callback := applyProgress(d.log, deployChanges, dnsChanges) err = apply(ctx, stateRes.State, planMap, d.opts.Destroy, callback) - _, saveErr := saveState(cfg, stateRes.State) + _, saveErr := saveState(d.cfg, stateRes.State) // Release lock if needed. - releaseErr := releaseLock(cfg, stateRes.LockInfo) + releaseErr := releaseLock(d.cfg, stateRes.LockInfo) switch { case err != nil: @@ -105,7 +107,7 @@ func (d *Deploy) Run(ctx context.Context, cfg *config.Project) error { d.log.Printf("All changes applied in %s.\n", time.Since(start).Truncate(timeTruncate)) - err = d.showStateStatus(cfg, stateRes.State) + err = d.showStateStatus(stateRes.State) if err != nil { return err } @@ -118,12 +120,12 @@ type dnsSetup struct { dns *types.DNS } -func (d *Deploy) showStateStatus(cfg *config.Project, state *types.StateData) error { +func (d *Deploy) showStateStatus(state *types.StateData) error { var dns []*dnsSetup dnsMap := make(map[string]*types.DNS) - for _, app := range cfg.Apps { + for _, app := range d.cfg.Apps { appState, ok := state.AppStates[app.ID()] if !ok || !appState.DNS.Manual || (appState.DNS.CNAME == "" && appState.DNS.IP == "") { continue @@ -175,7 +177,7 @@ func (d *Deploy) showStateStatus(cfg *config.Project, state *types.StateData) er var apps []config.App - for _, app := range cfg.Apps { + for _, app := range d.cfg.Apps { _, ok := state.AppStates[app.ID()] if !ok { continue @@ -195,7 +197,7 @@ func (d *Deploy) showStateStatus(cfg *config.Project, state *types.StateData) er d.log.Section().Println("App External URLs") for _, app := range apps { - d.log.Printf("%s %s %s\n", appURLStyle.Sprint("https://"+app.URL()), pterm.Gray("==>"), appNameStyle.Sprint(app.Name())) + d.log.Printf("%s %s %s\n", appURLStyle.Sprint(app.URL()), pterm.Gray("==>"), appNameStyle.Sprint(app.Name())) } } diff --git a/pkg/actions/force_unlock.go b/pkg/actions/force_unlock.go index 3ea1e82..15affab 100644 --- a/pkg/actions/force_unlock.go +++ b/pkg/actions/force_unlock.go @@ -10,16 +10,18 @@ import ( type ForceUnlock struct { log logger.Logger + cfg *config.Project } -func NewForceUnlock(log logger.Logger) *ForceUnlock { +func NewForceUnlock(log logger.Logger, cfg *config.Project) *ForceUnlock { return &ForceUnlock{ + cfg: cfg, log: log, } } -func (f *ForceUnlock) Run(ctx context.Context, cfg *config.Project, lockID string) error { - return releaseLock(cfg, lockID) +func (f *ForceUnlock) Run(ctx context.Context, lockID string) error { + return releaseLock(f.cfg, lockID) } func releaseLock(cfg *config.Project, lockID string) error { diff --git a/pkg/actions/init.go b/pkg/actions/init.go index a51ed16..dfa2b20 100644 --- a/pkg/actions/init.go +++ b/pkg/actions/init.go @@ -5,7 +5,6 @@ import ( "context" "errors" "fmt" - "io/ioutil" "os" "path/filepath" "text/template" @@ -21,6 +20,7 @@ import ( "github.com/outblocks/outblocks-cli/pkg/logger" "github.com/outblocks/outblocks-cli/pkg/plugins" "github.com/outblocks/outblocks-cli/templates" + plugin_util "github.com/outblocks/outblocks-plugin-go/util" "github.com/pterm/pterm" ) @@ -50,7 +50,7 @@ type InitOptions struct { func (o *InitOptions) Validate() error { return validation.ValidateStruct(o, - validation.Field(&o.Name, validation.Required, validation.Match(config.ValidNameRegex)), + validation.Field(&o.Name, validation.Required, validation.By(validateInitName)), ) } @@ -71,7 +71,7 @@ func funcMap() template.FuncMap { func (d *Init) Run(ctx context.Context) error { curDir, err := os.Getwd() if err != nil { - return fmt.Errorf("getting current working dir error: %w", err) + return fmt.Errorf("can't get current working dir: %w", err) } cfg := &config.Project{} @@ -147,7 +147,7 @@ func (d *Init) Run(ctx context.Context) error { return err } - err = ioutil.WriteFile(config.ProjectYAMLName+".yaml", projectYAML.Bytes(), 0644) + err = plugin_util.WriteFile(config.ProjectYAMLName+".yaml", projectYAML.Bytes(), 0644) if err != nil { return err } @@ -162,7 +162,7 @@ func (d *Init) Run(ctx context.Context) error { return err } - err = ioutil.WriteFile("dev.values.yaml", valuesYAML.Bytes(), 0644) + err = plugin_util.WriteFile("dev.values.yaml", valuesYAML.Bytes(), 0644) if err != nil { return err } @@ -170,6 +170,10 @@ func (d *Init) Run(ctx context.Context) error { return err } +func validateInitName(val interface{}) error { + return util.RegexValidator(config.ValidNameRegex, "must start with a letter and consist only of letters, numbers, underscore or hyphens")(val) +} + func (d *Init) prompt(ctx context.Context, cfg *config.Project, loader *plugins.Loader, curDir string) (*config.Project, error) { var qs []*survey.Question @@ -177,9 +181,14 @@ func (d *Init) prompt(ctx context.Context, cfg *config.Project, loader *plugins. qs = append(qs, &survey.Question{ Name: "name", Prompt: &survey.Input{Message: "Name of project:", Default: filepath.Base(curDir)}, - Validate: util.RegexValidator(config.ValidNameRegex, "must start with a letter and consist only of letters, numbers, underscore or hyphens"), + Validate: validateInitName, }) } else { + err := validateInitName(d.opts.Name) + if err != nil { + return nil, err + } + d.log.Printf("%s %s\n", pterm.Bold.Sprint("Name of project:"), pterm.Cyan(d.opts.Name)) } @@ -221,43 +230,37 @@ func (d *Init) prompt(ctx context.Context, cfg *config.Project, loader *plugins. d.log.Printf("%s %s\n", pterm.Bold.Sprint("Main domain you plan to use for deployments"), pterm.Cyan(d.opts.DNSDomain)) } - answers := *d.opts - + // Ask questions. if len(qs) != 0 { - err := survey.Ask(qs, &answers) + err := survey.Ask(qs, d.opts) if err != nil { return nil, errInitCanceled } } - err := answers.Validate() - if err != nil { - return nil, err - } - - cfg.Name = answers.Name + cfg.Name = d.opts.Name - _, latestDeployVersion, err := loader.MatchingVersion(ctx, answers.DeployPlugin, "", nil) + _, latestDeployVersion, err := loader.MatchingVersion(ctx, d.opts.DeployPlugin, "", nil) if err != nil { - return nil, fmt.Errorf("error retrieving latest version of plugin '%s': %w", answers.RunPlugin, err) + return nil, fmt.Errorf("error retrieving latest version of plugin '%s': %w", d.opts.RunPlugin, err) } - _, latestRunVersion, err := loader.MatchingVersion(ctx, answers.RunPlugin, "", nil) + _, latestRunVersion, err := loader.MatchingVersion(ctx, d.opts.RunPlugin, "", nil) if err != nil { - return nil, fmt.Errorf("error retrieving latest version of plugin '%s': %w", answers.RunPlugin, err) + return nil, fmt.Errorf("error retrieving latest version of plugin '%s': %w", d.opts.RunPlugin, err) } cfg.Plugins = []*config.Plugin{ - {Name: answers.DeployPlugin, Version: fmt.Sprintf("^%s", latestDeployVersion.String())}, - {Name: answers.RunPlugin, Version: fmt.Sprintf("^%s", latestRunVersion.String())}, + {Name: d.opts.DeployPlugin, Version: fmt.Sprintf("^%s", latestDeployVersion.String())}, + {Name: d.opts.RunPlugin, Version: fmt.Sprintf("^%s", latestRunVersion.String())}, } cfg.State = &config.State{ - Type: answers.DeployPlugin, + Type: d.opts.DeployPlugin, } cfg.DNS = append(cfg.DNS, &config.DNS{ - Domain: answers.DNSDomain, + Domain: d.opts.DNSDomain, }) return cfg, nil diff --git a/pkg/actions/plugin_list.go b/pkg/actions/plugin_list.go index f94ada4..9db6dcd 100644 --- a/pkg/actions/plugin_list.go +++ b/pkg/actions/plugin_list.go @@ -13,23 +13,25 @@ import ( type PluginList struct { log logger.Logger loader *plugins.Loader + cfg *config.Project } -func NewPluginList(log logger.Logger, loader *plugins.Loader) *PluginList { +func NewPluginList(log logger.Logger, cfg *config.Project, loader *plugins.Loader) *PluginList { return &PluginList{ log: log, + cfg: cfg, loader: loader, } } -func (d *PluginList) Run(ctx context.Context, cfg *config.Project) error { - prog, _ := d.log.ProgressBar().WithTotal(len(cfg.Plugins)).WithTitle("Checking for plugin updates...").Start() +func (d *PluginList) Run(ctx context.Context) error { + prog, _ := d.log.ProgressBar().WithTotal(len(d.cfg.Plugins)).WithTitle("Checking for plugin updates...").Start() data := [][]string{ {"Name", "Range", "Current", "Wanted", "Latest"}, } - for _, p := range cfg.Plugins { + for _, p := range d.cfg.Plugins { prog.UpdateTitle(fmt.Sprintf("Checking for plugin updates: %s", p.Name)) matching, latest, err := d.loader.MatchingVersion(ctx, p.Name, p.Source, p.VerConstr()) diff --git a/pkg/actions/plugin_update.go b/pkg/actions/plugin_update.go index 3a7eb11..fc72790 100644 --- a/pkg/actions/plugin_update.go +++ b/pkg/actions/plugin_update.go @@ -13,22 +13,24 @@ import ( type PluginUpdate struct { log logger.Logger loader *plugins.Loader + cfg *config.Project } -func NewPluginUpdate(log logger.Logger, loader *plugins.Loader) *PluginUpdate { +func NewPluginUpdate(log logger.Logger, cfg *config.Project, loader *plugins.Loader) *PluginUpdate { return &PluginUpdate{ log: log, + cfg: cfg, loader: loader, } } -func (d *PluginUpdate) Run(ctx context.Context, cfg *config.Project) error { - prog, _ := d.log.ProgressBar().WithTotal(len(cfg.Plugins)).WithTitle("Checking for plugin updates...").Start() - loadedPlugins := make([]*plugins.Plugin, len(cfg.Plugins)) +func (d *PluginUpdate) Run(ctx context.Context) error { + prog, _ := d.log.ProgressBar().WithTotal(len(d.cfg.Plugins)).WithTitle("Checking for plugin updates...").Start() + loadedPlugins := make([]*plugins.Plugin, len(d.cfg.Plugins)) var updatedPlugins []*config.Plugin - for i, p := range cfg.Plugins { + for i, p := range d.cfg.Plugins { prog.UpdateTitle(fmt.Sprintf("Checking for plugin updates: %s", p.Name)) cur := p.Loaded().Version @@ -59,7 +61,7 @@ func (d *PluginUpdate) Run(ctx context.Context, cfg *config.Project) error { prog.Increment() } - cfg.SetLoadedPlugins(loadedPlugins) + d.cfg.SetLoadedPlugins(loadedPlugins) // Print updated plugins info. if len(updatedPlugins) == 0 { diff --git a/pkg/actions/run.go b/pkg/actions/run.go new file mode 100644 index 0000000..34a78dc --- /dev/null +++ b/pkg/actions/run.go @@ -0,0 +1,136 @@ +package actions + +import ( + "context" + "fmt" + "os" + + "github.com/otiai10/copy" + "github.com/outblocks/outblocks-cli/pkg/clipath" + "github.com/outblocks/outblocks-cli/pkg/config" + "github.com/outblocks/outblocks-cli/pkg/logger" + "github.com/outblocks/outblocks-cli/pkg/plugins" + plugin_go "github.com/outblocks/outblocks-plugin-go" + "github.com/txn2/txeh" +) + +type Run struct { + log logger.Logger + cfg *config.Project + opts *RunOptions + + hosts *txeh.Hosts + addedHosts []string +} + +type RunOptions struct { + LocalIP string + HostsSuffix string + AddHosts bool +} + +func NewRun(log logger.Logger, cfg *config.Project, opts *RunOptions) *Run { + return &Run{ + log: log, + cfg: cfg, + opts: opts, + } +} + +func (d *Run) cleanup() error { + if len(d.addedHosts) > 0 { + d.hosts.RemoveHosts(d.addedHosts) + + return d.hosts.Save() + } + + return nil +} + +func (d *Run) AddHosts(hosts ...string) { + d.addedHosts = append(d.addedHosts, hosts...) + d.hosts.AddHosts(d.opts.LocalIP, hosts) +} + +func (d *Run) init() error { + var err error + + d.hosts, err = txeh.NewHostsDefault() + if err != nil { + return err + } + + backupHosts := clipath.DataPath("hosts.original") + if _, err := os.Stat(backupHosts); os.IsNotExist(err) { + if err = copy.Copy(d.hosts.WriteFilePath, backupHosts); err != nil { + return fmt.Errorf("cannot backup hosts file: %w", err) + } + } + + return err +} + +func prepareRunMap(cfg *config.Project) map[*plugins.Plugin]*plugin_go.RunRequest { + runMap := make(map[*plugins.Plugin]*plugin_go.RunRequest) + + for _, app := range cfg.Apps { + runPlugin := app.RunPlugin() + appType := app.PluginType() + + if _, ok := runMap[runPlugin]; !ok { + runMap[runPlugin] = &plugin_go.RunRequest{} + } + + runMap[runPlugin].Apps = append(runMap[runPlugin].Apps, appType) + } + + for _, dep := range cfg.Dependencies { + runPlugin := dep.RunPlugin() + depType := dep.PluginType() + + if _, ok := runMap[runPlugin]; !ok { + runMap[runPlugin] = &plugin_go.RunRequest{} + } + + runMap[runPlugin].Dependencies = append(runMap[runPlugin].Dependencies, depType) + } + + return runMap +} + +func run(_ context.Context, runMap map[*plugins.Plugin]*plugin_go.RunRequest) (map[*plugins.Plugin]*plugin_go.RunDoneResponse, error) { // nolint: unparam + retMap := make(map[*plugins.Plugin]*plugin_go.RunDoneResponse) + + for plug, req := range runMap { + fmt.Println("RUN", plug.Name, req) + } + + return retMap, nil +} + +func (d *Run) Run(ctx context.Context) error { + err := d.init() + if err != nil { + return err + } + + spinner, _ := d.log.Spinner().WithRemoveWhenDone(true).Start("Preparing...") + + runMap := prepareRunMap(d.cfg) + runRetMap, err := run(ctx, runMap) + + _ = spinner.Stop() + + if err != nil { + return err + } + + fmt.Println(runMap, runRetMap) + + // TODO: RUN + // d.hosts.AddHosts("127.0.0.1", []string{"test_me.local.test"}) + + // fmt.Println(d.hosts.RenderHostsFile()) + + return d.cleanup() +} diff --git a/pkg/actions/util.go b/pkg/actions/util.go index c465875..76eabdf 100644 --- a/pkg/actions/util.go +++ b/pkg/actions/util.go @@ -149,8 +149,13 @@ func calculateTotalSteps(chg []*change) int { steps := 0 for _, c := range chg { - for _, v := range c.info { - steps += len(v) + for changeID, v := range c.info { + if changeID.planType == types.PlanRecreate { + // Recreate steps are doubled. + steps += 2 * len(v) + } else { + steps += len(v) + } } } @@ -338,7 +343,7 @@ func applyProgress(log logger.Logger, deployChanges, dnsChanges []*change) func( log.Successln(success) - if act.Progress == act.Total { + if act.Progress == act.Total || act.Type == types.PlanRecreate { p.Increment() } } diff --git a/pkg/config/app.go b/pkg/config/app.go index 0937db3..24bf136 100644 --- a/pkg/config/app.go +++ b/pkg/config/app.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "net/url" "path/filepath" "regexp" "strings" @@ -13,8 +14,8 @@ import ( ) var ( - ValidURLRegex = regexp.MustCompile(`^([a-zA-Z][a-zA-Z0-9-]*)((\.)([a-zA-Z][a-zA-Z0-9-]*)){1,}(/[a-zA-Z0-9-_]+)*(/)?$`) - ValidNameRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_-]{0,20}$`) + ValidURLRegex = regexp.MustCompile(`^(https?://)?([a-zA-Z][a-zA-Z0-9-]*)((\.)([a-zA-Z][a-zA-Z0-9-]*)){1,}(/[a-zA-Z0-9-_]+)*(/)?$`) + ValidNameRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_-]{0,30}$`) ValidAppTypes = []string{TypeStatic, TypeFunction, TypeService} ) @@ -41,6 +42,7 @@ type BasicApp struct { Needs map[string]*AppNeed `json:"needs"` Other map[string]interface{} `yaml:"-,remain"` + url *url.URL yamlPath string yamlData []byte deployPlugin *plugins.Plugin @@ -63,8 +65,15 @@ func (a *BasicApp) Normalize(cfg *Project) error { if a.AppURL != "" { a.AppURL = strings.ToLower(a.AppURL) - if strings.Count(a.AppURL, "/") == 0 { - a.AppURL += "/" + if !strings.HasPrefix(a.AppURL, "http") { + a.AppURL = "https://" + a.AppURL + } + + var err error + + a.url, err = url.Parse(a.AppURL) + if err != nil { + return a.yamlError("$.url", "App.URL is invalid") } } @@ -168,11 +177,16 @@ func (a *BasicApp) PluginType() *types.App { needs[k] = n.PluginType() } + var appURL string + if a.url != nil { + appURL = a.url.String() + } + return &types.App{ ID: a.ID(), Name: a.AppName, Type: a.Type(), - URL: a.AppURL, + URL: appURL, Needs: needs, Properties: a.Other, } @@ -195,6 +209,10 @@ func (a *BasicApp) Name() string { } func (a *BasicApp) URL() string { + if a.url != nil { + return a.url.String() + } + return a.AppURL } diff --git a/pkg/config/app_static.go b/pkg/config/app_static.go index 740a781..bdc30c7 100644 --- a/pkg/config/app_static.go +++ b/pkg/config/app_static.go @@ -28,6 +28,7 @@ var ( type StaticApp struct { BasicApp `json:",inline"` Build *StaticAppBuild `json:"build,omitempty"` + Dev *StaticAppDev `json:"dev,omitempty"` Routing string `json:"routing"` } @@ -36,6 +37,10 @@ type StaticAppBuild struct { Dir string `json:"dir"` } +type StaticAppDev struct { + Command string `json:"command"` +} + func LoadStaticAppData(path string, data []byte) (*StaticApp, error) { out := &StaticApp{} @@ -66,6 +71,7 @@ func (s *StaticApp) PluginType() *types.App { base.Properties["routing"] = s.Routing base.Properties["build"] = s.Build + base.Properties["dev"] = s.Dev return base } diff --git a/pkg/config/project.go b/pkg/config/project.go index e4225bf..a4dad07 100644 --- a/pkg/config/project.go +++ b/pkg/config/project.go @@ -261,10 +261,10 @@ func (p *Project) YAMLData() []byte { } func (p *Project) FindDNSPlugin(url string) *plugins.Plugin { - url = strings.SplitN(url, "/", 2)[0] + host := strings.SplitN(url, "/", 2)[0] for _, dns := range p.DNS { - if strings.HasSuffix(url, dns.Domain) { + if strings.HasSuffix(host, dns.Domain) { return dns.plugin } } diff --git a/pkg/config/state.go b/pkg/config/state.go index 1f8aec7..8e75adb 100644 --- a/pkg/config/state.go +++ b/pkg/config/state.go @@ -8,6 +8,7 @@ import ( validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/outblocks/outblocks-cli/pkg/plugins" "github.com/outblocks/outblocks-plugin-go/types" + plugin_util "github.com/outblocks/outblocks-plugin-go/util" ) const ( @@ -61,7 +62,7 @@ func (s *State) SaveLocal(d *types.StateData) error { return err } - return ioutil.WriteFile(s.LocalPath(), data, 0644) + return plugin_util.WriteFile(s.LocalPath(), data, 0644) } func (s *State) Normalize(cfg *Project) error { diff --git a/pkg/plugins/client/client.go b/pkg/plugins/client/client.go index 2f22517..db34839 100644 --- a/pkg/plugins/client/client.go +++ b/pkg/plugins/client/client.go @@ -67,21 +67,27 @@ func (c *Client) lazyInit(_ context.Context) error { s := bufio.NewScanner(stderrPipe) for s.Scan() { b := s.Bytes() + if len(b) == 0 { + continue + } + level := b[0] + prefix := fmt.Sprintf("%s: ", c.name) + switch plugin_log.Level(level) { case plugin_log.LevelError: - c.log.Errorf("plugin '%s': %s\n", c.name, string(b[1:])) + c.log.Errorf("%s%s\n", prefix, string(b[1:])) case plugin_log.LevelWarn: - c.log.Warnf("plugin '%s': %s\n", c.name, string(b[1:])) + c.log.Warnf("%s%s': %s\n", prefix, string(b[1:])) case plugin_log.LevelInfo: - c.log.Infof("plugin '%s': %s\n", c.name, string(b[1:])) + c.log.Infof("%s%s\n", prefix, string(b[1:])) case plugin_log.LevelDebug: - c.log.Debugf("plugin '%s': %s\n", c.name, string(b[1:])) + c.log.Debugf("%s%s\n", prefix, string(b[1:])) case plugin_log.LevelSuccess: - c.log.Successf("plugin '%s': %s\n", c.name, string(b[1:])) + c.log.Successf("%s%s\n", prefix, string(b[1:])) default: - c.log.Errorf("plugin '%s': %s\n", c.name, s.Text()) + c.log.Errorf("%s%s\n", prefix, s.Text()) } } }() @@ -145,6 +151,8 @@ func mapResponseType(header *plugin_go.ResponseHeader) plugin_go.Response { return &plugin_go.ValidationErrorResponse{} case plugin_go.ResponseTypeInit: return &plugin_go.InitResponse{} + case plugin_go.ResponseTypeRunDone: + return &plugin_go.RunDoneResponse{} default: return nil } diff --git a/pkg/plugins/client/run.go b/pkg/plugins/client/run.go new file mode 100644 index 0000000..2333219 --- /dev/null +++ b/pkg/plugins/client/run.go @@ -0,0 +1,54 @@ +package client + +import ( + "context" + + plugin_go "github.com/outblocks/outblocks-plugin-go" + "github.com/outblocks/outblocks-plugin-go/types" +) + +func (c *Client) Run(ctx context.Context, apps []*types.App, deps []*types.Dependency) (ret *plugin_go.RunDoneResponse, err error) { + stream, err := c.lazyStartBiDi(ctx, &plugin_go.RunRequest{ + Apps: apps, + Dependencies: deps, + }) + + if err != nil && !IsPluginError(err) { + err = NewPluginError(c, "run error", err) + } + + // if err != nil { + // return nil, err + // } + + // for { + // res, err := stream.Recv() + // if err == io.EOF { + // break + // } + + // if err != nil { + // _ = stream.Close() + // return ret, NewPluginError(c, "apply error", err) + // } + + // switch r := res.(type) { + // case *plugin_go.ApplyResponse: + // if callback != nil { + // for _, act := range r.Actions { + // callback(act) + // } + // } + // case *plugin_go.ApplyDoneResponse: + // ret = r + // default: + // return ret, NewPluginError(c, "unexpected response to apply request", err) + // } + // } + + if ret == nil { + return nil, NewPluginError(c, "empty run response", nil) + } + + return ret, stream.DrainAndClose() +} diff --git a/pkg/plugins/downloader_github.go b/pkg/plugins/downloader_github.go index 051450f..c146b8a 100644 --- a/pkg/plugins/downloader_github.go +++ b/pkg/plugins/downloader_github.go @@ -3,7 +3,6 @@ package plugins import ( "context" "fmt" - "io/ioutil" "net/http" "os" "path/filepath" @@ -15,6 +14,7 @@ import ( "github.com/mholt/archiver/v3" "github.com/outblocks/outblocks-cli/pkg/clipath" "github.com/outblocks/outblocks-cli/pkg/getter" + plugin_util "github.com/outblocks/outblocks-plugin-go/util" "golang.org/x/oauth2" ) @@ -93,7 +93,7 @@ func (d *GitHubDownloader) Download(ctx context.Context, pi *pluginInfo) (*Downl return nil, fmt.Errorf("downloading file error: %w", err) } - err = ioutil.WriteFile(dest, b.Bytes(), 0755) + err = plugin_util.WriteFile(dest, b.Bytes(), 0755) if err != nil { return nil, fmt.Errorf("writing downloaded file error: %w", err) } diff --git a/schema/schema-app.json b/schema/schema-app.json index c4889e6..4d541b8 100644 --- a/schema/schema-app.json +++ b/schema/schema-app.json @@ -25,8 +25,12 @@ "description": "Application dependencies to inject.", "$ref": "#/definitions/Needs" }, + "dev": { + "description": "Dev config used for dev builds (during run).", + "$ref": "#/definitions/Dev" + }, "build": { - "description": "Build override.", + "description": "Build config used for production builds (during deployments).", "$ref": "#/definitions/Build" }, "src": { @@ -61,6 +65,16 @@ } } }, + "Dev": { + "title": "Dev", + "type": "object", + "additionalProperties": false, + "properties": { + "command": { + "type": "string" + } + } + }, "Build": { "title": "Build", "type": "object", diff --git a/templates/app-static.yaml.tpl b/templates/app-static.yaml.tpl index 3e23e76..b1967cd 100644 --- a/templates/app-static.yaml.tpl +++ b/templates/app-static.yaml.tpl @@ -23,6 +23,13 @@ build: # Directory where generated files will end up. dir: {{.App.Build.Dir}} +# Dev defines where how development is handled of application during `ok run`. +dev: +{{- if .App.Dev.Command }} + # Command to be run to for dev mode. + command: {{.App.Dev.Command}} +{{ end }} + # Routing to be used: # 'react' for react browser routing. # 'disabled' for no additional routing.