Skip to content

Commit f966757

Browse files
committed
alpha: app adding, listing, template improvements and fixed plugin loading
1 parent 4f044cd commit f966757

File tree

17 files changed

+567
-77
lines changed

17 files changed

+567
-77
lines changed

cmd/apps.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"github.com/outblocks/outblocks-cli/pkg/actions"
45
"github.com/outblocks/outblocks-cli/pkg/config"
56
"github.com/spf13/cobra"
67
)
@@ -16,6 +17,8 @@ func (e *Executor) newAppsCmd() *cobra.Command {
1617
},
1718
}
1819

20+
listOpts := &actions.AppListOptions{}
21+
1922
list := &cobra.Command{
2023
Use: "list",
2124
Short: "List apps",
@@ -25,8 +28,11 @@ func (e *Executor) newAppsCmd() *cobra.Command {
2528
},
2629
SilenceUsage: true,
2730
RunE: func(cmd *cobra.Command, args []string) error {
28-
// TODO: app list
29-
return nil
31+
if e.cfg == nil {
32+
return config.ErrProjectConfigNotFound
33+
}
34+
35+
return actions.NewAppList(e.Log(), listOpts).Run(cmd.Context(), e.cfg)
3036
},
3137
}
3238

@@ -50,6 +56,8 @@ func (e *Executor) newAppsCmd() *cobra.Command {
5056
},
5157
}
5258

59+
addOpts := &actions.AppAddOptions{}
60+
5361
add := &cobra.Command{
5462
Use: "add",
5563
Short: "Add a new app",
@@ -59,11 +67,24 @@ func (e *Executor) newAppsCmd() *cobra.Command {
5967
},
6068
SilenceUsage: true,
6169
RunE: func(cmd *cobra.Command, args []string) error {
62-
// TODO: app add
63-
return nil
70+
if e.cfg == nil {
71+
return config.ErrProjectConfigNotFound
72+
}
73+
74+
return actions.NewAppAdd(e.Log(), addOpts).Run(cmd.Context(), e.cfg)
6475
},
6576
}
6677

78+
f := add.Flags()
79+
f.BoolVar(&addOpts.Overwrite, "overwrite", false, "do not ask if application definition already exists")
80+
f.StringVar(&addOpts.Name, "name", "", "application name")
81+
f.StringVar(&addOpts.URL, "url", "", "application URL")
82+
f.StringVar(&addOpts.Type, "type", "", "application type (options: static, function, service)")
83+
f.StringVar(&addOpts.Static.BuildCommand, "static-build-command", "", "static app build command")
84+
f.StringVar(&addOpts.Static.BuildDir, "static-build-dir", "", "static app build dir")
85+
f.StringVar(&addOpts.Static.Routing, "static-routing", "", "static app routing (options: react, disabled)")
86+
f.StringVarP(&addOpts.OutputPath, "output-path", "o", "", "output path, defaults to: <app_type>/<app_name>")
87+
6788
cmd.AddCommand(
6889
list,
6990
del,

internal/util/util.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package util
2+
3+
import (
4+
"errors"
5+
"reflect"
6+
"regexp"
7+
)
8+
9+
func InterfaceSlice(slice interface{}) []interface{} {
10+
s := reflect.ValueOf(slice)
11+
if s.Kind() != reflect.Slice {
12+
panic("InterfaceSlice() given a non-slice type")
13+
}
14+
15+
if s.IsNil() {
16+
return nil
17+
}
18+
19+
ret := make([]interface{}, s.Len())
20+
21+
for i := 0; i < s.Len(); i++ {
22+
ret[i] = s.Index(i).Interface()
23+
}
24+
25+
return ret
26+
}
27+
28+
func RegexValidator(regex *regexp.Regexp, msg string) func(interface{}) error {
29+
return func(val interface{}) error {
30+
// since we are validating an Input, the assertion will always succeed
31+
if str, ok := val.(string); !ok || !regex.MatchString(str) {
32+
return errors.New(msg)
33+
}
34+
35+
return nil
36+
}
37+
}

pkg/actions/app_add.go

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
package actions
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"errors"
7+
"fmt"
8+
"io/ioutil"
9+
"os"
10+
"path/filepath"
11+
"regexp"
12+
"strings"
13+
"text/template"
14+
15+
"github.com/AlecAivazis/survey/v2"
16+
"github.com/Masterminds/sprig"
17+
validation "github.com/go-ozzo/ozzo-validation/v4"
18+
"github.com/outblocks/outblocks-cli/internal/fileutil"
19+
"github.com/outblocks/outblocks-cli/internal/util"
20+
"github.com/outblocks/outblocks-cli/pkg/config"
21+
"github.com/outblocks/outblocks-cli/pkg/logger"
22+
"github.com/outblocks/outblocks-cli/templates"
23+
"github.com/pterm/pterm"
24+
)
25+
26+
var (
27+
errAppAddCanceled = errors.New("adding app canceled")
28+
validValueRegex = regexp.MustCompile(`^[a-zA-Z0-9{}\-_.]+$`)
29+
)
30+
31+
type AppAdd struct {
32+
log logger.Logger
33+
opts *AppAddOptions
34+
}
35+
36+
type staticAppInfo struct {
37+
App config.StaticApp
38+
URL string
39+
Type string
40+
}
41+
42+
type AppStaticOptions struct {
43+
BuildCommand string
44+
BuildDir string
45+
Routing string
46+
}
47+
48+
func (o *AppStaticOptions) Validate() error {
49+
return validation.ValidateStruct(o,
50+
validation.Field(&o.Routing, validation.In(util.InterfaceSlice(config.StaticAppRoutings)...)),
51+
)
52+
}
53+
54+
type AppAddOptions struct {
55+
Overwrite bool
56+
57+
OutputPath string
58+
Name string
59+
Type string
60+
URL string
61+
62+
Static AppStaticOptions
63+
}
64+
65+
func (o *AppAddOptions) Validate() error {
66+
return validation.ValidateStruct(o,
67+
validation.Field(&o.Name, validation.Required, validation.Match(config.ValidNameRegex)),
68+
validation.Field(&o.Type, validation.Required, validation.In(util.InterfaceSlice(config.ValidAppTypes)...)),
69+
validation.Field(&o.URL, validation.Required, validation.Match(validValueRegex)),
70+
validation.Field(&o.Static),
71+
)
72+
}
73+
74+
func NewAppAdd(log logger.Logger, opts *AppAddOptions) *AppAdd {
75+
return &AppAdd{
76+
log: log,
77+
opts: opts,
78+
}
79+
}
80+
81+
func (d *AppAdd) Run(ctx context.Context, cfg *config.Project) error {
82+
appInfo, err := d.prompt(ctx, cfg)
83+
if errors.Is(err, errAppAddCanceled) {
84+
d.log.Println("Adding application canceled.")
85+
return nil
86+
}
87+
88+
if err != nil {
89+
return err
90+
}
91+
92+
// Generate Application.YAML
93+
var (
94+
tmpl *template.Template
95+
path string
96+
)
97+
98+
switch app := appInfo.(type) {
99+
case *staticAppInfo:
100+
tmpl = template.Must(template.New("static_app").Funcs(sprig.TxtFuncMap()).Funcs(funcMap()).Parse(templates.StaticAppYAML))
101+
path = app.App.AppPath
102+
default:
103+
return fmt.Errorf("unsupported app type (WIP)")
104+
}
105+
106+
var appYAML bytes.Buffer
107+
108+
err = tmpl.Execute(&appYAML, appInfo)
109+
if err != nil {
110+
return err
111+
}
112+
113+
err = ioutil.WriteFile(filepath.Join(path, config.AppYAMLName+".yaml"), appYAML.Bytes(), 0644)
114+
if err != nil {
115+
return err
116+
}
117+
118+
return nil
119+
}
120+
121+
func (d *AppAdd) prompt(_ context.Context, cfg *config.Project) (interface{}, error) {
122+
var qs []*survey.Question
123+
124+
if d.opts.Name == "" {
125+
qs = append(qs, &survey.Question{
126+
Name: "name",
127+
Prompt: &survey.Input{Message: "Name of application:"},
128+
Validate: util.RegexValidator(config.ValidNameRegex, "must start with a letter and consist only of letters, numbers, underscore or hyphens"),
129+
})
130+
} else {
131+
d.log.Printf("%s %s\n", pterm.Bold.Sprint("Name of application:"), pterm.Cyan(d.opts.Name))
132+
}
133+
134+
if d.opts.Type == "" {
135+
qs = append(qs, &survey.Question{
136+
Name: "type",
137+
Prompt: &survey.Select{
138+
Message: "Type of application:",
139+
Options: config.ValidAppTypes,
140+
Default: config.TypeStatic,
141+
},
142+
})
143+
} else {
144+
d.opts.Type = strings.ToLower(d.opts.Type)
145+
d.log.Printf("%s %s\n", pterm.Bold.Sprint("Type of application:"), pterm.Cyan(d.opts.Type))
146+
}
147+
148+
if d.opts.URL == "" {
149+
defaultURL := ""
150+
151+
if len(cfg.DNS) > 0 {
152+
defaultURL = cfg.DNS[0].Domain
153+
}
154+
155+
qs = append(qs, &survey.Question{
156+
Name: "url",
157+
Prompt: &survey.Input{Message: "URL of application:", Default: defaultURL},
158+
Validate: util.RegexValidator(validValueRegex, "invalid URL, example example.com/some_path/run or using vars: ${var.base_url}/some_path/run"),
159+
})
160+
} else {
161+
d.opts.Type = strings.ToLower(d.opts.URL)
162+
d.log.Printf("%s %s\n", pterm.Bold.Sprint("URL of application:"), pterm.Cyan(d.opts.URL))
163+
}
164+
165+
answers := *d.opts
166+
167+
// Get basic info about app.
168+
if len(qs) != 0 {
169+
err := survey.Ask(qs, &answers)
170+
if err != nil {
171+
return nil, errAppAddCanceled
172+
}
173+
}
174+
175+
err := answers.Validate()
176+
if err != nil {
177+
return nil, err
178+
}
179+
180+
if answers.OutputPath == "" {
181+
answers.OutputPath = filepath.Join(cfg.Path, answers.Type, answers.Name)
182+
}
183+
184+
stat, err := os.Stat(answers.OutputPath)
185+
if os.IsNotExist(err) {
186+
err = os.MkdirAll(answers.OutputPath, 0755)
187+
if err != nil {
188+
return nil, err
189+
}
190+
}
191+
192+
if err != nil {
193+
return nil, err
194+
}
195+
196+
if stat != nil && !stat.IsDir() {
197+
return nil, fmt.Errorf("output path '%s' is not a directory", answers.OutputPath)
198+
}
199+
200+
if !d.opts.Overwrite && fileutil.FindYAML(filepath.Join(answers.OutputPath, config.AppYAMLName)) != "" {
201+
proceed := false
202+
prompt := &survey.Confirm{
203+
Message: "Application config already exists! Do you want to overwrite it?",
204+
}
205+
206+
_ = survey.AskOne(prompt, &proceed)
207+
208+
if !proceed {
209+
return nil, errAppAddCanceled
210+
}
211+
}
212+
213+
switch answers.Type {
214+
case config.TypeStatic:
215+
return d.promptStatic(&answers)
216+
default:
217+
return nil, fmt.Errorf("unsupported app type (WIP)")
218+
}
219+
}
220+
221+
func (d *AppAdd) promptStatic(answers *AppAddOptions) (*staticAppInfo, error) {
222+
var qs []*survey.Question
223+
224+
if answers.Static.BuildDir == "" {
225+
qs = append(qs, &survey.Question{
226+
Name: "builddir",
227+
Prompt: &survey.Input{Message: "Build directory of application:", Default: config.DefaultStaticAppBuildDir},
228+
})
229+
} else {
230+
d.log.Printf("%s %s\n", pterm.Bold.Sprint("Build directory of application:"), pterm.Cyan(answers.Static.BuildDir))
231+
}
232+
233+
if answers.Static.BuildCommand == "" {
234+
qs = append(qs, &survey.Question{
235+
Name: "buildcommand",
236+
Prompt: &survey.Input{Message: "Build command of application (optional):"},
237+
})
238+
} else {
239+
d.log.Printf("%s %s\n", pterm.Bold.Sprint("Build command of application:"), pterm.Cyan(answers.Static.BuildCommand))
240+
}
241+
242+
if answers.Static.Routing == "" {
243+
qs = append(qs, &survey.Question{
244+
Name: "routing",
245+
Prompt: &survey.Select{
246+
Message: "Routing of application:",
247+
Options: config.StaticAppRoutings,
248+
Default: config.StaticAppRoutingReact,
249+
},
250+
})
251+
} else {
252+
d.log.Printf("%s %s\n", pterm.Bold.Sprint("Routing of application:"), pterm.Cyan(answers.Static.BuildCommand))
253+
}
254+
255+
// Get info about static app.
256+
if len(qs) != 0 {
257+
err := survey.Ask(qs, &answers.Static)
258+
if err != nil {
259+
return nil, errAppAddCanceled
260+
}
261+
}
262+
263+
// Skip "type" if it can be deduced from path.
264+
if config.KnownType(config.DetectAppType(answers.OutputPath)) != "" {
265+
answers.Type = ""
266+
}
267+
268+
return &staticAppInfo{
269+
App: config.StaticApp{
270+
BasicApp: config.BasicApp{
271+
AppName: answers.Name,
272+
AppURL: answers.URL,
273+
AppPath: answers.OutputPath,
274+
},
275+
Build: &config.StaticAppBuild{
276+
Command: answers.Static.BuildCommand,
277+
Dir: answers.Static.BuildDir,
278+
},
279+
Routing: answers.Static.Routing,
280+
},
281+
282+
URL: answers.URL,
283+
Type: answers.Type,
284+
}, nil
285+
}

0 commit comments

Comments
 (0)