diff --git a/CHANGELOG.md b/CHANGELOG.md index f18dbec44..0c7e35cd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] ### Breaking: +- breaking(domain) - service version oriented `domain` commands have been moved under the `service domain` command. Versionless `domainv1` commands have been moved to the `domain` command ([#1615](https://github.com/fastly/cli/pull/1615)) ### Enhancements: - feat(rust): Allow testing with prerelease Rust versions ([#1604](https://github.com/fastly/cli/pull/1604)) diff --git a/pkg/app/run_test.go b/pkg/app/run_test.go index e951b37bc..b724d261f 100644 --- a/pkg/app/run_test.go +++ b/pkg/app/run_test.go @@ -73,7 +73,6 @@ dashboard dictionary dictionary-entry domain -domain-v1 healthcheck imageoptimizer install diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index 5dd59bd40..e6e704483 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -20,7 +20,6 @@ import ( "github.com/fastly/cli/pkg/commands/dictionary" "github.com/fastly/cli/pkg/commands/dictionaryentry" "github.com/fastly/cli/pkg/commands/domain" - "github.com/fastly/cli/pkg/commands/domainv1" "github.com/fastly/cli/pkg/commands/healthcheck" "github.com/fastly/cli/pkg/commands/imageoptimizerdefaults" "github.com/fastly/cli/pkg/commands/install" @@ -94,6 +93,7 @@ import ( "github.com/fastly/cli/pkg/commands/secretstore" "github.com/fastly/cli/pkg/commands/secretstoreentry" "github.com/fastly/cli/pkg/commands/service" + servicedomain "github.com/fastly/cli/pkg/commands/service/domain" servicepurge "github.com/fastly/cli/pkg/commands/service/purge" "github.com/fastly/cli/pkg/commands/serviceauth" "github.com/fastly/cli/pkg/commands/serviceversion" @@ -228,13 +228,6 @@ func Define( // nolint:revive // function-length domainDescribe := domain.NewDescribeCommand(domainCmdRoot.CmdClause, data) domainList := domain.NewListCommand(domainCmdRoot.CmdClause, data) domainUpdate := domain.NewUpdateCommand(domainCmdRoot.CmdClause, data) - domainValidate := domain.NewValidateCommand(domainCmdRoot.CmdClause, data) - domainv1CmdRoot := domainv1.NewRootCommand(app, data) - domainv1Create := domainv1.NewCreateCommand(domainv1CmdRoot.CmdClause, data) - domainv1Delete := domainv1.NewDeleteCommand(domainv1CmdRoot.CmdClause, data) - domainv1Describe := domainv1.NewDescribeCommand(domainv1CmdRoot.CmdClause, data) - domainv1List := domainv1.NewListCommand(domainv1CmdRoot.CmdClause, data) - domainv1Update := domainv1.NewUpdateCommand(domainv1CmdRoot.CmdClause, data) healthcheckCmdRoot := healthcheck.NewRootCommand(app, data) healthcheckCreate := healthcheck.NewCreateCommand(healthcheckCmdRoot.CmdClause, data) healthcheckDelete := healthcheck.NewDeleteCommand(healthcheckCmdRoot.CmdClause, data) @@ -639,6 +632,13 @@ func Define( // nolint:revive // function-length serviceVersionStage := serviceversion.NewStageCommand(serviceVersionCmdRoot.CmdClause, data) serviceVersionUnstage := serviceversion.NewUnstageCommand(serviceVersionCmdRoot.CmdClause, data) serviceVersionUpdate := serviceversion.NewUpdateCommand(serviceVersionCmdRoot.CmdClause, data) + servicedomainCmdRoot := servicedomain.NewRootCommand(serviceCmdRoot.CmdClause, data) + servicedomainCreate := servicedomain.NewCreateCommand(servicedomainCmdRoot.CmdClause, data) + servicedomainDelete := servicedomain.NewDeleteCommand(servicedomainCmdRoot.CmdClause, data) + servicedomainDescribe := servicedomain.NewDescribeCommand(servicedomainCmdRoot.CmdClause, data) + servicedomainList := servicedomain.NewListCommand(servicedomainCmdRoot.CmdClause, data) + servicedomainUpdate := servicedomain.NewUpdateCommand(servicedomainCmdRoot.CmdClause, data) + servicedomainValidate := servicedomain.NewValidateCommand(servicedomainCmdRoot.CmdClause, data) statsCmdRoot := stats.NewRootCommand(app, data) statsHistorical := stats.NewHistoricalCommand(statsCmdRoot.CmdClause, data) statsRealtime := stats.NewRealtimeCommand(statsCmdRoot.CmdClause, data) @@ -809,13 +809,6 @@ func Define( // nolint:revive // function-length domainDescribe, domainList, domainUpdate, - domainValidate, - domainv1CmdRoot, - domainv1Create, - domainv1Delete, - domainv1Describe, - domainv1List, - domainv1Update, healthcheckCmdRoot, healthcheckCreate, healthcheckDelete, @@ -1205,6 +1198,13 @@ func Define( // nolint:revive // function-length serviceauthDescribe, serviceauthList, serviceauthUpdate, + servicedomainCmdRoot, + servicedomainCreate, + servicedomainDelete, + servicedomainDescribe, + servicedomainList, + servicedomainUpdate, + servicedomainValidate, serviceVersionActivate, serviceVersionClone, serviceVersionCmdRoot, diff --git a/pkg/commands/domainv1/common.go b/pkg/commands/domain/common.go similarity index 98% rename from pkg/commands/domainv1/common.go rename to pkg/commands/domain/common.go index e4b0289a2..837ed1607 100644 --- a/pkg/commands/domainv1/common.go +++ b/pkg/commands/domain/common.go @@ -1,4 +1,4 @@ -package domainv1 +package domain import ( "fmt" diff --git a/pkg/commands/domain/create.go b/pkg/commands/domain/create.go index 8bafd3400..437fea761 100644 --- a/pkg/commands/domain/create.go +++ b/pkg/commands/domain/create.go @@ -2,14 +2,14 @@ package domain import ( "context" + "errors" + "fmt" "io" "github.com/fastly/go-fastly/v12/fastly" - - "4d63.com/optional" + "github.com/fastly/go-fastly/v12/fastly/domainmanagement/v1/domains" "github.com/fastly/cli/pkg/argparser" - "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" "github.com/fastly/cli/pkg/text" ) @@ -19,13 +19,11 @@ type CreateCommand struct { argparser.Base // Required. - serviceVersion argparser.OptionalServiceVersion + fqdn string + serviceID string // Optional. - autoClone argparser.OptionalAutoClone - comment argparser.OptionalString - name argparser.OptionalString - serviceName argparser.OptionalServiceNameID + description argparser.OptionalString } // NewCreateCommand returns a usable command registered under the parent. @@ -35,77 +33,52 @@ func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateComman Globals: g, }, } - c.CmdClause = parent.Command("create", "Create a domain on a Fastly service version").Alias("add") - - // Required. - c.RegisterFlag(argparser.StringFlagOpts{ - Name: argparser.FlagVersionName, - Description: argparser.FlagVersionDesc, - Dst: &c.serviceVersion.Value, - Required: true, - }) + c.CmdClause = parent.Command("create", "Create a domain").Alias("add") // Optional. - c.RegisterAutoCloneFlag(argparser.AutoCloneFlagOpts{ - Action: c.autoClone.Set, - Dst: &c.autoClone.Value, - }) - c.CmdClause.Flag("comment", "A descriptive note").Action(c.comment.Set).StringVar(&c.comment.Value) - c.CmdClause.Flag("name", "Domain name").Short('n').Action(c.name.Set).StringVar(&c.name.Value) + c.CmdClause.Flag("description", "The description for the domain").Action(c.description.Set).StringVar(&c.description.Value) + c.CmdClause.Flag("fqdn", "The fully qualified domain name").Required().StringVar(&c.fqdn) c.RegisterFlag(argparser.StringFlagOpts{ Name: argparser.FlagServiceIDName, - Description: argparser.FlagServiceIDDesc, - Dst: &g.Manifest.Flag.ServiceID, + Description: "The service_id associated with your domain", + Dst: &c.serviceID, Short: 's', }) - c.RegisterFlag(argparser.StringFlagOpts{ - Action: c.serviceName.Set, - Name: argparser.FlagServiceName, - Description: argparser.FlagServiceNameDesc, - 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 := argparser.ServiceDetails(argparser.ServiceDetailsOpts{ - Active: optional.Of(false), - Locked: optional.Of(false), - AutoCloneFlag: c.autoClone, - APIClient: c.Globals.APIClient, - Manifest: *c.Globals.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 + input := &domains.CreateInput{ + FQDN: &c.fqdn, } - input := fastly.CreateDomainInput{ - ServiceID: serviceID, - ServiceVersion: fastly.ToValue(serviceVersion.Number), + if c.serviceID != "" { + input.ServiceID = &c.serviceID } - if c.name.WasSet { - input.Name = &c.name.Value + + if c.description.WasSet { + input.Description = &c.description.Value } - if c.comment.WasSet { - input.Comment = &c.comment.Value + + fc, ok := c.Globals.APIClient.(*fastly.Client) + if !ok { + return errors.New("failed to convert interface to a fastly client") } - d, err := c.Globals.APIClient.CreateDomain(context.TODO(), &input) + + d, err := domains.Create(context.TODO(), fc, input) if err != nil { c.Globals.ErrLog.AddWithContext(err, map[string]any{ - "Service ID": serviceID, - "Service Version": fastly.ToValue(serviceVersion.Number), + "FQDN": c.fqdn, + "Service ID": c.serviceID, }) return err } - text.Success(out, "Created domain %s (service %s version %d)", fastly.ToValue(d.Name), fastly.ToValue(d.ServiceID), fastly.ToValue(d.ServiceVersion)) + serviceOutput := "" + if d.ServiceID != nil { + serviceOutput = fmt.Sprintf(", service-id: %s", *d.ServiceID) + } + + text.Success(out, "Created domain '%s' (domain-id: %s%s)", d.FQDN, d.DomainID, serviceOutput) return nil } diff --git a/pkg/commands/domain/delete.go b/pkg/commands/domain/delete.go index 155eaa3bb..d85c6a1e9 100644 --- a/pkg/commands/domain/delete.go +++ b/pkg/commands/domain/delete.go @@ -2,14 +2,13 @@ package domain import ( "context" + "errors" "io" "github.com/fastly/go-fastly/v12/fastly" - - "4d63.com/optional" + "github.com/fastly/go-fastly/v12/fastly/domainmanagement/v1/domains" "github.com/fastly/cli/pkg/argparser" - "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" "github.com/fastly/cli/pkg/text" ) @@ -17,10 +16,7 @@ import ( // DeleteCommand calls the Fastly API to delete domains. type DeleteCommand struct { argparser.Base - Input fastly.DeleteDomainInput - serviceName argparser.OptionalServiceNameID - serviceVersion argparser.OptionalServiceVersion - autoClone argparser.OptionalAutoClone + domainID string } // NewDeleteCommand returns a usable command registered under the parent. @@ -30,69 +26,33 @@ func NewDeleteCommand(parent argparser.Registerer, g *global.Data) *DeleteComman Globals: g, }, } - c.CmdClause = parent.Command("delete", "Delete a domain on a Fastly service version").Alias("remove") + c.CmdClause = parent.Command("delete", "Delete a domain").Alias("remove") // Required. - c.CmdClause.Flag("name", "Domain name").Short('n').Required().StringVar(&c.Input.Name) - c.RegisterFlag(argparser.StringFlagOpts{ - Name: argparser.FlagVersionName, - Description: argparser.FlagVersionDesc, - Dst: &c.serviceVersion.Value, - Required: true, - }) + c.CmdClause.Flag("domain-id", "The Domain Identifier (UUID)").Required().StringVar(&c.domainID) - // Optional. - c.RegisterAutoCloneFlag(argparser.AutoCloneFlagOpts{ - Action: c.autoClone.Set, - Dst: &c.autoClone.Value, - }) - c.RegisterFlag(argparser.StringFlagOpts{ - Name: argparser.FlagServiceIDName, - Description: argparser.FlagServiceIDDesc, - Dst: &g.Manifest.Flag.ServiceID, - Short: 's', - }) - c.RegisterFlag(argparser.StringFlagOpts{ - Action: c.serviceName.Set, - Name: argparser.FlagServiceName, - Description: argparser.FlagServiceNameDesc, - 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 := argparser.ServiceDetails(argparser.ServiceDetailsOpts{ - Active: optional.Of(false), - Locked: optional.Of(false), - AutoCloneFlag: c.autoClone, - APIClient: c.Globals.APIClient, - Manifest: *c.Globals.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 + fc, ok := c.Globals.APIClient.(*fastly.Client) + if !ok { + return errors.New("failed to convert interface to a fastly client") } - c.Input.ServiceID = serviceID - c.Input.ServiceVersion = fastly.ToValue(serviceVersion.Number) + input := &domains.DeleteInput{ + DomainID: &c.domainID, + } - if err := c.Globals.APIClient.DeleteDomain(context.TODO(), &c.Input); err != nil { + err := domains.Delete(context.TODO(), fc, input) + if err != nil { c.Globals.ErrLog.AddWithContext(err, map[string]any{ - "Service ID": serviceID, - "Service Version": fastly.ToValue(serviceVersion.Number), + "Domain ID": c.domainID, }) return err } - text.Success(out, "Deleted domain %s (service %s version %d)", c.Input.Name, c.Input.ServiceID, c.Input.ServiceVersion) + text.Success(out, "Deleted domain (domain-id: %s)", c.domainID) return nil } diff --git a/pkg/commands/domain/describe.go b/pkg/commands/domain/describe.go index e896582ab..d569c7300 100644 --- a/pkg/commands/domain/describe.go +++ b/pkg/commands/domain/describe.go @@ -2,10 +2,11 @@ package domain import ( "context" - "fmt" + "errors" "io" "github.com/fastly/go-fastly/v12/fastly" + "github.com/fastly/go-fastly/v12/fastly/domainmanagement/v1/domains" "github.com/fastly/cli/pkg/argparser" fsterr "github.com/fastly/cli/pkg/errors" @@ -16,10 +17,7 @@ import ( type DescribeCommand struct { argparser.Base argparser.JSONOutput - - Input fastly.GetDomainInput - serviceName argparser.OptionalServiceNameID - serviceVersion argparser.OptionalServiceVersion + domainID string } // NewDescribeCommand returns a usable command registered under the parent. @@ -29,31 +27,13 @@ func NewDescribeCommand(parent argparser.Registerer, g *global.Data) *DescribeCo Globals: g, }, } - c.CmdClause = parent.Command("describe", "Show detailed information about a domain on a Fastly service version").Alias("get") + c.CmdClause = parent.Command("describe", "Show detailed information about a domain").Alias("get") // Required. - c.CmdClause.Flag("name", "Name of domain").Short('n').Required().StringVar(&c.Input.Name) - c.RegisterFlag(argparser.StringFlagOpts{ - Name: argparser.FlagVersionName, - Description: argparser.FlagVersionDesc, - Dst: &c.serviceVersion.Value, - Required: true, - }) + c.CmdClause.Flag("domain-id", "The Domain Identifier (UUID)").Required().StringVar(&c.domainID) // Optional. c.RegisterFlagBool(c.JSONFlag()) // --json - c.RegisterFlag(argparser.StringFlagOpts{ - Name: argparser.FlagServiceIDName, - Description: argparser.FlagServiceIDDesc, - Dst: &g.Manifest.Flag.ServiceID, - Short: 's', - }) - c.RegisterFlag(argparser.StringFlagOpts{ - Action: c.serviceName.Set, - Name: argparser.FlagServiceName, - Description: argparser.FlagServiceNameDesc, - Dst: &c.serviceName.Value, - }) return &c } @@ -63,44 +43,34 @@ func (c *DescribeCommand) Exec(_ io.Reader, out io.Writer) error { return fsterr.ErrInvalidVerboseJSONCombo } - serviceID, serviceVersion, err := argparser.ServiceDetails(argparser.ServiceDetailsOpts{ - APIClient: c.Globals.APIClient, - Manifest: *c.Globals.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": fsterr.ServiceVersion(serviceVersion), - }) - return err + fc, ok := c.Globals.APIClient.(*fastly.Client) + if !ok { + return errors.New("failed to convert interface to a fastly client") } - c.Input.ServiceID = serviceID - c.Input.ServiceVersion = fastly.ToValue(serviceVersion.Number) + input := &domains.GetInput{ + DomainID: &c.domainID, + } - o, err := c.Globals.APIClient.GetDomain(context.TODO(), &c.Input) + d, err := domains.Get(context.TODO(), fc, input) if err != nil { c.Globals.ErrLog.AddWithContext(err, map[string]any{ - "Service ID": serviceID, - "Service Version": fastly.ToValue(serviceVersion.Number), + "Domain ID": c.domainID, }) return err } - if ok, err := c.WriteJSON(out, o); ok { + if ok, err := c.WriteJSON(out, d); ok { return err } - if !c.Globals.Verbose() { - fmt.Fprintf(out, "\nService ID: %s\n", fastly.ToValue(o.ServiceID)) + if d != nil { + cl := []domains.Data{*d} + if c.Globals.Verbose() { + printVerbose(out, cl) + } else { + printSummary(out, cl) + } } - fmt.Fprintf(out, "Version: %d\n", fastly.ToValue(o.ServiceVersion)) - fmt.Fprintf(out, "Name: %s\n", fastly.ToValue(o.Name)) - fmt.Fprintf(out, "Comment: %v\n", fastly.ToValue(o.Comment)) - return nil } diff --git a/pkg/commands/domain/doc.go b/pkg/commands/domain/doc.go index d15528c22..826671756 100644 --- a/pkg/commands/domain/doc.go +++ b/pkg/commands/domain/doc.go @@ -1,2 +1,2 @@ -// Package domain contains commands to inspect and manipulate Fastly service domains. +// Package domainv1 contains commands to inspect and manipulate Fastly domains. package domain diff --git a/pkg/commands/domain/domain_test.go b/pkg/commands/domain/domain_test.go index e63ec49a1..d4dc284fd 100644 --- a/pkg/commands/domain/domain_test.go +++ b/pkg/commands/domain/domain_test.go @@ -1,416 +1,290 @@ package domain_test import ( - "context" - "errors" + "bytes" "fmt" + "io" + "net/http" "strings" "testing" - "github.com/fastly/go-fastly/v12/fastly" + "github.com/fastly/go-fastly/v12/fastly/domainmanagement/v1/domains" root "github.com/fastly/cli/pkg/commands/domain" - "github.com/fastly/cli/pkg/mock" "github.com/fastly/cli/pkg/testutil" ) func TestDomainCreate(t *testing.T) { + fqdn := "www.example.com" + sid := "123" + did := "domain-id" + scenarios := []testutil.CLIScenario{ { - Args: "--version 1", - WantError: "error reading service: no service ID found", + Args: "", + WantError: "error parsing arguments: required flag --fqdn not provided", }, { - Args: "--service-id 123 --version 1 --name www.test.com --autoclone", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - CloneVersionFn: testutil.CloneVersionResult(4), - CreateDomainFn: createDomainOK, + Args: fmt.Sprintf("--fqdn %s --service-id %s", fqdn, sid), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusOK, + Status: http.StatusText(http.StatusOK), + Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(domains.Data{ + DomainID: did, + FQDN: fqdn, + ServiceID: &sid, + }))), + }, + }, }, - WantOutput: "Created domain www.test.com (service 123 version 4)", + WantOutput: fmt.Sprintf("SUCCESS: Created domain '%s' (domain-id: %s, service-id: %s)", fqdn, did, sid), }, { - Args: "--service-id 123 --version 1 --name www.test.com --autoclone", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - CloneVersionFn: testutil.CloneVersionResult(4), - CreateDomainFn: createDomainError, + Args: fmt.Sprintf("--fqdn %s", fqdn), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusOK, + Status: http.StatusText(http.StatusOK), + Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(domains.Data{ + DomainID: did, + FQDN: fqdn, + }))), + }, + }, + }, + WantOutput: fmt.Sprintf("SUCCESS: Created domain '%s' (domain-id: %s)", fqdn, did), + }, + { + Args: fmt.Sprintf("--fqdn %s", fqdn), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusBadRequest, + Status: http.StatusText(http.StatusBadRequest), + Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(` + { + "errors":[ + { + "title":"Invalid value for fqdn", + "detail":"fqdn has already been taken" + } + ] + } + `))), + }, + }, }, - WantError: errTest.Error(), + WantError: "400 - Bad Request", }, } testutil.RunCLIScenarios(t, []string{root.CommandName, "create"}, scenarios) } func TestDomainList(t *testing.T) { - scenarios := []testutil.CLIScenario{ - { - Args: "--service-id 123 --version 1", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - ListDomainsFn: listDomainsOK, - }, - WantOutput: listDomainsShortOutput, - }, - { - Args: "--service-id 123 --version 1 --verbose", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - ListDomainsFn: listDomainsOK, - }, - WantOutput: listDomainsVerboseOutput, - }, - { - Args: "--service-id 123 --version 1 -v", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - ListDomainsFn: listDomainsOK, + fqdn := "www.example.com" + sid := "123" + did := "domain-id" + description := "domain description" + + resp := testutil.GenJSON(domains.Collection{ + Data: []domains.Data{ + { + DomainID: did, + FQDN: fqdn, + ServiceID: &sid, + Description: description, }, - WantOutput: listDomainsVerboseOutput, }, + }) + + scenarios := []testutil.CLIScenario{ { - Args: "--verbose --service-id 123 --version 1", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - ListDomainsFn: listDomainsOK, - }, - WantOutput: listDomainsVerboseOutput, + Args: "--verbose --json", + WantError: "invalid flag combination, --verbose and --json", }, { - Args: "-v --service-id 123 --version 1", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - ListDomainsFn: listDomainsOK, + Args: "--json", + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusOK, + Status: http.StatusText(http.StatusOK), + Body: io.NopCloser(bytes.NewReader(resp)), + }, + }, }, - WantOutput: listDomainsVerboseOutput, + WantOutput: string(resp), }, { - Args: "--service-id 123 --version 1", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - ListDomainsFn: listDomainsError, + Args: "", + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusBadRequest, + Status: http.StatusText(http.StatusBadRequest), + Body: io.NopCloser(strings.NewReader(`{"error": "whoops"}`)), + }, + }, }, - WantError: errTest.Error(), + WantError: "400 - Bad Request", }, } testutil.RunCLIScenarios(t, []string{root.CommandName, "list"}, scenarios) } func TestDomainDescribe(t *testing.T) { + fqdn := "www.example.com" + sid := "123" + did := "domain-id" + description := "domain description" + + resp := testutil.GenJSON(domains.Data{ + DomainID: did, + FQDN: fqdn, + ServiceID: &sid, + Description: description, + }) + scenarios := []testutil.CLIScenario{ { - Args: "--service-id 123 --version 1", - WantError: "error parsing arguments: required flag --name not provided", + Args: "", + WantError: "error parsing arguments: required flag --domain-id not provided", }, { - Args: "--service-id 123 --version 1 --name www.test.com", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - GetDomainFn: getDomainError, + Args: fmt.Sprintf("--domain-id %s --json", did), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusOK, + Status: http.StatusText(http.StatusOK), + Body: io.NopCloser(bytes.NewReader(resp)), + }, + }, }, - WantError: errTest.Error(), + WantOutput: string(resp), }, { - Args: "--service-id 123 --version 1 --name www.test.com", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - GetDomainFn: getDomainOK, + Args: fmt.Sprintf("--domain-id %s --json", did), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusBadRequest, + Status: http.StatusText(http.StatusBadRequest), + Body: io.NopCloser(strings.NewReader(`{"error": "whoops"}`)), + }, + }, }, - WantOutput: describeDomainOutput, + WantError: "400 - Bad Request", }, } testutil.RunCLIScenarios(t, []string{root.CommandName, "describe"}, scenarios) } func TestDomainUpdate(t *testing.T) { + fqdn := "www.example.com" + sid := "123" + did := "domain-id" + scenarios := []testutil.CLIScenario{ { - Args: "--service-id 123 --version 1 --new-name www.test.com --comment ", - WantError: "error parsing arguments: required flag --name not provided", + Args: "", + WantError: "error parsing arguments: required flag --domain-id not provided", }, { - Args: "--service-id 123 --version 1 --name www.test.com --autoclone", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - CloneVersionFn: testutil.CloneVersionResult(4), - UpdateDomainFn: updateDomainOK, + Args: fmt.Sprintf("--domain-id %s --service-id %s", did, sid), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusOK, + Status: http.StatusText(http.StatusOK), + Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(domains.Data{ + DomainID: did, + FQDN: fqdn, + ServiceID: &sid, + }))), + }, + }, }, - WantError: "error parsing arguments: must provide either --new-name or --comment to update domain", + WantOutput: fmt.Sprintf("SUCCESS: Updated domain '%s' (domain-id: %s, service-id: %s)", fqdn, did, sid), }, { - Args: "--service-id 123 --version 1 --name www.test.com --new-name www.example.com --autoclone", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - CloneVersionFn: testutil.CloneVersionResult(4), - UpdateDomainFn: updateDomainError, + Args: fmt.Sprintf("--domain-id %s", did), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusOK, + Status: http.StatusText(http.StatusOK), + Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(domains.Data{ + DomainID: did, + FQDN: fqdn, + }))), + }, + }, }, - WantError: errTest.Error(), + WantOutput: fmt.Sprintf("SUCCESS: Updated domain '%s' (domain-id: %s)", fqdn, did), }, { - Args: "--service-id 123 --version 1 --name www.test.com --new-name www.example.com --autoclone", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - CloneVersionFn: testutil.CloneVersionResult(4), - UpdateDomainFn: updateDomainOK, + Args: fmt.Sprintf("--domain-id %s", did), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusBadRequest, + Status: http.StatusText(http.StatusBadRequest), + Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(` + { + "errors":[ + { + "title":"Invalid value for domain-id", + "detail":"whoops" + } + ] + } + `))), + }, + }, }, - WantOutput: "Updated domain www.example.com (service 123 version 4)", + WantError: "400 - Bad Request", }, } testutil.RunCLIScenarios(t, []string{root.CommandName, "update"}, scenarios) } func TestDomainDelete(t *testing.T) { - scenarios := []testutil.CLIScenario{ - { - Args: "--service-id 123 --version 1", - WantError: "error parsing arguments: required flag --name not provided", - }, - { - Args: "--service-id 123 --version 1 --name www.test.com --autoclone", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - CloneVersionFn: testutil.CloneVersionResult(4), - DeleteDomainFn: deleteDomainError, - }, - WantError: errTest.Error(), - }, - { - Args: "--service-id 123 --version 1 --name www.test.com --autoclone", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - CloneVersionFn: testutil.CloneVersionResult(4), - DeleteDomainFn: deleteDomainOK, - }, - WantOutput: "Deleted domain www.test.com (service 123 version 4)", - }, - } - testutil.RunCLIScenarios(t, []string{root.CommandName, "delete"}, scenarios) -} + did := "domain-id" -func TestDomainValidate(t *testing.T) { scenarios := []testutil.CLIScenario{ { - Name: "validate missing --version flag", - WantError: "error parsing arguments: required flag --version not provided", + Args: "", + WantError: "error parsing arguments: required flag --domain-id not provided", }, { - Name: "validate missing --service-id flag", - Args: "--version 3", - WantError: "error reading service: no service ID found", - }, - { - Name: "validate missing --name flag", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - }, - Args: "--service-id 123 --version 3", - WantError: "error parsing arguments: must provide --name flag", - }, - { - Name: "validate ValidateDomain API error", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - ValidateDomainFn: func(_ context.Context, _ *fastly.ValidateDomainInput) (*fastly.DomainValidationResult, error) { - return nil, testutil.Err + Args: fmt.Sprintf("--domain-id %s", did), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusNoContent, + Status: http.StatusText(http.StatusNoContent), + }, }, }, - Args: "--name foo.example.com --service-id 123 --version 3", - WantError: testutil.Err.Error(), + WantOutput: fmt.Sprintf("SUCCESS: Deleted domain (domain-id: %s)", did), }, { - Name: "validate ValidateAllDomains API error", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - ValidateAllDomainsFn: func(_ context.Context, _ *fastly.ValidateAllDomainsInput) ([]*fastly.DomainValidationResult, error) { - return nil, testutil.Err + Args: fmt.Sprintf("--domain-id %s", did), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusBadRequest, + Status: http.StatusText(http.StatusBadRequest), + Body: io.NopCloser(strings.NewReader(`{"error": "whoops"}`)), + }, }, }, - Args: "--all --service-id 123 --version 3", - WantError: testutil.Err.Error(), - }, - { - Name: "validate ValidateDomain API success", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - ValidateDomainFn: validateDomain, - }, - Args: "--name foo.example.com --service-id 123 --version 3", - WantOutput: validateAPISuccess(3), - }, - { - Name: "validate ValidateAllDomains API success", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - ValidateAllDomainsFn: validateAllDomains, - }, - Args: "--all --service-id 123 --version 3", - WantOutput: validateAllAPISuccess(), - }, - { - Name: "validate missing --autoclone flag is OK", - API: mock.API{ - ListVersionsFn: testutil.ListVersions, - ValidateDomainFn: validateDomain, - }, - Args: "--name foo.example.com --service-id 123 --version 1", - WantOutput: validateAPISuccess(1), + WantError: "400 - Bad Request", }, } - - testutil.RunCLIScenarios(t, []string{root.CommandName, "validate"}, scenarios) -} - -var errTest = errors.New("fixture error") - -func createDomainOK(_ context.Context, i *fastly.CreateDomainInput) (*fastly.Domain, error) { - return &fastly.Domain{ - ServiceID: fastly.ToPointer(i.ServiceID), - ServiceVersion: fastly.ToPointer(i.ServiceVersion), - Name: i.Name, - }, nil -} - -func createDomainError(_ context.Context, _ *fastly.CreateDomainInput) (*fastly.Domain, error) { - return nil, errTest -} - -func listDomainsOK(_ context.Context, i *fastly.ListDomainsInput) ([]*fastly.Domain, error) { - return []*fastly.Domain{ - { - ServiceID: fastly.ToPointer(i.ServiceID), - ServiceVersion: fastly.ToPointer(i.ServiceVersion), - Name: fastly.ToPointer("www.test.com"), - Comment: fastly.ToPointer("test"), - }, - { - ServiceID: fastly.ToPointer(i.ServiceID), - ServiceVersion: fastly.ToPointer(i.ServiceVersion), - Name: fastly.ToPointer("www.example.com"), - Comment: fastly.ToPointer("example"), - }, - }, nil -} - -func listDomainsError(_ context.Context, _ *fastly.ListDomainsInput) ([]*fastly.Domain, error) { - return nil, errTest -} - -var listDomainsShortOutput = strings.TrimSpace(` -SERVICE VERSION NAME COMMENT -123 1 www.test.com test -123 1 www.example.com example -`) + "\n" - -var listDomainsVerboseOutput = strings.TrimSpace(` -Fastly API endpoint: https://api.fastly.com -Fastly API token provided via config file (profile: user) - -Service ID (via --service-id): 123 - -Version: 1 - Domain 1/2 - Name: www.test.com - Comment: test - Domain 2/2 - Name: www.example.com - Comment: example -`) + "\n\n" - -func getDomainOK(_ context.Context, i *fastly.GetDomainInput) (*fastly.Domain, error) { - return &fastly.Domain{ - ServiceID: fastly.ToPointer(i.ServiceID), - ServiceVersion: fastly.ToPointer(i.ServiceVersion), - Name: fastly.ToPointer(i.Name), - Comment: fastly.ToPointer("test"), - }, nil -} - -func getDomainError(_ context.Context, _ *fastly.GetDomainInput) (*fastly.Domain, error) { - return nil, errTest -} - -var describeDomainOutput = "\n" + strings.TrimSpace(` -Service ID: 123 -Version: 1 -Name: www.test.com -Comment: test -`) + "\n" - -func updateDomainOK(_ context.Context, i *fastly.UpdateDomainInput) (*fastly.Domain, error) { - return &fastly.Domain{ - ServiceID: fastly.ToPointer(i.ServiceID), - ServiceVersion: fastly.ToPointer(i.ServiceVersion), - Name: i.NewName, - }, nil -} - -func updateDomainError(_ context.Context, _ *fastly.UpdateDomainInput) (*fastly.Domain, error) { - return nil, errTest -} - -func deleteDomainOK(_ context.Context, _ *fastly.DeleteDomainInput) error { - return nil -} - -func deleteDomainError(_ context.Context, _ *fastly.DeleteDomainInput) error { - return errTest -} - -func validateDomain(_ context.Context, i *fastly.ValidateDomainInput) (*fastly.DomainValidationResult, error) { - return &fastly.DomainValidationResult{ - Metadata: &fastly.DomainMetadata{ - ServiceID: fastly.ToPointer(i.ServiceID), - ServiceVersion: fastly.ToPointer(i.ServiceVersion), - Name: fastly.ToPointer(i.Name), - }, - CName: fastly.ToPointer("foo"), - Valid: fastly.ToPointer(true), - }, nil -} - -func validateAllDomains(_ context.Context, i *fastly.ValidateAllDomainsInput) ([]*fastly.DomainValidationResult, error) { - return []*fastly.DomainValidationResult{ - { - Metadata: &fastly.DomainMetadata{ - ServiceID: fastly.ToPointer(i.ServiceID), - ServiceVersion: fastly.ToPointer(i.ServiceVersion), - Name: fastly.ToPointer("foo.example.com"), - }, - CName: fastly.ToPointer("foo"), - Valid: fastly.ToPointer(true), - }, - { - Metadata: &fastly.DomainMetadata{ - ServiceID: fastly.ToPointer(i.ServiceID), - ServiceVersion: fastly.ToPointer(i.ServiceVersion), - Name: fastly.ToPointer("bar.example.com"), - }, - CName: fastly.ToPointer("bar"), - Valid: fastly.ToPointer(true), - }, - }, nil -} - -func validateAPISuccess(version int) string { - return fmt.Sprintf(` -Service ID: 123 -Service Version: %d - -Name: foo.example.com -Valid: true -CNAME: foo`, version) -} - -func validateAllAPISuccess() string { - return ` -Service ID: 123 -Service Version: 3 - -Name: foo.example.com -Valid: true -CNAME: foo - -Name: bar.example.com -Valid: true -CNAME: bar` + testutil.RunCLIScenarios(t, []string{root.CommandName, "delete"}, scenarios) } diff --git a/pkg/commands/domain/list.go b/pkg/commands/domain/list.go index 82f5d5ff4..09fc73ece 100644 --- a/pkg/commands/domain/list.go +++ b/pkg/commands/domain/list.go @@ -2,10 +2,11 @@ package domain import ( "context" - "fmt" + "errors" "io" "github.com/fastly/go-fastly/v12/fastly" + "github.com/fastly/go-fastly/v12/fastly/domainmanagement/v1/domains" "github.com/fastly/cli/pkg/argparser" fsterr "github.com/fastly/cli/pkg/errors" @@ -18,9 +19,11 @@ type ListCommand struct { argparser.Base argparser.JSONOutput - Input fastly.ListDomainsInput - serviceName argparser.OptionalServiceNameID - serviceVersion argparser.OptionalServiceVersion + cursor argparser.OptionalString + fqdn argparser.OptionalString + limit argparser.OptionalInt + serviceID argparser.OptionalString + sort argparser.OptionalString } // NewListCommand returns a usable command registered under the parent. @@ -30,93 +33,91 @@ func NewListCommand(parent argparser.Registerer, g *global.Data) *ListCommand { Globals: g, }, } - c.CmdClause = parent.Command("list", "List domains on a Fastly service version") - - // Required. - c.RegisterFlag(argparser.StringFlagOpts{ - Name: argparser.FlagVersionName, - Description: argparser.FlagVersionDesc, - Dst: &c.serviceVersion.Value, - Required: true, - }) + c.CmdClause = parent.Command("list", "List domains") // Optional. + c.CmdClause.Flag("cursor", "Cursor value from the next_cursor field of a previous response, used to retrieve the next page").Action(c.cursor.Set).StringVar(&c.cursor.Value) + c.CmdClause.Flag("fqdn", "Filters results by the FQDN using a fuzzy/partial match").Action(c.fqdn.Set).StringVar(&c.fqdn.Value) c.RegisterFlagBool(c.JSONFlag()) // --json + c.CmdClause.Flag("limit", "Limit how many results are returned").Action(c.limit.Set).IntVar(&c.limit.Value) c.RegisterFlag(argparser.StringFlagOpts{ + Action: c.serviceID.Set, Name: argparser.FlagServiceIDName, - Description: argparser.FlagServiceIDDesc, - Dst: &g.Manifest.Flag.ServiceID, + Description: "Filter results based on a service_id", + Dst: &c.serviceID.Value, Short: 's', }) - c.RegisterFlag(argparser.StringFlagOpts{ - Action: c.serviceName.Set, - Name: argparser.FlagServiceName, - Description: argparser.FlagServiceNameDesc, - Dst: &c.serviceName.Value, - }) + c.CmdClause.Flag("sort", "The order in which to list the results").Action(c.sort.Set).StringVar(&c.sort.Value) return &c } // Exec invokes the application logic for the command. -func (c *ListCommand) Exec(_ io.Reader, out io.Writer) error { +func (c *ListCommand) Exec(in 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, - 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": fsterr.ServiceVersion(serviceVersion), - }) - return err - } - - c.Input.ServiceID = serviceID - c.Input.ServiceVersion = fastly.ToValue(serviceVersion.Number) + input := &domains.ListInput{} - o, err := c.Globals.APIClient.ListDomains(context.TODO(), &c.Input) - if err != nil { - c.Globals.ErrLog.AddWithContext(err, map[string]any{ - "Service ID": serviceID, - "Service Version": fastly.ToValue(serviceVersion.Number), - }) - return err + if c.serviceID.WasSet { + input.ServiceID = &c.serviceID.Value + } + if c.cursor.WasSet { + input.Cursor = &c.cursor.Value + } + if c.fqdn.WasSet { + input.FQDN = &c.fqdn.Value + } + if c.limit.WasSet { + input.Limit = &c.limit.Value + } + if c.sort.WasSet { + input.Sort = &c.sort.Value } - if ok, err := c.WriteJSON(out, o); ok { - return err + fc, ok := c.Globals.APIClient.(*fastly.Client) + if !ok { + return errors.New("failed to convert interface to a fastly client") } - if !c.Globals.Verbose() { - tw := text.NewTable(out) - tw.AddHeader("SERVICE", "VERSION", "NAME", "COMMENT") - for _, domain := range o { - tw.AddLine( - fastly.ToValue(domain.ServiceID), - fastly.ToValue(domain.ServiceVersion), - fastly.ToValue(domain.Name), - fastly.ToValue(domain.Comment), - ) + for { + cl, err := domains.List(context.TODO(), fc, input) + if err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]any{ + "Cursor": c.cursor.Value, + "FQDN": c.fqdn.Value, + "Limit": c.limit.Value, + "Service ID": c.serviceID.Value, + "Sort": c.sort.Value, + }) + return err } - tw.Print() - return nil - } - fmt.Fprintf(out, "Version: %d\n", c.Input.ServiceVersion) - for i, domain := range o { - fmt.Fprintf(out, "\tDomain %d/%d\n", i+1, len(o)) - fmt.Fprintf(out, "\t\tName: %s\n", fastly.ToValue(domain.Name)) - fmt.Fprintf(out, "\t\tComment: %v\n", fastly.ToValue(domain.Comment)) - } - fmt.Fprintln(out) + if ok, err := c.WriteJSON(out, cl); ok { + // No pagination prompt w/ JSON output. + return err + } - return nil + if c.Globals.Verbose() { + printVerbose(out, cl.Data) + } else { + printSummary(out, cl.Data) + } + + if cl != nil && cl.Meta.NextCursor != "" { + // Check if 'out' is interactive before prompting. + if !c.Globals.Flags.NonInteractive && !c.Globals.Flags.AutoYes && text.IsTTY(out) { + printNext, err := text.AskYesNo(out, "Print next page [y/N]: ", in) + if err != nil { + return err + } + if printNext { + input.Cursor = &cl.Meta.NextCursor + continue + } + } + } + + return nil + } } diff --git a/pkg/commands/domain/root.go b/pkg/commands/domain/root.go index 560814c35..010393278 100644 --- a/pkg/commands/domain/root.go +++ b/pkg/commands/domain/root.go @@ -21,7 +21,7 @@ const CommandName = "domain" func NewRootCommand(parent argparser.Registerer, g *global.Data) *RootCommand { var c RootCommand c.Globals = g - c.CmdClause = parent.Command(CommandName, "Manipulate Fastly service version domains") + c.CmdClause = parent.Command(CommandName, "Manipulate Fastly domains") return &c } diff --git a/pkg/commands/domain/update.go b/pkg/commands/domain/update.go index 3612c6a04..d6977c110 100644 --- a/pkg/commands/domain/update.go +++ b/pkg/commands/domain/update.go @@ -2,15 +2,14 @@ package domain import ( "context" + "errors" "fmt" "io" "github.com/fastly/go-fastly/v12/fastly" - - "4d63.com/optional" + "github.com/fastly/go-fastly/v12/fastly/domainmanagement/v1/domains" "github.com/fastly/cli/pkg/argparser" - "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" "github.com/fastly/cli/pkg/text" ) @@ -18,13 +17,9 @@ import ( // UpdateCommand calls the Fastly API to update domains. type UpdateCommand struct { argparser.Base - input fastly.UpdateDomainInput - serviceName argparser.OptionalServiceNameID - serviceVersion argparser.OptionalServiceVersion - autoClone argparser.OptionalAutoClone - - NewName argparser.OptionalString - Comment argparser.OptionalString + domainID string + serviceID string + description argparser.OptionalString } // NewUpdateCommand returns a usable command registered under the parent. @@ -34,86 +29,50 @@ func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateComman Globals: g, }, } - c.CmdClause = parent.Command("update", "Update a domain on a Fastly service version") + c.CmdClause = parent.Command("update", "Update a domain") // Required. - c.CmdClause.Flag("name", "Domain name").Short('n').Required().StringVar(&c.input.Name) - c.RegisterFlag(argparser.StringFlagOpts{ - Name: argparser.FlagVersionName, - Description: argparser.FlagVersionDesc, - Dst: &c.serviceVersion.Value, - Required: true, - }) + c.CmdClause.Flag("domain-id", "The Domain Identifier (UUID)").Required().StringVar(&c.domainID) + + // Optional + c.CmdClause.Flag("description", "The description for the domain").Action(c.description.Set).StringVar(&c.description.Value) + c.CmdClause.Flag("service-id", "The service_id associated with your domain (omit to unset)").StringVar(&c.serviceID) - // Optional. - c.RegisterAutoCloneFlag(argparser.AutoCloneFlagOpts{ - Action: c.autoClone.Set, - Dst: &c.autoClone.Value, - }) - c.CmdClause.Flag("comment", "A descriptive note").Action(c.Comment.Set).StringVar(&c.Comment.Value) - c.CmdClause.Flag("new-name", "New domain name").Action(c.NewName.Set).StringVar(&c.NewName.Value) - c.RegisterFlag(argparser.StringFlagOpts{ - Name: argparser.FlagServiceIDName, - Description: argparser.FlagServiceIDDesc, - Dst: &g.Manifest.Flag.ServiceID, - Short: 's', - }) - c.RegisterFlag(argparser.StringFlagOpts{ - Action: c.serviceName.Set, - Name: argparser.FlagServiceName, - Description: argparser.FlagServiceNameDesc, - 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 := argparser.ServiceDetails(argparser.ServiceDetailsOpts{ - Active: optional.Of(false), - Locked: optional.Of(false), - AutoCloneFlag: c.autoClone, - APIClient: c.Globals.APIClient, - Manifest: *c.Globals.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 + input := &domains.UpdateInput{ + DomainID: &c.domainID, } - - c.input.ServiceID = serviceID - c.input.ServiceVersion = fastly.ToValue(serviceVersion.Number) - - // If neither arguments are provided, error with useful message. - if !c.NewName.WasSet && !c.Comment.WasSet { - return fmt.Errorf("error parsing arguments: must provide either --new-name or --comment to update domain") + if c.serviceID != "" { + input.ServiceID = &c.serviceID } - if c.NewName.WasSet { - c.input.NewName = &c.NewName.Value + if c.description.WasSet { + input.Description = &c.description.Value } - if c.Comment.WasSet { - c.input.Comment = &c.Comment.Value + + fc, ok := c.Globals.APIClient.(*fastly.Client) + if !ok { + return errors.New("failed to convert interface to a fastly client") } - d, err := c.Globals.APIClient.UpdateDomain(context.TODO(), &c.input) + d, err := domains.Update(context.TODO(), fc, input) if err != nil { c.Globals.ErrLog.AddWithContext(err, map[string]any{ - "Service ID": serviceID, - "Service Version": fastly.ToValue(serviceVersion.Number), - "New Name": c.NewName.Value, - "Comment": c.Comment.Value, + "Domain ID": c.domainID, + "Service ID": c.serviceID, }) return err } - text.Success(out, "Updated domain %s (service %s version %d)", fastly.ToValue(d.Name), fastly.ToValue(d.ServiceID), fastly.ToValue(d.ServiceVersion)) + serviceOutput := "" + if d.ServiceID != nil { + serviceOutput = fmt.Sprintf(", service-id: %s", *d.ServiceID) + } + + text.Success(out, "Updated domain '%s' (domain-id: %s%s)", d.FQDN, d.DomainID, serviceOutput) return nil } diff --git a/pkg/commands/domainv1/create.go b/pkg/commands/domainv1/create.go deleted file mode 100644 index 26fbb45c0..000000000 --- a/pkg/commands/domainv1/create.go +++ /dev/null @@ -1,84 +0,0 @@ -package domainv1 - -import ( - "context" - "errors" - "fmt" - "io" - - "github.com/fastly/go-fastly/v12/fastly" - "github.com/fastly/go-fastly/v12/fastly/domainmanagement/v1/domains" - - "github.com/fastly/cli/pkg/argparser" - "github.com/fastly/cli/pkg/global" - "github.com/fastly/cli/pkg/text" -) - -// CreateCommand calls the Fastly API to create domains. -type CreateCommand struct { - argparser.Base - - // Required. - fqdn string - serviceID string - - // Optional. - description argparser.OptionalString -} - -// NewCreateCommand returns a usable command registered under the parent. -func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateCommand { - c := CreateCommand{ - Base: argparser.Base{ - Globals: g, - }, - } - c.CmdClause = parent.Command("create", "Create a domain").Alias("add") - - // Optional. - c.CmdClause.Flag("description", "The description for the domain").Action(c.description.Set).StringVar(&c.description.Value) - c.CmdClause.Flag("fqdn", "The fully qualified domain name").Required().StringVar(&c.fqdn) - c.RegisterFlag(argparser.StringFlagOpts{ - Name: argparser.FlagServiceIDName, - Description: "The service_id associated with your domain", - Dst: &c.serviceID, - Short: 's', - }) - return &c -} - -// Exec invokes the application logic for the command. -func (c *CreateCommand) Exec(_ io.Reader, out io.Writer) error { - input := &domains.CreateInput{ - FQDN: &c.fqdn, - } - if c.serviceID != "" { - input.ServiceID = &c.serviceID - } - - if c.description.WasSet { - input.Description = &c.description.Value - } - - fc, ok := c.Globals.APIClient.(*fastly.Client) - if !ok { - return errors.New("failed to convert interface to a fastly client") - } - - d, err := domains.Create(context.TODO(), fc, input) - if err != nil { - c.Globals.ErrLog.AddWithContext(err, map[string]any{ - "FQDN": c.fqdn, - "Service ID": c.serviceID, - }) - return err - } - - serviceOutput := "" - if d.ServiceID != nil { - serviceOutput = fmt.Sprintf(", service-id: %s", *d.ServiceID) - } - - text.Success(out, "Created domain '%s' (domain-id: %s%s)", d.FQDN, d.DomainID, serviceOutput) - return nil -} diff --git a/pkg/commands/domainv1/delete.go b/pkg/commands/domainv1/delete.go deleted file mode 100644 index f3eab555b..000000000 --- a/pkg/commands/domainv1/delete.go +++ /dev/null @@ -1,58 +0,0 @@ -package domainv1 - -import ( - "context" - "errors" - "io" - - "github.com/fastly/go-fastly/v12/fastly" - "github.com/fastly/go-fastly/v12/fastly/domainmanagement/v1/domains" - - "github.com/fastly/cli/pkg/argparser" - "github.com/fastly/cli/pkg/global" - "github.com/fastly/cli/pkg/text" -) - -// DeleteCommand calls the Fastly API to delete domains. -type DeleteCommand struct { - argparser.Base - domainID string -} - -// NewDeleteCommand returns a usable command registered under the parent. -func NewDeleteCommand(parent argparser.Registerer, g *global.Data) *DeleteCommand { - c := DeleteCommand{ - Base: argparser.Base{ - Globals: g, - }, - } - c.CmdClause = parent.Command("delete", "Delete a domain").Alias("remove") - - // Required. - c.CmdClause.Flag("domain-id", "The Domain Identifier (UUID)").Required().StringVar(&c.domainID) - - return &c -} - -// Exec invokes the application logic for the command. -func (c *DeleteCommand) Exec(_ io.Reader, out io.Writer) error { - fc, ok := c.Globals.APIClient.(*fastly.Client) - if !ok { - return errors.New("failed to convert interface to a fastly client") - } - - input := &domains.DeleteInput{ - DomainID: &c.domainID, - } - - err := domains.Delete(context.TODO(), fc, input) - if err != nil { - c.Globals.ErrLog.AddWithContext(err, map[string]any{ - "Domain ID": c.domainID, - }) - return err - } - - text.Success(out, "Deleted domain (domain-id: %s)", c.domainID) - return nil -} diff --git a/pkg/commands/domainv1/describe.go b/pkg/commands/domainv1/describe.go deleted file mode 100644 index 82aeeb911..000000000 --- a/pkg/commands/domainv1/describe.go +++ /dev/null @@ -1,76 +0,0 @@ -package domainv1 - -import ( - "context" - "errors" - "io" - - "github.com/fastly/go-fastly/v12/fastly" - "github.com/fastly/go-fastly/v12/fastly/domainmanagement/v1/domains" - - "github.com/fastly/cli/pkg/argparser" - fsterr "github.com/fastly/cli/pkg/errors" - "github.com/fastly/cli/pkg/global" -) - -// DescribeCommand calls the Fastly API to describe a domain. -type DescribeCommand struct { - argparser.Base - argparser.JSONOutput - domainID string -} - -// NewDescribeCommand returns a usable command registered under the parent. -func NewDescribeCommand(parent argparser.Registerer, g *global.Data) *DescribeCommand { - c := DescribeCommand{ - Base: argparser.Base{ - Globals: g, - }, - } - c.CmdClause = parent.Command("describe", "Show detailed information about a domain").Alias("get") - - // Required. - c.CmdClause.Flag("domain-id", "The Domain Identifier (UUID)").Required().StringVar(&c.domainID) - - // Optional. - c.RegisterFlagBool(c.JSONFlag()) // --json - 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 fsterr.ErrInvalidVerboseJSONCombo - } - - fc, ok := c.Globals.APIClient.(*fastly.Client) - if !ok { - return errors.New("failed to convert interface to a fastly client") - } - - input := &domains.GetInput{ - DomainID: &c.domainID, - } - - d, err := domains.Get(context.TODO(), fc, input) - if err != nil { - c.Globals.ErrLog.AddWithContext(err, map[string]any{ - "Domain ID": c.domainID, - }) - return err - } - - if ok, err := c.WriteJSON(out, d); ok { - return err - } - - if d != nil { - cl := []domains.Data{*d} - if c.Globals.Verbose() { - printVerbose(out, cl) - } else { - printSummary(out, cl) - } - } - return nil -} diff --git a/pkg/commands/domainv1/doc.go b/pkg/commands/domainv1/doc.go deleted file mode 100644 index bcaed6abe..000000000 --- a/pkg/commands/domainv1/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package domainv1 contains commands to inspect and manipulate Fastly domains. -package domainv1 diff --git a/pkg/commands/domainv1/domain_test.go b/pkg/commands/domainv1/domain_test.go deleted file mode 100644 index 93bc916b7..000000000 --- a/pkg/commands/domainv1/domain_test.go +++ /dev/null @@ -1,290 +0,0 @@ -package domainv1_test - -import ( - "bytes" - "fmt" - "io" - "net/http" - "strings" - "testing" - - "github.com/fastly/go-fastly/v12/fastly/domainmanagement/v1/domains" - - root "github.com/fastly/cli/pkg/commands/domainv1" - "github.com/fastly/cli/pkg/testutil" -) - -func TestDomainV1Create(t *testing.T) { - fqdn := "www.example.com" - sid := "123" - did := "domain-id" - - scenarios := []testutil.CLIScenario{ - { - Args: "", - WantError: "error parsing arguments: required flag --fqdn not provided", - }, - { - Args: fmt.Sprintf("--fqdn %s --service-id %s", fqdn, sid), - Client: &http.Client{ - Transport: &testutil.MockRoundTripper{ - Response: &http.Response{ - StatusCode: http.StatusOK, - Status: http.StatusText(http.StatusOK), - Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(domains.Data{ - DomainID: did, - FQDN: fqdn, - ServiceID: &sid, - }))), - }, - }, - }, - WantOutput: fmt.Sprintf("SUCCESS: Created domain '%s' (domain-id: %s, service-id: %s)", fqdn, did, sid), - }, - { - Args: fmt.Sprintf("--fqdn %s", fqdn), - Client: &http.Client{ - Transport: &testutil.MockRoundTripper{ - Response: &http.Response{ - StatusCode: http.StatusOK, - Status: http.StatusText(http.StatusOK), - Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(domains.Data{ - DomainID: did, - FQDN: fqdn, - }))), - }, - }, - }, - WantOutput: fmt.Sprintf("SUCCESS: Created domain '%s' (domain-id: %s)", fqdn, did), - }, - { - Args: fmt.Sprintf("--fqdn %s", fqdn), - Client: &http.Client{ - Transport: &testutil.MockRoundTripper{ - Response: &http.Response{ - StatusCode: http.StatusBadRequest, - Status: http.StatusText(http.StatusBadRequest), - Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(` - { - "errors":[ - { - "title":"Invalid value for fqdn", - "detail":"fqdn has already been taken" - } - ] - } - `))), - }, - }, - }, - WantError: "400 - Bad Request", - }, - } - testutil.RunCLIScenarios(t, []string{root.CommandName, "create"}, scenarios) -} - -func TestDomainV1List(t *testing.T) { - fqdn := "www.example.com" - sid := "123" - did := "domain-id" - description := "domain description" - - resp := testutil.GenJSON(domains.Collection{ - Data: []domains.Data{ - { - DomainID: did, - FQDN: fqdn, - ServiceID: &sid, - Description: description, - }, - }, - }) - - scenarios := []testutil.CLIScenario{ - { - Args: "--verbose --json", - WantError: "invalid flag combination, --verbose and --json", - }, - { - Args: "--json", - Client: &http.Client{ - Transport: &testutil.MockRoundTripper{ - Response: &http.Response{ - StatusCode: http.StatusOK, - Status: http.StatusText(http.StatusOK), - Body: io.NopCloser(bytes.NewReader(resp)), - }, - }, - }, - WantOutput: string(resp), - }, - { - Args: "", - Client: &http.Client{ - Transport: &testutil.MockRoundTripper{ - Response: &http.Response{ - StatusCode: http.StatusBadRequest, - Status: http.StatusText(http.StatusBadRequest), - Body: io.NopCloser(strings.NewReader(`{"error": "whoops"}`)), - }, - }, - }, - WantError: "400 - Bad Request", - }, - } - testutil.RunCLIScenarios(t, []string{root.CommandName, "list"}, scenarios) -} - -func TestDomainV1Describe(t *testing.T) { - fqdn := "www.example.com" - sid := "123" - did := "domain-id" - description := "domain description" - - resp := testutil.GenJSON(domains.Data{ - DomainID: did, - FQDN: fqdn, - ServiceID: &sid, - Description: description, - }) - - scenarios := []testutil.CLIScenario{ - { - Args: "", - WantError: "error parsing arguments: required flag --domain-id not provided", - }, - { - Args: fmt.Sprintf("--domain-id %s --json", did), - Client: &http.Client{ - Transport: &testutil.MockRoundTripper{ - Response: &http.Response{ - StatusCode: http.StatusOK, - Status: http.StatusText(http.StatusOK), - Body: io.NopCloser(bytes.NewReader(resp)), - }, - }, - }, - WantOutput: string(resp), - }, - { - Args: fmt.Sprintf("--domain-id %s --json", did), - Client: &http.Client{ - Transport: &testutil.MockRoundTripper{ - Response: &http.Response{ - StatusCode: http.StatusBadRequest, - Status: http.StatusText(http.StatusBadRequest), - Body: io.NopCloser(strings.NewReader(`{"error": "whoops"}`)), - }, - }, - }, - WantError: "400 - Bad Request", - }, - } - testutil.RunCLIScenarios(t, []string{root.CommandName, "describe"}, scenarios) -} - -func TestDomainV1Update(t *testing.T) { - fqdn := "www.example.com" - sid := "123" - did := "domain-id" - - scenarios := []testutil.CLIScenario{ - { - Args: "", - WantError: "error parsing arguments: required flag --domain-id not provided", - }, - { - Args: fmt.Sprintf("--domain-id %s --service-id %s", did, sid), - Client: &http.Client{ - Transport: &testutil.MockRoundTripper{ - Response: &http.Response{ - StatusCode: http.StatusOK, - Status: http.StatusText(http.StatusOK), - Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(domains.Data{ - DomainID: did, - FQDN: fqdn, - ServiceID: &sid, - }))), - }, - }, - }, - WantOutput: fmt.Sprintf("SUCCESS: Updated domain '%s' (domain-id: %s, service-id: %s)", fqdn, did, sid), - }, - { - Args: fmt.Sprintf("--domain-id %s", did), - Client: &http.Client{ - Transport: &testutil.MockRoundTripper{ - Response: &http.Response{ - StatusCode: http.StatusOK, - Status: http.StatusText(http.StatusOK), - Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(domains.Data{ - DomainID: did, - FQDN: fqdn, - }))), - }, - }, - }, - WantOutput: fmt.Sprintf("SUCCESS: Updated domain '%s' (domain-id: %s)", fqdn, did), - }, - { - Args: fmt.Sprintf("--domain-id %s", did), - Client: &http.Client{ - Transport: &testutil.MockRoundTripper{ - Response: &http.Response{ - StatusCode: http.StatusBadRequest, - Status: http.StatusText(http.StatusBadRequest), - Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(` - { - "errors":[ - { - "title":"Invalid value for domain-id", - "detail":"whoops" - } - ] - } - `))), - }, - }, - }, - WantError: "400 - Bad Request", - }, - } - testutil.RunCLIScenarios(t, []string{root.CommandName, "update"}, scenarios) -} - -func TestDomainV1Delete(t *testing.T) { - did := "domain-id" - - scenarios := []testutil.CLIScenario{ - { - Args: "", - WantError: "error parsing arguments: required flag --domain-id not provided", - }, - { - Args: fmt.Sprintf("--domain-id %s", did), - Client: &http.Client{ - Transport: &testutil.MockRoundTripper{ - Response: &http.Response{ - StatusCode: http.StatusNoContent, - Status: http.StatusText(http.StatusNoContent), - }, - }, - }, - WantOutput: fmt.Sprintf("SUCCESS: Deleted domain (domain-id: %s)", did), - }, - { - Args: fmt.Sprintf("--domain-id %s", did), - Client: &http.Client{ - Transport: &testutil.MockRoundTripper{ - Response: &http.Response{ - StatusCode: http.StatusBadRequest, - Status: http.StatusText(http.StatusBadRequest), - Body: io.NopCloser(strings.NewReader(`{"error": "whoops"}`)), - }, - }, - }, - WantError: "400 - Bad Request", - }, - } - testutil.RunCLIScenarios(t, []string{root.CommandName, "delete"}, scenarios) -} diff --git a/pkg/commands/domainv1/list.go b/pkg/commands/domainv1/list.go deleted file mode 100644 index 4a0f8b71e..000000000 --- a/pkg/commands/domainv1/list.go +++ /dev/null @@ -1,123 +0,0 @@ -package domainv1 - -import ( - "context" - "errors" - "io" - - "github.com/fastly/go-fastly/v12/fastly" - "github.com/fastly/go-fastly/v12/fastly/domainmanagement/v1/domains" - - "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" -) - -// ListCommand calls the Fastly API to list domains. -type ListCommand struct { - argparser.Base - argparser.JSONOutput - - cursor argparser.OptionalString - fqdn argparser.OptionalString - limit argparser.OptionalInt - serviceID argparser.OptionalString - sort argparser.OptionalString -} - -// NewListCommand returns a usable command registered under the parent. -func NewListCommand(parent argparser.Registerer, g *global.Data) *ListCommand { - c := ListCommand{ - Base: argparser.Base{ - Globals: g, - }, - } - c.CmdClause = parent.Command("list", "List domains") - - // Optional. - c.CmdClause.Flag("cursor", "Cursor value from the next_cursor field of a previous response, used to retrieve the next page").Action(c.cursor.Set).StringVar(&c.cursor.Value) - c.CmdClause.Flag("fqdn", "Filters results by the FQDN using a fuzzy/partial match").Action(c.fqdn.Set).StringVar(&c.fqdn.Value) - c.RegisterFlagBool(c.JSONFlag()) // --json - c.CmdClause.Flag("limit", "Limit how many results are returned").Action(c.limit.Set).IntVar(&c.limit.Value) - c.RegisterFlag(argparser.StringFlagOpts{ - Action: c.serviceID.Set, - Name: argparser.FlagServiceIDName, - Description: "Filter results based on a service_id", - Dst: &c.serviceID.Value, - Short: 's', - }) - c.CmdClause.Flag("sort", "The order in which to list the results").Action(c.sort.Set).StringVar(&c.sort.Value) - return &c -} - -// Exec invokes the application logic for the command. -func (c *ListCommand) Exec(in io.Reader, out io.Writer) error { - if c.Globals.Verbose() && c.JSONOutput.Enabled { - return fsterr.ErrInvalidVerboseJSONCombo - } - - input := &domains.ListInput{} - - if c.serviceID.WasSet { - input.ServiceID = &c.serviceID.Value - } - if c.cursor.WasSet { - input.Cursor = &c.cursor.Value - } - if c.fqdn.WasSet { - input.FQDN = &c.fqdn.Value - } - if c.limit.WasSet { - input.Limit = &c.limit.Value - } - if c.sort.WasSet { - input.Sort = &c.sort.Value - } - - fc, ok := c.Globals.APIClient.(*fastly.Client) - if !ok { - return errors.New("failed to convert interface to a fastly client") - } - - for { - cl, err := domains.List(context.TODO(), fc, input) - if err != nil { - c.Globals.ErrLog.AddWithContext(err, map[string]any{ - "Cursor": c.cursor.Value, - "FQDN": c.fqdn.Value, - "Limit": c.limit.Value, - "Service ID": c.serviceID.Value, - "Sort": c.sort.Value, - }) - return err - } - - if ok, err := c.WriteJSON(out, cl); ok { - // No pagination prompt w/ JSON output. - return err - } - - if c.Globals.Verbose() { - printVerbose(out, cl.Data) - } else { - printSummary(out, cl.Data) - } - - if cl != nil && cl.Meta.NextCursor != "" { - // Check if 'out' is interactive before prompting. - if !c.Globals.Flags.NonInteractive && !c.Globals.Flags.AutoYes && text.IsTTY(out) { - printNext, err := text.AskYesNo(out, "Print next page [y/N]: ", in) - if err != nil { - return err - } - if printNext { - input.Cursor = &cl.Meta.NextCursor - continue - } - } - } - - return nil - } -} diff --git a/pkg/commands/domainv1/update.go b/pkg/commands/domainv1/update.go deleted file mode 100644 index de74f6b21..000000000 --- a/pkg/commands/domainv1/update.go +++ /dev/null @@ -1,78 +0,0 @@ -package domainv1 - -import ( - "context" - "errors" - "fmt" - "io" - - "github.com/fastly/go-fastly/v12/fastly" - "github.com/fastly/go-fastly/v12/fastly/domainmanagement/v1/domains" - - "github.com/fastly/cli/pkg/argparser" - "github.com/fastly/cli/pkg/global" - "github.com/fastly/cli/pkg/text" -) - -// UpdateCommand calls the Fastly API to update domains. -type UpdateCommand struct { - argparser.Base - domainID string - serviceID string - description argparser.OptionalString -} - -// NewUpdateCommand returns a usable command registered under the parent. -func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateCommand { - c := UpdateCommand{ - Base: argparser.Base{ - Globals: g, - }, - } - c.CmdClause = parent.Command("update", "Update a domain") - - // Required. - c.CmdClause.Flag("domain-id", "The Domain Identifier (UUID)").Required().StringVar(&c.domainID) - - // Optional - c.CmdClause.Flag("description", "The description for the domain").Action(c.description.Set).StringVar(&c.description.Value) - c.CmdClause.Flag("service-id", "The service_id associated with your domain (omit to unset)").StringVar(&c.serviceID) - - return &c -} - -// Exec invokes the application logic for the command. -func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { - input := &domains.UpdateInput{ - DomainID: &c.domainID, - } - if c.serviceID != "" { - input.ServiceID = &c.serviceID - } - - if c.description.WasSet { - input.Description = &c.description.Value - } - - fc, ok := c.Globals.APIClient.(*fastly.Client) - if !ok { - return errors.New("failed to convert interface to a fastly client") - } - - d, err := domains.Update(context.TODO(), fc, input) - if err != nil { - c.Globals.ErrLog.AddWithContext(err, map[string]any{ - "Domain ID": c.domainID, - "Service ID": c.serviceID, - }) - return err - } - - serviceOutput := "" - if d.ServiceID != nil { - serviceOutput = fmt.Sprintf(", service-id: %s", *d.ServiceID) - } - - text.Success(out, "Updated domain '%s' (domain-id: %s%s)", d.FQDN, d.DomainID, serviceOutput) - return nil -} diff --git a/pkg/commands/service/domain/create.go b/pkg/commands/service/domain/create.go new file mode 100644 index 000000000..8bafd3400 --- /dev/null +++ b/pkg/commands/service/domain/create.go @@ -0,0 +1,111 @@ +package domain + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v12/fastly" + + "4d63.com/optional" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/text" +) + +// CreateCommand calls the Fastly API to create domains. +type CreateCommand struct { + argparser.Base + + // Required. + serviceVersion argparser.OptionalServiceVersion + + // Optional. + autoClone argparser.OptionalAutoClone + comment argparser.OptionalString + name argparser.OptionalString + serviceName argparser.OptionalServiceNameID +} + +// NewCreateCommand returns a usable command registered under the parent. +func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateCommand { + c := CreateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("create", "Create a domain on a Fastly service version").Alias("add") + + // Required. + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagVersionName, + Description: argparser.FlagVersionDesc, + Dst: &c.serviceVersion.Value, + Required: true, + }) + + // Optional. + c.RegisterAutoCloneFlag(argparser.AutoCloneFlagOpts{ + Action: c.autoClone.Set, + Dst: &c.autoClone.Value, + }) + c.CmdClause.Flag("comment", "A descriptive note").Action(c.comment.Set).StringVar(&c.comment.Value) + c.CmdClause.Flag("name", "Domain name").Short('n').Action(c.name.Set).StringVar(&c.name.Value) + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagServiceIDName, + Description: argparser.FlagServiceIDDesc, + Dst: &g.Manifest.Flag.ServiceID, + Short: 's', + }) + c.RegisterFlag(argparser.StringFlagOpts{ + Action: c.serviceName.Set, + Name: argparser.FlagServiceName, + Description: argparser.FlagServiceNameDesc, + 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 := argparser.ServiceDetails(argparser.ServiceDetailsOpts{ + Active: optional.Of(false), + Locked: optional.Of(false), + AutoCloneFlag: c.autoClone, + APIClient: c.Globals.APIClient, + Manifest: *c.Globals.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 + } + input := fastly.CreateDomainInput{ + ServiceID: serviceID, + ServiceVersion: fastly.ToValue(serviceVersion.Number), + } + if c.name.WasSet { + input.Name = &c.name.Value + } + if c.comment.WasSet { + input.Comment = &c.comment.Value + } + d, err := c.Globals.APIClient.CreateDomain(context.TODO(), &input) + if err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]any{ + "Service ID": serviceID, + "Service Version": fastly.ToValue(serviceVersion.Number), + }) + return err + } + + text.Success(out, "Created domain %s (service %s version %d)", fastly.ToValue(d.Name), fastly.ToValue(d.ServiceID), fastly.ToValue(d.ServiceVersion)) + return nil +} diff --git a/pkg/commands/service/domain/delete.go b/pkg/commands/service/domain/delete.go new file mode 100644 index 000000000..155eaa3bb --- /dev/null +++ b/pkg/commands/service/domain/delete.go @@ -0,0 +1,98 @@ +package domain + +import ( + "context" + "io" + + "github.com/fastly/go-fastly/v12/fastly" + + "4d63.com/optional" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/text" +) + +// DeleteCommand calls the Fastly API to delete domains. +type DeleteCommand struct { + argparser.Base + Input fastly.DeleteDomainInput + serviceName argparser.OptionalServiceNameID + serviceVersion argparser.OptionalServiceVersion + autoClone argparser.OptionalAutoClone +} + +// NewDeleteCommand returns a usable command registered under the parent. +func NewDeleteCommand(parent argparser.Registerer, g *global.Data) *DeleteCommand { + c := DeleteCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("delete", "Delete a domain on a Fastly service version").Alias("remove") + + // Required. + c.CmdClause.Flag("name", "Domain name").Short('n').Required().StringVar(&c.Input.Name) + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagVersionName, + Description: argparser.FlagVersionDesc, + Dst: &c.serviceVersion.Value, + Required: true, + }) + + // Optional. + c.RegisterAutoCloneFlag(argparser.AutoCloneFlagOpts{ + Action: c.autoClone.Set, + Dst: &c.autoClone.Value, + }) + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagServiceIDName, + Description: argparser.FlagServiceIDDesc, + Dst: &g.Manifest.Flag.ServiceID, + Short: 's', + }) + c.RegisterFlag(argparser.StringFlagOpts{ + Action: c.serviceName.Set, + Name: argparser.FlagServiceName, + Description: argparser.FlagServiceNameDesc, + 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 := argparser.ServiceDetails(argparser.ServiceDetailsOpts{ + Active: optional.Of(false), + Locked: optional.Of(false), + AutoCloneFlag: c.autoClone, + APIClient: c.Globals.APIClient, + Manifest: *c.Globals.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 + } + + c.Input.ServiceID = serviceID + c.Input.ServiceVersion = fastly.ToValue(serviceVersion.Number) + + if err := c.Globals.APIClient.DeleteDomain(context.TODO(), &c.Input); err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]any{ + "Service ID": serviceID, + "Service Version": fastly.ToValue(serviceVersion.Number), + }) + return err + } + + text.Success(out, "Deleted domain %s (service %s version %d)", c.Input.Name, c.Input.ServiceID, c.Input.ServiceVersion) + return nil +} diff --git a/pkg/commands/service/domain/describe.go b/pkg/commands/service/domain/describe.go new file mode 100644 index 000000000..e896582ab --- /dev/null +++ b/pkg/commands/service/domain/describe.go @@ -0,0 +1,106 @@ +package domain + +import ( + "context" + "fmt" + "io" + + "github.com/fastly/go-fastly/v12/fastly" + + "github.com/fastly/cli/pkg/argparser" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" +) + +// DescribeCommand calls the Fastly API to describe a domain. +type DescribeCommand struct { + argparser.Base + argparser.JSONOutput + + Input fastly.GetDomainInput + serviceName argparser.OptionalServiceNameID + serviceVersion argparser.OptionalServiceVersion +} + +// NewDescribeCommand returns a usable command registered under the parent. +func NewDescribeCommand(parent argparser.Registerer, g *global.Data) *DescribeCommand { + c := DescribeCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("describe", "Show detailed information about a domain on a Fastly service version").Alias("get") + + // Required. + c.CmdClause.Flag("name", "Name of domain").Short('n').Required().StringVar(&c.Input.Name) + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagVersionName, + Description: argparser.FlagVersionDesc, + Dst: &c.serviceVersion.Value, + Required: true, + }) + + // Optional. + c.RegisterFlagBool(c.JSONFlag()) // --json + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagServiceIDName, + Description: argparser.FlagServiceIDDesc, + Dst: &g.Manifest.Flag.ServiceID, + Short: 's', + }) + c.RegisterFlag(argparser.StringFlagOpts{ + Action: c.serviceName.Set, + Name: argparser.FlagServiceName, + Description: argparser.FlagServiceNameDesc, + 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 fsterr.ErrInvalidVerboseJSONCombo + } + + serviceID, serviceVersion, err := argparser.ServiceDetails(argparser.ServiceDetailsOpts{ + APIClient: c.Globals.APIClient, + Manifest: *c.Globals.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": fsterr.ServiceVersion(serviceVersion), + }) + return err + } + + c.Input.ServiceID = serviceID + c.Input.ServiceVersion = fastly.ToValue(serviceVersion.Number) + + o, err := c.Globals.APIClient.GetDomain(context.TODO(), &c.Input) + if err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]any{ + "Service ID": serviceID, + "Service Version": fastly.ToValue(serviceVersion.Number), + }) + return err + } + + if ok, err := c.WriteJSON(out, o); ok { + return err + } + + if !c.Globals.Verbose() { + fmt.Fprintf(out, "\nService ID: %s\n", fastly.ToValue(o.ServiceID)) + } + fmt.Fprintf(out, "Version: %d\n", fastly.ToValue(o.ServiceVersion)) + fmt.Fprintf(out, "Name: %s\n", fastly.ToValue(o.Name)) + fmt.Fprintf(out, "Comment: %v\n", fastly.ToValue(o.Comment)) + + return nil +} diff --git a/pkg/commands/service/domain/doc.go b/pkg/commands/service/domain/doc.go new file mode 100644 index 000000000..d15528c22 --- /dev/null +++ b/pkg/commands/service/domain/doc.go @@ -0,0 +1,2 @@ +// Package domain contains commands to inspect and manipulate Fastly service domains. +package domain diff --git a/pkg/commands/service/domain/domain_test.go b/pkg/commands/service/domain/domain_test.go new file mode 100644 index 000000000..b55c347fd --- /dev/null +++ b/pkg/commands/service/domain/domain_test.go @@ -0,0 +1,417 @@ +package domain_test + +import ( + "context" + "errors" + "fmt" + "strings" + "testing" + + "github.com/fastly/go-fastly/v12/fastly" + + root "github.com/fastly/cli/pkg/commands/service" + sub "github.com/fastly/cli/pkg/commands/service/domain" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" +) + +func TestDomainCreate(t *testing.T) { + scenarios := []testutil.CLIScenario{ + { + Args: "--version 1", + WantError: "error reading service: no service ID found", + }, + { + Args: "--service-id 123 --version 1 --name www.test.com --autoclone", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + CloneVersionFn: testutil.CloneVersionResult(4), + CreateDomainFn: createDomainOK, + }, + WantOutput: "Created domain www.test.com (service 123 version 4)", + }, + { + Args: "--service-id 123 --version 1 --name www.test.com --autoclone", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + CloneVersionFn: testutil.CloneVersionResult(4), + CreateDomainFn: createDomainError, + }, + WantError: errTest.Error(), + }, + } + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "create"}, scenarios) +} + +func TestDomainList(t *testing.T) { + scenarios := []testutil.CLIScenario{ + { + Args: "--service-id 123 --version 1", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ListDomainsFn: listDomainsOK, + }, + WantOutput: listDomainsShortOutput, + }, + { + Args: "--service-id 123 --version 1 --verbose", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ListDomainsFn: listDomainsOK, + }, + WantOutput: listDomainsVerboseOutput, + }, + { + Args: "--service-id 123 --version 1 -v", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ListDomainsFn: listDomainsOK, + }, + WantOutput: listDomainsVerboseOutput, + }, + { + Args: "--verbose --service-id 123 --version 1", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ListDomainsFn: listDomainsOK, + }, + WantOutput: listDomainsVerboseOutput, + }, + { + Args: "-v --service-id 123 --version 1", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ListDomainsFn: listDomainsOK, + }, + WantOutput: listDomainsVerboseOutput, + }, + { + Args: "--service-id 123 --version 1", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ListDomainsFn: listDomainsError, + }, + WantError: errTest.Error(), + }, + } + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "list"}, scenarios) +} + +func TestDomainDescribe(t *testing.T) { + scenarios := []testutil.CLIScenario{ + { + Args: "--service-id 123 --version 1", + WantError: "error parsing arguments: required flag --name not provided", + }, + { + Args: "--service-id 123 --version 1 --name www.test.com", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + GetDomainFn: getDomainError, + }, + WantError: errTest.Error(), + }, + { + Args: "--service-id 123 --version 1 --name www.test.com", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + GetDomainFn: getDomainOK, + }, + WantOutput: describeDomainOutput, + }, + } + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "describe"}, scenarios) +} + +func TestDomainUpdate(t *testing.T) { + scenarios := []testutil.CLIScenario{ + { + Args: "--service-id 123 --version 1 --new-name www.test.com --comment ", + WantError: "error parsing arguments: required flag --name not provided", + }, + { + Args: "--service-id 123 --version 1 --name www.test.com --autoclone", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + CloneVersionFn: testutil.CloneVersionResult(4), + UpdateDomainFn: updateDomainOK, + }, + WantError: "error parsing arguments: must provide either --new-name or --comment to update domain", + }, + { + Args: "--service-id 123 --version 1 --name www.test.com --new-name www.example.com --autoclone", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + CloneVersionFn: testutil.CloneVersionResult(4), + UpdateDomainFn: updateDomainError, + }, + WantError: errTest.Error(), + }, + { + Args: "--service-id 123 --version 1 --name www.test.com --new-name www.example.com --autoclone", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + CloneVersionFn: testutil.CloneVersionResult(4), + UpdateDomainFn: updateDomainOK, + }, + WantOutput: "Updated domain www.example.com (service 123 version 4)", + }, + } + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "update"}, scenarios) +} + +func TestDomainDelete(t *testing.T) { + scenarios := []testutil.CLIScenario{ + { + Args: "--service-id 123 --version 1", + WantError: "error parsing arguments: required flag --name not provided", + }, + { + Args: "--service-id 123 --version 1 --name www.test.com --autoclone", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + CloneVersionFn: testutil.CloneVersionResult(4), + DeleteDomainFn: deleteDomainError, + }, + WantError: errTest.Error(), + }, + { + Args: "--service-id 123 --version 1 --name www.test.com --autoclone", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + CloneVersionFn: testutil.CloneVersionResult(4), + DeleteDomainFn: deleteDomainOK, + }, + WantOutput: "Deleted domain www.test.com (service 123 version 4)", + }, + } + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "delete"}, scenarios) +} + +func TestDomainValidate(t *testing.T) { + scenarios := []testutil.CLIScenario{ + { + Name: "validate missing --version flag", + WantError: "error parsing arguments: required flag --version not provided", + }, + { + Name: "validate missing --service-id flag", + Args: "--version 3", + WantError: "error reading service: no service ID found", + }, + { + Name: "validate missing --name flag", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + }, + Args: "--service-id 123 --version 3", + WantError: "error parsing arguments: must provide --name flag", + }, + { + Name: "validate ValidateDomain API error", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateDomainFn: func(_ context.Context, _ *fastly.ValidateDomainInput) (*fastly.DomainValidationResult, error) { + return nil, testutil.Err + }, + }, + Args: "--name foo.example.com --service-id 123 --version 3", + WantError: testutil.Err.Error(), + }, + { + Name: "validate ValidateAllDomains API error", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateAllDomainsFn: func(_ context.Context, _ *fastly.ValidateAllDomainsInput) ([]*fastly.DomainValidationResult, error) { + return nil, testutil.Err + }, + }, + Args: "--all --service-id 123 --version 3", + WantError: testutil.Err.Error(), + }, + { + Name: "validate ValidateDomain API success", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateDomainFn: validateDomain, + }, + Args: "--name foo.example.com --service-id 123 --version 3", + WantOutput: validateAPISuccess(3), + }, + { + Name: "validate ValidateAllDomains API success", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateAllDomainsFn: validateAllDomains, + }, + Args: "--all --service-id 123 --version 3", + WantOutput: validateAllAPISuccess(), + }, + { + Name: "validate missing --autoclone flag is OK", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateDomainFn: validateDomain, + }, + Args: "--name foo.example.com --service-id 123 --version 1", + WantOutput: validateAPISuccess(1), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "validate"}, scenarios) +} + +var errTest = errors.New("fixture error") + +func createDomainOK(_ context.Context, i *fastly.CreateDomainInput) (*fastly.Domain, error) { + return &fastly.Domain{ + ServiceID: fastly.ToPointer(i.ServiceID), + ServiceVersion: fastly.ToPointer(i.ServiceVersion), + Name: i.Name, + }, nil +} + +func createDomainError(_ context.Context, _ *fastly.CreateDomainInput) (*fastly.Domain, error) { + return nil, errTest +} + +func listDomainsOK(_ context.Context, i *fastly.ListDomainsInput) ([]*fastly.Domain, error) { + return []*fastly.Domain{ + { + ServiceID: fastly.ToPointer(i.ServiceID), + ServiceVersion: fastly.ToPointer(i.ServiceVersion), + Name: fastly.ToPointer("www.test.com"), + Comment: fastly.ToPointer("test"), + }, + { + ServiceID: fastly.ToPointer(i.ServiceID), + ServiceVersion: fastly.ToPointer(i.ServiceVersion), + Name: fastly.ToPointer("www.example.com"), + Comment: fastly.ToPointer("example"), + }, + }, nil +} + +func listDomainsError(_ context.Context, _ *fastly.ListDomainsInput) ([]*fastly.Domain, error) { + return nil, errTest +} + +var listDomainsShortOutput = strings.TrimSpace(` +SERVICE VERSION NAME COMMENT +123 1 www.test.com test +123 1 www.example.com example +`) + "\n" + +var listDomainsVerboseOutput = strings.TrimSpace(` +Fastly API endpoint: https://api.fastly.com +Fastly API token provided via config file (profile: user) + +Service ID (via --service-id): 123 + +Version: 1 + Domain 1/2 + Name: www.test.com + Comment: test + Domain 2/2 + Name: www.example.com + Comment: example +`) + "\n\n" + +func getDomainOK(_ context.Context, i *fastly.GetDomainInput) (*fastly.Domain, error) { + return &fastly.Domain{ + ServiceID: fastly.ToPointer(i.ServiceID), + ServiceVersion: fastly.ToPointer(i.ServiceVersion), + Name: fastly.ToPointer(i.Name), + Comment: fastly.ToPointer("test"), + }, nil +} + +func getDomainError(_ context.Context, _ *fastly.GetDomainInput) (*fastly.Domain, error) { + return nil, errTest +} + +var describeDomainOutput = "\n" + strings.TrimSpace(` +Service ID: 123 +Version: 1 +Name: www.test.com +Comment: test +`) + "\n" + +func updateDomainOK(_ context.Context, i *fastly.UpdateDomainInput) (*fastly.Domain, error) { + return &fastly.Domain{ + ServiceID: fastly.ToPointer(i.ServiceID), + ServiceVersion: fastly.ToPointer(i.ServiceVersion), + Name: i.NewName, + }, nil +} + +func updateDomainError(_ context.Context, _ *fastly.UpdateDomainInput) (*fastly.Domain, error) { + return nil, errTest +} + +func deleteDomainOK(_ context.Context, _ *fastly.DeleteDomainInput) error { + return nil +} + +func deleteDomainError(_ context.Context, _ *fastly.DeleteDomainInput) error { + return errTest +} + +func validateDomain(_ context.Context, i *fastly.ValidateDomainInput) (*fastly.DomainValidationResult, error) { + return &fastly.DomainValidationResult{ + Metadata: &fastly.DomainMetadata{ + ServiceID: fastly.ToPointer(i.ServiceID), + ServiceVersion: fastly.ToPointer(i.ServiceVersion), + Name: fastly.ToPointer(i.Name), + }, + CName: fastly.ToPointer("foo"), + Valid: fastly.ToPointer(true), + }, nil +} + +func validateAllDomains(_ context.Context, i *fastly.ValidateAllDomainsInput) ([]*fastly.DomainValidationResult, error) { + return []*fastly.DomainValidationResult{ + { + Metadata: &fastly.DomainMetadata{ + ServiceID: fastly.ToPointer(i.ServiceID), + ServiceVersion: fastly.ToPointer(i.ServiceVersion), + Name: fastly.ToPointer("foo.example.com"), + }, + CName: fastly.ToPointer("foo"), + Valid: fastly.ToPointer(true), + }, + { + Metadata: &fastly.DomainMetadata{ + ServiceID: fastly.ToPointer(i.ServiceID), + ServiceVersion: fastly.ToPointer(i.ServiceVersion), + Name: fastly.ToPointer("bar.example.com"), + }, + CName: fastly.ToPointer("bar"), + Valid: fastly.ToPointer(true), + }, + }, nil +} + +func validateAPISuccess(version int) string { + return fmt.Sprintf(` +Service ID: 123 +Service Version: %d + +Name: foo.example.com +Valid: true +CNAME: foo`, version) +} + +func validateAllAPISuccess() string { + return ` +Service ID: 123 +Service Version: 3 + +Name: foo.example.com +Valid: true +CNAME: foo + +Name: bar.example.com +Valid: true +CNAME: bar` +} diff --git a/pkg/commands/service/domain/list.go b/pkg/commands/service/domain/list.go new file mode 100644 index 000000000..82f5d5ff4 --- /dev/null +++ b/pkg/commands/service/domain/list.go @@ -0,0 +1,122 @@ +package domain + +import ( + "context" + "fmt" + "io" + + "github.com/fastly/go-fastly/v12/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" +) + +// ListCommand calls the Fastly API to list domains. +type ListCommand struct { + argparser.Base + argparser.JSONOutput + + Input fastly.ListDomainsInput + serviceName argparser.OptionalServiceNameID + serviceVersion argparser.OptionalServiceVersion +} + +// NewListCommand returns a usable command registered under the parent. +func NewListCommand(parent argparser.Registerer, g *global.Data) *ListCommand { + c := ListCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("list", "List domains on a Fastly service version") + + // Required. + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagVersionName, + Description: argparser.FlagVersionDesc, + Dst: &c.serviceVersion.Value, + Required: true, + }) + + // Optional. + c.RegisterFlagBool(c.JSONFlag()) // --json + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagServiceIDName, + Description: argparser.FlagServiceIDDesc, + Dst: &g.Manifest.Flag.ServiceID, + Short: 's', + }) + c.RegisterFlag(argparser.StringFlagOpts{ + Action: c.serviceName.Set, + Name: argparser.FlagServiceName, + Description: argparser.FlagServiceNameDesc, + 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 fsterr.ErrInvalidVerboseJSONCombo + } + + serviceID, serviceVersion, err := argparser.ServiceDetails(argparser.ServiceDetailsOpts{ + APIClient: c.Globals.APIClient, + Manifest: *c.Globals.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": fsterr.ServiceVersion(serviceVersion), + }) + return err + } + + c.Input.ServiceID = serviceID + c.Input.ServiceVersion = fastly.ToValue(serviceVersion.Number) + + o, err := c.Globals.APIClient.ListDomains(context.TODO(), &c.Input) + if err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]any{ + "Service ID": serviceID, + "Service Version": fastly.ToValue(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", "COMMENT") + for _, domain := range o { + tw.AddLine( + fastly.ToValue(domain.ServiceID), + fastly.ToValue(domain.ServiceVersion), + fastly.ToValue(domain.Name), + fastly.ToValue(domain.Comment), + ) + } + tw.Print() + return nil + } + + fmt.Fprintf(out, "Version: %d\n", c.Input.ServiceVersion) + for i, domain := range o { + fmt.Fprintf(out, "\tDomain %d/%d\n", i+1, len(o)) + fmt.Fprintf(out, "\t\tName: %s\n", fastly.ToValue(domain.Name)) + fmt.Fprintf(out, "\t\tComment: %v\n", fastly.ToValue(domain.Comment)) + } + fmt.Fprintln(out) + + return nil +} diff --git a/pkg/commands/domainv1/root.go b/pkg/commands/service/domain/root.go similarity index 83% rename from pkg/commands/domainv1/root.go rename to pkg/commands/service/domain/root.go index 945db65cc..560814c35 100644 --- a/pkg/commands/domainv1/root.go +++ b/pkg/commands/service/domain/root.go @@ -1,4 +1,4 @@ -package domainv1 +package domain import ( "io" @@ -15,13 +15,13 @@ type RootCommand struct { } // CommandName is the string to be used to invoke this command. -const CommandName = "domain-v1" +const CommandName = "domain" // NewRootCommand returns a new command registered in the parent. func NewRootCommand(parent argparser.Registerer, g *global.Data) *RootCommand { var c RootCommand c.Globals = g - c.CmdClause = parent.Command(CommandName, "Manipulate Fastly domains") + c.CmdClause = parent.Command(CommandName, "Manipulate Fastly service version domains") return &c } diff --git a/pkg/commands/service/domain/update.go b/pkg/commands/service/domain/update.go new file mode 100644 index 000000000..3612c6a04 --- /dev/null +++ b/pkg/commands/service/domain/update.go @@ -0,0 +1,119 @@ +package domain + +import ( + "context" + "fmt" + "io" + + "github.com/fastly/go-fastly/v12/fastly" + + "4d63.com/optional" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/text" +) + +// UpdateCommand calls the Fastly API to update domains. +type UpdateCommand struct { + argparser.Base + input fastly.UpdateDomainInput + serviceName argparser.OptionalServiceNameID + serviceVersion argparser.OptionalServiceVersion + autoClone argparser.OptionalAutoClone + + NewName argparser.OptionalString + Comment argparser.OptionalString +} + +// NewUpdateCommand returns a usable command registered under the parent. +func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateCommand { + c := UpdateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("update", "Update a domain on a Fastly service version") + + // Required. + c.CmdClause.Flag("name", "Domain name").Short('n').Required().StringVar(&c.input.Name) + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagVersionName, + Description: argparser.FlagVersionDesc, + Dst: &c.serviceVersion.Value, + Required: true, + }) + + // Optional. + c.RegisterAutoCloneFlag(argparser.AutoCloneFlagOpts{ + Action: c.autoClone.Set, + Dst: &c.autoClone.Value, + }) + c.CmdClause.Flag("comment", "A descriptive note").Action(c.Comment.Set).StringVar(&c.Comment.Value) + c.CmdClause.Flag("new-name", "New domain name").Action(c.NewName.Set).StringVar(&c.NewName.Value) + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagServiceIDName, + Description: argparser.FlagServiceIDDesc, + Dst: &g.Manifest.Flag.ServiceID, + Short: 's', + }) + c.RegisterFlag(argparser.StringFlagOpts{ + Action: c.serviceName.Set, + Name: argparser.FlagServiceName, + Description: argparser.FlagServiceNameDesc, + 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 := argparser.ServiceDetails(argparser.ServiceDetailsOpts{ + Active: optional.Of(false), + Locked: optional.Of(false), + AutoCloneFlag: c.autoClone, + APIClient: c.Globals.APIClient, + Manifest: *c.Globals.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 + } + + c.input.ServiceID = serviceID + c.input.ServiceVersion = fastly.ToValue(serviceVersion.Number) + + // If neither arguments are provided, error with useful message. + if !c.NewName.WasSet && !c.Comment.WasSet { + return fmt.Errorf("error parsing arguments: must provide either --new-name or --comment to update domain") + } + + if c.NewName.WasSet { + c.input.NewName = &c.NewName.Value + } + if c.Comment.WasSet { + c.input.Comment = &c.Comment.Value + } + + d, err := c.Globals.APIClient.UpdateDomain(context.TODO(), &c.input) + if err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]any{ + "Service ID": serviceID, + "Service Version": fastly.ToValue(serviceVersion.Number), + "New Name": c.NewName.Value, + "Comment": c.Comment.Value, + }) + return err + } + + text.Success(out, "Updated domain %s (service %s version %d)", fastly.ToValue(d.Name), fastly.ToValue(d.ServiceID), fastly.ToValue(d.ServiceVersion)) + return nil +} diff --git a/pkg/commands/domain/validate.go b/pkg/commands/service/domain/validate.go similarity index 100% rename from pkg/commands/domain/validate.go rename to pkg/commands/service/domain/validate.go