Skip to content

Commit

Permalink
Wait on create op for endpoint service (#1560)
Browse files Browse the repository at this point in the history
Signed-off-by: Modular Magician <magic-modules@google.com>

Co-authored-by: emily <emilyye@google.com>
  • Loading branch information
modular-magician and emilymye committed Dec 26, 2019
1 parent a6f1bb9 commit 511af9d
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 53 deletions.
91 changes: 63 additions & 28 deletions google-beta/resource_endpoints_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"log"

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"google.golang.org/api/servicemanagement/v1"
Expand Down Expand Up @@ -117,7 +118,7 @@ func resourceEndpointsService() *schema.Resource {
}
}

func getOpenAPIConfigSource(configText string) servicemanagement.ConfigSource {
func getEndpointServiceOpenAPIConfigSource(configText string) *servicemanagement.ConfigSource {
// We need to provide a ConfigSource object to the API whenever submitting a
// new config. A ConfigSource contains a ConfigFile which contains the b64
// encoded contents of the file. OpenAPI requires only one file.
Expand All @@ -126,12 +127,12 @@ func getOpenAPIConfigSource(configText string) servicemanagement.ConfigSource {
FileType: "OPEN_API_YAML",
FilePath: "heredoc.yaml",
}
return servicemanagement.ConfigSource{
return &servicemanagement.ConfigSource{
Files: []*servicemanagement.ConfigFile{&configfile},
}
}

func getGRPCConfigSource(serviceConfig, protoConfig string) servicemanagement.ConfigSource {
func getEndpointServiceGRPCConfigSource(serviceConfig, protoConfig string) *servicemanagement.ConfigSource {
// gRPC requires both the file specifying the service and the compiled protobuf,
// but they can be in any order.
ymlConfigfile := servicemanagement.ConfigFile{
Expand All @@ -144,7 +145,7 @@ func getGRPCConfigSource(serviceConfig, protoConfig string) servicemanagement.Co
FileType: "FILE_DESCRIPTOR_SET_PROTO",
FilePath: "api_def.pb",
}
return servicemanagement.ConfigSource{
return &servicemanagement.ConfigSource{
Files: []*servicemanagement.ConfigFile{&ymlConfigfile, &protoConfigfile},
}
}
Expand All @@ -155,6 +156,7 @@ func resourceEndpointsServiceCreate(d *schema.ResourceData, meta interface{}) er
if err != nil {
return err
}

// If the service doesn't exist, we'll need to create it, but if it does, it
// will be reused. This is unusual for Terraform, but it causes the behavior
// that users will want and accept. Users of Endpoints are not thinking in
Expand All @@ -164,15 +166,28 @@ func resourceEndpointsServiceCreate(d *schema.ResourceData, meta interface{}) er
// so that we can perform the rollout without further disruption, which is the
// action that a user running `terraform apply` is going to want.
serviceName := d.Get("service_name").(string)
servicesService := servicemanagement.NewServicesService(config.clientServiceMan)
_, err = servicesService.Get(serviceName).Do()
log.Printf("[DEBUG] Create Endpoint Service %q", serviceName)

log.Printf("[DEBUG] Checking for existing ManagedService %q", serviceName)
_, err = config.clientServiceMan.Services.Get(serviceName).Do()
if err != nil {
_, err = servicesService.Create(&servicemanagement.ManagedService{ProducerProjectId: project, ServiceName: serviceName}).Do()
log.Printf("[DEBUG] Creating new ServiceManagement ManagedService %q", serviceName)
op, err := config.clientServiceMan.Services.Create(
&servicemanagement.ManagedService{
ProducerProjectId: project,
ServiceName: serviceName,
}).Do()
if err != nil {
return err
}

_, err = serviceManagementOperationWait(config, op, "Creating new ManagedService.")
if err != nil {
return err
}
}
// Do a rollout using the update mechanism.

// Use update to set other fields like config.
err = resourceEndpointsServiceUpdate(d, meta)
if err != nil {
return err
Expand All @@ -182,6 +197,20 @@ func resourceEndpointsServiceCreate(d *schema.ResourceData, meta interface{}) er
return resourceEndpointsServiceRead(d, meta)
}

func expandEndpointServiceConfigSource(d *schema.ResourceData, meta interface{}) (*servicemanagement.ConfigSource, error) {
if openapiConfig, ok := d.GetOk("openapi_config"); ok {
return getEndpointServiceOpenAPIConfigSource(openapiConfig.(string)), nil
}

grpcConfig, gok := d.GetOk("grpc_config")
protocOutput, pok := d.GetOk("protoc_output_base64")
if gok && pok {
return getEndpointServiceGRPCConfigSource(grpcConfig.(string), protocOutput.(string)), nil
}

return nil, errors.New("Could not parse config - either openapi_config or both grpc_config and protoc_output_base64 must be set.")
}

func resourceEndpointsServiceUpdate(d *schema.ResourceData, meta interface{}) error {
// This update is not quite standard for a terraform resource. Instead of
// using the go client library to send an HTTP request to update something
Expand All @@ -194,27 +223,26 @@ func resourceEndpointsServiceUpdate(d *schema.ResourceData, meta interface{}) er
// rollouts or A/B testing is going to need a more precise tool than this resource.
config := meta.(*Config)
serviceName := d.Get("service_name").(string)
var source servicemanagement.ConfigSource
if openapiConfig, ok := d.GetOk("openapi_config"); ok {
source = getOpenAPIConfigSource(openapiConfig.(string))
} else {
grpcConfig, gok := d.GetOk("grpc_config")
protocOutput, pok := d.GetOk("protoc_output_base64")

if gok && pok {
source = getGRPCConfigSource(grpcConfig.(string), protocOutput.(string))
} else {
return errors.New("Could not decypher config - please either set openapi_config or set both grpc_config and protoc_output_base64.")
}

log.Printf("[DEBUG] Updating ManagedService %q", serviceName)

cfgSource, err := expandEndpointServiceConfigSource(d, meta)
if err != nil {
return err
}

configService := servicemanagement.NewServicesConfigsService(config.clientServiceMan)
log.Printf("[DEBUG] Updating ManagedService %q", serviceName)
// The difference between "submit" and "create" is that submit parses the config
// you provide, where "create" requires the config in a pre-parsed format.
// "submit" will be a lot more flexible for users and will always be up-to-date
// with any new features that arise - this is why you provide a YAML config
// instead of providing the config in HCL.
op, err := configService.Submit(serviceName, &servicemanagement.SubmitConfigSourceRequest{ConfigSource: &source}).Do()
log.Printf("[DEBUG] Submitting config for ManagedService %q", serviceName)
op, err := config.clientServiceMan.Services.Configs.Submit(
serviceName,
&servicemanagement.SubmitConfigSourceRequest{
ConfigSource: cfgSource,
}).Do()
if err != nil {
return err
}
Expand All @@ -228,28 +256,32 @@ func resourceEndpointsServiceUpdate(d *schema.ResourceData, meta interface{}) er
}

// Next, we create a new rollout with the new config value, and wait for it to complete.
rolloutService := servicemanagement.NewServicesRolloutsService(config.clientServiceMan)
rollout := servicemanagement.Rollout{
ServiceName: serviceName,
TrafficPercentStrategy: &servicemanagement.TrafficPercentStrategy{
Percentages: map[string]float64{serviceConfig.ServiceConfig.Id: 100.0},
},
}
op, err = rolloutService.Create(serviceName, &rollout).Do()

log.Printf("[DEBUG] Creating new rollout for ManagedService %q", serviceName)
op, err = config.clientServiceMan.Services.Rollouts.Create(serviceName, &rollout).Do()
if err != nil {
return err
}
_, err = serviceManagementOperationWait(config, op, "Performing service rollout.")
if err != nil {
return err
}

return resourceEndpointsServiceRead(d, meta)
}

func resourceEndpointsServiceDelete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
servicesService := servicemanagement.NewServicesService(config.clientServiceMan)
op, err := servicesService.Delete(d.Get("service_name").(string)).Do()

log.Printf("[DEBUG] Deleting ManagedService %q", d.Id())

op, err := config.clientServiceMan.Services.Delete(d.Get("service_name").(string)).Do()
if err != nil {
return err
}
Expand All @@ -260,11 +292,14 @@ func resourceEndpointsServiceDelete(d *schema.ResourceData, meta interface{}) er

func resourceEndpointsServiceRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
servicesService := servicemanagement.NewServicesService(config.clientServiceMan)
service, err := servicesService.GetConfig(d.Get("service_name").(string)).Do()

log.Printf("[DEBUG] Reading ManagedService %q", d.Id())

service, err := config.clientServiceMan.Services.GetConfig(d.Get("service_name").(string)).Do()
if err != nil {
return err
}

d.Set("config_id", service.Id)
d.Set("dns_address", service.Name)
d.Set("apis", flattenServiceManagementAPIs(service.Apis))
Expand Down
78 changes: 53 additions & 25 deletions google-beta/resource_endpoints_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,45 @@ package google

import (
"reflect"
"strings"
"testing"

"fmt"

"github.com/hashicorp/terraform-plugin-sdk/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
"google.golang.org/api/servicemanagement/v1"
)

func TestAccEndpointsService_basic(t *testing.T) {
t.Parallel()
random_name := "t-" + acctest.RandString(10)
serviceId := "tf-test" + acctest.RandString(10)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
PreCheck: func() { testAccPreCheck(t) },
CheckDestroy: testAccCheckEndpointServiceDestroy,
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccEndpointsService_basic(random_name),
Check: testAccCheckEndpointExistsByName(random_name),
Config: testAccEndpointsService_basic(serviceId, getTestProjectFromEnv()),
Check: testAccCheckEndpointExistsByName(serviceId),
},
},
})
}

func TestAccEndpointsService_grpc(t *testing.T) {
t.Parallel()
random_name := "t-" + acctest.RandString(10)
serviceId := "tf-test" + acctest.RandString(10)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckEndpointServiceDestroy,
Steps: []resource.TestStep{
{
Config: testAccEndpointsService_grpc(random_name),
Check: testAccCheckEndpointExistsByName(random_name),
Config: testAccEndpointsService_grpc(serviceId, getTestProjectFromEnv()),
Check: testAccCheckEndpointExistsByName(serviceId),
},
},
})
Expand Down Expand Up @@ -96,18 +98,18 @@ func TestEndpointsService_grpcMigrateState(t *testing.T) {
}
}

func testAccEndpointsService_basic(random_name string) string {
func testAccEndpointsService_basic(serviceId, project string) string {
return fmt.Sprintf(`
resource "google_endpoints_service" "endpoints_service" {
service_name = "%s.endpoints.%s.cloud.goog"
project = "%s"
service_name = "%[1]s.endpoints.%[2]s.cloud.goog"
project = "%[2]s"
openapi_config = <<EOF
swagger: "2.0"
info:
description: "A simple Google Cloud Endpoints API example."
title: "Endpoints Example"
version: "1.0.0"
host: "%s.endpoints.%s.cloud.goog"
host: "%[1]s.endpoints.%[2]s.cloud.goog"
basePath: "/"
consumes:
- "application/json"
Expand Down Expand Up @@ -144,18 +146,18 @@ definitions:
EOF
}
`, random_name, getTestProjectFromEnv(), getTestProjectFromEnv(), random_name, getTestProjectFromEnv())
`, serviceId, project)
}

func testAccEndpointsService_grpc(random_name string) string {
func testAccEndpointsService_grpc(serviceId, project string) string {
return fmt.Sprintf(`
resource "google_endpoints_service" "endpoints_service" {
service_name = "%s.endpoints.%s.cloud.goog"
project = "%s"
service_name = "%[1]s.endpoints.%[2]s.cloud.goog"
project = "%[2]s"
grpc_config = <<EOF
type: google.api.Service
config_version: 3
name: %s.endpoints.%s.cloud.goog
name: %[1]s.endpoints.%[2]s.cloud.goog
usage:
rules:
- selector: endpoints.examples.bookstore.Bookstore.ListShelves
Expand All @@ -164,21 +166,47 @@ EOF
protoc_output_base64 = filebase64("test-fixtures/test_api_descriptor.pb")
}
`, random_name, getTestProjectFromEnv(), getTestProjectFromEnv(), random_name, getTestProjectFromEnv())
`, serviceId, project)
}

func testAccCheckEndpointExistsByName(random_name string) resource.TestCheckFunc {
func testAccCheckEndpointExistsByName(serviceId string) resource.TestCheckFunc {
return func(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
servicesService := servicemanagement.NewServicesService(config.clientServiceMan)
service, err := servicesService.GetConfig(fmt.Sprintf("%s.endpoints.%s.cloud.goog", random_name, config.Project)).Do()
service, err := config.clientServiceMan.Services.GetConfig(
fmt.Sprintf("%s.endpoints.%s.cloud.goog", serviceId, config.Project)).Do()
if err != nil {
return err
}
if service != nil {
return nil
} else {
return fmt.Errorf("Service %s.endpoints.%s.cloud.goog does not seem to exist.", random_name, config.Project)
return fmt.Errorf("Service %s.endpoints.%s.cloud.goog does not seem to exist.", serviceId, config.Project)
}
}
}

func testAccCheckEndpointServiceDestroy(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)

for name, rs := range s.RootModule().Resources {
if strings.HasPrefix(name, "data.") {
continue
}
if rs.Type != "google_endpoints_service" {
continue
}

serviceName := rs.Primary.Attributes["service_name"]
service, err := config.clientServiceMan.Services.GetConfig(serviceName).Do()
if err != nil {
// ServiceManagement returns 403 if service doesn't exist.
if !isGoogleApiErrorWithCode(err, 403) {
return err
}
}
if service != nil {
return fmt.Errorf("expected service %q to have been destroyed, got %+v", service.Name, service)
}
}
return nil
}
1 change: 1 addition & 0 deletions google-beta/resource_sql_database_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"

sqladmin "google.golang.org/api/sqladmin/v1beta4"
)

Expand Down

0 comments on commit 511af9d

Please sign in to comment.