From 8d91a2891e1707b85da830e41cd45514a7b209c2 Mon Sep 17 00:00:00 2001 From: Colton McCurdy Date: Thu, 14 May 2020 19:17:39 -0400 Subject: [PATCH] logging: adds DigitalOcean Spaces logging endpoint support Signed-off-by: Colton McCurdy --- pkg/api/interface.go | 6 + pkg/app/run.go | 15 + pkg/app/run_test.go | 105 ++++ pkg/logging/digitalocean/create.go | 147 ++++++ pkg/logging/digitalocean/delete.go | 47 ++ pkg/logging/digitalocean/describe.go | 65 +++ .../digitalocean_integration_test.go | 489 ++++++++++++++++++ pkg/logging/digitalocean/digitalocean_test.go | 280 ++++++++++ pkg/logging/digitalocean/doc.go | 3 + pkg/logging/digitalocean/list.go | 81 +++ pkg/logging/digitalocean/root.go | 28 + pkg/logging/digitalocean/update.go | 187 +++++++ pkg/mock/api.go | 31 ++ 13 files changed, 1484 insertions(+) create mode 100644 pkg/logging/digitalocean/create.go create mode 100644 pkg/logging/digitalocean/delete.go create mode 100644 pkg/logging/digitalocean/describe.go create mode 100644 pkg/logging/digitalocean/digitalocean_integration_test.go create mode 100644 pkg/logging/digitalocean/digitalocean_test.go create mode 100644 pkg/logging/digitalocean/doc.go create mode 100644 pkg/logging/digitalocean/list.go create mode 100644 pkg/logging/digitalocean/root.go create mode 100644 pkg/logging/digitalocean/update.go diff --git a/pkg/api/interface.go b/pkg/api/interface.go index 98c64e839..4bda6c7ea 100644 --- a/pkg/api/interface.go +++ b/pkg/api/interface.go @@ -148,6 +148,12 @@ type Interface interface { UpdateCloudfiles(*fastly.UpdateCloudfilesInput) (*fastly.Cloudfiles, error) DeleteCloudfiles(*fastly.DeleteCloudfilesInput) error + CreateDigitalOcean(*fastly.CreateDigitalOceanInput) (*fastly.DigitalOcean, error) + ListDigitalOceans(*fastly.ListDigitalOceansInput) ([]*fastly.DigitalOcean, error) + GetDigitalOcean(*fastly.GetDigitalOceanInput) (*fastly.DigitalOcean, error) + UpdateDigitalOcean(*fastly.UpdateDigitalOceanInput) (*fastly.DigitalOcean, error) + DeleteDigitalOcean(*fastly.DeleteDigitalOceanInput) error + GetUser(*fastly.GetUserInput) (*fastly.User, error) GetRegions() (*fastly.RegionsResponse, error) diff --git a/pkg/app/run.go b/pkg/app/run.go index fb4fad8ad..ce9f4aef9 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -21,6 +21,7 @@ import ( "github.com/fastly/cli/pkg/logging" "github.com/fastly/cli/pkg/logging/bigquery" "github.com/fastly/cli/pkg/logging/cloudfiles" + "github.com/fastly/cli/pkg/logging/digitalocean" "github.com/fastly/cli/pkg/logging/ftp" "github.com/fastly/cli/pkg/logging/gcs" "github.com/fastly/cli/pkg/logging/heroku" @@ -247,6 +248,13 @@ func Run(args []string, env config.Environment, file config.File, configFilePath cloudfilesUpdate := cloudfiles.NewUpdateCommand(cloudfilesRoot.CmdClause, &globals) cloudfilesDelete := cloudfiles.NewDeleteCommand(cloudfilesRoot.CmdClause, &globals) + digitaloceanRoot := digitalocean.NewRootCommand(loggingRoot.CmdClause, &globals) + digitaloceanCreate := digitalocean.NewCreateCommand(digitaloceanRoot.CmdClause, &globals) + digitaloceanList := digitalocean.NewListCommand(digitaloceanRoot.CmdClause, &globals) + digitaloceanDescribe := digitalocean.NewDescribeCommand(digitaloceanRoot.CmdClause, &globals) + digitaloceanUpdate := digitalocean.NewUpdateCommand(digitaloceanRoot.CmdClause, &globals) + digitaloceanDelete := digitalocean.NewDeleteCommand(digitaloceanRoot.CmdClause, &globals) + statsRoot := stats.NewRootCommand(app, &globals) statsRegions := stats.NewRegionsCommand(statsRoot.CmdClause, &globals) statsHistorical := stats.NewHistoricalCommand(statsRoot.CmdClause, &globals) @@ -415,6 +423,13 @@ func Run(args []string, env config.Environment, file config.File, configFilePath cloudfilesUpdate, cloudfilesDelete, + digitaloceanRoot, + digitaloceanCreate, + digitaloceanList, + digitaloceanDescribe, + digitaloceanUpdate, + digitaloceanDelete, + statsRoot, statsRegions, statsHistorical, diff --git a/pkg/app/run_test.go b/pkg/app/run_test.go index e6b2f6c6c..4bd06c9ea 100644 --- a/pkg/app/run_test.go +++ b/pkg/app/run_test.go @@ -1910,6 +1910,111 @@ COMMANDS --version=VERSION Number of service version -n, --name=NAME The name of the Cloudfiles logging object + logging digitalocean create --name=NAME --version=VERSION --bucket=BUCKET --access-key=ACCESS-KEY --secret-key=SECRET-KEY [] + Create a DigitalOcean Spaces logging endpoint on a Fastly service version + + -n, --name=NAME The name of the DigitalOcean Spaces logging + object. Used as a primary key for API access + -s, --service-id=SERVICE-ID Service ID + --version=VERSION Number of service version + --bucket=BUCKET The name of the DigitalOcean Space + --access-key=ACCESS-KEY Your DigitalOcean Spaces account access key + --secret-key=SECRET-KEY Your DigitalOcean Spaces account secret key + --domain=DOMAIN The domain of the DigitalOcean Spaces endpoint + (default 'nyc3.digitaloceanspaces.com') + --path=PATH The path to upload logs to + --period=PERIOD How frequently log files are finalized so they + can be available for reading (in seconds, + default 3600) + --gzip-level=GZIP-LEVEL What level of GZIP encoding to have when + dumping logs (default 0, no compression) + --format=FORMAT Apache style log formatting + --message-type=MESSAGE-TYPE + How the message should be formatted. One of: + classic (default), loggly, logplex or blank + --format-version=FORMAT-VERSION + The version of the custom logging format used + for the configured endpoint. Can be either 2 + (default) or 1 + --response-condition=RESPONSE-CONDITION + The name of an existing condition in the + configured endpoint, or leave blank to always + execute + --timestamp-format=TIMESTAMP-FORMAT + strftime specified timestamp formatting + (default "%Y-%m-%dT%H:%M:%S.000") + --placement=PLACEMENT Where in the generated VCL the logging call + should be placed, overriding any format_version + default. Can be none or waf_debug + --public-key=PUBLIC-KEY A PGP public key that Fastly will use to + encrypt your log files before writing them to + disk + + logging digitalocean list --version=VERSION [] + List DigitalOcean Spaces logging endpoints on a Fastly service version + + -s, --service-id=SERVICE-ID Service ID + --version=VERSION Number of service version + + logging digitalocean describe --version=VERSION --name=NAME [] + Show detailed information about a DigitalOcean Spaces logging endpoint on a + Fastly service version + + -s, --service-id=SERVICE-ID Service ID + --version=VERSION Number of service version + -d, --name=NAME The name of the DigitalOcean Spaces logging + object + + logging digitalocean update --version=VERSION --name=NAME [] + Update a DigitalOcean Spaces logging endpoint on a Fastly service version + + -s, --service-id=SERVICE-ID Service ID + --version=VERSION Number of service version + -n, --name=NAME The name of the DigitalOcean Spaces logging + object + --new-name=NEW-NAME New name of the DigitalOcean Spaces logging + object + --bucket=BUCKET The name of the DigitalOcean Space + --domain=DOMAIN The domain of the DigitalOcean Spaces endpoint + (default 'nyc3.digitaloceanspaces.com') + --access-key=ACCESS-KEY Your DigitalOcean Spaces account access key + --secret-key=SECRET-KEY Your DigitalOcean Spaces account secret key + --path=PATH The path to upload logs to + --period=PERIOD How frequently log files are finalized so they + can be available for reading (in seconds, + default 3600) + --gzip-level=GZIP-LEVEL What level of GZIP encoding to have when + dumping logs (default 0, no compression) + --format=FORMAT Apache style log formatting + --format-version=FORMAT-VERSION + The version of the custom logging format used + for the configured endpoint. Can be either 2 + (default) or 1 + --response-condition=RESPONSE-CONDITION + The name of an existing condition in the + configured endpoint, or leave blank to always + execute + --message-type=MESSAGE-TYPE + How the message should be formatted. One of: + classic (default), loggly, logplex or blank + --timestamp-format=TIMESTAMP-FORMAT + strftime specified timestamp formatting + (default "%Y-%m-%dT%H:%M:%S.000") + --placement=PLACEMENT Where in the generated VCL the logging call + should be placed, overriding any format_version + default. Can be none or waf_debug + --public-key=PUBLIC-KEY A PGP public key that Fastly will use to + encrypt your log files before writing them to + disk + + logging digitalocean delete --version=VERSION --name=NAME [] + Delete a DigitalOcean Spaces logging endpoint on a Fastly service version + + -s, --service-id=SERVICE-ID Service ID + --version=VERSION Number of service version + -n, --name=NAME The name of the DigitalOcean Spaces logging + object + stats regions List stats regions diff --git a/pkg/logging/digitalocean/create.go b/pkg/logging/digitalocean/create.go new file mode 100644 index 000000000..d8d5db5ab --- /dev/null +++ b/pkg/logging/digitalocean/create.go @@ -0,0 +1,147 @@ +package digitalocean + +import ( + "io" + + "github.com/fastly/cli/pkg/common" + "github.com/fastly/cli/pkg/compute/manifest" + "github.com/fastly/cli/pkg/config" + "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/text" + "github.com/fastly/go-fastly/fastly" +) + +// CreateCommand calls the Fastly API to create DigitalOcean Spaces logging endpoints. +type CreateCommand struct { + common.Base + manifest manifest.Data + + // required + EndpointName string // Can't shaddow common.Base method Name(). + Version int + BucketName string + AccessKey string + SecretKey string + + // optional + Domain common.OptionalString + Path common.OptionalString + Period common.OptionalUint + GzipLevel common.OptionalUint + MessageType common.OptionalString + Format common.OptionalString + FormatVersion common.OptionalUint + ResponseCondition common.OptionalString + TimestampFormat common.OptionalString + Placement common.OptionalString + PublicKey common.OptionalString +} + +// NewCreateCommand returns a usable command registered under the parent. +func NewCreateCommand(parent common.Registerer, globals *config.Data) *CreateCommand { + var c CreateCommand + c.Globals = globals + c.manifest.File.Read(manifest.Filename) + c.CmdClause = parent.Command("create", "Create a DigitalOcean Spaces logging endpoint on a Fastly service version").Alias("add") + + c.CmdClause.Flag("name", "The name of the DigitalOcean Spaces logging object. Used as a primary key for API access").Short('n').Required().StringVar(&c.EndpointName) + c.CmdClause.Flag("service-id", "Service ID").Short('s').StringVar(&c.manifest.Flag.ServiceID) + c.CmdClause.Flag("version", "Number of service version").Required().IntVar(&c.Version) + + c.CmdClause.Flag("bucket", "The name of the DigitalOcean Space").Required().StringVar(&c.BucketName) + c.CmdClause.Flag("access-key", "Your DigitalOcean Spaces account access key").Required().StringVar(&c.AccessKey) + c.CmdClause.Flag("secret-key", "Your DigitalOcean Spaces account secret key").Required().StringVar(&c.SecretKey) + + c.CmdClause.Flag("domain", "The domain of the DigitalOcean Spaces endpoint (default 'nyc3.digitaloceanspaces.com')").Action(c.Domain.Set).StringVar(&c.Domain.Value) + c.CmdClause.Flag("path", "The path to upload logs to").Action(c.Path.Set).StringVar(&c.Path.Value) + c.CmdClause.Flag("period", "How frequently log files are finalized so they can be available for reading (in seconds, default 3600)").Action(c.Period.Set).UintVar(&c.Period.Value) + c.CmdClause.Flag("gzip-level", "What level of GZIP encoding to have when dumping logs (default 0, no compression)").Action(c.GzipLevel.Set).UintVar(&c.GzipLevel.Value) + c.CmdClause.Flag("format", "Apache style log formatting").Action(c.Format.Set).StringVar(&c.Format.Value) + c.CmdClause.Flag("message-type", "How the message should be formatted. One of: classic (default), loggly, logplex or blank").Action(c.MessageType.Set).StringVar(&c.MessageType.Value) + c.CmdClause.Flag("format-version", "The version of the custom logging format used for the configured endpoint. Can be either 2 (default) or 1").Action(c.FormatVersion.Set).UintVar(&c.FormatVersion.Value) + c.CmdClause.Flag("response-condition", "The name of an existing condition in the configured endpoint, or leave blank to always execute").Action(c.ResponseCondition.Set).StringVar(&c.ResponseCondition.Value) + c.CmdClause.Flag("timestamp-format", `strftime specified timestamp formatting (default "%Y-%m-%dT%H:%M:%S.000")`).Action(c.TimestampFormat.Set).StringVar(&c.TimestampFormat.Value) + c.CmdClause.Flag("placement", "Where in the generated VCL the logging call should be placed, overriding any format_version default. Can be none or waf_debug").Action(c.Placement.Set).StringVar(&c.Placement.Value) + c.CmdClause.Flag("public-key", "A PGP public key that Fastly will use to encrypt your log files before writing them to disk").Action(c.PublicKey.Set).StringVar(&c.PublicKey.Value) + + return &c +} + +// createInput transforms values parsed from CLI flags into an object to be used by the API client library. +func (c *CreateCommand) createInput() (*fastly.CreateDigitalOceanInput, error) { + var input fastly.CreateDigitalOceanInput + + serviceID, source := c.manifest.ServiceID() + if source == manifest.SourceUndefined { + return nil, errors.ErrNoServiceID + } + + input.Service = serviceID + input.Version = c.Version + input.Name = fastly.String(c.EndpointName) + input.BucketName = fastly.String(c.BucketName) + input.AccessKey = fastly.String(c.AccessKey) + input.SecretKey = fastly.String(c.SecretKey) + + if c.Domain.Valid { + input.Domain = fastly.String(c.Domain.Value) + } + + if c.Path.Valid { + input.Path = fastly.String(c.Path.Value) + } + + if c.Period.Valid { + input.Period = fastly.Uint(c.Period.Value) + } + + if c.GzipLevel.Valid { + input.GzipLevel = fastly.Uint(c.GzipLevel.Value) + } + + if c.Format.Valid { + input.Format = fastly.String(c.Format.Value) + } + + if c.FormatVersion.Valid { + input.FormatVersion = fastly.Uint(c.FormatVersion.Value) + } + + if c.ResponseCondition.Valid { + input.ResponseCondition = fastly.String(c.ResponseCondition.Value) + } + + if c.MessageType.Valid { + input.MessageType = fastly.String(c.MessageType.Value) + } + + if c.TimestampFormat.Valid { + input.TimestampFormat = fastly.String(c.TimestampFormat.Value) + } + + if c.Placement.Valid { + input.Placement = fastly.String(c.Placement.Value) + } + + if c.PublicKey.Valid { + input.PublicKey = fastly.String(c.PublicKey.Value) + } + + return &input, nil +} + +// Exec invokes the application logic for the command. +func (c *CreateCommand) Exec(in io.Reader, out io.Writer) error { + input, err := c.createInput() + if err != nil { + return err + } + + d, err := c.Globals.Client.CreateDigitalOcean(input) + if err != nil { + return err + } + + text.Success(out, "Created DigitalOcean Spaces logging endpoint %s (service %s version %d)", d.Name, d.ServiceID, d.Version) + return nil +} diff --git a/pkg/logging/digitalocean/delete.go b/pkg/logging/digitalocean/delete.go new file mode 100644 index 000000000..d1064e100 --- /dev/null +++ b/pkg/logging/digitalocean/delete.go @@ -0,0 +1,47 @@ +package digitalocean + +import ( + "io" + + "github.com/fastly/cli/pkg/common" + "github.com/fastly/cli/pkg/compute/manifest" + "github.com/fastly/cli/pkg/config" + "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/text" + "github.com/fastly/go-fastly/fastly" +) + +// DeleteCommand calls the Fastly API to delete DigitalOcean Spaces logging endpoints. +type DeleteCommand struct { + common.Base + manifest manifest.Data + Input fastly.DeleteDigitalOceanInput +} + +// NewDeleteCommand returns a usable command registered under the parent. +func NewDeleteCommand(parent common.Registerer, globals *config.Data) *DeleteCommand { + var c DeleteCommand + c.Globals = globals + c.manifest.File.Read(manifest.Filename) + c.CmdClause = parent.Command("delete", "Delete a DigitalOcean Spaces logging endpoint on a Fastly service version").Alias("remove") + c.CmdClause.Flag("service-id", "Service ID").Short('s').StringVar(&c.manifest.Flag.ServiceID) + c.CmdClause.Flag("version", "Number of service version").Required().IntVar(&c.Input.Version) + c.CmdClause.Flag("name", "The name of the DigitalOcean Spaces logging object").Short('n').Required().StringVar(&c.Input.Name) + return &c +} + +// Exec invokes the application logic for the command. +func (c *DeleteCommand) Exec(in io.Reader, out io.Writer) error { + serviceID, source := c.manifest.ServiceID() + if source == manifest.SourceUndefined { + return errors.ErrNoServiceID + } + c.Input.Service = serviceID + + if err := c.Globals.Client.DeleteDigitalOcean(&c.Input); err != nil { + return err + } + + text.Success(out, "Deleted DigitalOcean Spaces logging endpoint %s (service %s version %d)", c.Input.Name, c.Input.Service, c.Input.Version) + return nil +} diff --git a/pkg/logging/digitalocean/describe.go b/pkg/logging/digitalocean/describe.go new file mode 100644 index 000000000..4c8b06eb5 --- /dev/null +++ b/pkg/logging/digitalocean/describe.go @@ -0,0 +1,65 @@ +package digitalocean + +import ( + "fmt" + "io" + + "github.com/fastly/cli/pkg/common" + "github.com/fastly/cli/pkg/compute/manifest" + "github.com/fastly/cli/pkg/config" + "github.com/fastly/cli/pkg/errors" + "github.com/fastly/go-fastly/fastly" +) + +// DescribeCommand calls the Fastly API to describe a DigitalOcean Spaces logging endpoint. +type DescribeCommand struct { + common.Base + manifest manifest.Data + Input fastly.GetDigitalOceanInput +} + +// NewDescribeCommand returns a usable command registered under the parent. +func NewDescribeCommand(parent common.Registerer, globals *config.Data) *DescribeCommand { + var c DescribeCommand + c.Globals = globals + c.manifest.File.Read(manifest.Filename) + c.CmdClause = parent.Command("describe", "Show detailed information about a DigitalOcean Spaces logging endpoint on a Fastly service version").Alias("get") + c.CmdClause.Flag("service-id", "Service ID").Short('s').StringVar(&c.manifest.Flag.ServiceID) + c.CmdClause.Flag("version", "Number of service version").Required().IntVar(&c.Input.Version) + c.CmdClause.Flag("name", "The name of the DigitalOcean Spaces logging object").Short('d').Required().StringVar(&c.Input.Name) + return &c +} + +// Exec invokes the application logic for the command. +func (c *DescribeCommand) Exec(in io.Reader, out io.Writer) error { + serviceID, source := c.manifest.ServiceID() + if source == manifest.SourceUndefined { + return errors.ErrNoServiceID + } + c.Input.Service = serviceID + + digitalocean, err := c.Globals.Client.GetDigitalOcean(&c.Input) + if err != nil { + return err + } + + fmt.Fprintf(out, "Service ID: %s\n", digitalocean.ServiceID) + fmt.Fprintf(out, "Version: %d\n", digitalocean.Version) + fmt.Fprintf(out, "Name: %s\n", digitalocean.Name) + fmt.Fprintf(out, "Bucket: %s\n", digitalocean.BucketName) + fmt.Fprintf(out, "Domain: %s\n", digitalocean.Domain) + fmt.Fprintf(out, "Access key: %s\n", digitalocean.AccessKey) + fmt.Fprintf(out, "Secret key: %s\n", digitalocean.SecretKey) + fmt.Fprintf(out, "Path: %s\n", digitalocean.Path) + fmt.Fprintf(out, "Period: %d\n", digitalocean.Period) + fmt.Fprintf(out, "GZip level: %d\n", digitalocean.GzipLevel) + fmt.Fprintf(out, "Format: %s\n", digitalocean.Format) + fmt.Fprintf(out, "Format version: %d\n", digitalocean.FormatVersion) + fmt.Fprintf(out, "Response condition: %s\n", digitalocean.ResponseCondition) + fmt.Fprintf(out, "Message type: %s\n", digitalocean.MessageType) + fmt.Fprintf(out, "Timestamp format: %s\n", digitalocean.TimestampFormat) + fmt.Fprintf(out, "Placement: %s\n", digitalocean.Placement) + fmt.Fprintf(out, "Public key: %s\n", digitalocean.PublicKey) + + return nil +} diff --git a/pkg/logging/digitalocean/digitalocean_integration_test.go b/pkg/logging/digitalocean/digitalocean_integration_test.go new file mode 100644 index 000000000..8e4e686e1 --- /dev/null +++ b/pkg/logging/digitalocean/digitalocean_integration_test.go @@ -0,0 +1,489 @@ +package digitalocean_test + +import ( + "bytes" + "errors" + "io" + "net/http" + "strings" + "testing" + + "github.com/fastly/cli/pkg/app" + "github.com/fastly/cli/pkg/config" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" + "github.com/fastly/cli/pkg/update" + "github.com/fastly/go-fastly/fastly" +) + +func TestDigitalOceanCreate(t *testing.T) { + for _, testcase := range []struct { + args []string + api mock.API + wantError string + wantOutput string + }{ + { + args: []string{"logging", "digitalocean", "create", "--service-id", "123", "--version", "1", "--name", "log", "--access-key", "foo", "--secret-key", "abc"}, + wantError: "error parsing arguments: required flag --bucket not provided", + }, + { + args: []string{"logging", "digitalocean", "create", "--service-id", "123", "--version", "1", "--name", "log", "--bucket", "log", "--secret-key", "abc"}, + wantError: "error parsing arguments: required flag --access-key not provided", + }, + { + args: []string{"logging", "digitalocean", "create", "--service-id", "123", "--version", "1", "--name", "log", "--bucket", "log", "--access-key", "foo"}, + wantError: "error parsing arguments: required flag --secret-key not provided", + }, + { + args: []string{"logging", "digitalocean", "create", "--service-id", "123", "--version", "1", "--name", "log", "--bucket", "log", "--access-key", "foo", "--secret-key", "abc"}, + api: mock.API{CreateDigitalOceanFn: createDigitalOceanOK}, + wantOutput: "Created DigitalOcean Spaces logging endpoint log (service 123 version 1)", + }, + { + args: []string{"logging", "digitalocean", "create", "--service-id", "123", "--version", "1", "--name", "log", "--bucket", "log", "--access-key", "foo", "--secret-key", "abc"}, + api: mock.API{CreateDigitalOceanFn: createDigitalOceanError}, + wantError: errTest.Error(), + }, + } { + t.Run(strings.Join(testcase.args, " "), func(t *testing.T) { + var ( + args = testcase.args + env = config.Environment{} + file = config.File{} + appConfigFile = "/dev/null" + clientFactory = mock.APIClient(testcase.api) + httpClient = http.DefaultClient + versioner update.Versioner = nil + in io.Reader = nil + out bytes.Buffer + ) + err := app.Run(args, env, file, appConfigFile, clientFactory, httpClient, versioner, in, &out) + testutil.AssertErrorContains(t, err, testcase.wantError) + testutil.AssertStringContains(t, out.String(), testcase.wantOutput) + }) + } +} + +func TestDigitalOceanList(t *testing.T) { + for _, testcase := range []struct { + args []string + api mock.API + wantError string + wantOutput string + }{ + { + args: []string{"logging", "digitalocean", "list", "--service-id", "123", "--version", "1"}, + api: mock.API{ListDigitalOceansFn: listDigitalOceansOK}, + wantOutput: listDigitalOceansShortOutput, + }, + { + args: []string{"logging", "digitalocean", "list", "--service-id", "123", "--version", "1", "--verbose"}, + api: mock.API{ListDigitalOceansFn: listDigitalOceansOK}, + wantOutput: listDigitalOceansVerboseOutput, + }, + { + args: []string{"logging", "digitalocean", "list", "--service-id", "123", "--version", "1", "-v"}, + api: mock.API{ListDigitalOceansFn: listDigitalOceansOK}, + wantOutput: listDigitalOceansVerboseOutput, + }, + { + args: []string{"logging", "digitalocean", "--verbose", "list", "--service-id", "123", "--version", "1"}, + api: mock.API{ListDigitalOceansFn: listDigitalOceansOK}, + wantOutput: listDigitalOceansVerboseOutput, + }, + { + args: []string{"logging", "-v", "digitalocean", "list", "--service-id", "123", "--version", "1"}, + api: mock.API{ListDigitalOceansFn: listDigitalOceansOK}, + wantOutput: listDigitalOceansVerboseOutput, + }, + { + args: []string{"logging", "digitalocean", "list", "--service-id", "123", "--version", "1"}, + api: mock.API{ListDigitalOceansFn: listDigitalOceansError}, + wantError: errTest.Error(), + }, + } { + t.Run(strings.Join(testcase.args, " "), func(t *testing.T) { + var ( + args = testcase.args + env = config.Environment{} + file = config.File{} + appConfigFile = "/dev/null" + clientFactory = mock.APIClient(testcase.api) + httpClient = http.DefaultClient + versioner update.Versioner = nil + in io.Reader = nil + out bytes.Buffer + ) + err := app.Run(args, env, file, appConfigFile, clientFactory, httpClient, versioner, in, &out) + testutil.AssertErrorContains(t, err, testcase.wantError) + testutil.AssertString(t, testcase.wantOutput, out.String()) + }) + } +} + +func TestDigitalOceanDescribe(t *testing.T) { + for _, testcase := range []struct { + args []string + api mock.API + wantError string + wantOutput string + }{ + { + args: []string{"logging", "digitalocean", "describe", "--service-id", "123", "--version", "1"}, + wantError: "error parsing arguments: required flag --name not provided", + }, + { + args: []string{"logging", "digitalocean", "describe", "--service-id", "123", "--version", "1", "--name", "logs"}, + api: mock.API{GetDigitalOceanFn: getDigitalOceanError}, + wantError: errTest.Error(), + }, + { + args: []string{"logging", "digitalocean", "describe", "--service-id", "123", "--version", "1", "--name", "logs"}, + api: mock.API{GetDigitalOceanFn: getDigitalOceanOK}, + wantOutput: describeDigitalOceanOutput, + }, + } { + t.Run(strings.Join(testcase.args, " "), func(t *testing.T) { + var ( + args = testcase.args + env = config.Environment{} + file = config.File{} + appConfigFile = "/dev/null" + clientFactory = mock.APIClient(testcase.api) + httpClient = http.DefaultClient + versioner update.Versioner = nil + in io.Reader = nil + out bytes.Buffer + ) + err := app.Run(args, env, file, appConfigFile, clientFactory, httpClient, versioner, in, &out) + testutil.AssertErrorContains(t, err, testcase.wantError) + testutil.AssertString(t, testcase.wantOutput, out.String()) + }) + } +} + +func TestDigitalOceanUpdate(t *testing.T) { + for _, testcase := range []struct { + args []string + api mock.API + wantError string + wantOutput string + }{ + { + args: []string{"logging", "digitalocean", "update", "--service-id", "123", "--version", "1", "--new-name", "log"}, + wantError: "error parsing arguments: required flag --name not provided", + }, + { + args: []string{"logging", "digitalocean", "update", "--service-id", "123", "--version", "1", "--name", "logs", "--new-name", "log"}, + api: mock.API{ + GetDigitalOceanFn: getDigitalOceanError, + UpdateDigitalOceanFn: updateDigitalOceanOK, + }, + wantError: errTest.Error(), + }, + { + args: []string{"logging", "digitalocean", "update", "--service-id", "123", "--version", "1", "--name", "logs", "--new-name", "log"}, + api: mock.API{ + GetDigitalOceanFn: getDigitalOceanOK, + UpdateDigitalOceanFn: updateDigitalOceanError, + }, + wantError: errTest.Error(), + }, + { + args: []string{"logging", "digitalocean", "update", "--service-id", "123", "--version", "1", "--name", "logs", "--new-name", "log"}, + api: mock.API{ + GetDigitalOceanFn: getDigitalOceanOK, + UpdateDigitalOceanFn: updateDigitalOceanOK, + }, + wantOutput: "Updated DigitalOcean Spaces logging endpoint log (service 123 version 1)", + }, + } { + t.Run(strings.Join(testcase.args, " "), func(t *testing.T) { + var ( + args = testcase.args + env = config.Environment{} + file = config.File{} + appConfigFile = "/dev/null" + clientFactory = mock.APIClient(testcase.api) + httpClient = http.DefaultClient + versioner update.Versioner = nil + in io.Reader = nil + out bytes.Buffer + ) + err := app.Run(args, env, file, appConfigFile, clientFactory, httpClient, versioner, in, &out) + testutil.AssertErrorContains(t, err, testcase.wantError) + testutil.AssertStringContains(t, out.String(), testcase.wantOutput) + }) + } +} + +func TestDigitalOceanDelete(t *testing.T) { + for _, testcase := range []struct { + args []string + api mock.API + wantError string + wantOutput string + }{ + { + args: []string{"logging", "digitalocean", "delete", "--service-id", "123", "--version", "1"}, + wantError: "error parsing arguments: required flag --name not provided", + }, + { + args: []string{"logging", "digitalocean", "delete", "--service-id", "123", "--version", "1", "--name", "logs"}, + api: mock.API{DeleteDigitalOceanFn: deleteDigitalOceanError}, + wantError: errTest.Error(), + }, + { + args: []string{"logging", "digitalocean", "delete", "--service-id", "123", "--version", "1", "--name", "logs"}, + api: mock.API{DeleteDigitalOceanFn: deleteDigitalOceanOK}, + wantOutput: "Deleted DigitalOcean Spaces logging endpoint logs (service 123 version 1)", + }, + } { + t.Run(strings.Join(testcase.args, " "), func(t *testing.T) { + var ( + args = testcase.args + env = config.Environment{} + file = config.File{} + appConfigFile = "/dev/null" + clientFactory = mock.APIClient(testcase.api) + httpClient = http.DefaultClient + versioner update.Versioner = nil + in io.Reader = nil + out bytes.Buffer + ) + err := app.Run(args, env, file, appConfigFile, clientFactory, httpClient, versioner, in, &out) + testutil.AssertErrorContains(t, err, testcase.wantError) + testutil.AssertStringContains(t, out.String(), testcase.wantOutput) + }) + } +} + +var errTest = errors.New("fixture error") + +func createDigitalOceanOK(i *fastly.CreateDigitalOceanInput) (*fastly.DigitalOcean, error) { + s := fastly.DigitalOcean{ + ServiceID: i.Service, + Version: i.Version, + } + + if i.Name != nil { + s.Name = *i.Name + } + + return &s, nil +} + +func createDigitalOceanError(i *fastly.CreateDigitalOceanInput) (*fastly.DigitalOcean, error) { + return nil, errTest +} + +func listDigitalOceansOK(i *fastly.ListDigitalOceansInput) ([]*fastly.DigitalOcean, error) { + return []*fastly.DigitalOcean{ + { + ServiceID: i.Service, + Version: i.Version, + Name: "logs", + BucketName: "my-logs", + Domain: "https://digitalocean.us-east-1.amazonaws.com", + AccessKey: "1234", + SecretKey: "-----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCA", + Path: "logs/", + Period: 3600, + GzipLevel: 9, + Format: `%h %l %u %t "%r" %>s %b`, + FormatVersion: 2, + ResponseCondition: "Prevent default logging", + MessageType: "classic", + TimestampFormat: "%Y-%m-%dT%H:%M:%S.000", + Placement: "none", + PublicKey: pgpPublicKey(), + }, + { + ServiceID: i.Service, + Version: i.Version, + Name: "analytics", + BucketName: "analytics", + AccessKey: "1234", + SecretKey: "-----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCA", + Domain: "https://digitalocean.us-east-2.amazonaws.com", + Path: "logs/", + Period: 86400, + GzipLevel: 9, + Format: `%h %l %u %t "%r" %>s %b`, + FormatVersion: 2, + MessageType: "classic", + ResponseCondition: "Prevent default logging", + TimestampFormat: "%Y-%m-%dT%H:%M:%S.000", + Placement: "none", + PublicKey: pgpPublicKey(), + }, + }, nil +} + +func listDigitalOceansError(i *fastly.ListDigitalOceansInput) ([]*fastly.DigitalOcean, error) { + return nil, errTest +} + +var listDigitalOceansShortOutput = strings.TrimSpace(` +SERVICE VERSION NAME +123 1 logs +123 1 analytics +`) + "\n" + +var listDigitalOceansVerboseOutput = strings.TrimSpace(` +Fastly API token not provided +Fastly API endpoint: https://api.fastly.com +Service ID: 123 +Version: 1 + DigitalOcean 1/2 + Service ID: 123 + Version: 1 + Name: logs + Bucket: my-logs + Domain: https://digitalocean.us-east-1.amazonaws.com + Access key: 1234 + Secret key: -----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCA + Path: logs/ + Period: 3600 + GZip level: 9 + Format: %h %l %u %t "%r" %>s %b + Format version: 2 + Response condition: Prevent default logging + Message type: classic + Timestamp format: %Y-%m-%dT%H:%M:%S.000 + Placement: none + Public key: `+pgpPublicKey()+` + DigitalOcean 2/2 + Service ID: 123 + Version: 1 + Name: analytics + Bucket: analytics + Domain: https://digitalocean.us-east-2.amazonaws.com + Access key: 1234 + Secret key: -----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCA + Path: logs/ + Period: 86400 + GZip level: 9 + Format: %h %l %u %t "%r" %>s %b + Format version: 2 + Response condition: Prevent default logging + Message type: classic + Timestamp format: %Y-%m-%dT%H:%M:%S.000 + Placement: none + Public key: `+pgpPublicKey()+` +`) + "\n\n" + +func getDigitalOceanOK(i *fastly.GetDigitalOceanInput) (*fastly.DigitalOcean, error) { + return &fastly.DigitalOcean{ + ServiceID: i.Service, + Version: i.Version, + Name: "logs", + BucketName: "my-logs", + Domain: "https://digitalocean.us-east-1.amazonaws.com", + AccessKey: "1234", + SecretKey: "-----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCA", + Path: "logs/", + Period: 3600, + GzipLevel: 9, + Format: `%h %l %u %t "%r" %>s %b`, + FormatVersion: 2, + ResponseCondition: "Prevent default logging", + MessageType: "classic", + TimestampFormat: "%Y-%m-%dT%H:%M:%S.000", + Placement: "none", + PublicKey: pgpPublicKey(), + }, nil +} + +func getDigitalOceanError(i *fastly.GetDigitalOceanInput) (*fastly.DigitalOcean, error) { + return nil, errTest +} + +var describeDigitalOceanOutput = strings.TrimSpace(` +Service ID: 123 +Version: 1 +Name: logs +Bucket: my-logs +Domain: https://digitalocean.us-east-1.amazonaws.com +Access key: 1234 +Secret key: -----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCA +Path: logs/ +Period: 3600 +GZip level: 9 +Format: %h %l %u %t "%r" %>s %b +Format version: 2 +Response condition: Prevent default logging +Message type: classic +Timestamp format: %Y-%m-%dT%H:%M:%S.000 +Placement: none +Public key: `+pgpPublicKey()+` +`) + "\n" + +func updateDigitalOceanOK(i *fastly.UpdateDigitalOceanInput) (*fastly.DigitalOcean, error) { + return &fastly.DigitalOcean{ + ServiceID: i.Service, + Version: i.Version, + Name: "log", + BucketName: "my-logs", + Domain: "https://digitalocean.us-east-1.amazonaws.com", + AccessKey: "1234", + SecretKey: "-----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCA", + Path: "logs/", + Period: 3600, + GzipLevel: 9, + Format: `%h %l %u %t "%r" %>s %b`, + FormatVersion: 2, + ResponseCondition: "Prevent default logging", + MessageType: "classic", + TimestampFormat: "%Y-%m-%dT%H:%M:%S.000", + Placement: "none", + PublicKey: pgpPublicKey(), + }, nil +} + +func updateDigitalOceanError(i *fastly.UpdateDigitalOceanInput) (*fastly.DigitalOcean, error) { + return nil, errTest +} + +func deleteDigitalOceanOK(i *fastly.DeleteDigitalOceanInput) error { + return nil +} + +func deleteDigitalOceanError(i *fastly.DeleteDigitalOceanInput) error { + return errTest +} + +// pgpPublicKey returns a PEM encoded PGP public key suitable for testing. +func pgpPublicKey() string { + return strings.TrimSpace(`-----BEGIN PGP PUBLIC KEY BLOCK----- +mQENBFyUD8sBCACyFnB39AuuTygseek+eA4fo0cgwva6/FSjnWq7riouQee8GgQ/ +ibXTRyv4iVlwI12GswvMTIy7zNvs1R54i0qvsLr+IZ4GVGJqs6ZJnvQcqe3xPoR4 +8AnBfw90o32r/LuHf6QCJXi+AEu35koNlNAvLJ2B+KACaNB7N0EeWmqpV/1V2k9p +lDYk+th7LcCuaFNGqKS/PrMnnMqR6VDLCjHhNx4KR79b0Twm/2qp6an3hyNRu8Gn +dwxpf1/BUu3JWf+LqkN4Y3mbOmSUL3MaJNvyQguUzTfS0P0uGuBDHrJCVkMZCzDB +89ag55jCPHyGeHBTd02gHMWzsg3WMBWvCsrzABEBAAG0JXRlcnJhZm9ybSAodGVz +dCkgPHRlc3RAdGVycmFmb3JtLmNvbT6JAU4EEwEIADgWIQSHYyc6Kj9l6HzQsau6 +vFFc9jxV/wUCXJQPywIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRC6vFFc +9jxV/815CAClb32OxV7wG01yF97TzlyTl8TnvjMtoG29Mw4nSyg+mjM3b8N7iXm9 +OLX59fbDAWtBSldSZE22RXd3CvlFOG/EnKBXSjBtEqfyxYSnyOPkMPBYWGL/ApkX +SvPYJ4LKdvipYToKFh3y9kk2gk1DcDBDyaaHvR+3rv1u3aoy7/s2EltAfDS3ZQIq +7/cWTLJml/lleeB/Y6rPj8xqeCYhE5ahw9gsV/Mdqatl24V9Tks30iijx0Hhw+Gx +kATUikMGr2GDVqoIRga5kXI7CzYff4rkc0Twn47fMHHHe/KY9M2yVnMHUXmAZwbG +M1cMI/NH1DjevCKdGBLcRJlhuLPKF/anuQENBFyUD8sBCADIpd7r7GuPd6n/Ikxe +u6h7umV6IIPoAm88xCYpTbSZiaK30Svh6Ywra9jfE2KlU9o6Y/art8ip0VJ3m07L +4RSfSpnzqgSwdjSq5hNour2Fo/BzYhK7yaz2AzVSbe33R0+RYhb4b/6N+bKbjwGF +ftCsqVFMH+PyvYkLbvxyQrHlA9woAZaNThI1ztO5rGSnGUR8xt84eup28WIFKg0K +UEGUcTzz+8QGAwAra+0ewPXo/AkO+8BvZjDidP417u6gpBHOJ9qYIcO9FxHeqFyu +YrjlrxowEgXn5wO8xuNz6Vu1vhHGDHGDsRbZF8pv1d5O+0F1G7ttZ2GRRgVBZPwi +kiyRABEBAAGJATYEGAEIACAWIQSHYyc6Kj9l6HzQsau6vFFc9jxV/wUCXJQPywIb +DAAKCRC6vFFc9jxV/9YOCACe8qmOSnKQpQfW+PqYOqo3dt7JyweTs3FkD6NT8Zml +dYy/vkstbTjPpX6aTvUZjkb46BVi7AOneVHpD5GBqvRsZ9iVgDYHaehmLCdKiG5L +3Tp90NN+QY5WDbsGmsyk6+6ZMYejb4qYfweQeduOj27aavCJdLkCYMoRKfcFYI8c +FaNmEfKKy/r1PO20NXEG6t9t05K/frHy6ZG8bCNYdpagfFVot47r9JaQqWlTNtIR +5+zkkSq/eG9BEtRij3a6cTdQbktdBzx2KBeI0PYc1vlZR0LpuFKZqY9vlE6vTGLR +wMfrTEOvx0NxUM3rpaCgEmuWbB1G1Hu371oyr4srrr+N +=28dr +-----END PGP PUBLIC KEY BLOCK----- +`) +} diff --git a/pkg/logging/digitalocean/digitalocean_test.go b/pkg/logging/digitalocean/digitalocean_test.go new file mode 100644 index 000000000..150b53dc2 --- /dev/null +++ b/pkg/logging/digitalocean/digitalocean_test.go @@ -0,0 +1,280 @@ +package digitalocean + +import ( + "strings" + "testing" + + "github.com/fastly/cli/pkg/common" + "github.com/fastly/cli/pkg/compute/manifest" + "github.com/fastly/cli/pkg/config" + "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" + "github.com/fastly/go-fastly/fastly" +) + +func TestCreateDigitalOceanInput(t *testing.T) { + for _, testcase := range []struct { + name string + cmd *CreateCommand + want *fastly.CreateDigitalOceanInput + wantError string + }{ + { + name: "required values set flag serviceID", + cmd: createCommandRequired(), + want: &fastly.CreateDigitalOceanInput{ + Service: "123", + Version: 2, + Name: fastly.String("log"), + BucketName: fastly.String("bucket"), + AccessKey: fastly.String("access"), + SecretKey: fastly.String("secret"), + }, + }, + { + name: "all values set flag serviceID", + cmd: createCommandAll(), + want: &fastly.CreateDigitalOceanInput{ + Service: "123", + Version: 2, + Name: fastly.String("log"), + BucketName: fastly.String("bucket"), + Domain: fastly.String("nyc3.digitaloceanspaces.com"), + AccessKey: fastly.String("access"), + SecretKey: fastly.String("secret"), + Path: fastly.String("/log"), + Period: fastly.Uint(3600), + GzipLevel: fastly.Uint(2), + Format: fastly.String(`%h %l %u %t "%r" %>s %b`), + MessageType: fastly.String("classic"), + FormatVersion: fastly.Uint(2), + ResponseCondition: fastly.String("Prevent default logging"), + TimestampFormat: fastly.String("%Y-%m-%dT%H:%M:%S.000"), + Placement: fastly.String("none"), + PublicKey: fastly.String(pgpPublicKey()), + }, + }, + { + name: "error missing serviceID", + cmd: createCommandMissingServiceID(), + want: nil, + wantError: errors.ErrNoServiceID.Error(), + }, + } { + t.Run(testcase.name, func(t *testing.T) { + have, err := testcase.cmd.createInput() + testutil.AssertErrorContains(t, err, testcase.wantError) + testutil.AssertEqual(t, testcase.want, have) + }) + } +} + +func TestUpdateDigitalOceanInput(t *testing.T) { + for _, testcase := range []struct { + name string + cmd *UpdateCommand + api mock.API + want *fastly.UpdateDigitalOceanInput + wantError string + }{ + { + name: "all values set flag serviceID", + cmd: updateCommandAll(), + api: mock.API{GetDigitalOceanFn: getDigitalOceanOK}, + want: &fastly.UpdateDigitalOceanInput{ + Service: "123", + Version: 2, + Name: "logs", + NewName: fastly.String("new1"), + BucketName: fastly.String("new2"), + Domain: fastly.String("new3"), + AccessKey: fastly.String("new4"), + SecretKey: fastly.String("new5"), + Path: fastly.String("new6"), + Period: fastly.Uint(3601), + GzipLevel: fastly.Uint(3), + Format: fastly.String("new7"), + FormatVersion: fastly.Uint(3), + ResponseCondition: fastly.String("new8"), + MessageType: fastly.String("new9"), + TimestampFormat: fastly.String("new10"), + Placement: fastly.String("new11"), + PublicKey: fastly.String("new12"), + }, + }, + { + name: "no updates", + cmd: updateCommandNoUpdates(), + api: mock.API{GetDigitalOceanFn: getDigitalOceanOK}, + want: &fastly.UpdateDigitalOceanInput{ + Service: "123", + Version: 2, + Name: "logs", + NewName: fastly.String("logs"), + BucketName: fastly.String("bucket"), + Domain: fastly.String("nyc3.digitaloceanspaces.com"), + AccessKey: fastly.String("access"), + SecretKey: fastly.String("secret"), + Path: fastly.String("/log"), + Period: fastly.Uint(3600), + GzipLevel: fastly.Uint(2), + Format: fastly.String(`%h %l %u %t "%r" %>s %b`), + FormatVersion: fastly.Uint(2), + ResponseCondition: fastly.String("Prevent default logging"), + MessageType: fastly.String("classic"), + TimestampFormat: fastly.String("%Y-%m-%dT%H:%M:%S.000"), + Placement: fastly.String("none"), + PublicKey: fastly.String(pgpPublicKey()), + }, + }, + { + name: "error missing serviceID", + cmd: updateCommandMissingServiceID(), + want: nil, + wantError: errors.ErrNoServiceID.Error(), + }, + } { + t.Run(testcase.name, func(t *testing.T) { + testcase.cmd.Base.Globals.Client = testcase.api + + have, err := testcase.cmd.createInput() + testutil.AssertErrorContains(t, err, testcase.wantError) + testutil.AssertEqual(t, testcase.want, have) + }) + } +} + +func createCommandRequired() *CreateCommand { + return &CreateCommand{ + manifest: manifest.Data{Flag: manifest.Flag{ServiceID: "123"}}, + EndpointName: "log", + Version: 2, + BucketName: "bucket", + AccessKey: "access", + SecretKey: "secret", + } +} + +func createCommandAll() *CreateCommand { + return &CreateCommand{ + manifest: manifest.Data{Flag: manifest.Flag{ServiceID: "123"}}, + EndpointName: "log", + Version: 2, + BucketName: "bucket", + AccessKey: "access", + SecretKey: "secret", + Domain: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "nyc3.digitaloceanspaces.com"}, + Path: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "/log"}, + Period: common.OptionalUint{Optional: common.Optional{Valid: true}, Value: 3600}, + GzipLevel: common.OptionalUint{Optional: common.Optional{Valid: true}, Value: 2}, + Format: common.OptionalString{Optional: common.Optional{Valid: true}, Value: `%h %l %u %t "%r" %>s %b`}, + FormatVersion: common.OptionalUint{Optional: common.Optional{Valid: true}, Value: 2}, + ResponseCondition: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "Prevent default logging"}, + TimestampFormat: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "%Y-%m-%dT%H:%M:%S.000"}, + Placement: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "none"}, + MessageType: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "classic"}, + PublicKey: common.OptionalString{Optional: common.Optional{Valid: true}, Value: pgpPublicKey()}, + } +} + +func createCommandMissingServiceID() *CreateCommand { + res := createCommandAll() + res.manifest = manifest.Data{} + return res +} + +func updateCommandNoUpdates() *UpdateCommand { + return &UpdateCommand{ + Base: common.Base{Globals: &config.Data{Client: nil}}, + manifest: manifest.Data{Flag: manifest.Flag{ServiceID: "123"}}, + EndpointName: "log", + Version: 2, + } +} + +func updateCommandAll() *UpdateCommand { + return &UpdateCommand{ + Base: common.Base{Globals: &config.Data{Client: nil}}, + manifest: manifest.Data{Flag: manifest.Flag{ServiceID: "123"}}, + EndpointName: "log", + Version: 2, + NewName: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "new1"}, + BucketName: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "new2"}, + Domain: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "new3"}, + AccessKey: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "new4"}, + SecretKey: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "new5"}, + Path: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "new6"}, + Period: common.OptionalUint{Optional: common.Optional{Valid: true}, Value: 3601}, + GzipLevel: common.OptionalUint{Optional: common.Optional{Valid: true}, Value: 3}, + Format: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "new7"}, + FormatVersion: common.OptionalUint{Optional: common.Optional{Valid: true}, Value: 3}, + ResponseCondition: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "new8"}, + MessageType: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "new9"}, + TimestampFormat: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "new10"}, + Placement: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "new11"}, + PublicKey: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "new12"}, + } +} + +func updateCommandMissingServiceID() *UpdateCommand { + res := updateCommandAll() + res.manifest = manifest.Data{} + return res +} + +func getDigitalOceanOK(i *fastly.GetDigitalOceanInput) (*fastly.DigitalOcean, error) { + return &fastly.DigitalOcean{ + ServiceID: i.Service, + Version: i.Version, + Name: "logs", + BucketName: "bucket", + Domain: "nyc3.digitaloceanspaces.com", + AccessKey: "access", + SecretKey: "secret", + Path: "/log", + Period: 3600, + GzipLevel: 2, + Format: `%h %l %u %t "%r" %>s %b`, + FormatVersion: 2, + ResponseCondition: "Prevent default logging", + MessageType: "classic", + TimestampFormat: "%Y-%m-%dT%H:%M:%S.000", + Placement: "none", + PublicKey: pgpPublicKey(), + }, nil +} + +// pgpPublicKey returns a PEM encoded PGP public key suitable for testing. +func pgpPublicKey() string { + return strings.TrimSpace(`-----BEGIN PGP PUBLIC KEY BLOCK----- +mQENBFyUD8sBCACyFnB39AuuTygseek+eA4fo0cgwva6/FSjnWq7riouQee8GgQ/ +ibXTRyv4iVlwI12GswvMTIy7zNvs1R54i0qvsLr+IZ4GVGJqs6ZJnvQcqe3xPoR4 +8AnBfw90o32r/LuHf6QCJXi+AEu35koNlNAvLJ2B+KACaNB7N0EeWmqpV/1V2k9p +lDYk+th7LcCuaFNGqKS/PrMnnMqR6VDLCjHhNx4KR79b0Twm/2qp6an3hyNRu8Gn +dwxpf1/BUu3JWf+LqkN4Y3mbOmSUL3MaJNvyQguUzTfS0P0uGuBDHrJCVkMZCzDB +89ag55jCPHyGeHBTd02gHMWzsg3WMBWvCsrzABEBAAG0JXRlcnJhZm9ybSAodGVz +dCkgPHRlc3RAdGVycmFmb3JtLmNvbT6JAU4EEwEIADgWIQSHYyc6Kj9l6HzQsau6 +vFFc9jxV/wUCXJQPywIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRC6vFFc +9jxV/815CAClb32OxV7wG01yF97TzlyTl8TnvjMtoG29Mw4nSyg+mjM3b8N7iXm9 +OLX59fbDAWtBSldSZE22RXd3CvlFOG/EnKBXSjBtEqfyxYSnyOPkMPBYWGL/ApkX +SvPYJ4LKdvipYToKFh3y9kk2gk1DcDBDyaaHvR+3rv1u3aoy7/s2EltAfDS3ZQIq +7/cWTLJml/lleeB/Y6rPj8xqeCYhE5ahw9gsV/Mdqatl24V9Tks30iijx0Hhw+Gx +kATUikMGr2GDVqoIRga5kXI7CzYff4rkc0Twn47fMHHHe/KY9M2yVnMHUXmAZwbG +M1cMI/NH1DjevCKdGBLcRJlhuLPKF/anuQENBFyUD8sBCADIpd7r7GuPd6n/Ikxe +u6h7umV6IIPoAm88xCYpTbSZiaK30Svh6Ywra9jfE2KlU9o6Y/art8ip0VJ3m07L +4RSfSpnzqgSwdjSq5hNour2Fo/BzYhK7yaz2AzVSbe33R0+RYhb4b/6N+bKbjwGF +ftCsqVFMH+PyvYkLbvxyQrHlA9woAZaNThI1ztO5rGSnGUR8xt84eup28WIFKg0K +UEGUcTzz+8QGAwAra+0ewPXo/AkO+8BvZjDidP417u6gpBHOJ9qYIcO9FxHeqFyu +YrjlrxowEgXn5wO8xuNz6Vu1vhHGDHGDsRbZF8pv1d5O+0F1G7ttZ2GRRgVBZPwi +kiyRABEBAAGJATYEGAEIACAWIQSHYyc6Kj9l6HzQsau6vFFc9jxV/wUCXJQPywIb +DAAKCRC6vFFc9jxV/9YOCACe8qmOSnKQpQfW+PqYOqo3dt7JyweTs3FkD6NT8Zml +dYy/vkstbTjPpX6aTvUZjkb46BVi7AOneVHpD5GBqvRsZ9iVgDYHaehmLCdKiG5L +3Tp90NN+QY5WDbsGmsyk6+6ZMYejb4qYfweQeduOj27aavCJdLkCYMoRKfcFYI8c +FaNmEfKKy/r1PO20NXEG6t9t05K/frHy6ZG8bCNYdpagfFVot47r9JaQqWlTNtIR +5+zkkSq/eG9BEtRij3a6cTdQbktdBzx2KBeI0PYc1vlZR0LpuFKZqY9vlE6vTGLR +wMfrTEOvx0NxUM3rpaCgEmuWbB1G1Hu371oyr4srrr+N +=28dr +-----END PGP PUBLIC KEY BLOCK----- +`) +} diff --git a/pkg/logging/digitalocean/doc.go b/pkg/logging/digitalocean/doc.go new file mode 100644 index 000000000..9054630b7 --- /dev/null +++ b/pkg/logging/digitalocean/doc.go @@ -0,0 +1,3 @@ +// Package digitalocean contains commands to inspect and manipulate Fastly service DigitalOcean +// logging endpoints. +package digitalocean diff --git a/pkg/logging/digitalocean/list.go b/pkg/logging/digitalocean/list.go new file mode 100644 index 000000000..1e9dd43da --- /dev/null +++ b/pkg/logging/digitalocean/list.go @@ -0,0 +1,81 @@ +package digitalocean + +import ( + "fmt" + "io" + + "github.com/fastly/cli/pkg/common" + "github.com/fastly/cli/pkg/compute/manifest" + "github.com/fastly/cli/pkg/config" + "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/text" + "github.com/fastly/go-fastly/fastly" +) + +// ListCommand calls the Fastly API to list DigitalOcean Spaces logging endpoints. +type ListCommand struct { + common.Base + manifest manifest.Data + Input fastly.ListDigitalOceansInput +} + +// NewListCommand returns a usable command registered under the parent. +func NewListCommand(parent common.Registerer, globals *config.Data) *ListCommand { + var c ListCommand + c.Globals = globals + c.manifest.File.Read(manifest.Filename) + c.CmdClause = parent.Command("list", "List DigitalOcean Spaces logging endpoints on a Fastly service version") + c.CmdClause.Flag("service-id", "Service ID").Short('s').StringVar(&c.manifest.Flag.ServiceID) + c.CmdClause.Flag("version", "Number of service version").Required().IntVar(&c.Input.Version) + return &c +} + +// Exec invokes the application logic for the command. +func (c *ListCommand) Exec(in io.Reader, out io.Writer) error { + serviceID, source := c.manifest.ServiceID() + if source == manifest.SourceUndefined { + return errors.ErrNoServiceID + } + c.Input.Service = serviceID + + digitaloceans, err := c.Globals.Client.ListDigitalOceans(&c.Input) + if err != nil { + return err + } + + if !c.Globals.Verbose() { + tw := text.NewTable(out) + tw.AddHeader("SERVICE", "VERSION", "NAME") + for _, digitalocean := range digitaloceans { + tw.AddLine(digitalocean.ServiceID, digitalocean.Version, digitalocean.Name) + } + tw.Print() + return nil + } + + fmt.Fprintf(out, "Service ID: %s\n", c.Input.Service) + fmt.Fprintf(out, "Version: %d\n", c.Input.Version) + for i, digitalocean := range digitaloceans { + fmt.Fprintf(out, "\tDigitalOcean %d/%d\n", i+1, len(digitaloceans)) + fmt.Fprintf(out, "\t\tService ID: %s\n", digitalocean.ServiceID) + fmt.Fprintf(out, "\t\tVersion: %d\n", digitalocean.Version) + fmt.Fprintf(out, "\t\tName: %s\n", digitalocean.Name) + fmt.Fprintf(out, "\t\tBucket: %s\n", digitalocean.BucketName) + fmt.Fprintf(out, "\t\tDomain: %s\n", digitalocean.Domain) + fmt.Fprintf(out, "\t\tAccess key: %s\n", digitalocean.AccessKey) + fmt.Fprintf(out, "\t\tSecret key: %s\n", digitalocean.SecretKey) + fmt.Fprintf(out, "\t\tPath: %s\n", digitalocean.Path) + fmt.Fprintf(out, "\t\tPeriod: %d\n", digitalocean.Period) + fmt.Fprintf(out, "\t\tGZip level: %d\n", digitalocean.GzipLevel) + fmt.Fprintf(out, "\t\tFormat: %s\n", digitalocean.Format) + fmt.Fprintf(out, "\t\tFormat version: %d\n", digitalocean.FormatVersion) + fmt.Fprintf(out, "\t\tResponse condition: %s\n", digitalocean.ResponseCondition) + fmt.Fprintf(out, "\t\tMessage type: %s\n", digitalocean.MessageType) + fmt.Fprintf(out, "\t\tTimestamp format: %s\n", digitalocean.TimestampFormat) + fmt.Fprintf(out, "\t\tPlacement: %s\n", digitalocean.Placement) + fmt.Fprintf(out, "\t\tPublic key: %s\n", digitalocean.PublicKey) + } + fmt.Fprintln(out) + + return nil +} diff --git a/pkg/logging/digitalocean/root.go b/pkg/logging/digitalocean/root.go new file mode 100644 index 000000000..4c48ddbf7 --- /dev/null +++ b/pkg/logging/digitalocean/root.go @@ -0,0 +1,28 @@ +package digitalocean + +import ( + "io" + + "github.com/fastly/cli/pkg/common" + "github.com/fastly/cli/pkg/config" +) + +// RootCommand is the parent command for all subcommands in this package. +// It should be installed under the primary root command. +type RootCommand struct { + common.Base + // no flags +} + +// NewRootCommand returns a new command registered in the parent. +func NewRootCommand(parent common.Registerer, globals *config.Data) *RootCommand { + var c RootCommand + c.Globals = globals + c.CmdClause = parent.Command("digitalocean", "Manipulate Fastly service version DigitalOcean Spaces logging endpoints") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(in io.Reader, out io.Writer) error { + panic("unreachable") +} diff --git a/pkg/logging/digitalocean/update.go b/pkg/logging/digitalocean/update.go new file mode 100644 index 000000000..440505c90 --- /dev/null +++ b/pkg/logging/digitalocean/update.go @@ -0,0 +1,187 @@ +package digitalocean + +import ( + "io" + + "github.com/fastly/cli/pkg/common" + "github.com/fastly/cli/pkg/compute/manifest" + "github.com/fastly/cli/pkg/config" + "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/text" + "github.com/fastly/go-fastly/fastly" +) + +// UpdateCommand calls the Fastly API to update DigitalOcean Spaces logging endpoints. +type UpdateCommand struct { + common.Base + manifest manifest.Data + + //required + EndpointName string + Version int + + // optional + NewName common.OptionalString + BucketName common.OptionalString + Domain common.OptionalString + AccessKey common.OptionalString + SecretKey common.OptionalString + Path common.OptionalString + Period common.OptionalUint + GzipLevel common.OptionalUint + Format common.OptionalString + FormatVersion common.OptionalUint + ResponseCondition common.OptionalString + MessageType common.OptionalString + TimestampFormat common.OptionalString + Placement common.OptionalString + PublicKey common.OptionalString +} + +// NewUpdateCommand returns a usable command registered under the parent. +func NewUpdateCommand(parent common.Registerer, globals *config.Data) *UpdateCommand { + var c UpdateCommand + c.Globals = globals + c.manifest.File.Read(manifest.Filename) + + c.CmdClause = parent.Command("update", "Update a DigitalOcean Spaces logging endpoint on a Fastly service version") + + c.CmdClause.Flag("service-id", "Service ID").Short('s').StringVar(&c.manifest.Flag.ServiceID) + c.CmdClause.Flag("version", "Number of service version").Required().IntVar(&c.Version) + c.CmdClause.Flag("name", "The name of the DigitalOcean Spaces logging object").Short('n').Required().StringVar(&c.EndpointName) + + c.CmdClause.Flag("new-name", "New name of the DigitalOcean Spaces logging object").Action(c.NewName.Set).StringVar(&c.NewName.Value) + c.CmdClause.Flag("bucket", "The name of the DigitalOcean Space").Action(c.BucketName.Set).StringVar(&c.BucketName.Value) + c.CmdClause.Flag("domain", "The domain of the DigitalOcean Spaces endpoint (default 'nyc3.digitaloceanspaces.com')").Action(c.Domain.Set).StringVar(&c.Domain.Value) + c.CmdClause.Flag("access-key", "Your DigitalOcean Spaces account access key").Action(c.AccessKey.Set).StringVar(&c.AccessKey.Value) + c.CmdClause.Flag("secret-key", "Your DigitalOcean Spaces account secret key").Action(c.SecretKey.Set).StringVar(&c.SecretKey.Value) + c.CmdClause.Flag("path", "The path to upload logs to").Action(c.Path.Set).StringVar(&c.Path.Value) + c.CmdClause.Flag("period", "How frequently log files are finalized so they can be available for reading (in seconds, default 3600)").Action(c.Period.Set).UintVar(&c.Period.Value) + c.CmdClause.Flag("gzip-level", "What level of GZIP encoding to have when dumping logs (default 0, no compression)").Action(c.GzipLevel.Set).UintVar(&c.GzipLevel.Value) + c.CmdClause.Flag("format", "Apache style log formatting").Action(c.Format.Set).StringVar(&c.Format.Value) + c.CmdClause.Flag("format-version", "The version of the custom logging format used for the configured endpoint. Can be either 2 (default) or 1").Action(c.FormatVersion.Set).UintVar(&c.FormatVersion.Value) + c.CmdClause.Flag("response-condition", "The name of an existing condition in the configured endpoint, or leave blank to always execute").Action(c.ResponseCondition.Set).StringVar(&c.ResponseCondition.Value) + c.CmdClause.Flag("message-type", "How the message should be formatted. One of: classic (default), loggly, logplex or blank").Action(c.MessageType.Set).StringVar(&c.MessageType.Value) + c.CmdClause.Flag("timestamp-format", `strftime specified timestamp formatting (default "%Y-%m-%dT%H:%M:%S.000")`).Action(c.TimestampFormat.Set).StringVar(&c.TimestampFormat.Value) + c.CmdClause.Flag("placement", "Where in the generated VCL the logging call should be placed, overriding any format_version default. Can be none or waf_debug").Action(c.Placement.Set).StringVar(&c.Placement.Value) + c.CmdClause.Flag("public-key", "A PGP public key that Fastly will use to encrypt your log files before writing them to disk").Action(c.PublicKey.Set).StringVar(&c.PublicKey.Value) + + return &c +} + +// createInput transforms values parsed from CLI flags into an object to be used by the API client library. +func (c *UpdateCommand) createInput() (*fastly.UpdateDigitalOceanInput, error) { + serviceID, source := c.manifest.ServiceID() + if source == manifest.SourceUndefined { + return nil, errors.ErrNoServiceID + } + + digitalocean, err := c.Globals.Client.GetDigitalOcean(&fastly.GetDigitalOceanInput{ + Service: serviceID, + Name: c.EndpointName, + Version: c.Version, + }) + if err != nil { + return nil, err + } + + input := fastly.UpdateDigitalOceanInput{ + Service: digitalocean.ServiceID, + Version: digitalocean.Version, + Name: digitalocean.Name, + NewName: fastly.String(digitalocean.Name), + BucketName: fastly.String(digitalocean.BucketName), + Domain: fastly.String(digitalocean.Domain), + AccessKey: fastly.String(digitalocean.AccessKey), + SecretKey: fastly.String(digitalocean.SecretKey), + Path: fastly.String(digitalocean.Path), + Period: fastly.Uint(digitalocean.Period), + GzipLevel: fastly.Uint(digitalocean.GzipLevel), + Format: fastly.String(digitalocean.Format), + FormatVersion: fastly.Uint(digitalocean.FormatVersion), + ResponseCondition: fastly.String(digitalocean.ResponseCondition), + MessageType: fastly.String(digitalocean.MessageType), + TimestampFormat: fastly.String(digitalocean.TimestampFormat), + Placement: fastly.String(digitalocean.Placement), + PublicKey: fastly.String(digitalocean.PublicKey), + } + + // Set new values if set by user. + if c.NewName.Valid { + input.NewName = fastly.String(c.NewName.Value) + } + + if c.BucketName.Valid { + input.BucketName = fastly.String(c.BucketName.Value) + } + + if c.Domain.Valid { + input.Domain = fastly.String(c.Domain.Value) + } + + if c.AccessKey.Valid { + input.AccessKey = fastly.String(c.AccessKey.Value) + } + + if c.SecretKey.Valid { + input.SecretKey = fastly.String(c.SecretKey.Value) + } + + if c.Path.Valid { + input.Path = fastly.String(c.Path.Value) + } + + if c.Period.Valid { + input.Period = fastly.Uint(c.Period.Value) + } + + if c.GzipLevel.Valid { + input.GzipLevel = fastly.Uint(c.GzipLevel.Value) + } + + if c.Format.Valid { + input.Format = fastly.String(c.Format.Value) + } + + if c.FormatVersion.Valid { + input.FormatVersion = fastly.Uint(c.FormatVersion.Value) + } + + if c.ResponseCondition.Valid { + input.ResponseCondition = fastly.String(c.ResponseCondition.Value) + } + + if c.MessageType.Valid { + input.MessageType = fastly.String(c.MessageType.Value) + } + + if c.TimestampFormat.Valid { + input.TimestampFormat = fastly.String(c.TimestampFormat.Value) + } + + if c.Placement.Valid { + input.Placement = fastly.String(c.Placement.Value) + } + + if c.PublicKey.Valid { + input.PublicKey = fastly.String(c.PublicKey.Value) + } + + return &input, nil +} + +// Exec invokes the application logic for the command. +func (c *UpdateCommand) Exec(in io.Reader, out io.Writer) error { + input, err := c.createInput() + if err != nil { + return err + } + + digitalocean, err := c.Globals.Client.UpdateDigitalOcean(input) + if err != nil { + return err + } + + text.Success(out, "Updated DigitalOcean Spaces logging endpoint %s (service %s version %d)", digitalocean.Name, digitalocean.ServiceID, digitalocean.Version) + return nil +} diff --git a/pkg/mock/api.go b/pkg/mock/api.go index e25a08050..0a220d911 100644 --- a/pkg/mock/api.go +++ b/pkg/mock/api.go @@ -139,6 +139,12 @@ type API struct { UpdateCloudfilesFn func(*fastly.UpdateCloudfilesInput) (*fastly.Cloudfiles, error) DeleteCloudfilesFn func(*fastly.DeleteCloudfilesInput) error + CreateDigitalOceanFn func(*fastly.CreateDigitalOceanInput) (*fastly.DigitalOcean, error) + ListDigitalOceansFn func(*fastly.ListDigitalOceansInput) ([]*fastly.DigitalOcean, error) + GetDigitalOceanFn func(*fastly.GetDigitalOceanInput) (*fastly.DigitalOcean, error) + UpdateDigitalOceanFn func(*fastly.UpdateDigitalOceanInput) (*fastly.DigitalOcean, error) + DeleteDigitalOceanFn func(*fastly.DeleteDigitalOceanInput) error + GetUserFn func(*fastly.GetUserInput) (*fastly.User, error) GetRegionsFn func() (*fastly.RegionsResponse, error) @@ -690,6 +696,31 @@ func (m API) DeleteCloudfiles(i *fastly.DeleteCloudfilesInput) error { return m.DeleteCloudfilesFn(i) } +// CreateDigitalOcean implements Interface. +func (m API) CreateDigitalOcean(i *fastly.CreateDigitalOceanInput) (*fastly.DigitalOcean, error) { + return m.CreateDigitalOceanFn(i) +} + +// ListDigitalOceans implements Interface. +func (m API) ListDigitalOceans(i *fastly.ListDigitalOceansInput) ([]*fastly.DigitalOcean, error) { + return m.ListDigitalOceansFn(i) +} + +// GetDigitalOcean implements Interface. +func (m API) GetDigitalOcean(i *fastly.GetDigitalOceanInput) (*fastly.DigitalOcean, error) { + return m.GetDigitalOceanFn(i) +} + +// UpdateDigitalOcean implements Interface. +func (m API) UpdateDigitalOcean(i *fastly.UpdateDigitalOceanInput) (*fastly.DigitalOcean, error) { + return m.UpdateDigitalOceanFn(i) +} + +// DeleteDigitalOcean implements Interface. +func (m API) DeleteDigitalOcean(i *fastly.DeleteDigitalOceanInput) error { + return m.DeleteDigitalOceanFn(i) +} + // GetUser implements Interface. func (m API) GetUser(i *fastly.GetUserInput) (*fastly.User, error) { return m.GetUserFn(i)