Skip to content

Commit 833831a

Browse files
author
Kamal Nasser
authored
apps: add apps propose command, expand spec validation (#926)
* add doctl apps propose, expand spec validation update apps commands to allow reading from stdin * update static app formatting
1 parent b4ef7c3 commit 833831a

File tree

9 files changed

+566
-139
lines changed

9 files changed

+566
-139
lines changed

args.go

+4
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ const (
3434
ArgActionStatus = "status"
3535
// ArgActionType is an action type argument.
3636
ArgActionType = "action-type"
37+
// ArgApp is the app ID.
38+
ArgApp = "app"
3739
// ArgAppSpec is a path to an app spec.
3840
ArgAppSpec = "spec"
3941
// ArgAppLogType the type of log.
@@ -140,6 +142,8 @@ const (
140142
ArgRecordTag = "record-tag"
141143
// ArgRegionSlug is a region slug argument.
142144
ArgRegionSlug = "region"
145+
// ArgSchemaOnly is a schema only argument.
146+
ArgSchemaOnly = "schema-only"
143147
// ArgSizeSlug is a size slug argument.
144148
ArgSizeSlug = "size"
145149
// ArgsSSHKeyPath is a ssh argument.

commands/apps.go

+115-64
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func Apps() *Command {
4040
Use: "apps",
4141
Aliases: []string{"app", "a"},
4242
Short: "Display commands for working with apps",
43-
Long: "The subcommands of `doctl app` manage your App Platform apps.",
43+
Long: "The subcommands of `doctl app` manage your App Platform apps. For documentation on app specs used by multiple commands, see https://www.digitalocean.com/docs/app-platform/concepts/app-spec.",
4444
},
4545
}
4646

@@ -54,7 +54,7 @@ func Apps() *Command {
5454
aliasOpt("c"),
5555
displayerType(&displayers.Apps{}),
5656
)
57-
AddStringFlag(create, doctl.ArgAppSpec, "", "", "Path to an app spec in JSON or YAML format. For more information about app specs, see https://www.digitalocean.com/docs/app-platform/concepts/app-spec", requiredOpt())
57+
AddStringFlag(create, doctl.ArgAppSpec, "", "", `Path to an app spec in JSON or YAML format. Set to "-" to read from stdin.`, requiredOpt())
5858

5959
CmdBuilder(
6060
cmd,
@@ -92,7 +92,7 @@ Only basic information is included with the text output format. For complete app
9292
aliasOpt("u"),
9393
displayerType(&displayers.Apps{}),
9494
)
95-
AddStringFlag(update, doctl.ArgAppSpec, "", "", "Path to an app spec in JSON or YAML format.", requiredOpt())
95+
AddStringFlag(update, doctl.ArgAppSpec, "", "", `Path to an app spec in JSON or YAML format. Set to "-" to read from stdin.`, requiredOpt())
9696

9797
deleteApp := CmdBuilder(
9898
cmd,
@@ -177,6 +177,21 @@ Three types of logs are supported and can be configured with --`+doctl.ArgAppLog
177177
displayerType(&displayers.AppRegions{}),
178178
)
179179

180+
propose := CmdBuilder(
181+
cmd,
182+
RunAppsPropose,
183+
"propose",
184+
"Propose an app spec",
185+
`Reviews and validates an app specification for a new or existing app. The request returns some information about the proposed app, including app cost and upgrade cost. If an existing app ID is specified, the app spec is treated as a proposed update to the existing app.
186+
187+
Only basic information is included with the text output format. For complete app details including an updated app spec, use the JSON format.`,
188+
Writer,
189+
aliasOpt("c"),
190+
displayerType(&displayers.Apps{}),
191+
)
192+
AddStringFlag(propose, doctl.ArgAppSpec, "", "", "Path to an app spec in JSON or YAML format. For more information about app specs, see https://www.digitalocean.com/docs/app-platform/concepts/app-spec", requiredOpt())
193+
AddStringFlag(propose, doctl.ArgApp, "", "", "An optional existing app ID. If specified, the app spec will be treated as a proposed update to the existing app.")
194+
180195
cmd.AddCommand(appsSpec())
181196
cmd.AddCommand(appsTier())
182197

@@ -190,21 +205,7 @@ func RunAppsCreate(c *CmdConfig) error {
190205
return err
191206
}
192207

193-
specFile, err := os.Open(specPath) // guardrails-disable-line
194-
if err != nil {
195-
if os.IsNotExist(err) {
196-
return fmt.Errorf("Failed to open app spec: %s does not exist", specPath)
197-
}
198-
return fmt.Errorf("Failed to open app spec: %w", err)
199-
}
200-
defer specFile.Close()
201-
202-
specBytes, err := ioutil.ReadAll(specFile)
203-
if err != nil {
204-
return fmt.Errorf("Failed to read app spec: %w", err)
205-
}
206-
207-
appSpec, err := parseAppSpec(specBytes)
208+
appSpec, err := readAppSpec(os.Stdin, specPath)
208209
if err != nil {
209210
return err
210211
}
@@ -255,21 +256,7 @@ func RunAppsUpdate(c *CmdConfig) error {
255256
return err
256257
}
257258

258-
specFile, err := os.Open(specPath) // guardrails-disable-line
259-
if err != nil {
260-
if os.IsNotExist(err) {
261-
return fmt.Errorf("Failed to open app spec: %s does not exist", specPath)
262-
}
263-
return fmt.Errorf("Failed to open app spec: %w", err)
264-
}
265-
defer specFile.Close()
266-
267-
specBytes, err := ioutil.ReadAll(specFile)
268-
if err != nil {
269-
return fmt.Errorf("Failed to read app spec: %w", err)
270-
}
271-
272-
appSpec, err := parseAppSpec(specBytes)
259+
appSpec, err := readAppSpec(os.Stdin, specPath)
273260
if err != nil {
274261
return err
275262
}
@@ -531,6 +518,65 @@ func RunAppsGetLogs(c *CmdConfig) error {
531518
return nil
532519
}
533520

521+
// RunAppsPropose proposes an app spec
522+
func RunAppsPropose(c *CmdConfig) error {
523+
appID, err := c.Doit.GetString(c.NS, doctl.ArgApp)
524+
if err != nil {
525+
return err
526+
}
527+
528+
specPath, err := c.Doit.GetString(c.NS, doctl.ArgAppSpec)
529+
if err != nil {
530+
return err
531+
}
532+
533+
appSpec, err := readAppSpec(os.Stdin, specPath)
534+
if err != nil {
535+
return err
536+
}
537+
538+
res, err := c.Apps().Propose(&godo.AppProposeRequest{
539+
Spec: appSpec,
540+
AppID: appID,
541+
})
542+
543+
if err != nil {
544+
// most likely an invalid app spec. The error message would start with "error validating app spec"
545+
return err
546+
}
547+
548+
return c.Display(displayers.AppProposeResponse{Res: res})
549+
}
550+
551+
func readAppSpec(stdin io.Reader, path string) (*godo.AppSpec, error) {
552+
var spec io.Reader
553+
if path == "-" {
554+
spec = stdin
555+
} else {
556+
specFile, err := os.Open(path) // guardrails-disable-line
557+
if err != nil {
558+
if os.IsNotExist(err) {
559+
return nil, fmt.Errorf("opening app spec: %s does not exist", path)
560+
}
561+
return nil, fmt.Errorf("opening app spec: %w", err)
562+
}
563+
defer specFile.Close()
564+
spec = specFile
565+
}
566+
567+
byt, err := ioutil.ReadAll(spec)
568+
if err != nil {
569+
return nil, fmt.Errorf("reading app spec: %w", err)
570+
}
571+
572+
s, err := parseAppSpec(byt)
573+
if err != nil {
574+
return nil, fmt.Errorf("parsing app spec: %w", err)
575+
}
576+
577+
return s, nil
578+
}
579+
534580
func parseAppSpec(spec []byte) (*godo.AppSpec, error) {
535581
jsonSpec, err := yaml.YAMLToJSON(spec)
536582
if err != nil {
@@ -542,7 +588,7 @@ func parseAppSpec(spec []byte) (*godo.AppSpec, error) {
542588

543589
var appSpec godo.AppSpec
544590
if err := dec.Decode(&appSpec); err != nil {
545-
return nil, fmt.Errorf("Failed to parse app spec: %v", err)
591+
return nil, err
546592
}
547593

548594
return &appSpec, nil
@@ -561,11 +607,12 @@ func appsSpec() *Command {
561607
562608
Optionally, pass a deployment ID to get the spec of that specific deployment.`, Writer)
563609
AddStringFlag(getCmd, doctl.ArgAppDeployment, "", "", "optional: a deployment ID")
564-
AddStringFlag(getCmd, doctl.ArgFormat, "", "yaml", `the format to output the spec as; either "yaml" or "json"`)
610+
AddStringFlag(getCmd, doctl.ArgFormat, "", "yaml", `the format to output the spec in; either "yaml" or "json"`)
565611

566-
CmdBuilder(cmd, RunAppsSpecValidate(os.Stdin), "validate <spec file>", "Validate an application spec", `Use this command to check whether a given app spec (YAML or JSON) is valid.
612+
validateCmd := CmdBuilder(cmd, RunAppsSpecValidate, "validate <spec file>", "Validate an application spec", `Use this command to check whether a given app spec (YAML or JSON) is valid.
567613
568614
You may pass - as the filename to read from stdin.`, Writer)
615+
AddBoolFlag(validateCmd, doctl.ArgSchemaOnly, "", false, "Only validate the spec schema and not the correctness of the spec.")
569616

570617
return cmd
571618
}
@@ -620,41 +667,45 @@ func RunAppsSpecGet(c *CmdConfig) error {
620667
}
621668

622669
// RunAppsSpecValidate validates an app spec file
623-
func RunAppsSpecValidate(stdin io.Reader) func(c *CmdConfig) error {
624-
return func(c *CmdConfig) error {
625-
if len(c.Args) < 1 {
626-
return doctl.NewMissingArgsErr(c.NS)
627-
}
670+
func RunAppsSpecValidate(c *CmdConfig) error {
671+
if len(c.Args) < 1 {
672+
return doctl.NewMissingArgsErr(c.NS)
673+
}
628674

629-
specPath := c.Args[0]
630-
var spec io.Reader
631-
if specPath == "-" {
632-
spec = stdin
633-
} else {
634-
specFile, err := os.Open(specPath) // guardrails-disable-line
635-
if err != nil {
636-
if os.IsNotExist(err) {
637-
return fmt.Errorf("Failed to open app spec: %s does not exist", specPath)
638-
}
639-
return fmt.Errorf("Failed to open app spec: %w", err)
640-
}
641-
defer specFile.Close()
642-
spec = specFile
643-
}
675+
specPath := c.Args[0]
676+
appSpec, err := readAppSpec(os.Stdin, specPath)
677+
if err != nil {
678+
return err
679+
}
644680

645-
specBytes, err := ioutil.ReadAll(spec)
646-
if err != nil {
647-
return fmt.Errorf("Failed to read app spec: %w", err)
648-
}
681+
schemaOnly, err := c.Doit.GetBool(c.NS, doctl.ArgSchemaOnly)
682+
if err != nil {
683+
return err
684+
}
649685

650-
_, err = parseAppSpec(specBytes)
686+
if schemaOnly {
687+
ymlSpec, err := yaml.Marshal(appSpec)
651688
if err != nil {
652-
return err
689+
return fmt.Errorf("marshaling the spec as yaml: %v", err)
653690
}
691+
_, err = c.Out.Write(ymlSpec)
692+
return err
693+
}
654694

655-
c.Out.Write([]byte("The spec is valid.\n"))
656-
return nil
695+
res, err := c.Apps().Propose(&godo.AppProposeRequest{
696+
Spec: appSpec,
697+
})
698+
if err != nil {
699+
// most likely an invalid app spec. The error message would start with "error validating app spec"
700+
return err
701+
}
702+
703+
ymlSpec, err := yaml.Marshal(res.Spec)
704+
if err != nil {
705+
return fmt.Errorf("marshaling the spec as yaml: %v", err)
657706
}
707+
_, err = c.Out.Write(ymlSpec)
708+
return err
658709
}
659710

660711
// RunAppsListRegions lists all app platform regions.

0 commit comments

Comments
 (0)