Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new google_project_service resource for fine-grained service control. #668

Merged
merged 3 commits into from
Nov 7, 2017
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions google/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ func Provider() terraform.ResourceProvider {
"google_project_iam_policy": resourceGoogleProjectIamPolicy(),
"google_project_iam_binding": resourceGoogleProjectIamBinding(),
"google_project_iam_member": resourceGoogleProjectIamMember(),
"google_project_service": resourceGoogleProjectService(),
"google_project_services": resourceGoogleProjectServices(),
"google_pubsub_topic": resourcePubsubTopic(),
"google_pubsub_subscription": resourcePubsubSubscription(),
Expand Down
87 changes: 87 additions & 0 deletions google/resource_google_project_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package google

import (
"fmt"

"github.com/hashicorp/terraform/helper/schema"
)

func resourceGoogleProjectService() *schema.Resource {
return &schema.Resource{
Create: resourceGoogleProjectServiceCreate,
Read: resourceGoogleProjectServiceRead,
Delete: resourceGoogleProjectServiceDelete,

Schema: map[string]*schema.Schema{
"service": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"project": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
},
}
}

func resourceGoogleProjectServiceCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

project, err := getProject(d, config)
if err != nil {
return err
}

srv := d.Get("service").(string)

if err = enableService(srv, project, config); err != nil {
return fmt.Errorf("Error enabling service: %s", err)
}

d.SetId(srv)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we include the project in this, so multiple projects can enable the same service within the same Terraform state?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! Done.

return resourceGoogleProjectServiceRead(d, meta)
}

func resourceGoogleProjectServiceRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

project, err := getProject(d, config)
if err != nil {
return err
}

services, err := getApiServices(project, config, map[string]struct{}{})
if err != nil {
return err
}

for _, s := range services {
if s == d.Id() {
d.Set("service", s)
return nil
}
}

// The service is not enabled server-side, so remove it from state
d.SetId("")
return nil
}

func resourceGoogleProjectServiceDelete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

project, err := getProject(d, config)
if err != nil {
return err
}

if err = disableService(d.Id(), project, config); err != nil {
return fmt.Errorf("Error disabling service: %s", err)
}

d.SetId("")
return nil
}
85 changes: 85 additions & 0 deletions google/resource_google_project_service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package google

import (
"fmt"
"testing"

"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)

// Test that services can be enabled and disabled on a project
func TestAccGoogleProjectService_basic(t *testing.T) {
t.Parallel()

pid := "terraform-" + acctest.RandString(10)
services := []string{"iam.googleapis.com", "cloudresourcemanager.googleapis.com"}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccGoogleProjectService_basic(services, pid, pname, org),
Check: resource.ComposeTestCheckFunc(
testAccCheckProjectService(services, pid, true),
),
},
// Use a separate TestStep rather than a CheckDestroy because we need the project to still exist.
resource.TestStep{
Config: testAccGoogleProject_create(pid, pname, org),
Check: resource.ComposeTestCheckFunc(
testAccCheckProjectService(services, pid, false),
),
},
},
})
}

func testAccCheckProjectService(services []string, pid string, expectEnabled bool) resource.TestCheckFunc {
return func(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)

apiServices, err := getApiServices(pid, config, map[string]struct{}{})
if err != nil {
return fmt.Errorf("Error listing services for project %q: %v", pid, err)
}

for _, expected := range services {
exists := false
for _, actual := range apiServices {
if expected == actual {
exists = true
}
}
if expectEnabled && !exists {
return fmt.Errorf("Expected service %s is not enabled server-side (found %v)", expected, apiServices)
}
if !expectEnabled && exists {
return fmt.Errorf("Expected disabled service %s is enabled server-side", expected)
}
}

return nil
}
}

func testAccGoogleProjectService_basic(services []string, pid, name, org string) string {
return fmt.Sprintf(`
resource "google_project" "acceptance" {
project_id = "%s"
name = "%s"
org_id = "%s"
}

resource "google_project_service" "test" {
project = "${google_project.acceptance.project_id}"
service = "%s"
}

resource "google_project_service" "test2" {
project = "${google_project.acceptance.project_id}"
service = "%s"
}
`, pid, name, org, services[0], services[1])
}
46 changes: 29 additions & 17 deletions google/resource_google_project_services.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func resourceGoogleProjectServices() *schema.Resource {

// These services can only be enabled as a side-effect of enabling other services,
// so don't bother storing them in the config or using them for diffing.
var ignore = map[string]struct{}{
var ignoreProjectServices = map[string]struct{}{
"containeranalysis.googleapis.com": struct{}{},
"dataproc-control.googleapis.com": struct{}{},
"source.googleapis.com": struct{}{},
Expand All @@ -50,7 +50,7 @@ func resourceGoogleProjectServicesCreate(d *schema.ResourceData, meta interface{
cfgServices := getConfigServices(d)

// Get services from API
apiServices, err := getApiServices(pid, config)
apiServices, err := getApiServices(pid, config, ignoreProjectServices)
if err != nil {
return fmt.Errorf("Error creating services: %v", err)
}
Expand All @@ -69,7 +69,7 @@ func resourceGoogleProjectServicesCreate(d *schema.ResourceData, meta interface{
func resourceGoogleProjectServicesRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

services, err := getApiServices(d.Id(), config)
services, err := getApiServices(d.Id(), config, ignoreProjectServices)
if err != nil {
return err
}
Expand All @@ -88,7 +88,7 @@ func resourceGoogleProjectServicesUpdate(d *schema.ResourceData, meta interface{
cfgServices := getConfigServices(d)

// Get services from API
apiServices, err := getApiServices(pid, config)
apiServices, err := getApiServices(pid, config, ignoreProjectServices)
if err != nil {
return fmt.Errorf("Error updating services: %v", err)
}
Expand Down Expand Up @@ -164,7 +164,7 @@ func getConfigServices(d *schema.ResourceData) (services []string) {
}

// Retrieve a project's services from the API
func getApiServices(pid string, config *Config) ([]string, error) {
func getApiServices(pid string, config *Config, ignore map[string]struct{}) ([]string, error) {
apiServices := make([]string, 0)
// Get services from the API
token := ""
Expand All @@ -186,28 +186,40 @@ func getApiServices(pid string, config *Config) ([]string, error) {

func enableService(s, pid string, config *Config) error {
esr := newEnableServiceRequest(pid)
sop, err := config.clientServiceMan.Services.Enable(s, esr).Do()
err := retry(func() error {
sop, err := config.clientServiceMan.Services.Enable(s, esr).Do()
if err != nil {
return err
}
waitErr := serviceManagementOperationWait(config, sop, "api to enable")
if waitErr != nil {
return waitErr
}
return nil
})
if err != nil {
return fmt.Errorf("Error enabling service %q for project %q: %v", s, pid, err)
}
// Wait for the operation to complete
waitErr := serviceManagementOperationWait(config, sop, "api to enable")
if waitErr != nil {
return waitErr
}
return nil
}

func disableService(s, pid string, config *Config) error {
dsr := newDisableServiceRequest(pid)
sop, err := config.clientServiceMan.Services.Disable(s, dsr).Do()
err := retry(func() error {
sop, err := config.clientServiceMan.Services.Disable(s, dsr).Do()
if err != nil {
return err
}
// Wait for the operation to complete
waitErr := serviceManagementOperationWait(config, sop, "api to disable")
if waitErr != nil {
return waitErr
}
return nil
})
if err != nil {
return fmt.Errorf("Error disabling service %q for project %q: %v", s, pid, err)
}
// Wait for the operation to complete
waitErr := serviceManagementOperationWait(config, sop, "api to disable")
if waitErr != nil {
return waitErr
}
return nil
}

Expand Down
2 changes: 1 addition & 1 deletion google/resource_google_project_services_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ func testProjectServicesMatch(services []string, pid string) resource.TestCheckF
return func(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)

apiServices, err := getApiServices(pid, config)
apiServices, err := getApiServices(pid, config, ignoreProjectServices)
if err != nil {
return fmt.Errorf("Error listing services for project %q: %v", pid, err)
}
Expand Down
34 changes: 34 additions & 0 deletions website/docs/r/google_project_service.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
layout: "google"
page_title: "Google: google_project_service"
sidebar_current: "docs-google-project-service"
description: |-
Allows management of a single API service for a Google Cloud Platform project.
---

# google\_project\_service

Allows management of a single API service for an existing Google Cloud Platform project.

For a list of services available, visit the
[API library page](https://console.cloud.google.com/apis/library) or run `gcloud service-management list`.

~> **Note:** This resource _must not_ be used in conjunction with
`google_project_services` or they will fight over which services should be enabled.

## Example Usage

```hcl
resource "google_project_service" "project" {
project = "your-project-id"
service = "iam.googleapis.com"
}
```

## Argument Reference

The following arguments are supported:

* `service` - (Required) The service to enable.

* `project` - (Optional) The project ID. If not provided, the provider project is used.
4 changes: 4 additions & 0 deletions website/docs/r/google_project_services.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ in the config will be removed.
For a list of services available, visit the
[API library page](https://console.cloud.google.com/apis/library) or run `gcloud service-management list`.

~> **Note:** This resource attempts to be the authoritative source on which APIs are enabled, which can
lead to conflicts when certain APIs or actions enable other APIs. To just ensure that a specific
API is enabled, use the [google_project_service](google_project_service.html) resource.

## Example Usage

```hcl
Expand Down
3 changes: 3 additions & 0 deletions website/google.erb
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@
<li<%= sidebar_current("docs-google-project-iam-policy") %>>
<a href="/docs/providers/google/r/google_project_iam_policy.html">google_project_iam_policy</a>
</li>
<li<%= sidebar_current("docs-google-project-service") %>>
<a href="/docs/providers/google/r/google_project_service.html">google_project_service</a>
</li>
<li<%= sidebar_current("docs-google-project-services") %>>
<a href="/docs/providers/google/r/google_project_services.html">google_project_services</a>
</li>
Expand Down