From ae6c1b600d33269f7e08c301a7a7fc04ed3e9cd8 Mon Sep 17 00:00:00 2001 From: James Cuzella Date: Sun, 22 Oct 2023 13:27:09 -0600 Subject: [PATCH 1/5] integration/apps_spec_test: Add tests for apps spec validate-offline --- integration/apps_spec_test.go | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/integration/apps_spec_test.go b/integration/apps_spec_test.go index 2b74fe156..281bc2a00 100644 --- a/integration/apps_spec_test.go +++ b/integration/apps_spec_test.go @@ -238,3 +238,50 @@ services: expect.Equal(expectedOutput, strings.TrimSpace(string(output))) }) }) + +var _ = suite("apps/spec/validate-offline", func(t *testing.T, when spec.G, it spec.S) { + var ( + expect *require.Assertions + ) + + it.Before(func() { + expect = require.New(t) + }) + + it("accepts a valid spec", func() { + cmd := exec.Command(builtBinaryPath, + "apps", "spec", "validate-offline", + "-", + ) + byt, err := json.Marshal(testAppSpec) + expect.NoError(err) + + cmd.Stdin = bytes.NewReader(byt) + + output, err := cmd.CombinedOutput() + expect.NoError(err) + + expectedOutput := "name: test\nservices:\n- github:\n branch: main\n repo: digitalocean/doctl\n name: service" + expect.Equal(expectedOutput, strings.TrimSpace(string(output))) + }) + + it("fails on invalid specs", func() { + cmd := exec.Command(builtBinaryPath, + "apps", "spec", "validate-offline", + "-", + ) + testSpec := `name: test +services: + name: service + github: + repo: digitalocean/doctl +` + cmd.Stdin = strings.NewReader(testSpec) + + output, err := cmd.CombinedOutput() + expect.Equal("exit status 1", err.Error()) + + expectedOutput := "Error: parsing app spec: json: cannot unmarshal object into Go struct field AppSpec.services of type []*godo.AppServiceSpec" + expect.Equal(expectedOutput, strings.TrimSpace(string(output))) + }) +}) From 385ae5f1ac85d530f663376b0900b3dfcb2541c3 Mon Sep 17 00:00:00 2001 From: James Cuzella Date: Sun, 22 Oct 2023 13:31:42 -0600 Subject: [PATCH 2/5] commands/apps: Add apps spec validate-offline command (Fixes #1449) Implement a new command to validate an app spec without requiring auth & connection to the API. This is useful for validating app specs in CI pipelines and untrusted environments. As there is no currently published [YAML schema][1] for use with [`redhat.vscode-yaml`][2], this seems to be the best approach for now. [1]: https://www.schemastore.org/json/ [2]: https://github.com/redhat-developer/yaml-language-server --- commands/apps.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/commands/apps.go b/commands/apps.go index fa3f3e190..5b0ec77bd 100644 --- a/commands/apps.go +++ b/commands/apps.go @@ -709,6 +709,11 @@ Optionally, pass a deployment ID to get the spec of that specific deployment.`, You may pass - as the filename to read from stdin.`, Writer) AddBoolFlag(validateCmd, doctl.ArgSchemaOnly, "", false, "Only validate the spec schema and not the correctness of the spec.") + cmdBuilderWithInit(cmd, RunAppsSpecValidateOffline, "validate-offline ", "Validate an application spec offline (schema-only)", `Use this command to check whether a given app spec (YAML or JSON) is valid +without connecting to DigitalOcean API. (schema-only) + +You may pass - as the filename to read from stdin.`, Writer, false) + return cmd } @@ -761,6 +766,35 @@ func RunAppsSpecGet(c *CmdConfig) error { } } +// ValidateAppSpecSchema validates an app spec (schema-only) +// returns the marshaled yaml spec as a byte array, or error +func ValidateAppSpecSchema(appSpec *godo.AppSpec) ([]byte, error) { + ymlSpec, err := yaml.Marshal(appSpec) + if err != nil { + return []byte{}, fmt.Errorf("marshaling the spec as yaml: %v", err) + } + return ymlSpec, err +} + +// RunAppsSpecValidateOffline validates an app spec file without requiring auth & connection to the API +func RunAppsSpecValidateOffline(c *CmdConfig) error { + if len(c.Args) < 1 { + return doctl.NewMissingArgsErr(c.NS) + } + + specPath := c.Args[0] + appSpec, err := apps.ReadAppSpec(os.Stdin, specPath) + if err != nil { + return err + } + ymlSpec, err := ValidateAppSpecSchema(appSpec) + if err != nil { + return err + } + _, err = c.Out.Write(ymlSpec) + return err +} + // RunAppsSpecValidate validates an app spec file func RunAppsSpecValidate(c *CmdConfig) error { if len(c.Args) < 1 { From 6204426ab866b60851066d30ca14912210135a9c Mon Sep 17 00:00:00 2001 From: James Cuzella Date: Sun, 22 Oct 2023 14:43:45 -0600 Subject: [PATCH 3/5] commands/apps: Refactor spec validation into a common function --- commands/apps.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commands/apps.go b/commands/apps.go index 5b0ec77bd..c44f70397 100644 --- a/commands/apps.go +++ b/commands/apps.go @@ -813,9 +813,9 @@ func RunAppsSpecValidate(c *CmdConfig) error { } if schemaOnly { - ymlSpec, err := yaml.Marshal(appSpec) + ymlSpec, err := ValidateAppSpecSchema(appSpec) if err != nil { - return fmt.Errorf("marshaling the spec as yaml: %v", err) + return err } _, err = c.Out.Write(ymlSpec) return err From 8319fec676c0af5dfaad7486d526a5d0f1c54903 Mon Sep 17 00:00:00 2001 From: James Cuzella Date: Mon, 23 Oct 2023 17:29:06 -0600 Subject: [PATCH 4/5] commands/apps: refactor validate-offline -> --schema-only (Fixes #1449) (#1) Thanks to @andrewsomething for the suggestion! Reference: - https://github.com/digitalocean/doctl/pull/1450#pullrequestreview-1693429116 --- commands/apps.go | 48 ++++++++--------------------------- integration/apps_spec_test.go | 47 ---------------------------------- 2 files changed, 10 insertions(+), 85 deletions(-) diff --git a/commands/apps.go b/commands/apps.go index c44f70397..32eccbdfd 100644 --- a/commands/apps.go +++ b/commands/apps.go @@ -704,15 +704,10 @@ Optionally, pass a deployment ID to get the spec of that specific deployment.`, AddStringFlag(getCmd, doctl.ArgAppDeployment, "", "", "optional: a deployment ID") AddStringFlag(getCmd, doctl.ArgFormat, "", "yaml", `the format to output the spec in; either "yaml" or "json"`) - validateCmd := CmdBuilder(cmd, RunAppsSpecValidate, "validate ", "Validate an application spec", `Use this command to check whether a given app spec (YAML or JSON) is valid. - -You may pass - as the filename to read from stdin.`, Writer) - AddBoolFlag(validateCmd, doctl.ArgSchemaOnly, "", false, "Only validate the spec schema and not the correctness of the spec.") - - cmdBuilderWithInit(cmd, RunAppsSpecValidateOffline, "validate-offline ", "Validate an application spec offline (schema-only)", `Use this command to check whether a given app spec (YAML or JSON) is valid -without connecting to DigitalOcean API. (schema-only) + validateCmd := cmdBuilderWithInit(cmd, RunAppsSpecValidate, "validate ", "Validate an application spec", `Use this command to check whether a given app spec (YAML or JSON) is valid. You may pass - as the filename to read from stdin.`, Writer, false) + AddBoolFlag(validateCmd, doctl.ArgSchemaOnly, "", false, "Only validate the spec schema and not the correctness of the spec.") return cmd } @@ -766,36 +761,8 @@ func RunAppsSpecGet(c *CmdConfig) error { } } -// ValidateAppSpecSchema validates an app spec (schema-only) -// returns the marshaled yaml spec as a byte array, or error -func ValidateAppSpecSchema(appSpec *godo.AppSpec) ([]byte, error) { - ymlSpec, err := yaml.Marshal(appSpec) - if err != nil { - return []byte{}, fmt.Errorf("marshaling the spec as yaml: %v", err) - } - return ymlSpec, err -} - -// RunAppsSpecValidateOffline validates an app spec file without requiring auth & connection to the API -func RunAppsSpecValidateOffline(c *CmdConfig) error { - if len(c.Args) < 1 { - return doctl.NewMissingArgsErr(c.NS) - } - - specPath := c.Args[0] - appSpec, err := apps.ReadAppSpec(os.Stdin, specPath) - if err != nil { - return err - } - ymlSpec, err := ValidateAppSpecSchema(appSpec) - if err != nil { - return err - } - _, err = c.Out.Write(ymlSpec) - return err -} - // RunAppsSpecValidate validates an app spec file +// doesn't require auth & connection to the API with doctl.ArgSchemaOnly flag func RunAppsSpecValidate(c *CmdConfig) error { if len(c.Args) < 1 { return doctl.NewMissingArgsErr(c.NS) @@ -812,15 +779,20 @@ func RunAppsSpecValidate(c *CmdConfig) error { return err } + // validate schema only (offline) if schemaOnly { - ymlSpec, err := ValidateAppSpecSchema(appSpec) + ymlSpec, err := yaml.Marshal(appSpec) if err != nil { - return err + return fmt.Errorf("marshaling the spec as yaml: %v", err) } _, err = c.Out.Write(ymlSpec) return err } + // validate the spec against the API + if err := c.initServices(c); err != nil { + return err + } res, err := c.Apps().Propose(&godo.AppProposeRequest{ Spec: appSpec, }) diff --git a/integration/apps_spec_test.go b/integration/apps_spec_test.go index 281bc2a00..2b74fe156 100644 --- a/integration/apps_spec_test.go +++ b/integration/apps_spec_test.go @@ -238,50 +238,3 @@ services: expect.Equal(expectedOutput, strings.TrimSpace(string(output))) }) }) - -var _ = suite("apps/spec/validate-offline", func(t *testing.T, when spec.G, it spec.S) { - var ( - expect *require.Assertions - ) - - it.Before(func() { - expect = require.New(t) - }) - - it("accepts a valid spec", func() { - cmd := exec.Command(builtBinaryPath, - "apps", "spec", "validate-offline", - "-", - ) - byt, err := json.Marshal(testAppSpec) - expect.NoError(err) - - cmd.Stdin = bytes.NewReader(byt) - - output, err := cmd.CombinedOutput() - expect.NoError(err) - - expectedOutput := "name: test\nservices:\n- github:\n branch: main\n repo: digitalocean/doctl\n name: service" - expect.Equal(expectedOutput, strings.TrimSpace(string(output))) - }) - - it("fails on invalid specs", func() { - cmd := exec.Command(builtBinaryPath, - "apps", "spec", "validate-offline", - "-", - ) - testSpec := `name: test -services: - name: service - github: - repo: digitalocean/doctl -` - cmd.Stdin = strings.NewReader(testSpec) - - output, err := cmd.CombinedOutput() - expect.Equal("exit status 1", err.Error()) - - expectedOutput := "Error: parsing app spec: json: cannot unmarshal object into Go struct field AppSpec.services of type []*godo.AppServiceSpec" - expect.Equal(expectedOutput, strings.TrimSpace(string(output))) - }) -}) From c54c61719245b7afa85405717609b86a51a5f3eb Mon Sep 17 00:00:00 2001 From: Andrew Starr-Bochicchio Date: Tue, 24 Oct 2023 10:18:50 -0400 Subject: [PATCH 5/5] Add integration test without auth. --- integration/apps_spec_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/integration/apps_spec_test.go b/integration/apps_spec_test.go index 2b74fe156..d32cd5441 100644 --- a/integration/apps_spec_test.go +++ b/integration/apps_spec_test.go @@ -197,6 +197,24 @@ var _ = suite("apps/spec/validate", func(t *testing.T, when spec.G, it spec.S) { expect.Equal(expectedOutput, strings.TrimSpace(string(output))) }) + it("schema-only works without auth", func() { + cmd := exec.Command(builtBinaryPath, + "-u", server.URL, + "apps", "spec", "validate", + "--schema-only", "-", + ) + byt, err := json.Marshal(testAppSpec) + expect.NoError(err) + + cmd.Stdin = bytes.NewReader(byt) + + output, err := cmd.CombinedOutput() + expect.NoError(err) + + expectedOutput := "name: test\nservices:\n- github:\n branch: main\n repo: digitalocean/doctl\n name: service" + expect.Equal(expectedOutput, strings.TrimSpace(string(output))) + }) + it("calls proposeapp", func() { cmd := exec.Command(builtBinaryPath, "-t", "some-magic-token",