Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- feat(apisecurity/discoveredoperations): add support for 'list' and 'update' support for 'API discovery'. [#1689](https://github.com/fastly/cli/pull/1689)
- feat(apisecurity/operations): add CRUD support for 'API Inventory' operations. [#1689](https://github.com/fastly/cli/pull/1689)
- feat(apisecurity/tags): add API Security Operations tag support ([#1688](https://github.com/fastly/cli/pull/1688))
- feat(service/version): add support for service validation. [#1695](https://github.com/fastly/cli/pull/1695)

### Dependencies:
- build(deps): `golang.org/x/net` from 0.50.0 to 0.51.0 ([#1674](https://github.com/fastly/cli/pull/1674))
Expand Down
1 change: 1 addition & 0 deletions pkg/api/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type Interface interface {
DeactivateVersion(context.Context, *fastly.DeactivateVersionInput) (*fastly.Version, error)
LockVersion(context.Context, *fastly.LockVersionInput) (*fastly.Version, error)
LatestVersion(context.Context, *fastly.LatestVersionInput) (*fastly.Version, error)
ValidateVersion(context.Context, *fastly.ValidateVersionInput) (bool, string, error)

CreateDomain(context.Context, *fastly.CreateDomainInput) (*fastly.Domain, error)
ListDomains(context.Context, *fastly.ListDomainsInput) ([]*fastly.Domain, error)
Expand Down
2 changes: 2 additions & 0 deletions pkg/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,7 @@ func Define( // nolint:revive // function-length
serviceVersionStage := serviceversion.NewStageCommand(serviceVersionCmdRoot.CmdClause, data)
serviceVersionUnstage := serviceversion.NewUnstageCommand(serviceVersionCmdRoot.CmdClause, data)
serviceVersionUpdate := serviceversion.NewUpdateCommand(serviceVersionCmdRoot.CmdClause, data)
serviceVersionValidate := serviceversion.NewValidateCommand(serviceVersionCmdRoot.CmdClause, data)
servicedomainCmdRoot := servicedomain.NewRootCommand(serviceCmdRoot.CmdClause, data)
servicedomainCreate := servicedomain.NewCreateCommand(servicedomainCmdRoot.CmdClause, data)
servicedomainDelete := servicedomain.NewDeleteCommand(servicedomainCmdRoot.CmdClause, data)
Expand Down Expand Up @@ -1614,6 +1615,7 @@ func Define( // nolint:revive // function-length
serviceVersionStage,
serviceVersionUnstage,
serviceVersionUpdate,
serviceVersionValidate,
}...)
cmds = append(cmds, ssoCommands...)
cmds = append(cmds, []argparser.Command{
Expand Down
110 changes: 110 additions & 0 deletions pkg/commands/service/version/serviceversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,3 +481,113 @@ func lockVersionOK(_ context.Context, i *fastly.LockVersionInput) (*fastly.Versi
func lockVersionError(_ context.Context, _ *fastly.LockVersionInput) (*fastly.Version, error) {
return nil, testutil.Err
}

func TestVersionValidate(t *testing.T) {
scenarios := []testutil.CLIScenario{
{
Name: "validate missing --service-id flag",
Args: "--version 1",
WantError: "error parsing arguments: required flag --service-id not provided",
},
{
Name: "validate missing --version flag",
Args: "--service-id 123",
WantError: "error parsing arguments: required flag --version not provided",
},
{
Name: "validate successful - valid version without message",
Args: "--service-id 123 --version 1",
API: &mock.API{
ListVersionsFn: testutil.ListVersions,
ValidateVersionFn: validateVersionValid(""),
},
WantOutput: "Service 123 version 1 is valid",
},
{
Name: "validate successful - valid version with message",
Args: "--service-id 123 --version 1",
API: &mock.API{
ListVersionsFn: testutil.ListVersions,
ValidateVersionFn: validateVersionValid("All checks passed"),
},
WantOutput: "Service 123 version 1 is valid: All checks passed",
},
{
Name: "validate successful - invalid version without message",
Args: "--service-id 123 --version 2",
API: &mock.API{
ListVersionsFn: testutil.ListVersions,
ValidateVersionFn: validateVersionInvalid(""),
},
WantOutput: "Service 123 version 2 is not valid",
},
{
Name: "validate successful - invalid version with message",
Args: "--service-id 123 --version 2",
API: &mock.API{
ListVersionsFn: testutil.ListVersions,
ValidateVersionFn: validateVersionInvalid("Missing required backend"),
},
WantOutput: "Service 123 version 2 is not valid: Missing required backend",
},
{
Name: "validate with json output - valid version",
Args: "--service-id 123 --version 1 --json",
API: &mock.API{
ListVersionsFn: testutil.ListVersions,
ValidateVersionFn: validateVersionValid("All checks passed"),
},
WantOutput: validateVersionValidJSONOutput,
},
{
Name: "validate with json output - invalid version",
Args: "--service-id 123 --version 2 --json",
API: &mock.API{
ListVersionsFn: testutil.ListVersions,
ValidateVersionFn: validateVersionInvalid("Missing required backend"),
},
WantOutput: validateVersionInvalidJSONOutput,
},
{
Name: "validate error from API",
Args: "--service-id 123 --version 1",
API: &mock.API{
ListVersionsFn: testutil.ListVersions,
ValidateVersionFn: validateVersionError,
},
WantError: testutil.Err.Error(),
},
}

testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "validate"}, scenarios)
}

func validateVersionValid(message string) func(context.Context, *fastly.ValidateVersionInput) (bool, string, error) {
return func(_ context.Context, _ *fastly.ValidateVersionInput) (bool, string, error) {
return true, message, nil
}
}

func validateVersionInvalid(message string) func(context.Context, *fastly.ValidateVersionInput) (bool, string, error) {
return func(_ context.Context, _ *fastly.ValidateVersionInput) (bool, string, error) {
return false, message, nil
}
}

func validateVersionError(_ context.Context, _ *fastly.ValidateVersionInput) (bool, string, error) {
return false, "", testutil.Err
}

var validateVersionValidJSONOutput = strings.TrimSpace(`
{
"message": "All checks passed",
"valid": true
}
`) + "\n"

var validateVersionInvalidJSONOutput = strings.TrimSpace(`
{
"message": "Missing required backend",
"valid": false
}
`) + "\n"
103 changes: 103 additions & 0 deletions pkg/commands/service/version/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package version

import (
"context"
"io"

"github.com/fastly/go-fastly/v13/fastly"

"github.com/fastly/cli/pkg/argparser"
fsterr "github.com/fastly/cli/pkg/errors"
"github.com/fastly/cli/pkg/global"
"github.com/fastly/cli/pkg/text"
)

// ValidateCommand calls the Fastly API to validate a service version.
type ValidateCommand struct {
argparser.Base
argparser.JSONOutput

input fastly.ValidateVersionInput
Comment thread
anthony-gomez-fastly marked this conversation as resolved.
serviceVersion argparser.OptionalServiceVersion
}

// NewValidateCommand returns a usable command registered under the parent.
func NewValidateCommand(parent argparser.Registerer, g *global.Data) *ValidateCommand {
c := ValidateCommand{
Base: argparser.Base{
Globals: g,
},
}
c.CmdClause = parent.Command("validate", "Validate a service version")
c.RegisterFlagBool(c.JSONFlag()) // --json
c.RegisterFlag(argparser.StringFlagOpts{
Name: argparser.FlagServiceIDName,
Description: argparser.FlagServiceIDDesc,
Dst: &g.Manifest.Flag.ServiceID,
Short: 's',
Required: true,
})
c.RegisterFlag(argparser.StringFlagOpts{
Name: argparser.FlagVersionName,
Description: argparser.FlagVersionDesc,
Dst: &c.serviceVersion.Value,
Required: true,
})
return &c
}

// Exec invokes the application logic for the command.
func (c *ValidateCommand) Exec(_ io.Reader, out io.Writer) error {
if c.Globals.Verbose() && c.JSONOutput.Enabled {
return fsterr.ErrInvalidVerboseJSONCombo
}

serviceID, serviceVersion, err := argparser.ServiceDetails(argparser.ServiceDetailsOpts{
APIClient: c.Globals.APIClient,
Manifest: *c.Globals.Manifest,
Out: out,
ServiceVersionFlag: c.serviceVersion,
VerboseMode: c.Globals.Flags.Verbose,
})
if err != nil {
c.Globals.ErrLog.AddWithContext(err, map[string]any{
"Service ID": serviceID,
"Service Version": fsterr.ServiceVersion(serviceVersion),
})
return err
}

c.input.ServiceID = serviceID
c.input.ServiceVersion = fastly.ToValue(serviceVersion.Number)

valid, msg, err := c.Globals.APIClient.ValidateVersion(context.TODO(), &c.input)
if err != nil {
c.Globals.ErrLog.AddWithContext(err, map[string]any{
"Service ID": serviceID,
})
return err
}

if ok, err := c.WriteJSON(out, map[string]any{
"valid": valid,
"message": msg,
}); ok {
return err
}

if valid {
if msg != "" {
text.Success(out, "Service %s version %d is valid: %s", serviceID, c.input.ServiceVersion, msg)
} else {
text.Success(out, "Service %s version %d is valid", serviceID, c.input.ServiceVersion)
}
} else {
if msg != "" {
text.Error(out, "Service %s version %d is not valid: %s", serviceID, c.input.ServiceVersion, msg)
} else {
text.Error(out, "Service %s version %d is not valid", serviceID, c.input.ServiceVersion)
}
}

return nil
}
6 changes: 6 additions & 0 deletions pkg/mock/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type API struct {
DeactivateVersionFn func(context.Context, *fastly.DeactivateVersionInput) (*fastly.Version, error)
LockVersionFn func(context.Context, *fastly.LockVersionInput) (*fastly.Version, error)
LatestVersionFn func(context.Context, *fastly.LatestVersionInput) (*fastly.Version, error)
ValidateVersionFn func(context.Context, *fastly.ValidateVersionInput) (bool, string, error)

CreateDomainFn func(context.Context, *fastly.CreateDomainInput) (*fastly.Domain, error)
ListDomainsFn func(context.Context, *fastly.ListDomainsInput) ([]*fastly.Domain, error)
Expand Down Expand Up @@ -507,6 +508,11 @@ func (m API) LatestVersion(ctx context.Context, i *fastly.LatestVersionInput) (*
return m.LatestVersionFn(ctx, i)
}

// ValidateVersion implements Interface.
func (m API) ValidateVersion(ctx context.Context, i *fastly.ValidateVersionInput) (bool, string, error) {
return m.ValidateVersionFn(ctx, i)
}

// CreateDomain implements Interface.
func (m API) CreateDomain(ctx context.Context, i *fastly.CreateDomainInput) (*fastly.Domain, error) {
return m.CreateDomainFn(ctx, i)
Expand Down
Loading