From 4a3943d793a3cb91d0b0c7842ec236382b6754f9 Mon Sep 17 00:00:00 2001 From: Colton McCurdy Date: Thu, 14 May 2020 13:43:00 -0400 Subject: [PATCH] logging: adds Loggly logging endpoint support Signed-off-by: Colton McCurdy --- pkg/api/interface.go | 6 + pkg/app/run.go | 15 + pkg/app/run_test.go | 66 +++ pkg/logging/loggly/create.go | 100 +++++ pkg/logging/loggly/delete.go | 47 +++ pkg/logging/loggly/describe.go | 56 +++ pkg/logging/loggly/doc.go | 3 + pkg/logging/loggly/list.go | 72 ++++ pkg/logging/loggly/loggly_integration_test.go | 383 ++++++++++++++++++ pkg/logging/loggly/loggly_test.go | 187 +++++++++ pkg/logging/loggly/root.go | 28 ++ pkg/logging/loggly/update.go | 123 ++++++ pkg/mock/api.go | 31 ++ 13 files changed, 1117 insertions(+) create mode 100644 pkg/logging/loggly/create.go create mode 100644 pkg/logging/loggly/delete.go create mode 100644 pkg/logging/loggly/describe.go create mode 100644 pkg/logging/loggly/doc.go create mode 100644 pkg/logging/loggly/list.go create mode 100644 pkg/logging/loggly/loggly_integration_test.go create mode 100644 pkg/logging/loggly/loggly_test.go create mode 100644 pkg/logging/loggly/root.go create mode 100644 pkg/logging/loggly/update.go diff --git a/pkg/api/interface.go b/pkg/api/interface.go index 55993f6ab..e59db4f12 100644 --- a/pkg/api/interface.go +++ b/pkg/api/interface.go @@ -112,6 +112,12 @@ type Interface interface { UpdateScalyr(*fastly.UpdateScalyrInput) (*fastly.Scalyr, error) DeleteScalyr(*fastly.DeleteScalyrInput) error + CreateLoggly(*fastly.CreateLogglyInput) (*fastly.Loggly, error) + ListLoggly(*fastly.ListLogglyInput) ([]*fastly.Loggly, error) + GetLoggly(*fastly.GetLogglyInput) (*fastly.Loggly, error) + UpdateLoggly(*fastly.UpdateLogglyInput) (*fastly.Loggly, error) + DeleteLoggly(*fastly.DeleteLogglyInput) error + GetUser(*fastly.GetUserInput) (*fastly.User, error) GetRegions() (*fastly.RegionsResponse, error) diff --git a/pkg/app/run.go b/pkg/app/run.go index e12ac3dea..11d72347f 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -23,6 +23,7 @@ import ( "github.com/fastly/cli/pkg/logging/ftp" "github.com/fastly/cli/pkg/logging/gcs" "github.com/fastly/cli/pkg/logging/logentries" + "github.com/fastly/cli/pkg/logging/loggly" "github.com/fastly/cli/pkg/logging/papertrail" "github.com/fastly/cli/pkg/logging/s3" "github.com/fastly/cli/pkg/logging/scalyr" @@ -199,6 +200,13 @@ func Run(args []string, env config.Environment, file config.File, configFilePath scalyrUpdate := scalyr.NewUpdateCommand(scalyrRoot.CmdClause, &globals) scalyrDelete := scalyr.NewDeleteCommand(scalyrRoot.CmdClause, &globals) + logglyRoot := loggly.NewRootCommand(loggingRoot.CmdClause, &globals) + logglyCreate := loggly.NewCreateCommand(logglyRoot.CmdClause, &globals) + logglyList := loggly.NewListCommand(logglyRoot.CmdClause, &globals) + logglyDescribe := loggly.NewDescribeCommand(logglyRoot.CmdClause, &globals) + logglyUpdate := loggly.NewUpdateCommand(logglyRoot.CmdClause, &globals) + logglyDelete := loggly.NewDeleteCommand(logglyRoot.CmdClause, &globals) + statsRoot := stats.NewRootCommand(app, &globals) statsRegions := stats.NewRegionsCommand(statsRoot.CmdClause, &globals) statsHistorical := stats.NewHistoricalCommand(statsRoot.CmdClause, &globals) @@ -325,6 +333,13 @@ func Run(args []string, env config.Environment, file config.File, configFilePath scalyrUpdate, scalyrDelete, + logglyRoot, + logglyCreate, + logglyList, + logglyDescribe, + logglyUpdate, + logglyDelete, + statsRoot, statsRegions, statsHistorical, diff --git a/pkg/app/run_test.go b/pkg/app/run_test.go index c11e5954e..0fa140a82 100644 --- a/pkg/app/run_test.go +++ b/pkg/app/run_test.go @@ -1427,6 +1427,71 @@ COMMANDS --version=VERSION Number of service version -n, --name=NAME The name of the Scalyr logging object + logging loggly create --name=NAME --version=VERSION --auth-token=AUTH-TOKEN [] + Create a Loggly logging endpoint on a Fastly service version + + -n, --name=NAME The name of the Loggly logging object. Used as + a primary key for API access + -s, --service-id=SERVICE-ID Service ID + --version=VERSION Number of service version + --auth-token=AUTH-TOKEN The token to use for authentication + (https://www.loggly.com/docs/customer-token-authentication-token/) + --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 + --placement=PLACEMENT Where in the generated VCL the logging call + should be placed, overriding any format_version + default. Can be none or waf_debug + + logging loggly list --version=VERSION [] + List Loggly endpoints on a Fastly service version + + -s, --service-id=SERVICE-ID Service ID + --version=VERSION Number of service version + + logging loggly describe --version=VERSION --name=NAME [] + Show detailed information about a Loggly 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 Loggly logging object + + logging loggly update --version=VERSION --name=NAME [] + Update a Loggly 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 Loggly logging object + --new-name=NEW-NAME New name of the Loggly logging object + --auth-token=AUTH-TOKEN The token to use for authentication + (https://www.loggly.com/docs/customer-token-authentication-token/) + --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 + --placement=PLACEMENT Where in the generated VCL the logging call + should be placed, overriding any format_version + default. Can be none or waf_debug + + logging loggly delete --version=VERSION --name=NAME [] + Delete a Loggly 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 Loggly logging object + stats regions List stats regions @@ -1452,4 +1517,5 @@ For help on a specific command, try e.g. fastly help configure fastly configure --help + `) + "\n\n" diff --git a/pkg/logging/loggly/create.go b/pkg/logging/loggly/create.go new file mode 100644 index 000000000..aa2d9e876 --- /dev/null +++ b/pkg/logging/loggly/create.go @@ -0,0 +1,100 @@ +package loggly + +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 Loggly logging endpoints. +type CreateCommand struct { + common.Base + manifest manifest.Data + + // required + EndpointName string // Can't shaddow common.Base method Name(). + Token string + Version int + + // optional + Format common.OptionalString + FormatVersion common.OptionalUint + ResponseCondition common.OptionalString + Placement 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 Loggly logging endpoint on a Fastly service version").Alias("add") + + c.CmdClause.Flag("name", "The name of the Loggly 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("auth-token", "The token to use for authentication (https://www.loggly.com/docs/customer-token-authentication-token/)").Required().StringVar(&c.Token) + + 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("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) + + 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.CreateLogglyInput, error) { + var input fastly.CreateLogglyInput + + 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.Token = fastly.String(c.Token) + + 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.Placement.Valid { + input.Placement = fastly.String(c.Placement.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.CreateLoggly(input) + if err != nil { + return err + } + + text.Success(out, "Created Loggly logging endpoint %s (service %s version %d)", d.Name, d.ServiceID, d.Version) + return nil +} diff --git a/pkg/logging/loggly/delete.go b/pkg/logging/loggly/delete.go new file mode 100644 index 000000000..e5fd67a7c --- /dev/null +++ b/pkg/logging/loggly/delete.go @@ -0,0 +1,47 @@ +package loggly + +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 Loggly logging endpoints. +type DeleteCommand struct { + common.Base + manifest manifest.Data + Input fastly.DeleteLogglyInput +} + +// 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 Loggly 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 Loggly 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.DeleteLoggly(&c.Input); err != nil { + return err + } + + text.Success(out, "Deleted Loggly logging endpoint %s (service %s version %d)", c.Input.Name, c.Input.Service, c.Input.Version) + return nil +} diff --git a/pkg/logging/loggly/describe.go b/pkg/logging/loggly/describe.go new file mode 100644 index 000000000..99d8c1071 --- /dev/null +++ b/pkg/logging/loggly/describe.go @@ -0,0 +1,56 @@ +package loggly + +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 Loggly logging endpoint. +type DescribeCommand struct { + common.Base + manifest manifest.Data + Input fastly.GetLogglyInput +} + +// 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 Loggly 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 Loggly 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 + + loggly, err := c.Globals.Client.GetLoggly(&c.Input) + if err != nil { + return err + } + + fmt.Fprintf(out, "Service ID: %s\n", loggly.ServiceID) + fmt.Fprintf(out, "Version: %d\n", loggly.Version) + fmt.Fprintf(out, "Name: %s\n", loggly.Name) + fmt.Fprintf(out, "Token: %s\n", loggly.Token) + fmt.Fprintf(out, "Format: %s\n", loggly.Format) + fmt.Fprintf(out, "Format version: %d\n", loggly.FormatVersion) + fmt.Fprintf(out, "Response condition: %s\n", loggly.ResponseCondition) + fmt.Fprintf(out, "Placement: %s\n", loggly.Placement) + + return nil +} diff --git a/pkg/logging/loggly/doc.go b/pkg/logging/loggly/doc.go new file mode 100644 index 000000000..feb2e4029 --- /dev/null +++ b/pkg/logging/loggly/doc.go @@ -0,0 +1,3 @@ +// Package loggly contains commands to inspect and manipulate Fastly service Loggly +// logging endpoints. +package loggly diff --git a/pkg/logging/loggly/list.go b/pkg/logging/loggly/list.go new file mode 100644 index 000000000..d57cf822e --- /dev/null +++ b/pkg/logging/loggly/list.go @@ -0,0 +1,72 @@ +package loggly + +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 Loggly logging endpoints. +type ListCommand struct { + common.Base + manifest manifest.Data + Input fastly.ListLogglyInput +} + +// 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 Loggly 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 + + logglys, err := c.Globals.Client.ListLoggly(&c.Input) + if err != nil { + return err + } + + if !c.Globals.Verbose() { + tw := text.NewTable(out) + tw.AddHeader("SERVICE", "VERSION", "NAME") + for _, loggly := range logglys { + tw.AddLine(loggly.ServiceID, loggly.Version, loggly.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, loggly := range logglys { + fmt.Fprintf(out, "\tLoggly %d/%d\n", i+1, len(logglys)) + fmt.Fprintf(out, "\t\tService ID: %s\n", loggly.ServiceID) + fmt.Fprintf(out, "\t\tVersion: %d\n", loggly.Version) + fmt.Fprintf(out, "\t\tName: %s\n", loggly.Name) + fmt.Fprintf(out, "\t\tToken: %s\n", loggly.Token) + fmt.Fprintf(out, "\t\tFormat: %s\n", loggly.Format) + fmt.Fprintf(out, "\t\tFormat version: %d\n", loggly.FormatVersion) + fmt.Fprintf(out, "\t\tResponse condition: %s\n", loggly.ResponseCondition) + fmt.Fprintf(out, "\t\tPlacement: %s\n", loggly.Placement) + } + fmt.Fprintln(out) + + return nil +} diff --git a/pkg/logging/loggly/loggly_integration_test.go b/pkg/logging/loggly/loggly_integration_test.go new file mode 100644 index 000000000..fccdae82f --- /dev/null +++ b/pkg/logging/loggly/loggly_integration_test.go @@ -0,0 +1,383 @@ +package loggly_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 TestLogglyCreate(t *testing.T) { + for _, testcase := range []struct { + args []string + api mock.API + wantError string + wantOutput string + }{ + { + args: []string{"logging", "loggly", "create", "--service-id", "123", "--version", "1", "--name", "log"}, + wantError: "error parsing arguments: required flag --auth-token not provided", + }, + { + args: []string{"logging", "loggly", "create", "--service-id", "123", "--version", "1", "--name", "log", "--auth-token", "abc"}, + api: mock.API{CreateLogglyFn: createLogglyOK}, + wantOutput: "Created Loggly logging endpoint log (service 123 version 1)", + }, + { + args: []string{"logging", "loggly", "create", "--service-id", "123", "--version", "1", "--name", "log", "--auth-token", "abc"}, + api: mock.API{CreateLogglyFn: createLogglyError}, + 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 TestLogglyList(t *testing.T) { + for _, testcase := range []struct { + args []string + api mock.API + wantError string + wantOutput string + }{ + { + args: []string{"logging", "loggly", "list", "--service-id", "123", "--version", "1"}, + api: mock.API{ListLogglyFn: listLogglysOK}, + wantOutput: listLogglysShortOutput, + }, + { + args: []string{"logging", "loggly", "list", "--service-id", "123", "--version", "1", "--verbose"}, + api: mock.API{ListLogglyFn: listLogglysOK}, + wantOutput: listLogglysVerboseOutput, + }, + { + args: []string{"logging", "loggly", "list", "--service-id", "123", "--version", "1", "-v"}, + api: mock.API{ListLogglyFn: listLogglysOK}, + wantOutput: listLogglysVerboseOutput, + }, + { + args: []string{"logging", "loggly", "--verbose", "list", "--service-id", "123", "--version", "1"}, + api: mock.API{ListLogglyFn: listLogglysOK}, + wantOutput: listLogglysVerboseOutput, + }, + { + args: []string{"logging", "-v", "loggly", "list", "--service-id", "123", "--version", "1"}, + api: mock.API{ListLogglyFn: listLogglysOK}, + wantOutput: listLogglysVerboseOutput, + }, + { + args: []string{"logging", "loggly", "list", "--service-id", "123", "--version", "1"}, + api: mock.API{ListLogglyFn: listLogglysError}, + 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 TestLogglyDescribe(t *testing.T) { + for _, testcase := range []struct { + args []string + api mock.API + wantError string + wantOutput string + }{ + { + args: []string{"logging", "loggly", "describe", "--service-id", "123", "--version", "1"}, + wantError: "error parsing arguments: required flag --name not provided", + }, + { + args: []string{"logging", "loggly", "describe", "--service-id", "123", "--version", "1", "--name", "logs"}, + api: mock.API{GetLogglyFn: getLogglyError}, + wantError: errTest.Error(), + }, + { + args: []string{"logging", "loggly", "describe", "--service-id", "123", "--version", "1", "--name", "logs"}, + api: mock.API{GetLogglyFn: getLogglyOK}, + wantOutput: describeLogglyOutput, + }, + } { + 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 TestLogglyUpdate(t *testing.T) { + for _, testcase := range []struct { + args []string + api mock.API + wantError string + wantOutput string + }{ + { + args: []string{"logging", "loggly", "update", "--service-id", "123", "--version", "1", "--new-name", "log"}, + wantError: "error parsing arguments: required flag --name not provided", + }, + { + args: []string{"logging", "loggly", "update", "--service-id", "123", "--version", "1", "--name", "logs", "--new-name", "log"}, + api: mock.API{ + GetLogglyFn: getLogglyError, + UpdateLogglyFn: updateLogglyOK, + }, + wantError: errTest.Error(), + }, + { + args: []string{"logging", "loggly", "update", "--service-id", "123", "--version", "1", "--name", "logs", "--new-name", "log"}, + api: mock.API{ + GetLogglyFn: getLogglyOK, + UpdateLogglyFn: updateLogglyError, + }, + wantError: errTest.Error(), + }, + { + args: []string{"logging", "loggly", "update", "--service-id", "123", "--version", "1", "--name", "logs", "--new-name", "log"}, + api: mock.API{ + GetLogglyFn: getLogglyOK, + UpdateLogglyFn: updateLogglyOK, + }, + wantOutput: "Updated Loggly 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 TestLogglyDelete(t *testing.T) { + for _, testcase := range []struct { + args []string + api mock.API + wantError string + wantOutput string + }{ + { + args: []string{"logging", "loggly", "delete", "--service-id", "123", "--version", "1"}, + wantError: "error parsing arguments: required flag --name not provided", + }, + { + args: []string{"logging", "loggly", "delete", "--service-id", "123", "--version", "1", "--name", "logs"}, + api: mock.API{DeleteLogglyFn: deleteLogglyError}, + wantError: errTest.Error(), + }, + { + args: []string{"logging", "loggly", "delete", "--service-id", "123", "--version", "1", "--name", "logs"}, + api: mock.API{DeleteLogglyFn: deleteLogglyOK}, + wantOutput: "Deleted Loggly 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 createLogglyOK(i *fastly.CreateLogglyInput) (*fastly.Loggly, error) { + s := fastly.Loggly{ + ServiceID: i.Service, + Version: i.Version, + } + + if i.Name != nil { + s.Name = *i.Name + } + + return &s, nil +} + +func createLogglyError(i *fastly.CreateLogglyInput) (*fastly.Loggly, error) { + return nil, errTest +} + +func listLogglysOK(i *fastly.ListLogglyInput) ([]*fastly.Loggly, error) { + return []*fastly.Loggly{ + { + ServiceID: i.Service, + Version: i.Version, + Name: "logs", + Token: "abc", + Format: `%h %l %u %t "%r" %>s %b`, + FormatVersion: 2, + ResponseCondition: "Prevent default logging", + Placement: "none", + }, + { + ServiceID: i.Service, + Version: i.Version, + Name: "analytics", + Token: "abc", + Format: `%h %l %u %t "%r" %>s %b`, + FormatVersion: 2, + ResponseCondition: "Prevent default logging", + Placement: "none", + }, + }, nil +} + +func listLogglysError(i *fastly.ListLogglyInput) ([]*fastly.Loggly, error) { + return nil, errTest +} + +var listLogglysShortOutput = strings.TrimSpace(` +SERVICE VERSION NAME +123 1 logs +123 1 analytics +`) + "\n" + +var listLogglysVerboseOutput = strings.TrimSpace(` +Fastly API token not provided +Fastly API endpoint: https://api.fastly.com +Service ID: 123 +Version: 1 + Loggly 1/2 + Service ID: 123 + Version: 1 + Name: logs + Token: abc + Format: %h %l %u %t "%r" %>s %b + Format version: 2 + Response condition: Prevent default logging + Placement: none + Loggly 2/2 + Service ID: 123 + Version: 1 + Name: analytics + Token: abc + Format: %h %l %u %t "%r" %>s %b + Format version: 2 + Response condition: Prevent default logging + Placement: none +`) + "\n\n" + +func getLogglyOK(i *fastly.GetLogglyInput) (*fastly.Loggly, error) { + return &fastly.Loggly{ + ServiceID: i.Service, + Version: i.Version, + Name: "logs", + Token: "abc", + Format: `%h %l %u %t "%r" %>s %b`, + FormatVersion: 2, + ResponseCondition: "Prevent default logging", + Placement: "none", + }, nil +} + +func getLogglyError(i *fastly.GetLogglyInput) (*fastly.Loggly, error) { + return nil, errTest +} + +var describeLogglyOutput = strings.TrimSpace(` +Service ID: 123 +Version: 1 +Name: logs +Token: abc +Format: %h %l %u %t "%r" %>s %b +Format version: 2 +Response condition: Prevent default logging +Placement: none +`) + "\n" + +func updateLogglyOK(i *fastly.UpdateLogglyInput) (*fastly.Loggly, error) { + return &fastly.Loggly{ + ServiceID: i.Service, + Version: i.Version, + Name: "log", + Token: "abc", + Format: `%h %l %u %t "%r" %>s %b`, + FormatVersion: 2, + ResponseCondition: "Prevent default logging", + }, nil +} + +func updateLogglyError(i *fastly.UpdateLogglyInput) (*fastly.Loggly, error) { + return nil, errTest +} + +func deleteLogglyOK(i *fastly.DeleteLogglyInput) error { + return nil +} + +func deleteLogglyError(i *fastly.DeleteLogglyInput) error { + return errTest +} diff --git a/pkg/logging/loggly/loggly_test.go b/pkg/logging/loggly/loggly_test.go new file mode 100644 index 000000000..1915cd337 --- /dev/null +++ b/pkg/logging/loggly/loggly_test.go @@ -0,0 +1,187 @@ +package loggly + +import ( + "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 TestCreateLogglyInput(t *testing.T) { + for _, testcase := range []struct { + name string + cmd *CreateCommand + want *fastly.CreateLogglyInput + wantError string + }{ + { + name: "required values set flag serviceID", + cmd: createCommandRequired(), + want: &fastly.CreateLogglyInput{ + Service: "123", + Version: 2, + Name: fastly.String("log"), + Token: fastly.String("tkn"), + }, + }, + { + name: "all values set flag serviceID", + cmd: createCommandOK(), + want: &fastly.CreateLogglyInput{ + Service: "123", + Version: 2, + Name: fastly.String("log"), + Format: fastly.String(`%h %l %u %t "%r" %>s %b`), + FormatVersion: fastly.Uint(2), + Token: fastly.String("tkn"), + ResponseCondition: fastly.String("Prevent default logging"), + Placement: fastly.String("none"), + }, + }, + { + 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 TestUpdateSFTPInput(t *testing.T) { + for _, testcase := range []struct { + name string + cmd *UpdateCommand + api mock.API + want *fastly.UpdateLogglyInput + wantError string + }{ + { + name: "no updates", + cmd: updateCommandNoUpdates(), + api: mock.API{GetLogglyFn: getLogglyOK}, + want: &fastly.UpdateLogglyInput{ + Service: "123", + Version: 2, + Name: "logs", + NewName: fastly.String("logs"), + Format: fastly.String(`%h %l %u %t "%r" %>s %b`), + FormatVersion: fastly.Uint(2), + Token: fastly.String("tkn"), + ResponseCondition: fastly.String("Prevent default logging"), + Placement: fastly.String("none"), + }, + }, + { + name: "all values set flag serviceID", + cmd: updateCommandAll(), + api: mock.API{GetLogglyFn: getLogglyOK}, + want: &fastly.UpdateLogglyInput{ + Service: "123", + Version: 2, + Name: "logs", + NewName: fastly.String("new1"), + Format: fastly.String("new2"), + FormatVersion: fastly.Uint(3), + Token: fastly.String("new3"), + ResponseCondition: fastly.String("new4"), + Placement: fastly.String("new5"), + }, + }, + { + 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 createCommandOK() *CreateCommand { + return &CreateCommand{ + manifest: manifest.Data{Flag: manifest.Flag{ServiceID: "123"}}, + EndpointName: "log", + Token: "tkn", + Version: 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"}, + Placement: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "none"}, + } +} + +func createCommandRequired() *CreateCommand { + return &CreateCommand{ + manifest: manifest.Data{Flag: manifest.Flag{ServiceID: "123"}}, + EndpointName: "log", + Token: "tkn", + Version: 2, + } +} + +func createCommandMissingServiceID() *CreateCommand { + res := createCommandOK() + 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: "logs", + 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"}, + Format: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "new2"}, + FormatVersion: common.OptionalUint{Optional: common.Optional{Valid: true}, Value: 3}, + Token: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "new3"}, + ResponseCondition: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "new4"}, + Placement: common.OptionalString{Optional: common.Optional{Valid: true}, Value: "new5"}, + } +} + +func updateCommandMissingServiceID() *UpdateCommand { + res := updateCommandAll() + res.manifest = manifest.Data{} + return res +} + +func getLogglyOK(i *fastly.GetLogglyInput) (*fastly.Loggly, error) { + return &fastly.Loggly{ + ServiceID: i.Service, + Version: i.Version, + Name: "logs", + Token: "tkn", + Format: `%h %l %u %t "%r" %>s %b`, + FormatVersion: 2, + ResponseCondition: "Prevent default logging", + Placement: "none", + }, nil +} diff --git a/pkg/logging/loggly/root.go b/pkg/logging/loggly/root.go new file mode 100644 index 000000000..d7502dd98 --- /dev/null +++ b/pkg/logging/loggly/root.go @@ -0,0 +1,28 @@ +package loggly + +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("loggly", "Manipulate Fastly service version Loggly 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/loggly/update.go b/pkg/logging/loggly/update.go new file mode 100644 index 000000000..f6350cb99 --- /dev/null +++ b/pkg/logging/loggly/update.go @@ -0,0 +1,123 @@ +package loggly + +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 Loggly logging endpoints. +type UpdateCommand struct { + common.Base + manifest manifest.Data + + // required + EndpointName string // Can't shaddow common.Base method Name(). + Version int + + // optional + NewName common.OptionalString + Format common.OptionalString + FormatVersion common.OptionalUint + Token common.OptionalString + ResponseCondition common.OptionalString + Placement 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 Loggly 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 Loggly logging object").Short('n').Required().StringVar(&c.EndpointName) + + c.CmdClause.Flag("new-name", "New name of the Loggly logging object").Action(c.NewName.Set).StringVar(&c.NewName.Value) + c.CmdClause.Flag("auth-token", "The token to use for authentication (https://www.loggly.com/docs/customer-token-authentication-token/)").Action(c.Token.Set).StringVar(&c.Token.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("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) + + 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.UpdateLogglyInput, error) { + serviceID, source := c.manifest.ServiceID() + if source == manifest.SourceUndefined { + return nil, errors.ErrNoServiceID + } + + loggly, err := c.Globals.Client.GetLoggly(&fastly.GetLogglyInput{ + Service: serviceID, + Name: c.EndpointName, + Version: c.Version, + }) + if err != nil { + return nil, err + } + + input := fastly.UpdateLogglyInput{ + Service: loggly.ServiceID, + Version: loggly.Version, + Name: loggly.Name, + NewName: fastly.String(loggly.Name), + Format: fastly.String(loggly.Format), + FormatVersion: fastly.Uint(loggly.FormatVersion), + Token: fastly.String(loggly.Token), + ResponseCondition: fastly.String(loggly.ResponseCondition), + Placement: fastly.String(loggly.Placement), + } + + if c.NewName.Valid { + input.NewName = fastly.String(c.NewName.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.Token.Valid { + input.Token = fastly.String(c.Token.Value) + } + + if c.ResponseCondition.Valid { + input.ResponseCondition = fastly.String(c.ResponseCondition.Value) + } + + if c.Placement.Valid { + input.Placement = fastly.String(c.Placement.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 + } + + loggly, err := c.Globals.Client.UpdateLoggly(input) + if err != nil { + return err + } + + text.Success(out, "Updated Loggly logging endpoint %s (service %s version %d)", loggly.Name, loggly.ServiceID, loggly.Version) + return nil +} diff --git a/pkg/mock/api.go b/pkg/mock/api.go index bdfee3b2c..138da18ac 100644 --- a/pkg/mock/api.go +++ b/pkg/mock/api.go @@ -103,6 +103,12 @@ type API struct { UpdateScalyrFn func(*fastly.UpdateScalyrInput) (*fastly.Scalyr, error) DeleteScalyrFn func(*fastly.DeleteScalyrInput) error + CreateLogglyFn func(*fastly.CreateLogglyInput) (*fastly.Loggly, error) + ListLogglyFn func(*fastly.ListLogglyInput) ([]*fastly.Loggly, error) + GetLogglyFn func(*fastly.GetLogglyInput) (*fastly.Loggly, error) + UpdateLogglyFn func(*fastly.UpdateLogglyInput) (*fastly.Loggly, error) + DeleteLogglyFn func(*fastly.DeleteLogglyInput) error + GetUserFn func(*fastly.GetUserInput) (*fastly.User, error) GetRegionsFn func() (*fastly.RegionsResponse, error) @@ -504,6 +510,31 @@ func (m API) DeleteScalyr(i *fastly.DeleteScalyrInput) error { return m.DeleteScalyrFn(i) } +// CreateLoggly implements Interface. +func (m API) CreateLoggly(i *fastly.CreateLogglyInput) (*fastly.Loggly, error) { + return m.CreateLogglyFn(i) +} + +// ListLoggly implements Interface. +func (m API) ListLoggly(i *fastly.ListLogglyInput) ([]*fastly.Loggly, error) { + return m.ListLogglyFn(i) +} + +// GetLoggly implements Interface. +func (m API) GetLoggly(i *fastly.GetLogglyInput) (*fastly.Loggly, error) { + return m.GetLogglyFn(i) +} + +// UpdateLoggly implements Interface. +func (m API) UpdateLoggly(i *fastly.UpdateLogglyInput) (*fastly.Loggly, error) { + return m.UpdateLogglyFn(i) +} + +// DeleteLoggly implements Interface. +func (m API) DeleteLoggly(i *fastly.DeleteLogglyInput) error { + return m.DeleteLogglyFn(i) +} + // GetUser implements Interface. func (m API) GetUser(i *fastly.GetUserInput) (*fastly.User, error) { return m.GetUserFn(i)