Add support for Sumologic logging to Fastly provider (#12541)

This commit is contained in:
Marcin Suterski 2017-04-10 07:04:14 -04:00 committed by Paul Stack
parent d8193fd91c
commit 8e2ff8e6ca
9 changed files with 341 additions and 11 deletions

View File

@ -539,7 +539,7 @@ func resourceServiceV1() *schema.Resource {
Optional: true,
Default: 1,
Description: "The version of the custom logging format used for the configured endpoint. Can be either 1 or 2. (Default: 1)",
ValidateFunc: validateS3FormatVersion,
ValidateFunc: validateLoggingFormatVersion,
},
"timestamp_format": {
Type: schema.TypeString,
@ -594,6 +594,52 @@ func resourceServiceV1() *schema.Resource {
},
},
},
"sumologic": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
// Required fields
"name": {
Type: schema.TypeString,
Required: true,
Description: "Unique name to refer to this logging setup",
},
"url": {
Type: schema.TypeString,
Required: true,
Description: "The URL to POST to.",
},
// Optional fields
"format": {
Type: schema.TypeString,
Optional: true,
Default: "%h %l %u %t %r %>s",
Description: "Apache-style string or VCL variables to use for log formatting",
},
"format_version": {
Type: schema.TypeInt,
Optional: true,
Default: 1,
Description: "The version of the custom logging format used for the configured endpoint. Can be either 1 or 2. (Default: 1)",
ValidateFunc: validateLoggingFormatVersion,
},
"response_condition": {
Type: schema.TypeString,
Optional: true,
Default: "",
Description: "Name of a condition to apply this logging.",
},
"message_type": {
Type: schema.TypeString,
Optional: true,
Default: "classic",
Description: "How the message should be formatted.",
ValidateFunc: validateLoggingMessageType,
},
},
},
},
"response_object": {
Type: schema.TypeSet,
@ -1344,6 +1390,59 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error {
}
}
// find difference in Sumologic
if d.HasChange("sumologic") {
os, ns := d.GetChange("sumologic")
if os == nil {
os = new(schema.Set)
}
if ns == nil {
ns = new(schema.Set)
}
oss := os.(*schema.Set)
nss := ns.(*schema.Set)
removeSumologic := oss.Difference(nss).List()
addSumologic := nss.Difference(oss).List()
// DELETE old sumologic configurations
for _, pRaw := range removeSumologic {
sf := pRaw.(map[string]interface{})
opts := gofastly.DeleteSumologicInput{
Service: d.Id(),
Version: latestVersion,
Name: sf["name"].(string),
}
log.Printf("[DEBUG] Fastly Sumologic removal opts: %#v", opts)
err := conn.DeleteSumologic(&opts)
if err != nil {
return err
}
}
// POST new/updated Sumologic
for _, pRaw := range addSumologic {
sf := pRaw.(map[string]interface{})
opts := gofastly.CreateSumologicInput{
Service: d.Id(),
Version: latestVersion,
Name: sf["name"].(string),
URL: sf["url"].(string),
Format: sf["format"].(string),
FormatVersion: sf["format_version"].(int),
ResponseCondition: sf["response_condition"].(string),
MessageType: sf["message_type"].(string),
}
log.Printf("[DEBUG] Create Sumologic Opts: %#v", opts)
_, err := conn.CreateSumologic(&opts)
if err != nil {
return err
}
}
}
// find difference in Response Object
if d.HasChange("response_object") {
or, nr := d.GetChange("response_object")
@ -1761,6 +1860,22 @@ func resourceServiceV1Read(d *schema.ResourceData, meta interface{}) error {
log.Printf("[WARN] Error setting Papertrail for (%s): %s", d.Id(), err)
}
// refresh Sumologic Logging
log.Printf("[DEBUG] Refreshing Sumologic for (%s)", d.Id())
sumologicList, err := conn.ListSumologics(&gofastly.ListSumologicsInput{
Service: d.Id(),
Version: s.ActiveVersion.Number,
})
if err != nil {
return fmt.Errorf("[ERR] Error looking up Sumologic for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err)
}
sul := flattenSumologics(sumologicList)
if err := d.Set("sumologic", sul); err != nil {
log.Printf("[WARN] Error setting Sumologic for (%s): %s", d.Id(), err)
}
// refresh Response Objects
log.Printf("[DEBUG] Refreshing Response Object for (%s)", d.Id())
responseObjectList, err := conn.ListResponseObjects(&gofastly.ListResponseObjectsInput{
@ -2179,7 +2294,7 @@ func flattenS3s(s3List []*gofastly.S3) []map[string]interface{} {
func flattenPapertrails(papertrailList []*gofastly.Papertrail) []map[string]interface{} {
var pl []map[string]interface{}
for _, p := range papertrailList {
// Convert S3s to a map for saving to state.
// Convert Papertrails to a map for saving to state.
ns := map[string]interface{}{
"name": p.Name,
"address": p.Address,
@ -2201,6 +2316,32 @@ func flattenPapertrails(papertrailList []*gofastly.Papertrail) []map[string]inte
return pl
}
func flattenSumologics(sumologicList []*gofastly.Sumologic) []map[string]interface{} {
var l []map[string]interface{}
for _, p := range sumologicList {
// Convert Sumologic to a map for saving to state.
ns := map[string]interface{}{
"name": p.Name,
"url": p.URL,
"format": p.Format,
"response_condition": p.ResponseCondition,
"message_type": p.MessageType,
"format_version": int(p.FormatVersion),
}
// prune any empty values that come from the default string value in structs
for k, v := range ns {
if v == "" {
delete(ns, k)
}
}
l = append(l, ns)
}
return l
}
func flattenResponseObjects(responseObjectList []*gofastly.ResponseObject) []map[string]interface{} {
var rol []map[string]interface{}
for _, ro := range responseObjectList {

View File

@ -0,0 +1,126 @@
package fastly
import (
"fmt"
"reflect"
"testing"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
gofastly "github.com/sethvargo/go-fastly"
)
func TestResourceFastlyFlattenSumologic(t *testing.T) {
cases := []struct {
remote []*gofastly.Sumologic
local []map[string]interface{}
}{
{
remote: []*gofastly.Sumologic{
&gofastly.Sumologic{
Name: "sumo collector",
URL: "https://sumologic.com/collector/1",
Format: "log format",
FormatVersion: 2,
MessageType: "classic",
ResponseCondition: "condition 1",
},
},
local: []map[string]interface{}{
map[string]interface{}{
"name": "sumo collector",
"url": "https://sumologic.com/collector/1",
"format": "log format",
"format_version": 2,
"message_type": "classic",
"response_condition": "condition 1",
},
},
},
}
for _, c := range cases {
out := flattenSumologics(c.remote)
if !reflect.DeepEqual(out, c.local) {
t.Fatalf("Error matching:\nexpected: %#v\ngot: %#v", c.local, out)
}
}
}
func TestAccFastlyServiceV1_sumologic(t *testing.T) {
var service gofastly.ServiceDetail
name := fmt.Sprintf("tf-test-%s", acctest.RandString(10))
sumologicName := fmt.Sprintf("sumologic %s", acctest.RandString(3))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckServiceV1Destroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccServiceV1Config_sumologic(name, sumologicName),
Check: resource.ComposeTestCheckFunc(
testAccCheckServiceV1Exists("fastly_service_v1.foo", &service),
testAccCheckFastlyServiceV1Attributes_sumologic(&service, name, sumologicName),
),
},
},
})
}
func testAccCheckFastlyServiceV1Attributes_sumologic(service *gofastly.ServiceDetail, name, sumologic string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if service.Name != name {
return fmt.Errorf("Bad name, expected (%s), got (%s)", name, service.Name)
}
conn := testAccProvider.Meta().(*FastlyClient).conn
sumologicList, err := conn.ListSumologics(&gofastly.ListSumologicsInput{
Service: service.ID,
Version: service.ActiveVersion.Number,
})
if err != nil {
return fmt.Errorf("[ERR] Error looking up Sumologics for (%s), version (%s): %s", service.Name, service.ActiveVersion.Number, err)
}
if len(sumologicList) != 1 {
return fmt.Errorf("Sumologic missing, expected: 1, got: %d", len(sumologicList))
}
if sumologicList[0].Name != sumologic {
return fmt.Errorf("Sumologic name mismatch, expected: %s, got: %#v", sumologic, sumologicList[0].Name)
}
return nil
}
}
func testAccServiceV1Config_sumologic(name, sumologic string) string {
backendName := fmt.Sprintf("%s.aws.amazon.com", acctest.RandString(3))
return fmt.Sprintf(`
resource "fastly_service_v1" "foo" {
name = "%s"
domain {
name = "test.notadomain.com"
comment = "tf-testing-domain"
}
backend {
address = "%s"
name = "tf -test backend"
}
sumologic {
name = "%s"
url = "https://sumologic.com/collector/1"
format_version = 2
}
force_destroy = true
}`, name, backendName, sumologic)
}

View File

@ -2,7 +2,7 @@ package fastly
import "fmt"
func validateS3FormatVersion(v interface{}, k string) (ws []string, errors []error) {
func validateLoggingFormatVersion(v interface{}, k string) (ws []string, errors []error) {
value := uint(v.(int))
validVersions := map[uint]struct{}{
1: {},
@ -15,3 +15,19 @@ func validateS3FormatVersion(v interface{}, k string) (ws []string, errors []err
}
return
}
func validateLoggingMessageType(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
validTypes := map[string]struct{}{
"classic": {},
"loggly": {},
"logplex": {},
"blank": {},
}
if _, ok := validTypes[value]; !ok {
errors = append(errors, fmt.Errorf(
"%q must be one of ['classic', 'loggly', 'logplex', 'blank']", k))
}
return
}

View File

@ -2,13 +2,13 @@ package fastly
import "testing"
func TestValidateS3FormatVersion(t *testing.T) {
func TestValidateLoggingFormatVersion(t *testing.T) {
validVersions := []int{
1,
2,
}
for _, v := range validVersions {
_, errors := validateS3FormatVersion(v, "format_version")
_, errors := validateLoggingFormatVersion(v, "format_version")
if len(errors) != 0 {
t.Fatalf("%q should be a valid format version: %q", v, errors)
}
@ -21,9 +21,35 @@ func TestValidateS3FormatVersion(t *testing.T) {
5,
}
for _, v := range invalidVersions {
_, errors := validateS3FormatVersion(v, "format_version")
_, errors := validateLoggingFormatVersion(v, "format_version")
if len(errors) != 1 {
t.Fatalf("%q should not be a valid format version", v)
}
}
}
func TestValidateLoggingMessageType(t *testing.T) {
validTypes := []string{
"classic",
"loggly",
"logplex",
"blank",
}
for _, v := range validTypes {
_, errors := validateLoggingMessageType(v, "message_type")
if len(errors) != 0 {
t.Fatalf("%q should be a valid message type: %q", v, errors)
}
}
invalidTypes := []string{
"invalid_type_1",
"invalid_type_2",
}
for _, v := range invalidTypes {
_, errors := validateLoggingMessageType(v, "message_type")
if len(errors) != 1 {
t.Fatalf("%q should not be a valid message type", v)
}
}
}

View File

@ -4,7 +4,7 @@ import (
"io"
"net/http"
"net/url"
"path"
"strings"
)
// RequestOptions is the list of options to pass to the request.
@ -31,7 +31,7 @@ func (c *Client) RawRequest(verb, p string, ro *RequestOptions) (*http.Request,
// Append the path to the URL.
u := *c.url
u.Path = path.Join(c.url.Path, p)
u.Path = strings.TrimRight(c.url.Path, "/") + "/" + strings.TrimLeft(p, "/")
// Add the token and other params.
var params = make(url.Values)

View File

@ -16,6 +16,8 @@ type Sumologic struct {
URL string `mapstructure:"url"`
Format string `mapstructure:"format"`
ResponseCondition string `mapstructure:"response_condition"`
MessageType string `mapstructure:"message_type"`
FormatVersion int `mapstructure:"format_version"`
CreatedAt *time.Time `mapstructure:"created_at"`
UpdatedAt *time.Time `mapstructure:"updated_at"`
DeletedAt *time.Time `mapstructure:"deleted_at"`
@ -76,6 +78,8 @@ type CreateSumologicInput struct {
URL string `form:"url,omitempty"`
Format string `form:"format,omitempty"`
ResponseCondition string `form:"response_condition,omitempty"`
MessageType string `form:"message_type,omitempty"`
FormatVersion int `form:"format_version,omitempty"`
}
// CreateSumologic creates a new Fastly sumologic.
@ -154,6 +158,8 @@ type UpdateSumologicInput struct {
URL string `form:"url,omitempty"`
Format string `form:"format,omitempty"`
ResponseCondition string `form:"response_condition,omitempty"`
MessageType string `form:"message_type,omitempty"`
FormatVersion int `form:"format_version,omitempty"`
}
// UpdateSumologic updates a specific sumologic.

View File

@ -18,6 +18,7 @@ type Syslog struct {
TLSCACert string `mapstructure:"tls_ca_cert"`
Token string `mapstructure:"token"`
Format string `mapstructure:"format"`
FormatVersion uint `mapstructure:"format_version"`
ResponseCondition string `mapstructure:"response_condition"`
CreatedAt *time.Time `mapstructure:"created_at"`
UpdatedAt *time.Time `mapstructure:"updated_at"`
@ -81,6 +82,7 @@ type CreateSyslogInput struct {
TLSCACert string `form:"tls_ca_cert,omitempty"`
Token string `form:"token,omitempty"`
Format string `form:"format,omitempty"`
FormatVersion uint `form:"format_version,omitempty"`
ResponseCondition string `form:"response_condition,omitempty"`
}
@ -162,6 +164,7 @@ type UpdateSyslogInput struct {
TLSCACert string `form:"tls_ca_cert,omitempty"`
Token string `form:"token,omitempty"`
Format string `form:"format,omitempty"`
FormatVersion uint `form:"format_version,omitempty"`
ResponseCondition string `form:"response_condition,omitempty"`
}

6
vendor/vendor.json vendored
View File

@ -2858,10 +2858,10 @@
"revisionTime": "2017-03-13T16:33:22Z"
},
{
"checksumSHA1": "ySSmShoczI/i/5PzurH8Uhi/dbA=",
"checksumSHA1": "bCpL8ZdY+y7OGwiN3hZzbQI5oM0=",
"path": "github.com/sethvargo/go-fastly",
"revision": "247f42f7ecc6677aa1b6e30978d06fcc38f5f769",
"revisionTime": "2017-02-06T18:56:52Z"
"revision": "43b7f97296d6c8e3a7bc083ab91101fbbc8c2f94",
"revisionTime": "2017-02-28T16:12:19Z"
},
{
"checksumSHA1": "8tEiK6vhVXuUbnWME5XNWLgvtSo=",

View File

@ -153,6 +153,8 @@ order to destroy the Service, set `force_destroy` to `true`. Default `false`.
Defined below.
* `papertrail` - (Optional) A Papertrail endpoint to send streaming logs too.
Defined below.
* `sumologic` - (Optional) A Sumologic endpoint to send streaming logs too.
Defined below.
* `response_object` - (Optional) Allows you to create synthetic responses that exist entirely on the varnish machine. Useful for creating error or maintenance pages that exists outside the scope of your datacenter. Best when used with Condition objects.
* `vcl` - (Optional) A set of custom VCL configuration blocks. The
ability to upload custom VCL code is not enabled by default for new Fastly
@ -315,6 +317,15 @@ The `papertrail` block supports:
* `response_condition` - (Optional) Name of already defined `condition` to apply. This `condition` must be of type `RESPONSE`. For detailed information about Conditionals,
see [Fastly's Documentation on Conditionals][fastly-conditionals].
The `sumologic` block supports:
* `name` - (Required) A unique name to identify this Sumologic endpoint.
* `url` - (Required) The URL to Sumologic collector endpoint
* `format` - (Optional) Apache-style string or VCL variables to use for log formatting. Defaults to Apache Common Log format (`%h %l %u %t %r %>s`)
* `format_version` - (Optional) The version of the custom logging format used for the configured endpoint. Can be either 1 (the default, version 1 log format) or 2 (the version 2 log format).
* `response_condition` - (Optional) Name of already defined `condition` to apply. This `condition` must be of type `RESPONSE`. For detailed information about Conditionals, see [Fastly's Documentation on Conditionals][fastly-conditionals].
* `message_type` - (Optional) How the message should be formatted. One of: classic, loggly, logplex, blank. See [Fastly's Documentation on Sumologic][fastly-sumologic]
The `response_object` block supports:
* `name` - (Required) A unique name to identify this Response Object.
@ -357,3 +368,4 @@ Service.
[fastly-s3]: https://docs.fastly.com/guides/integrations/amazon-s3
[fastly-cname]: https://docs.fastly.com/guides/basic-setup/adding-cname-records
[fastly-conditionals]: https://docs.fastly.com/guides/conditions/using-conditions
[fastly-sumologic]: https://docs.fastly.com/api/logging#logging_sumologic