terraform-provider-google/google/resource_endpoints_service.go
The Magician 23db6b0c12 Fix golangci-lint errors (#3059)
Signed-off-by: Modular Magician <magic-modules@google.com>
2019-02-14 16:00:35 -08:00

318 lines
10 KiB
Go

package google
import (
"encoding/base64"
"encoding/json"
"errors"
"github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/servicemanagement/v1"
)
func resourceEndpointsService() *schema.Resource {
return &schema.Resource{
Create: resourceEndpointsServiceCreate,
Read: resourceEndpointsServiceRead,
Delete: resourceEndpointsServiceDelete,
Update: resourceEndpointsServiceUpdate,
// Migrates protoc_output -> protoc_output_base64.
SchemaVersion: 1,
MigrateState: migrateEndpointsService,
Schema: map[string]*schema.Schema{
"service_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"openapi_config": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"grpc_config", "protoc_output_base64"},
},
"grpc_config": {
Type: schema.TypeString,
Optional: true,
},
"protoc_output": {
Type: schema.TypeString,
Optional: true,
Computed: true,
Removed: "Please use protoc_output_base64 instead.",
},
"protoc_output_base64": {
Type: schema.TypeString,
Optional: true,
},
"project": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"config_id": {
Type: schema.TypeString,
Computed: true,
},
"apis": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Computed: true,
},
"syntax": {
Type: schema.TypeString,
Computed: true,
},
"version": {
Type: schema.TypeString,
Computed: true,
},
"methods": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Computed: true,
},
"syntax": {
Type: schema.TypeString,
Computed: true,
},
"request_type": {
Type: schema.TypeString,
Computed: true,
},
"response_type": {
Type: schema.TypeString,
Computed: true,
},
},
},
},
},
},
},
"dns_address": {
Type: schema.TypeString,
Computed: true,
},
"endpoints": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Computed: true,
},
"address": {
Type: schema.TypeString,
Computed: true,
},
},
},
},
},
}
}
func getOpenAPIConfigSource(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.
configfile := servicemanagement.ConfigFile{
FileContents: base64.StdEncoding.EncodeToString([]byte(configText)),
FileType: "OPEN_API_YAML",
FilePath: "heredoc.yaml",
}
return servicemanagement.ConfigSource{
Files: []*servicemanagement.ConfigFile{&configfile},
}
}
func getGRPCConfigSource(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{
FileContents: base64.StdEncoding.EncodeToString([]byte(serviceConfig)),
FileType: "SERVICE_CONFIG_YAML",
FilePath: "heredoc.yaml",
}
protoConfigfile := servicemanagement.ConfigFile{
FileContents: protoConfig,
FileType: "FILE_DESCRIPTOR_SET_PROTO",
FilePath: "api_def.pb",
}
return servicemanagement.ConfigSource{
Files: []*servicemanagement.ConfigFile{&ymlConfigfile, &protoConfigfile},
}
}
func resourceEndpointsServiceCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
project, err := getProject(d, config)
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
// terms of services, configs, and rollouts - they just want the setup declared
// in their config to happen. The fact that a service may need to be created
// is not interesting to them. Consequently, we create this service if necessary
// 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()
if err != nil {
_, err = servicesService.Create(&servicemanagement.ManagedService{ProducerProjectId: project, ServiceName: serviceName}).Do()
if err != nil {
return err
}
}
// Do a rollout using the update mechanism.
err = resourceEndpointsServiceUpdate(d, meta)
if err != nil {
return err
}
d.SetId(serviceName)
return resourceEndpointsServiceRead(d, meta)
}
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
// serverside, we have to push a new configuration, wait for it to be
// parsed and loaded, then create and push a rollout and wait for that
// rollout to be completed.
// There's a lot of moving parts there, and all of them have knobs that can
// be tweaked if the user is using gcloud. In the interest of simplicity,
// we currently only support full rollouts - anyone trying to do incremental
// 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.")
}
}
configService := servicemanagement.NewServicesConfigsService(config.clientServiceMan)
// 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()
if err != nil {
return err
}
s, err := serviceManagementOperationWait(config, op, "Submitting service config.")
if err != nil {
return err
}
var serviceConfig servicemanagement.SubmitConfigSourceResponse
if err := json.Unmarshal(s, &serviceConfig); err != nil {
return err
}
// 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()
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()
if err != nil {
return err
}
_, err = serviceManagementOperationWait(config, op, "Deleting service.")
d.SetId("")
return err
}
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()
if err != nil {
return err
}
d.Set("config_id", service.Id)
d.Set("dns_address", service.Name)
d.Set("apis", flattenServiceManagementAPIs(service.Apis))
d.Set("endpoints", flattenServiceManagementEndpoints(service.Endpoints))
return nil
}
func flattenServiceManagementAPIs(apis []*servicemanagement.Api) []map[string]interface{} {
flattened := make([]map[string]interface{}, len(apis))
for i, a := range apis {
flattened[i] = map[string]interface{}{
"name": a.Name,
"version": a.Version,
"syntax": a.Syntax,
"methods": flattenServiceManagementMethods(a.Methods),
}
}
return flattened
}
func flattenServiceManagementMethods(methods []*servicemanagement.Method) []map[string]interface{} {
flattened := make([]map[string]interface{}, len(methods))
for i, m := range methods {
flattened[i] = map[string]interface{}{
"name": m.Name,
"syntax": m.Syntax,
"request_type": m.RequestTypeUrl,
"response_type": m.ResponseTypeUrl,
}
}
return flattened
}
func flattenServiceManagementEndpoints(endpoints []*servicemanagement.Endpoint) []map[string]interface{} {
flattened := make([]map[string]interface{}, len(endpoints))
for i, e := range endpoints {
flattened[i] = map[string]interface{}{
"name": e.Name,
"address": e.Target,
}
}
return flattened
}