diff --git a/TESTING.md b/TESTING.md index 9cef18c8b..37f92fad8 100644 --- a/TESTING.md +++ b/TESTING.md @@ -21,7 +21,7 @@ make test TEST_ARGS="-run <...> " **Example**: ```sh -make test TEST_ARGS="-run TestBackendCreate ./pkg/backend/..." +make test TEST_ARGS="-run TestBackendCreate ./pkg/commands/backend" ``` Some integration tests aren't run outside of the CI environment, to enable these tests locally you'll need to set a specific environment variable relevant to the test. diff --git a/pkg/api/interface.go b/pkg/api/interface.go index 2a453bbb9..1ac457681 100644 --- a/pkg/api/interface.go +++ b/pkg/api/interface.go @@ -382,6 +382,12 @@ type Interface interface { GetERL(i *fastly.GetERLInput) (*fastly.ERL, error) ListERLs(i *fastly.ListERLsInput) ([]*fastly.ERL, error) UpdateERL(i *fastly.UpdateERLInput) (*fastly.ERL, error) + + CreateCondition(i *fastly.CreateConditionInput) (*fastly.Condition, error) + DeleteCondition(i *fastly.DeleteConditionInput) error + GetCondition(i *fastly.GetConditionInput) (*fastly.Condition, error) + ListConditions(i *fastly.ListConditionsInput) ([]*fastly.Condition, error) + UpdateCondition(i *fastly.UpdateConditionInput) (*fastly.Condition, error) } // RealtimeStatsInterface is the subset of go-fastly's realtime stats API used here. diff --git a/pkg/app/commands.go b/pkg/app/commands.go index 19b646972..2c05c8a30 100644 --- a/pkg/app/commands.go +++ b/pkg/app/commands.go @@ -70,6 +70,7 @@ import ( "github.com/fastly/cli/pkg/commands/update" "github.com/fastly/cli/pkg/commands/user" "github.com/fastly/cli/pkg/commands/vcl" + "github.com/fastly/cli/pkg/commands/vcl/condition" "github.com/fastly/cli/pkg/commands/vcl/custom" "github.com/fastly/cli/pkg/commands/vcl/snippet" "github.com/fastly/cli/pkg/commands/version" @@ -426,6 +427,12 @@ func defineCommands( userList := user.NewListCommand(userCmdRoot.CmdClause, g, m) userUpdate := user.NewUpdateCommand(userCmdRoot.CmdClause, g, m) vclCmdRoot := vcl.NewRootCommand(app, g) + vclConditionCmdRoot := condition.NewRootCommand(vclCmdRoot.CmdClause, g) + vclConditionCreate := condition.NewCreateCommand(vclConditionCmdRoot.CmdClause, g, m) + vclConditionDelete := condition.NewDeleteCommand(vclConditionCmdRoot.CmdClause, g, m) + vclConditionDescribe := condition.NewDescribeCommand(vclConditionCmdRoot.CmdClause, g, m) + vclConditionList := condition.NewListCommand(vclConditionCmdRoot.CmdClause, g, m) + vclConditionUpdate := condition.NewUpdateCommand(vclConditionCmdRoot.CmdClause, g, m) vclCustomCmdRoot := custom.NewRootCommand(vclCmdRoot.CmdClause, g) vclCustomCreate := custom.NewCreateCommand(vclCustomCmdRoot.CmdClause, g, m) vclCustomDelete := custom.NewDeleteCommand(vclCustomCmdRoot.CmdClause, g, m) @@ -780,6 +787,12 @@ func defineCommands( userList, userUpdate, vclCmdRoot, + vclConditionCmdRoot, + vclConditionCreate, + vclConditionDelete, + vclConditionDescribe, + vclConditionList, + vclConditionUpdate, vclCustomCmdRoot, vclCustomCreate, vclCustomDelete, diff --git a/pkg/app/metadata.json b/pkg/app/metadata.json index 37bb0064f..f9bd272e8 100644 --- a/pkg/app/metadata.json +++ b/pkg/app/metadata.json @@ -1352,6 +1352,33 @@ } }, "vcl": { + "condition": { + "create": { + "apis": [ + "https://developer.fastly.com/reference/api/vcl-services/condition/#create-condition" + ] + }, + "delete": { + "apis": [ + "https://developer.fastly.com/reference/api/vcl-services/condition/#delete-condition" + ] + }, + "describe": { + "apis": [ + "https://developer.fastly.com/reference/api/vcl-services/condition/#get-condition" + ] + }, + "list": { + "apis": [ + "https://developer.fastly.com/reference/api/vcl-services/condition/#list-conditions" + ] + }, + "update": { + "apis": [ + "https://developer.fastly.com/reference/api/vcl-services/condition/#update-condition" + ] + } + }, "custom": { "create": { "apis": [ diff --git a/pkg/commands/vcl/condition/condition_test.go b/pkg/commands/vcl/condition/condition_test.go new file mode 100644 index 000000000..f2a32fc89 --- /dev/null +++ b/pkg/commands/vcl/condition/condition_test.go @@ -0,0 +1,384 @@ +package condition_test + +import ( + "bytes" + "errors" + "strings" + "testing" + + "github.com/fastly/cli/pkg/app" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" + "github.com/fastly/go-fastly/v8/fastly" +) + +func TestConditionCreate(t *testing.T) { + args := testutil.Args + scenarios := []testutil.TestScenario{ + { + Args: args("vcl condition create --version 1"), + WantError: "error reading service: no service ID found", + }, + { + Args: args("vcl condition create --service-id 123 --version 1 --name always_false --statement false --type REQUEST --autoclone"), + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + CloneVersionFn: testutil.CloneVersionResult(4), + CreateConditionFn: createConditionOK, + }, + WantOutput: "Created condition always_false (service 123 version 4)", + }, + { + Args: args("vcl condition create --service-id 123 --version 1 --name always_false --statement false --type REQUEST --priority 10 --autoclone"), + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + CloneVersionFn: testutil.CloneVersionResult(4), + CreateConditionFn: createConditionError, + }, + WantError: errTest.Error(), + }, + } + + for _, testcase := range scenarios { + t.Run(testcase.Name, func(t *testing.T) { + var stdout bytes.Buffer + opts := testutil.NewRunOpts(testcase.Args, &stdout) + opts.APIClient = mock.APIClient(testcase.API) + err := app.Run(opts) + testutil.AssertErrorContains(t, err, testcase.WantError) + testutil.AssertStringContains(t, stdout.String(), testcase.WantOutput) + }) + } +} + +func TestConditionDelete(t *testing.T) { + args := testutil.Args + scenarios := []testutil.TestScenario{ + { + Args: args("vcl condition delete --service-id 123 --version 1"), + WantError: "error parsing arguments: required flag --name not provided", + }, + { + Args: args("vcl condition delete --service-id 123 --version 1 --name always_false --autoclone"), + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + CloneVersionFn: testutil.CloneVersionResult(4), + DeleteConditionFn: deleteConditionError, + }, + WantError: errTest.Error(), + }, + { + Args: args("vcl condition delete --service-id 123 --version 1 --name always_false --autoclone"), + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + CloneVersionFn: testutil.CloneVersionResult(4), + DeleteConditionFn: deleteConditionOK, + }, + WantOutput: "Deleted condition always_false (service 123 version 4)", + }, + } + for testcaseIdx := range scenarios { + testcase := &scenarios[testcaseIdx] + t.Run(testcase.Name, func(t *testing.T) { + var stdout bytes.Buffer + opts := testutil.NewRunOpts(testcase.Args, &stdout) + opts.APIClient = mock.APIClient(testcase.API) + err := app.Run(opts) + testutil.AssertErrorContains(t, err, testcase.WantError) + testutil.AssertStringContains(t, stdout.String(), testcase.WantOutput) + }) + } +} + +func TestConditionUpdate(t *testing.T) { + args := testutil.Args + scenarios := []testutil.TestScenario{ + { + Args: args("vcl condition update --service-id 123 --version 1 --new-name false_always --comment "), + WantError: "error parsing arguments: required flag --name not provided", + }, + { + Args: args("vcl condition update --service-id 123 --version 1 --name always_false --autoclone"), + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + CloneVersionFn: testutil.CloneVersionResult(4), + UpdateConditionFn: updateConditionOK, + }, + WantError: "error parsing arguments: must provide either --new-name, --statement, --type or --priority to update condition", + }, + { + Args: args("vcl condition update --service-id 123 --version 1 --name always_false --new-name false_always --autoclone"), + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + CloneVersionFn: testutil.CloneVersionResult(4), + UpdateConditionFn: updateConditionError, + }, + WantError: errTest.Error(), + }, + { + Args: args("vcl condition update --service-id 123 --version 1 --name always_false --new-name false_always --autoclone"), + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + CloneVersionFn: testutil.CloneVersionResult(4), + UpdateConditionFn: updateConditionOK, + }, + WantOutput: "Updated condition false_always (service 123 version 4)", + }, + } + for testcaseIdx := range scenarios { + testcase := &scenarios[testcaseIdx] + t.Run(testcase.Name, func(t *testing.T) { + var stdout bytes.Buffer + opts := testutil.NewRunOpts(testcase.Args, &stdout) + opts.APIClient = mock.APIClient(testcase.API) + err := app.Run(opts) + testutil.AssertErrorContains(t, err, testcase.WantError) + testutil.AssertStringContains(t, stdout.String(), testcase.WantOutput) + }) + } +} + +func TestConditionDescribe(t *testing.T) { + args := testutil.Args + scenarios := []testutil.TestScenario{ + { + Args: args("vcl condition describe --service-id 123 --version 1"), + WantError: "error parsing arguments: required flag --name not provided", + }, + { + Args: args("vcl condition describe --service-id 123 --version 1 --name always_false"), + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + GetConditionFn: getConditionError, + }, + WantError: errTest.Error(), + }, + { + Args: args("vcl condition describe --service-id 123 --version 1 --name always_false"), + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + GetConditionFn: getConditionOK, + }, + WantOutput: describeConditionOutput, + }, + } + for testcaseIdx := range scenarios { + testcase := &scenarios[testcaseIdx] + t.Run(testcase.Name, func(t *testing.T) { + var stdout bytes.Buffer + opts := testutil.NewRunOpts(testcase.Args, &stdout) + opts.APIClient = mock.APIClient(testcase.API) + err := app.Run(opts) + testutil.AssertErrorContains(t, err, testcase.WantError) + testutil.AssertString(t, testcase.WantOutput, stdout.String()) + }) + } +} + +func TestConditionList(t *testing.T) { + args := testutil.Args + scenarios := []testutil.TestScenario{ + { + Args: args("vcl condition list --service-id 123 --version 1"), + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ListConditionsFn: listConditionsOK, + }, + WantOutput: listConditionsShortOutput, + }, + { + Args: args("vcl condition list --service-id 123 --version 1 --verbose"), + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ListConditionsFn: listConditionsOK, + }, + WantOutput: listConditionsVerboseOutput, + }, + { + Args: args("vcl condition list --service-id 123 --version 1 -v"), + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ListConditionsFn: listConditionsOK, + }, + WantOutput: listConditionsVerboseOutput, + }, + { + Args: args("vcl condition --verbose list --service-id 123 --version 1"), + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ListConditionsFn: listConditionsOK, + }, + WantOutput: listConditionsVerboseOutput, + }, + { + Args: args("-v vcl condition list --service-id 123 --version 1"), + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ListConditionsFn: listConditionsOK, + }, + WantOutput: listConditionsVerboseOutput, + }, + { + Args: args("vcl condition list --service-id 123 --version 1"), + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ListConditionsFn: listConditionsError, + }, + WantError: errTest.Error(), + }, + } + for testcaseIdx := range scenarios { + testcase := &scenarios[testcaseIdx] + t.Run(testcase.Name, func(t *testing.T) { + var stdout bytes.Buffer + opts := testutil.NewRunOpts(testcase.Args, &stdout) + opts.APIClient = mock.APIClient(testcase.API) + err := app.Run(opts) + testutil.AssertErrorContains(t, err, testcase.WantError) + testutil.AssertString(t, testcase.WantOutput, stdout.String()) + }) + } +} + +var describeConditionOutput = "\n" + strings.TrimSpace(` +Service ID: 123 +Version: 1 +Name: always_false +Statement: false +Type: CACHE +Priority: 10 +`) + "\n" + +var listConditionsShortOutput = strings.TrimSpace(` +SERVICE VERSION NAME STATEMENT TYPE PRIORITY +123 1 always_false_request false REQUEST 10 +123 1 always_false_cache false CACHE 10 +`) + "\n" + +var listConditionsVerboseOutput = strings.TrimSpace(` +Fastly API token not provided +Fastly API endpoint: https://api.fastly.com + +Service ID (via --service-id): 123 + +Version: 1 + Condition 1/2 + Name: always_false_request + Statement: false + Type: REQUEST + Priority: 10 + Condition 2/2 + Name: always_false_cache + Statement: false + Type: CACHE + Priority: 10 +`) + "\n\n" + +var errTest = errors.New("fixture error") + +func createConditionOK(i *fastly.CreateConditionInput) (*fastly.Condition, error) { + var priority int = 10 + if i.Priority != nil { + priority = *i.Priority + } + + var conditionType string = "REQUEST" + if i.Type != nil { + conditionType = *i.Type + } + + return &fastly.Condition{ + ServiceID: i.ServiceID, + ServiceVersion: i.ServiceVersion, + Name: *i.Name, + Statement: *i.Statement, + Type: conditionType, + Priority: priority, + }, nil +} + +func createConditionError(i *fastly.CreateConditionInput) (*fastly.Condition, error) { + return nil, errTest +} + +func deleteConditionOK(i *fastly.DeleteConditionInput) error { + return nil +} + +func deleteConditionError(i *fastly.DeleteConditionInput) error { + return errTest +} + +func updateConditionOK(i *fastly.UpdateConditionInput) (*fastly.Condition, error) { + var priority int = 10 + if i.Priority != nil { + priority = *i.Priority + } + + var conditionType string = "REQUEST" + if i.Type != nil { + conditionType = *i.Type + } + + var statement string = "false" + if i.Statement != nil { + statement = *i.Type + } + + return &fastly.Condition{ + ServiceID: i.ServiceID, + ServiceVersion: i.ServiceVersion, + Name: i.Name, + Statement: statement, + Type: conditionType, + Priority: priority, + }, nil +} + +func updateConditionError(i *fastly.UpdateConditionInput) (*fastly.Condition, error) { + return nil, errTest +} + +func getConditionOK(i *fastly.GetConditionInput) (*fastly.Condition, error) { + var priority int = 10 + var conditionType string = "CACHE" + var statement string = "false" + + return &fastly.Condition{ + ServiceID: i.ServiceID, + ServiceVersion: i.ServiceVersion, + Name: i.Name, + Statement: statement, + Type: conditionType, + Priority: priority, + }, nil +} + +func getConditionError(i *fastly.GetConditionInput) (*fastly.Condition, error) { + return nil, errTest +} + +func listConditionsOK(i *fastly.ListConditionsInput) ([]*fastly.Condition, error) { + return []*fastly.Condition{ + { + ServiceID: i.ServiceID, + ServiceVersion: i.ServiceVersion, + Name: "always_false_request", + Statement: "false", + Type: "REQUEST", + Priority: 10, + }, + { + ServiceID: i.ServiceID, + ServiceVersion: i.ServiceVersion, + Name: "always_false_cache", + Statement: "false", + Type: "CACHE", + Priority: 10, + }, + }, nil +} + +func listConditionsError(i *fastly.ListConditionsInput) ([]*fastly.Condition, error) { + return nil, errTest +} diff --git a/pkg/commands/vcl/condition/create.go b/pkg/commands/vcl/condition/create.go new file mode 100644 index 000000000..f0a0c0a66 --- /dev/null +++ b/pkg/commands/vcl/condition/create.go @@ -0,0 +1,125 @@ +package condition + +import ( + "io" + + "github.com/fastly/cli/pkg/cmd" + "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/manifest" + "github.com/fastly/cli/pkg/text" + "github.com/fastly/go-fastly/v8/fastly" +) + +// ConditionTypes are the allowed input values for the --type flag. +// Reference: https://developer.fastly.com/reference/api/vcl-services/condition/ +var ConditionTypes = []string{"REQUEST", "CACHE", "RESPONSE", "PREFETCH"} + +// CreateCommand calls the Fastly API to create an appropriate resource. +type CreateCommand struct { + cmd.Base + manifest manifest.Data + + // Required. + serviceVersion cmd.OptionalServiceVersion + + // Optional. + autoClone cmd.OptionalAutoClone + conditionType cmd.OptionalString + name cmd.OptionalString + priority cmd.OptionalInt + serviceName cmd.OptionalServiceNameID + statement cmd.OptionalString +} + +// NewCreateCommand returns a usable command registered under the parent. +func NewCreateCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *CreateCommand { + c := CreateCommand{ + Base: cmd.Base{ + Globals: g, + }, + manifest: m, + } + c.CmdClause = parent.Command("create", "Create a condtion on a Fastly service version").Alias("add") + + // Required flags + c.RegisterFlag(cmd.StringFlagOpts{ + Name: cmd.FlagVersionName, + Description: cmd.FlagVersionDesc, + Dst: &c.serviceVersion.Value, + Required: true, + }) + + // Optional flags + c.RegisterAutoCloneFlag(cmd.AutoCloneFlagOpts{ + Action: c.autoClone.Set, + Dst: &c.autoClone.Value, + }) + c.CmdClause.Flag("name", "Condition name").Short('n').Action(c.name.Set).StringVar(&c.name.Value) + c.CmdClause.Flag("priority", "Condition priority").Action(c.priority.Set).IntVar(&c.priority.Value) + c.CmdClause.Flag("statement", "Condition statement").Action(c.statement.Set).StringVar(&c.statement.Value) + c.CmdClause.Flag("type", "Condition type").HintOptions(ConditionTypes...).Action(c.conditionType.Set).EnumVar(&c.conditionType.Value, ConditionTypes...) + c.RegisterFlag(cmd.StringFlagOpts{ + Name: cmd.FlagServiceIDName, + Description: cmd.FlagServiceIDDesc, + Dst: &c.manifest.Flag.ServiceID, + Short: 's', + }) + c.RegisterFlag(cmd.StringFlagOpts{ + Action: c.serviceName.Set, + Name: cmd.FlagServiceName, + Description: cmd.FlagServiceDesc, + Dst: &c.serviceName.Value, + }) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *CreateCommand) Exec(_ io.Reader, out io.Writer) error { + serviceID, serviceVersion, err := cmd.ServiceDetails(cmd.ServiceDetailsOpts{ + AutoCloneFlag: c.autoClone, + APIClient: c.Globals.APIClient, + Manifest: c.manifest, + Out: out, + ServiceNameFlag: c.serviceName, + ServiceVersionFlag: c.serviceVersion, + VerboseMode: c.Globals.Flags.Verbose, + }) + if err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]interface{}{ + "Service ID": serviceID, + "Service Version": errors.ServiceVersion(serviceVersion), + }) + return err + } + + input := fastly.CreateConditionInput{ + ServiceID: serviceID, + ServiceVersion: serviceVersion.Number, + } + + if c.name.WasSet { + input.Name = &c.name.Value + } + if c.statement.WasSet { + input.Statement = &c.statement.Value + } + if c.conditionType.WasSet { + input.Type = &c.conditionType.Value + } + if c.priority.WasSet { + input.Priority = &c.priority.Value + } + r, err := c.Globals.APIClient.CreateCondition(&input) + if err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]interface{}{ + "Service ID": serviceID, + "Service Version": serviceVersion.Number, + }) + return err + } + + text.Success(out, "Created condition %s (service %s version %d)", r.Name, r.ServiceID, r.ServiceVersion) + return nil +} diff --git a/pkg/commands/vcl/condition/delete.go b/pkg/commands/vcl/condition/delete.go new file mode 100644 index 000000000..1cd433f3b --- /dev/null +++ b/pkg/commands/vcl/condition/delete.go @@ -0,0 +1,98 @@ +package condition + +import ( + "io" + + "github.com/fastly/cli/pkg/cmd" + "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/manifest" + "github.com/fastly/cli/pkg/text" + "github.com/fastly/go-fastly/v8/fastly" +) + +// DeleteCommand calls the Fastly API to delete an appropriate resource. +type DeleteCommand struct { + cmd.Base + manifest manifest.Data + name string + serviceName cmd.OptionalServiceNameID + serviceVersion cmd.OptionalServiceVersion + autoClone cmd.OptionalAutoClone +} + +// NewDeleteCommand returns a usable command registered under the parent. +func NewDeleteCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *DeleteCommand { + c := DeleteCommand{ + Base: cmd.Base{ + Globals: g, + }, + manifest: m, + } + c.CmdClause = parent.Command("delete", "Delete a condition on a Fastly service version").Alias("remove") + + // Required flags + c.CmdClause.Flag("name", "Condition name").Short('n').Required().StringVar(&c.name) + c.RegisterFlag(cmd.StringFlagOpts{ + Name: cmd.FlagVersionName, + Description: cmd.FlagVersionDesc, + Dst: &c.serviceVersion.Value, + Required: true, + }) + + // Optional flags + c.RegisterAutoCloneFlag(cmd.AutoCloneFlagOpts{ + Action: c.autoClone.Set, + Dst: &c.autoClone.Value, + }) + c.RegisterFlag(cmd.StringFlagOpts{ + Name: cmd.FlagServiceIDName, + Description: cmd.FlagServiceIDDesc, + Dst: &c.manifest.Flag.ServiceID, + Short: 's', + }) + c.RegisterFlag(cmd.StringFlagOpts{ + Action: c.serviceName.Set, + Name: cmd.FlagServiceName, + Description: cmd.FlagServiceDesc, + Dst: &c.serviceName.Value, + }) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *DeleteCommand) Exec(_ io.Reader, out io.Writer) error { + serviceID, serviceVersion, err := cmd.ServiceDetails(cmd.ServiceDetailsOpts{ + AutoCloneFlag: c.autoClone, + APIClient: c.Globals.APIClient, + Manifest: c.manifest, + Out: out, + ServiceNameFlag: c.serviceName, + ServiceVersionFlag: c.serviceVersion, + VerboseMode: c.Globals.Flags.Verbose, + }) + if err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]any{ + "Service ID": serviceID, + "Service Version": errors.ServiceVersion(serviceVersion), + }) + return err + } + + var input fastly.DeleteConditionInput + input.ServiceID = serviceID + input.ServiceVersion = serviceVersion.Number + input.Name = c.name + + if err := c.Globals.APIClient.DeleteCondition(&input); err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]any{ + "Service ID": serviceID, + "Service Version": serviceVersion.Number, + }) + return err + } + + text.Success(out, "Deleted condition %s (service %s version %d)", c.name, serviceID, serviceVersion.Number) + return nil +} diff --git a/pkg/commands/vcl/condition/describe.go b/pkg/commands/vcl/condition/describe.go new file mode 100644 index 000000000..b9a32120e --- /dev/null +++ b/pkg/commands/vcl/condition/describe.go @@ -0,0 +1,110 @@ +package condition + +import ( + "fmt" + "io" + + "github.com/fastly/cli/pkg/cmd" + "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/manifest" + "github.com/fastly/go-fastly/v8/fastly" +) + +// DescribeCommand calls the Fastly API to describe an appropriate resource. +type DescribeCommand struct { + cmd.Base + cmd.JSONOutput + manifest manifest.Data + name string + serviceName cmd.OptionalServiceNameID + serviceVersion cmd.OptionalServiceVersion +} + +// NewDescribeCommand returns a usable command registered under the parent. +func NewDescribeCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *DescribeCommand { + var c DescribeCommand + c.CmdClause = parent.Command("describe", "Show detailed information about a condition on a Fastly service version").Alias("get") + c.Globals = g + c.manifest = m + + // Required flags + c.CmdClause.Flag("name", "Name of condition").Short('n').Required().StringVar(&c.name) + + c.RegisterFlag(cmd.StringFlagOpts{ + Name: cmd.FlagVersionName, + Description: cmd.FlagVersionDesc, + Dst: &c.serviceVersion.Value, + Required: true, + }) + + // Optional flags + c.RegisterFlagBool(c.JSONFlag()) + c.RegisterFlag(cmd.StringFlagOpts{ + Name: cmd.FlagServiceIDName, + Description: cmd.FlagServiceIDDesc, + Dst: &c.manifest.Flag.ServiceID, + Short: 's', + }) + c.RegisterFlag(cmd.StringFlagOpts{ + Action: c.serviceName.Set, + Name: cmd.FlagServiceName, + Description: cmd.FlagServiceDesc, + Dst: &c.serviceName.Value, + }) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *DescribeCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return errors.ErrInvalidVerboseJSONCombo + } + + serviceID, serviceVersion, err := cmd.ServiceDetails(cmd.ServiceDetailsOpts{ + AllowActiveLocked: true, + APIClient: c.Globals.APIClient, + Manifest: c.manifest, + Out: out, + ServiceNameFlag: c.serviceName, + ServiceVersionFlag: c.serviceVersion, + VerboseMode: c.Globals.Flags.Verbose, + }) + if err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]any{ + "Service ID": serviceID, + "Service Version": errors.ServiceVersion(serviceVersion), + }) + return err + } + + var input fastly.GetConditionInput + input.ServiceID = serviceID + input.ServiceVersion = serviceVersion.Number + input.Name = c.name + + r, err := c.Globals.APIClient.GetCondition(&input) + if err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]interface{}{ + "Service ID": serviceID, + "Service Version": serviceVersion.Number, + }) + return err + } + + if ok, err := c.WriteJSON(out, r); ok { + return err + } + + if !c.Globals.Verbose() { + fmt.Fprintf(out, "\nService ID: %s\n", r.ServiceID) + } + fmt.Fprintf(out, "Version: %d\n", r.ServiceVersion) + fmt.Fprintf(out, "Name: %s\n", r.Name) + fmt.Fprintf(out, "Statement: %s\n", r.Statement) + fmt.Fprintf(out, "Type: %s\n", r.Type) + fmt.Fprintf(out, "Priority: %d\n", r.Priority) + + return nil +} diff --git a/pkg/commands/vcl/condition/doc.go b/pkg/commands/vcl/condition/doc.go new file mode 100644 index 000000000..61714ecf7 --- /dev/null +++ b/pkg/commands/vcl/condition/doc.go @@ -0,0 +1,2 @@ +// Package condition contains commands to inspect and manipulate Fastly service condition. +package condition diff --git a/pkg/commands/vcl/condition/list.go b/pkg/commands/vcl/condition/list.go new file mode 100644 index 000000000..affc20eb2 --- /dev/null +++ b/pkg/commands/vcl/condition/list.go @@ -0,0 +1,119 @@ +package condition + +import ( + "fmt" + "io" + + "github.com/fastly/cli/pkg/cmd" + "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/manifest" + "github.com/fastly/cli/pkg/text" + "github.com/fastly/go-fastly/v8/fastly" +) + +// ListCommand calls the Fastly API to list appropriate resources. +type ListCommand struct { + cmd.Base + cmd.JSONOutput + + manifest manifest.Data + serviceName cmd.OptionalServiceNameID + serviceVersion cmd.OptionalServiceVersion +} + +// NewListCommand returns a usable command registered under the parent. +func NewListCommand(parent cmd.Registerer, g *global.Data, data manifest.Data) *ListCommand { + var c ListCommand + c.CmdClause = parent.Command("list", "List condition on a Fastly service version") + c.Globals = g + c.manifest = data + + // Required flags + c.RegisterFlag(cmd.StringFlagOpts{ + Name: cmd.FlagVersionName, + Description: cmd.FlagVersionDesc, + Dst: &c.serviceVersion.Value, + Required: true, + }) + + // Optional Flags + c.RegisterFlag(cmd.StringFlagOpts{ + Name: cmd.FlagServiceIDName, + Description: cmd.FlagServiceIDDesc, + Dst: &c.manifest.Flag.ServiceID, + Short: 's', + }) + c.RegisterFlag(cmd.StringFlagOpts{ + Action: c.serviceName.Set, + Name: cmd.FlagServiceName, + Description: cmd.FlagServiceDesc, + Dst: &c.serviceName.Value, + }) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *ListCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return errors.ErrInvalidVerboseJSONCombo + } + + serviceID, serviceVersion, err := cmd.ServiceDetails(cmd.ServiceDetailsOpts{ + AllowActiveLocked: true, + APIClient: c.Globals.APIClient, + Manifest: c.manifest, + Out: out, + ServiceNameFlag: c.serviceName, + ServiceVersionFlag: c.serviceVersion, + VerboseMode: c.Globals.Flags.Verbose, + }) + if err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]any{ + "Service ID": serviceID, + "Service Version": errors.ServiceVersion(serviceVersion), + }) + return err + } + + var input fastly.ListConditionsInput + + input.ServiceID = serviceID + input.ServiceVersion = serviceVersion.Number + + o, err := c.Globals.APIClient.ListConditions(&input) + if err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]any{ + "Service ID": serviceID, + "Service Version": serviceVersion.Number, + }) + return err + } + + if ok, err := c.WriteJSON(out, o); ok { + return err + } + + if !c.Globals.Verbose() { + tw := text.NewTable(out) + tw.AddHeader("SERVICE", "VERSION", "NAME", "STATEMENT", "TYPE", "PRIORITY") + for _, r := range o { + tw.AddLine(r.ServiceID, r.ServiceVersion, r.Name, r.Statement, r.Type, r.Priority) + } + tw.Print() + return nil + } + + fmt.Fprintf(out, "Version: %d\n", input.ServiceVersion) + for i, r := range o { + fmt.Fprintf(out, "\tCondition %d/%d\n", i+1, len(o)) + fmt.Fprintf(out, "\t\tName: %s\n", r.Name) + fmt.Fprintf(out, "\t\tStatement: %v\n", r.Statement) + fmt.Fprintf(out, "\t\tType: %v\n", r.Type) + fmt.Fprintf(out, "\t\tPriority: %v\n", r.Priority) + } + fmt.Fprintln(out) + + return nil +} diff --git a/pkg/commands/vcl/condition/root.go b/pkg/commands/vcl/condition/root.go new file mode 100644 index 000000000..a5cdb7728 --- /dev/null +++ b/pkg/commands/vcl/condition/root.go @@ -0,0 +1,28 @@ +package condition + +import ( + "io" + + "github.com/fastly/cli/pkg/cmd" + "github.com/fastly/cli/pkg/global" +) + +// RootCommand is the parent command for all subcommands in this package. +// It should be installed under the primary root command. +type RootCommand struct { + cmd.Base + // no flags +} + +// NewRootCommand returns a new command registered in the parent. +func NewRootCommand(parent cmd.Registerer, g *global.Data) *RootCommand { + var c RootCommand + c.Globals = g + c.CmdClause = parent.Command("condition", "Manipulate Fastly service version conditions") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/vcl/condition/update.go b/pkg/commands/vcl/condition/update.go new file mode 100644 index 000000000..f6d54fa4a --- /dev/null +++ b/pkg/commands/vcl/condition/update.go @@ -0,0 +1,128 @@ +package condition + +import ( + "fmt" + "io" + + "github.com/fastly/cli/pkg/cmd" + "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/manifest" + "github.com/fastly/cli/pkg/text" + "github.com/fastly/go-fastly/v8/fastly" +) + +// UpdateCommand calls the Fastly API to update an appropriate resource. +type UpdateCommand struct { + cmd.Base + manifest manifest.Data + input fastly.UpdateConditionInput + serviceName cmd.OptionalServiceNameID + serviceVersion cmd.OptionalServiceVersion + autoClone cmd.OptionalAutoClone + + newName cmd.OptionalString + statement cmd.OptionalString + conditionType cmd.OptionalString + priority cmd.OptionalInt + comment cmd.OptionalString +} + +// NewUpdateCommand returns a usable command registered under the parent. +func NewUpdateCommand(parent cmd.Registerer, globals *global.Data, data manifest.Data) *UpdateCommand { + var c UpdateCommand + c.CmdClause = parent.Command("update", "Update a condition on a Fastly service version") + c.Globals = globals + c.manifest = data + + // Required flags + c.CmdClause.Flag("name", "Domain name").Short('n').Required().StringVar(&c.input.Name) + c.RegisterFlag(cmd.StringFlagOpts{ + Name: cmd.FlagVersionName, + Description: cmd.FlagVersionDesc, + Dst: &c.serviceVersion.Value, + Required: true, + }) + + // Optional flags + c.RegisterAutoCloneFlag(cmd.AutoCloneFlagOpts{ + Action: c.autoClone.Set, + Dst: &c.autoClone.Value, + }) + c.CmdClause.Flag("new-name", "New condition name").Action(c.newName.Set).StringVar(&c.newName.Value) + c.CmdClause.Flag("priority", "Condition priority").Action(c.priority.Set).IntVar(&c.priority.Value) + c.CmdClause.Flag("statement", "Condition statement").Action(c.statement.Set).StringVar(&c.statement.Value) + c.CmdClause.Flag("type", "Condition type").Action(c.conditionType.Set).StringVar(&c.conditionType.Value) + c.CmdClause.Flag("comment", "Condition comment").Action(c.comment.Set).StringVar(&c.comment.Value) + + c.RegisterFlag(cmd.StringFlagOpts{ + Name: cmd.FlagServiceIDName, + Description: cmd.FlagServiceIDDesc, + Dst: &c.manifest.Flag.ServiceID, + Short: 's', + }) + c.RegisterFlag(cmd.StringFlagOpts{ + Action: c.serviceName.Set, + Name: cmd.FlagServiceName, + Description: cmd.FlagServiceDesc, + Dst: &c.serviceName.Value, + }) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { + serviceID, serviceVersion, err := cmd.ServiceDetails(cmd.ServiceDetailsOpts{ + AutoCloneFlag: c.autoClone, + APIClient: c.Globals.APIClient, + Manifest: c.manifest, + Out: out, + ServiceNameFlag: c.serviceName, + ServiceVersionFlag: c.serviceVersion, + VerboseMode: c.Globals.Flags.Verbose, + }) + if err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]interface{}{ + "Service ID": serviceID, + "Service Version": errors.ServiceVersion(serviceVersion), + }) + return err + } + + c.input.ServiceID = serviceID + c.input.ServiceVersion = serviceVersion.Number + + // If no argument are provided, error with useful message. + if !c.newName.WasSet && !c.priority.WasSet && !c.statement.WasSet && !c.conditionType.WasSet { + return fmt.Errorf("error parsing arguments: must provide either --new-name, --statement, --type or --priority to update condition") + } + + if c.newName.WasSet { + c.input.Name = c.newName.Value + } + if c.priority.WasSet { + c.input.Priority = &c.priority.Value + } + if c.conditionType.WasSet { + c.input.Type = &c.conditionType.Value + } + if c.statement.WasSet { + c.input.Statement = &c.statement.Value + } + if c.comment.WasSet { + c.input.Statement = &c.comment.Value + } + + r, err := c.Globals.APIClient.UpdateCondition(&c.input) + if err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]interface{}{ + "Service ID": serviceID, + "Service Version": serviceVersion.Number, + }) + return err + } + + text.Success(out, "Updated condition %s (service %s version %d)", r.Name, r.ServiceID, r.ServiceVersion) + return nil +} diff --git a/pkg/mock/api.go b/pkg/mock/api.go index db11b39ca..b36d33160 100644 --- a/pkg/mock/api.go +++ b/pkg/mock/api.go @@ -374,6 +374,12 @@ type API struct { GetERLFn func(i *fastly.GetERLInput) (*fastly.ERL, error) ListERLsFn func(i *fastly.ListERLsInput) ([]*fastly.ERL, error) UpdateERLFn func(i *fastly.UpdateERLInput) (*fastly.ERL, error) + + CreateConditionFn func(i *fastly.CreateConditionInput) (*fastly.Condition, error) + DeleteConditionFn func(i *fastly.DeleteConditionInput) error + GetConditionFn func(i *fastly.GetConditionInput) (*fastly.Condition, error) + ListConditionsFn func(i *fastly.ListConditionsInput) ([]*fastly.Condition, error) + UpdateConditionFn func(i *fastly.UpdateConditionInput) (*fastly.Condition, error) } // AllDatacenters implements Interface. @@ -1900,3 +1906,28 @@ func (m API) ListERLs(i *fastly.ListERLsInput) ([]*fastly.ERL, error) { func (m API) UpdateERL(i *fastly.UpdateERLInput) (*fastly.ERL, error) { return m.UpdateERLFn(i) } + +// CreateCondition implements Interface. +func (m API) CreateCondition(i *fastly.CreateConditionInput) (*fastly.Condition, error) { + return m.CreateConditionFn(i) +} + +// DeleteCondition implements Interface. +func (m API) DeleteCondition(i *fastly.DeleteConditionInput) error { + return m.DeleteConditionFn(i) +} + +// GetCondition implements Interface. +func (m API) GetCondition(i *fastly.GetConditionInput) (*fastly.Condition, error) { + return m.GetConditionFn(i) +} + +// ListConditions implements Interface. +func (m API) ListConditions(i *fastly.ListConditionsInput) ([]*fastly.Condition, error) { + return m.ListConditionsFn(i) +} + +// UpdateCondition implements Interface. +func (m API) UpdateCondition(i *fastly.UpdateConditionInput) (*fastly.Condition, error) { + return m.UpdateConditionFn(i) +}