Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

rmClient "github.com/hashicorp/terraform-provider-google/google/services/resourcemanager/client"
tpgserviceusage "github.com/hashicorp/terraform-provider-google/google/services/serviceusage"
"github.com/hashicorp/terraform-provider-google/google/registry"
"github.com/hashicorp/terraform-provider-google/google/tpgresource"
Expand Down Expand Up @@ -248,10 +247,6 @@ func resourceGoogleProjectServiceCreate(d *schema.ResourceData, meta interface{}

func resourceGoogleProjectServiceRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*transport_tpg.Config)
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
if err != nil {
return err
}

project, err := tpgresource.GetProject(d, config)
if err != nil {
Expand All @@ -260,17 +255,7 @@ func resourceGoogleProjectServiceRead(d *schema.ResourceData, meta interface{})
project = tpgresource.GetResourceNameFromSelfLink(project)

// Verify project for services still exists
projectGetCall := rmClient.NewClient(config, userAgent).Projects.Get(project)
if config.UserProjectOverride {
billingProject := project

// err == nil indicates that the billing_project value was found
if bp, err := tpgresource.GetBillingProject(d, config); err == nil {
billingProject = bp
}
projectGetCall.Header().Add("X-Goog-User-Project", billingProject)
}
p, err := projectGetCall.Do()
p, err := BatchRequestReadProject(project, d, config)

if err == nil && p.LifecycleState == "DELETE_REQUESTED" {
// Construct a 404 error for transport_tpg.HandleNotFoundError
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ import (
"log"
"time"

rmClient "github.com/hashicorp/terraform-provider-google/google/services/resourcemanager/client"
"github.com/hashicorp/terraform-provider-google/google/tpgresource"
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"google.golang.org/api/cloudresourcemanager/v1"
)

const (
batchKeyTmplServiceUsageEnableServices = "project/%s/services:batchEnable"
batchKeyTmplServiceUsageListServices = "project/%s/services"
batchKeyTmplServiceUsageGetProject = "project/%s/get"
)

// BatchRequestEnableServices can be used to batch requests to enable services
Expand Down Expand Up @@ -105,6 +108,37 @@ func BatchRequestReadServices(project string, d *schema.ResourceData, config *tr
d.Timeout(schema.TimeoutRead))
}

func BatchRequestReadProject(project string, d *schema.ResourceData, config *transport_tpg.Config) (*cloudresourcemanager.Project, error) {
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
if err != nil {
return nil, err
}

billingProject := project
// err == nil indicates that the billing_project value was found
if bp, err := tpgresource.GetBillingProject(d, config); err == nil {
billingProject = bp
}

req := &transport_tpg.BatchRequest{
ResourceName: project,
Body: nil,
// Use empty CombineF since the request is exactly the same no matter how many callers ask for this project.
CombineF: func(body interface{}, toAdd interface{}) (interface{}, error) { return nil, nil },
SendF: sendGetProject(config, billingProject, userAgent),
DebugId: fmt.Sprintf("Get Project %s", project),
}

resp, err := config.RequestBatcherServiceUsage.SendRequestWithTimeout(
fmt.Sprintf(batchKeyTmplServiceUsageGetProject, project),
req,
d.Timeout(schema.TimeoutRead))
if err != nil {
return nil, err
}
return resp.(*cloudresourcemanager.Project), nil
}

func combineServiceUsageServicesBatches(srvsRaw interface{}, toAddRaw interface{}) (interface{}, error) {
srvs, ok := srvsRaw.([]string)
if !ok {
Expand Down Expand Up @@ -133,3 +167,13 @@ func sendListServices(config *transport_tpg.Config, billingProject, userAgent st
return ListCurrentlyEnabledServices(project, billingProject, userAgent, config, timeout)
}
}

func sendGetProject(config *transport_tpg.Config, billingProject, userAgent string) transport_tpg.BatcherSendFunc {
return func(project string, _ interface{}) (interface{}, error) {
projectGetCall := rmClient.NewClient(config, userAgent).Projects.Get(project)
if config.UserProjectOverride {
projectGetCall.Header().Add("X-Goog-User-Project", billingProject)
}
return projectGetCall.Do()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package resourcemanager

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"sync"
"sync/atomic"
"testing"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"
)

func newTestConfigForProjectBatching(ts *httptest.Server) *transport_tpg.Config {
ctx := context.Background()
batchingConfig := &transport_tpg.BatchingConfig{
SendAfter: 100 * time.Millisecond,
EnableBatching: true,
}
return &transport_tpg.Config{
Context: ctx,
Client: ts.Client(),
CustomEndpoints: map[string]string{
// Matches resourcemanager's Product.CustomEndpointField.
"resource_manager_custom_endpoint": ts.URL + "/",
},
BatchingConfig: batchingConfig,
RequestBatcherServiceUsage: transport_tpg.NewRequestBatcher("Service Usage", ctx, batchingConfig),
}
}

func testProjectServiceResourceData(t *testing.T, project, service string) *schema.ResourceData {
t.Helper()
return schema.TestResourceDataRaw(t, ResourceGoogleProjectService().Schema, map[string]interface{}{
"project": project,
"service": service,
})
}

func TestBatchRequestReadProject_CollapsesConcurrentCallsIntoOneRequest(t *testing.T) {
const project = "my-project"
var requestCount int32

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&requestCount, 1)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"projectId": %q, "lifecycleState": "ACTIVE"}`, project)
}))
defer ts.Close()

config := newTestConfigForProjectBatching(ts)

const numCallers = 10
var wg sync.WaitGroup
wg.Add(numCallers)
errs := make([]error, numCallers)
lifecycleStates := make([]string, numCallers)

for i := 0; i < numCallers; i++ {
go func(idx int) {
defer wg.Done()
d := testProjectServiceResourceData(t, project, "foo.googleapis.com")
p, err := BatchRequestReadProject(project, d, config)
errs[idx] = err
if err == nil {
lifecycleStates[idx] = p.LifecycleState
}
}(i)
}
wg.Wait()

for i, err := range errs {
if err != nil {
t.Errorf("caller %d: unexpected error: %v", i, err)
}
if lifecycleStates[i] != "ACTIVE" {
t.Errorf("caller %d: expected lifecycleState ACTIVE, got %q", i, lifecycleStates[i])
}
}

if got := atomic.LoadInt32(&requestCount); got != 1 {
t.Errorf("expected exactly 1 HTTP request to be sent for %d concurrent callers, got %d", numCallers, got)
}
}

func TestBatchRequestReadProject_SeparateProjectsAreNotCombined(t *testing.T) {
var requestCount int32
seenProjects := make(map[string]bool)
var mu sync.Mutex

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&requestCount, 1)
mu.Lock()
seenProjects[r.URL.Path] = true
mu.Unlock()
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"lifecycleState": "ACTIVE"}`)
}))
defer ts.Close()

config := newTestConfigForProjectBatching(ts)

projects := []string{"project-a", "project-b"}
var wg sync.WaitGroup
wg.Add(len(projects))
for _, project := range projects {
go func(project string) {
defer wg.Done()
d := testProjectServiceResourceData(t, project, "foo.googleapis.com")
if _, err := BatchRequestReadProject(project, d, config); err != nil {
t.Errorf("project %s: unexpected error: %v", project, err)
}
}(project)
}
wg.Wait()

if got := atomic.LoadInt32(&requestCount); got != int32(len(projects)) {
t.Errorf("expected %d HTTP requests (one per distinct project), got %d", len(projects), got)
}
}
Loading