diff --git a/docs/resources/service_compute.md b/docs/resources/service_compute.md index a601edf40..7c3ad0daf 100644 --- a/docs/resources/service_compute.md +++ b/docs/resources/service_compute.md @@ -94,6 +94,7 @@ $ terraform import fastly_service_compute.demo xxxxxxxxxxxxxxxxxxxx@2 - `logging_ftp` (Block Set) (see [below for nested schema](#nestedblock--logging_ftp)) - `logging_gcs` (Block Set) (see [below for nested schema](#nestedblock--logging_gcs)) - `logging_googlepubsub` (Block Set) (see [below for nested schema](#nestedblock--logging_googlepubsub)) +- `logging_grafanacloudlogs` (Block Set) (see [below for nested schema](#nestedblock--logging_grafanacloudlogs)) - `logging_heroku` (Block Set) (see [below for nested schema](#nestedblock--logging_heroku)) - `logging_honeycomb` (Block Set) (see [below for nested schema](#nestedblock--logging_honeycomb)) - `logging_https` (Block Set) (see [below for nested schema](#nestedblock--logging_https)) @@ -390,6 +391,18 @@ Optional: - `user` (String) Your Google Cloud Platform service account email address. The `client_email` field in your service account authentication JSON. You may optionally provide this via an environment variable, `FASTLY_GOOGLE_PUBSUB_EMAIL`. + +### Nested Schema for `logging_grafanacloudlogs` + +Required: + +- `index` (String) The stream identifier as a JSON string +- `name` (String) The unique name of the GrafanaCloudLogs logging endpoint. It is important to note that changing this attribute will delete and recreate the resource +- `token` (String, Sensitive) The Access Policy Token key for your GrafanaCloudLogs account +- `url` (String) The URL to stream logs to +- `user` (String) The Grafana User ID + + ### Nested Schema for `logging_heroku` diff --git a/docs/resources/service_vcl.md b/docs/resources/service_vcl.md index 58e631237..31276bf01 100644 --- a/docs/resources/service_vcl.md +++ b/docs/resources/service_vcl.md @@ -270,6 +270,7 @@ $ terraform import fastly_service_vcl.demo xxxxxxxxxxxxxxxxxxxx@2 - `logging_ftp` (Block Set) (see [below for nested schema](#nestedblock--logging_ftp)) - `logging_gcs` (Block Set) (see [below for nested schema](#nestedblock--logging_gcs)) - `logging_googlepubsub` (Block Set) (see [below for nested schema](#nestedblock--logging_googlepubsub)) +- `logging_grafanacloudlogs` (Block Set) (see [below for nested schema](#nestedblock--logging_grafanacloudlogs)) - `logging_heroku` (Block Set) (see [below for nested schema](#nestedblock--logging_heroku)) - `logging_honeycomb` (Block Set) (see [below for nested schema](#nestedblock--logging_honeycomb)) - `logging_https` (Block Set) (see [below for nested schema](#nestedblock--logging_https)) @@ -748,6 +749,25 @@ Optional: - `user` (String) Your Google Cloud Platform service account email address. The `client_email` field in your service account authentication JSON. You may optionally provide this via an environment variable, `FASTLY_GOOGLE_PUBSUB_EMAIL`. + +### Nested Schema for `logging_grafanacloudlogs` + +Required: + +- `index` (String) The stream identifier as a JSON string +- `name` (String) The unique name of the GrafanaCloudLogs logging endpoint. It is important to note that changing this attribute will delete and recreate the resource +- `token` (String, Sensitive) The Access Policy Token key for your GrafanaCloudLogs account +- `url` (String) The URL to stream logs to +- `user` (String) The Grafana User ID + +Optional: + +- `format` (String) Apache-style string or VCL variables to use for log formatting. +- `format_version` (Number) The version of the custom logging format used for the configured endpoint. Can be either `1` or `2`. (default: `2`). +- `placement` (String) Where in the generated VCL the logging call should be placed. +- `response_condition` (String) The name of the condition to apply. + + ### Nested Schema for `logging_heroku` diff --git a/fastly/block_fastly_service_logging_grafanacloudlogs.go b/fastly/block_fastly_service_logging_grafanacloudlogs.go new file mode 100644 index 000000000..0d9288599 --- /dev/null +++ b/fastly/block_fastly_service_logging_grafanacloudlogs.go @@ -0,0 +1,293 @@ +package fastly + +import ( + "context" + "fmt" + "log" + + gofastly "github.com/fastly/go-fastly/v9/fastly" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// GrafanaCloudLogsServiceAttributeHandler provides a base implementation for ServiceAttributeDefinition. +type GrafanaCloudLogsServiceAttributeHandler struct { + *DefaultServiceAttributeHandler +} + +// NewServiceLoggingGrafanaCloudLogs returns a new resource. +func NewServiceLoggingGrafanaCloudLogs(sa ServiceMetadata) ServiceAttributeDefinition { + return ToServiceAttributeDefinition(&GrafanaCloudLogsServiceAttributeHandler{ + &DefaultServiceAttributeHandler{ + key: "logging_grafanacloudlogs", + serviceMetadata: sa, + }, + }) +} + +// Key returns the resource key. +func (h *GrafanaCloudLogsServiceAttributeHandler) Key() string { + return h.key +} + +// GetSchema returns the resource schema. +func (h *GrafanaCloudLogsServiceAttributeHandler) GetSchema() *schema.Schema { + blockAttributes := map[string]*schema.Schema{ + "index": { + Type: schema.TypeString, + Required: true, + Description: "The stream identifier as a JSON string", + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The unique name of the GrafanaCloudLogs logging endpoint. It is important to note that changing this attribute will delete and recreate the resource", + }, + "token": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: "The Access Policy Token key for your GrafanaCloudLogs account", + }, + "url": { + Type: schema.TypeString, + Required: true, + Description: "The URL to stream logs to", + }, + "user": { + Type: schema.TypeString, + Required: true, + Description: "The Grafana User ID", + }, + } + + if h.GetServiceMetadata().serviceType == ServiceTypeVCL { + blockAttributes["format"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "Apache-style string or VCL variables to use for log formatting.", + } + blockAttributes["format_version"] = &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 2, + Description: "The version of the custom logging format used for the configured endpoint. Can be either `1` or `2`. (default: `2`).", + ValidateDiagFunc: validateLoggingFormatVersion(), + } + blockAttributes["response_condition"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "The name of the condition to apply.", + } + blockAttributes["placement"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "Where in the generated VCL the logging call should be placed.", + ValidateDiagFunc: validateLoggingPlacement(), + } + } + + return &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: blockAttributes, + }, + } +} + +// Create creates the resource. +func (h *GrafanaCloudLogsServiceAttributeHandler) Create(_ context.Context, d *schema.ResourceData, resource map[string]any, serviceVersion int, conn *gofastly.Client) error { + opts := h.buildCreate(resource, d.Id(), serviceVersion) + + log.Printf("[DEBUG] Fastly GrafanaCloudLogs logging addition opts: %#v", opts) + + return createGrafanaCloudLogs(conn, opts) +} + +// Read refreshes the resource. +func (h *GrafanaCloudLogsServiceAttributeHandler) Read(_ context.Context, d *schema.ResourceData, _ map[string]any, serviceVersion int, conn *gofastly.Client) error { + localState := d.Get(h.GetKey()).(*schema.Set).List() + + if len(localState) > 0 || d.Get("imported").(bool) || d.Get("force_refresh").(bool) { + log.Printf("[DEBUG] Refreshing GrafanaCloudLogs logging endpoints for (%s)", d.Id()) + remoteState, err := conn.ListGrafanaCloudLogs(&gofastly.ListGrafanaCloudLogsInput{ + ServiceID: d.Id(), + ServiceVersion: serviceVersion, + }) + if err != nil { + return fmt.Errorf("error looking up GrafanaCloudLogs logging endpoints for (%s), version (%v): %s", d.Id(), serviceVersion, err) + } + + dll := flattenGrafanaCloudLogs(remoteState) + + for _, element := range dll { + h.pruneVCLLoggingAttributes(element) + } + + if err := d.Set(h.GetKey(), dll); err != nil { + log.Printf("[WARN] Error setting GrafanaCloudLogs logging endpoints for (%s): %s", d.Id(), err) + } + } + + return nil +} + +// Update updates the resource. +func (h *GrafanaCloudLogsServiceAttributeHandler) Update(_ context.Context, d *schema.ResourceData, resource, modified map[string]any, serviceVersion int, conn *gofastly.Client) error { + opts := gofastly.UpdateGrafanaCloudLogsInput{ + ServiceID: d.Id(), + ServiceVersion: serviceVersion, + Name: resource["name"].(string), + } + + // NOTE: When converting from an interface{} we lose the underlying type. + // Converting to the wrong type will result in a runtime panic. + if v, ok := modified["user"]; ok { + opts.User = gofastly.ToPointer(v.(string)) + } + if v, ok := modified["token"]; ok { + opts.Token = gofastly.ToPointer(v.(string)) + } + if v, ok := modified["url"]; ok { + opts.URL = gofastly.ToPointer(v.(string)) + } + if v, ok := modified["index"]; ok { + opts.Index = gofastly.ToPointer(v.(string)) + } + if v, ok := modified["format"]; ok { + opts.Format = gofastly.ToPointer(v.(string)) + } + if v, ok := modified["format_version"]; ok { + opts.FormatVersion = gofastly.ToPointer(v.(int)) + } + if v, ok := modified["response_condition"]; ok { + opts.ResponseCondition = gofastly.ToPointer(v.(string)) + } + if v, ok := modified["placement"]; ok { + opts.Placement = gofastly.ToPointer(v.(string)) + } + + log.Printf("[DEBUG] Update GrafanaCloudLogs Opts: %#v", opts) + _, err := conn.UpdateGrafanaCloudLogs(&opts) + if err != nil { + return err + } + return nil +} + +// Delete deletes the resource. +func (h *GrafanaCloudLogsServiceAttributeHandler) Delete(_ context.Context, d *schema.ResourceData, resource map[string]any, serviceVersion int, conn *gofastly.Client) error { + opts := h.buildDelete(resource, d.Id(), serviceVersion) + + log.Printf("[DEBUG] Fastly GrafanaCloudLogs logging endpoint removal opts: %#v", opts) + + return deleteGrafanaCloudLogs(conn, opts) +} + +func createGrafanaCloudLogs(conn *gofastly.Client, i *gofastly.CreateGrafanaCloudLogsInput) error { + _, err := conn.CreateGrafanaCloudLogs(i) + return err +} + +func deleteGrafanaCloudLogs(conn *gofastly.Client, i *gofastly.DeleteGrafanaCloudLogsInput) error { + err := conn.DeleteGrafanaCloudLogs(i) + + errRes, ok := err.(*gofastly.HTTPError) + if !ok { + return err + } + + // 404 response codes don't result in an error propagating because a 404 could + // indicate that a resource was deleted elsewhere. + if !errRes.IsNotFound() { + return err + } + + return nil +} + +// flattenGrafanaCloudLogs models data into format suitable for saving to Terraform state. +func flattenGrafanaCloudLogs(remoteState []*gofastly.GrafanaCloudLogs) []map[string]any { + var result []map[string]any + for _, resource := range remoteState { + data := map[string]any{} + + if resource.Name != nil { + data["name"] = *resource.Name + } + if resource.User != nil { + data["user"] = *resource.User + } + if resource.Token != nil { + data["token"] = *resource.Token + } + if resource.URL != nil { + data["url"] = *resource.URL + } + if resource.Index != nil { + data["index"] = *resource.Index + } + if resource.Format != nil { + data["format"] = *resource.Format + } + if resource.FormatVersion != nil { + data["format_version"] = *resource.FormatVersion + } + if resource.Placement != nil { + data["placement"] = *resource.Placement + } + if resource.ResponseCondition != nil { + data["response_condition"] = *resource.ResponseCondition + } + + // Prune any empty values that come from the default string value in structs. + for k, v := range data { + if v == "" { + delete(data, k) + } + } + + result = append(result, data) + } + + return result +} + +func (h *GrafanaCloudLogsServiceAttributeHandler) buildCreate(grafanacloudlogsMap any, serviceID string, serviceVersion int) *gofastly.CreateGrafanaCloudLogsInput { + resource := grafanacloudlogsMap.(map[string]any) + + vla := h.getVCLLoggingAttributes(resource) + opts := &gofastly.CreateGrafanaCloudLogsInput{ + Format: gofastly.ToPointer(vla.format), + FormatVersion: vla.formatVersion, + Name: gofastly.ToPointer(resource["name"].(string)), + ServiceID: serviceID, + ServiceVersion: serviceVersion, + User: gofastly.ToPointer(resource["user"].(string)), + Token: gofastly.ToPointer(resource["token"].(string)), + URL: gofastly.ToPointer(resource["url"].(string)), + Index: gofastly.ToPointer(resource["index"].(string)), + } + + // WARNING: The following fields shouldn't have an empty string passed. + // As it will cause the Fastly API to return an error. + // This is because go-fastly v7+ will not 'omitempty' due to pointer type. + if vla.placement != "" { + opts.Placement = gofastly.ToPointer(vla.placement) + } + if vla.responseCondition != "" { + opts.ResponseCondition = gofastly.ToPointer(vla.responseCondition) + } + + return opts +} + +func (h *GrafanaCloudLogsServiceAttributeHandler) buildDelete(grafanacloudlogsMap any, serviceID string, serviceVersion int) *gofastly.DeleteGrafanaCloudLogsInput { + resource := grafanacloudlogsMap.(map[string]any) + + return &gofastly.DeleteGrafanaCloudLogsInput{ + ServiceID: serviceID, + ServiceVersion: serviceVersion, + Name: resource["name"].(string), + } +} diff --git a/fastly/block_fastly_service_logging_grafanacloudlogs_test.go b/fastly/block_fastly_service_logging_grafanacloudlogs_test.go new file mode 100644 index 000000000..39ca16040 --- /dev/null +++ b/fastly/block_fastly_service_logging_grafanacloudlogs_test.go @@ -0,0 +1,337 @@ +package fastly + +import ( + "fmt" + "log" + "testing" + + gofastly "github.com/fastly/go-fastly/v9/fastly" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestResourceFastlyFlattenGrafanaCloudLogs(t *testing.T) { + cases := []struct { + remote []*gofastly.GrafanaCloudLogs + local []map[string]any + }{ + { + remote: []*gofastly.GrafanaCloudLogs{ + { + ServiceVersion: gofastly.ToPointer(1), + Name: gofastly.ToPointer("grafanacloudlogs-endpoint"), + User: gofastly.ToPointer("123456"), + Token: gofastly.ToPointer("token"), + URL: gofastly.ToPointer("https://test123.grafana.net"), + Index: gofastly.ToPointer("{\"label\": \"value\"}"), + + FormatVersion: gofastly.ToPointer(2), + }, + }, + local: []map[string]any{ + { + "name": "grafanacloudlogs-endpoint", + "user": "123456", + "token": "token", + "url": "https://test123.grafana.net", + "index": "{\"label\": \"value\"}", + "format_version": 2, + }, + }, + }, + } + + for _, c := range cases { + out := flattenGrafanaCloudLogs(c.remote) + if diff := cmp.Diff(out, c.local); diff != "" { + t.Fatalf("Error matching: %s", diff) + } + } +} + +var grafanacloudlogsDefaultFormat = `{ + "timestamp": "%{strftime(\{"%Y-%m-%dT%H:%M:%S"\}, time.start)}V", + "client_ip": "%{req.http.Fastly-Client-IP}V", + "geo_country": "%{client.geo.country_name}V", + "geo_city": "%{client.geo.city}V", + "host": "%{if(req.http.Fastly-Orig-Host, req.http.Fastly-Orig-Host, req.http.Host)}V", + "url": "%{json.escape(req.url)}V", + "request_method": "%{json.escape(req.method)}V", + "request_protocol": "%{json.escape(req.proto)}V", + "request_referer": "%{json.escape(req.http.referer)}V", + "request_user_agent": "%{json.escape(req.http.User-Agent)}V", + "response_state": "%{json.escape(fastly_info.state)}V", + "response_status": %{resp.status}V, + "response_reason": %{if(resp.response, "%22"+json.escape(resp.response)+"%22", "null")}V, + "response_body_size": %{resp.body_bytes_written}V, + "fastly_server": "%{json.escape(server.identity)}V", + "fastly_is_edge": %{if(fastly.ff.visits_this_service == 0, "true", "false")}V +}` + +func TestAccFastlyServiceVCL_logging_grafanacloudlogs_basic(t *testing.T) { + var service gofastly.ServiceDetail + name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + domain := fmt.Sprintf("fastly-test.%s.com", name) + + log1 := gofastly.GrafanaCloudLogs{ + Format: gofastly.ToPointer("%h %l %u %t \"%r\" %>s %b"), + FormatVersion: gofastly.ToPointer(2), + Name: gofastly.ToPointer("grafanacloudlogs-endpoint"), + ResponseCondition: gofastly.ToPointer(""), + ServiceVersion: gofastly.ToPointer(1), + User: gofastly.ToPointer("123456"), + Token: gofastly.ToPointer("token"), + URL: gofastly.ToPointer("https://test123.grafana.net"), + Index: gofastly.ToPointer("{\"label\": \"value\"}"), + } + + log1AfterUpdate := gofastly.GrafanaCloudLogs{ + Format: gofastly.ToPointer("%h %l %u %t \"%r\" %>s %b %T"), + FormatVersion: gofastly.ToPointer(2), + Name: gofastly.ToPointer("grafanacloudlogs-endpoint"), + ResponseCondition: gofastly.ToPointer(""), + ServiceVersion: gofastly.ToPointer(1), + User: gofastly.ToPointer("987654"), + Token: gofastly.ToPointer("t0k3n"), + URL: gofastly.ToPointer("https://test456.grafana.net"), + Index: gofastly.ToPointer("{\"label2\": \"value2\"}"), + } + + log2 := gofastly.GrafanaCloudLogs{ + Format: gofastly.ToPointer(grafanacloudlogsDefaultFormat + "\n"), + FormatVersion: gofastly.ToPointer(2), + Name: gofastly.ToPointer("another-grafanacloudlogs-endpoint"), + ResponseCondition: gofastly.ToPointer(""), + ServiceVersion: gofastly.ToPointer(1), + User: gofastly.ToPointer("123456"), + URL: gofastly.ToPointer("https://test789.grafana.net"), + Index: gofastly.ToPointer("{\"label3\": \"value3\"}"), + Token: gofastly.ToPointer("another-token"), + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckServiceVCLDestroy, + Steps: []resource.TestStep{ + { + Config: testAccServiceVCLGrafanaCloudLogsConfig(name, domain), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceExists("fastly_service_vcl.foo", &service), + testAccCheckFastlyServiceVCLGrafanaCloudLogsAttributes(&service, []*gofastly.GrafanaCloudLogs{&log1}, ServiceTypeVCL), + resource.TestCheckResourceAttr("fastly_service_vcl.foo", "name", name), + resource.TestCheckResourceAttr("fastly_service_vcl.foo", "logging_grafanacloudlogs.#", "1"), + ), + }, + + { + Config: testAccServiceVCLGrafanaCloudLogsConfigUpdate(name, domain), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceExists("fastly_service_vcl.foo", &service), + testAccCheckFastlyServiceVCLGrafanaCloudLogsAttributes(&service, []*gofastly.GrafanaCloudLogs{&log1AfterUpdate, &log2}, ServiceTypeVCL), + resource.TestCheckResourceAttr("fastly_service_vcl.foo", "name", name), + resource.TestCheckResourceAttr("fastly_service_vcl.foo", "logging_grafanacloudlogs.#", "2"), + ), + }, + }, + }) +} + +func TestAccFastlyServiceVCL_logging_grafanacloudlogs_basic_compute(t *testing.T) { + var service gofastly.ServiceDetail + name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + domain := fmt.Sprintf("fastly-test.%s.com", name) + + log1 := gofastly.GrafanaCloudLogs{ + ServiceVersion: gofastly.ToPointer(1), + Name: gofastly.ToPointer("grafanacloudlogs-endpoint"), + User: gofastly.ToPointer("123456"), + Token: gofastly.ToPointer("token"), + URL: gofastly.ToPointer("https://test123.grafana.net"), + Index: gofastly.ToPointer("{\"label\": \"value\"}"), + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckServiceVCLDestroy, + Steps: []resource.TestStep{ + { + Config: testAccServiceVCLGrafanaCloudLogsComputeConfig(name, domain), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceExists("fastly_service_compute.foo", &service), + testAccCheckFastlyServiceVCLGrafanaCloudLogsAttributes(&service, []*gofastly.GrafanaCloudLogs{&log1}, ServiceTypeCompute), + resource.TestCheckResourceAttr( + "fastly_service_compute.foo", "name", name), + resource.TestCheckResourceAttr( + "fastly_service_compute.foo", "logging_grafanacloudlogs.#", "1"), + ), + }, + }, + }) +} + +func testAccCheckFastlyServiceVCLGrafanaCloudLogsAttributes(service *gofastly.ServiceDetail, grafanacloudlogs []*gofastly.GrafanaCloudLogs, serviceType string) resource.TestCheckFunc { + return func(_ *terraform.State) error { + conn := testAccProvider.Meta().(*APIClient).conn + grafanacloudlogsList, err := conn.ListGrafanaCloudLogs(&gofastly.ListGrafanaCloudLogsInput{ + ServiceID: gofastly.ToValue(service.ServiceID), + ServiceVersion: gofastly.ToValue(service.ActiveVersion.Number), + }) + if err != nil { + return fmt.Errorf("error looking up GrafanaCloudLogs Logging for (%s), version (%d): %s", gofastly.ToValue(service.Name), gofastly.ToValue(service.ActiveVersion.Number), err) + } + + if len(grafanacloudlogsList) != len(grafanacloudlogs) { + return fmt.Errorf("grafanacloudlogs List count mismatch, expected (%d), got (%d)", len(grafanacloudlogs), len(grafanacloudlogsList)) + } + + log.Printf("[DEBUG] grafanacloudlogsList = %#v\n", grafanacloudlogsList) + + var found int + for _, d := range grafanacloudlogs { + for _, dl := range grafanacloudlogsList { + if gofastly.ToValue(d.Name) == gofastly.ToValue(dl.Name) { + // we don't know these things ahead of time, so populate them now + d.ServiceID = service.ServiceID + d.ServiceVersion = service.ActiveVersion.Number + // We don't track these, so clear them out because we also won't know + // these ahead of time + dl.CreatedAt = nil + dl.UpdatedAt = nil + + // Ignore VCL attributes for Compute and set to whatever is returned from the API. + if serviceType == ServiceTypeCompute { + dl.FormatVersion = d.FormatVersion + dl.Format = d.Format + dl.ResponseCondition = d.ResponseCondition + dl.Placement = d.Placement + } + + if diff := cmp.Diff(d, dl); diff != "" { + return fmt.Errorf("bad match GrafanaCloudLogs logging match: %s", diff) + } + found++ + } + } + } + + if found != len(grafanacloudlogs) { + return fmt.Errorf("error matching GrafanaCloudLogs Logging rules") + } + + return nil + } +} + +func testAccServiceVCLGrafanaCloudLogsConfig(name string, domain string) string { + return fmt.Sprintf(` +resource "fastly_service_vcl" "foo" { + name = "%s" + + domain { + name = "%s" + comment = "tf-grafanacloudlogs-logging" + } + + backend { + address = "aws.amazon.com" + name = "amazon docs" + } + + logging_grafanacloudlogs { + name = "grafanacloudlogs-endpoint" + user = "123456" + token = "token" + url = "https://test123.grafana.net" + index = "{\"label\": \"value\"}" + format = "%%h %%l %%u %%t \"%%r\" %%>s %%b" + } + + force_destroy = true +} +`, name, domain) +} + +func testAccServiceVCLGrafanaCloudLogsConfigUpdate(name, domain string) string { + return fmt.Sprintf(` +resource "fastly_service_vcl" "foo" { + name = "%s" + + domain { + name = "%s" + comment = "tf-grafanacloudlogs-logging" + } + + backend { + address = "aws.amazon.com" + name = "amazon docs" + } + + logging_grafanacloudlogs { + name = "grafanacloudlogs-endpoint" + user = "987654" + token = "t0k3n" + url = "https://test456.grafana.net" + index = "{\"label2\": \"value2\"}" + format = "%%h %%l %%u %%t \"%%r\" %%>s %%b %%T" + } + + logging_grafanacloudlogs { + name = "another-grafanacloudlogs-endpoint" + token = "another-token" + user = "123456" + url = "https://test789.grafana.net" + index = "{\"label3\": \"value3\"}" + format = <