diff --git a/.changelog/3735.txt b/.changelog/3735.txt new file mode 100644 index 00000000000..04d25415115 --- /dev/null +++ b/.changelog/3735.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +`google_active_directory_domain` +``` diff --git a/google/active_directory_operation.go b/google/active_directory_operation.go new file mode 100644 index 00000000000..d834873b729 --- /dev/null +++ b/google/active_directory_operation.go @@ -0,0 +1,72 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- +package google + +import ( + "encoding/json" + "fmt" + "time" +) + +type ActiveDirectoryOperationWaiter struct { + Config *Config + Project string + CommonOperationWaiter +} + +func (w *ActiveDirectoryOperationWaiter) QueryOp() (interface{}, error) { + if w == nil { + return nil, fmt.Errorf("Cannot query operation, it's unset or nil.") + } + // Returns the proper get. + url := fmt.Sprintf("https://managedidentities.googleapis.com/v1/%s", w.CommonOperationWaiter.Op.Name) + return sendRequest(w.Config, "GET", w.Project, url, nil) +} + +func createActiveDirectoryWaiter(config *Config, op map[string]interface{}, project, activity string) (*ActiveDirectoryOperationWaiter, error) { + if val, ok := op["name"]; !ok || val == "" { + // This was a synchronous call - there is no operation to wait for. + return nil, nil + } + w := &ActiveDirectoryOperationWaiter{ + Config: config, + Project: project, + } + if err := w.CommonOperationWaiter.SetOp(op); err != nil { + return nil, err + } + return w, nil +} + +// nolint: deadcode,unused +func activeDirectoryOperationWaitTimeWithResponse(config *Config, op map[string]interface{}, response *map[string]interface{}, project, activity string, timeout time.Duration) error { + w, err := createActiveDirectoryWaiter(config, op, project, activity) + if err != nil || w == nil { + // If w is nil, the op was synchronous. + return err + } + if err := OperationWait(w, activity, timeout, config.PollInterval); err != nil { + return err + } + return json.Unmarshal([]byte(w.CommonOperationWaiter.Op.Response), response) +} + +func activeDirectoryOperationWaitTime(config *Config, op map[string]interface{}, project, activity string, timeout time.Duration) error { + w, err := createActiveDirectoryWaiter(config, op, project, activity) + if err != nil || w == nil { + // If w is nil, the op was synchronous. + return err + } + return OperationWait(w, activity, timeout, config.PollInterval) +} diff --git a/google/config.go b/google/config.go index 63842f7cedc..f8062619b52 100644 --- a/google/config.go +++ b/google/config.go @@ -80,6 +80,7 @@ type Config struct { tokenSource oauth2.TokenSource AccessContextManagerBasePath string + ActiveDirectoryBasePath string AppEngineBasePath string BigQueryBasePath string BigqueryDataTransferBasePath string @@ -224,6 +225,7 @@ type Config struct { // Generated product base paths var AccessContextManagerDefaultBasePath = "https://accesscontextmanager.googleapis.com/v1/" +var ActiveDirectoryDefaultBasePath = "https://managedidentities.googleapis.com/v1/" var AppEngineDefaultBasePath = "https://appengine.googleapis.com/v1/" var BigQueryDefaultBasePath = "https://www.googleapis.com/bigquery/v2/" var BigqueryDataTransferDefaultBasePath = "https://bigquerydatatransfer.googleapis.com/v1/" @@ -767,6 +769,7 @@ func removeBasePathVersion(url string) string { func ConfigureBasePaths(c *Config) { // Generated Products c.AccessContextManagerBasePath = AccessContextManagerDefaultBasePath + c.ActiveDirectoryBasePath = ActiveDirectoryDefaultBasePath c.AppEngineBasePath = AppEngineDefaultBasePath c.BigQueryBasePath = BigQueryDefaultBasePath c.BigqueryDataTransferBasePath = BigqueryDataTransferDefaultBasePath diff --git a/google/provider.go b/google/provider.go index 69926cee11e..5b1d7a7b9b9 100644 --- a/google/provider.go +++ b/google/provider.go @@ -117,6 +117,14 @@ func Provider() terraform.ResourceProvider { "GOOGLE_ACCESS_CONTEXT_MANAGER_CUSTOM_ENDPOINT", }, AccessContextManagerDefaultBasePath), }, + "active_directory_custom_endpoint": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateCustomEndpoint, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{ + "GOOGLE_ACTIVE_DIRECTORY_CUSTOM_ENDPOINT", + }, ActiveDirectoryDefaultBasePath), + }, "app_engine_custom_endpoint": { Type: schema.TypeString, Optional: true, @@ -590,9 +598,9 @@ func Provider() terraform.ResourceProvider { return provider } -// Generated resources: 143 +// Generated resources: 144 // Generated IAM resources: 57 -// Total generated resources: 200 +// Total generated resources: 201 func ResourceMap() map[string]*schema.Resource { resourceMap, _ := ResourceMapWithErrors() return resourceMap @@ -605,6 +613,7 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) { "google_access_context_manager_access_level": resourceAccessContextManagerAccessLevel(), "google_access_context_manager_service_perimeter": resourceAccessContextManagerServicePerimeter(), "google_access_context_manager_service_perimeter_resource": resourceAccessContextManagerServicePerimeterResource(), + "google_active_directory_domain": resourceActiveDirectoryDomain(), "google_app_engine_domain_mapping": resourceAppEngineDomainMapping(), "google_app_engine_firewall_rule": resourceAppEngineFirewallRule(), "google_app_engine_standard_app_version": resourceAppEngineStandardAppVersion(), @@ -971,6 +980,7 @@ func providerConfigure(d *schema.ResourceData, p *schema.Provider, terraformVers // Generated products config.AccessContextManagerBasePath = d.Get("access_context_manager_custom_endpoint").(string) + config.ActiveDirectoryBasePath = d.Get("active_directory_custom_endpoint").(string) config.AppEngineBasePath = d.Get("app_engine_custom_endpoint").(string) config.BigQueryBasePath = d.Get("big_query_custom_endpoint").(string) config.BigqueryDataTransferBasePath = d.Get("bigquery_data_transfer_custom_endpoint").(string) diff --git a/google/resource_active_directory_domain.go b/google/resource_active_directory_domain.go new file mode 100644 index 00000000000..ded7de8e104 --- /dev/null +++ b/google/resource_active_directory_domain.go @@ -0,0 +1,417 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "fmt" + "log" + "reflect" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func resourceActiveDirectoryDomain() *schema.Resource { + return &schema.Resource{ + Create: resourceActiveDirectoryDomainCreate, + Read: resourceActiveDirectoryDomainRead, + Update: resourceActiveDirectoryDomainUpdate, + Delete: resourceActiveDirectoryDomainDelete, + + Importer: &schema.ResourceImporter{ + State: resourceActiveDirectoryDomainImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(60 * time.Minute), + Update: schema.DefaultTimeout(60 * time.Minute), + Delete: schema.DefaultTimeout(60 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "domain_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateADDomainName(), + Description: `The fully qualified domain name. e.g. mydomain.myorganization.com, with the restrictions, +https://cloud.google.com/managed-microsoft-ad/reference/rest/v1/projects.locations.global.domains.`, + }, + "locations": { + Type: schema.TypeList, + Required: true, + Description: `Locations where domain needs to be provisioned. [regions][compute/docs/regions-zones/] +e.g. us-west1 or us-east4 Service supports up to 4 locations at once. Each location will use a /26 block.`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "reserved_ip_range": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The CIDR range of internal addresses that are reserved for this domain. Reserved networks must be /24 or larger. +Ranges must be unique and non-overlapping with existing subnets in authorizedNetworks`, + }, + "admin": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: `The name of delegated administrator account used to perform Active Directory operations. +If not specified, setupadmin will be used.`, + Default: "setupadmin", + }, + "authorized_networks": { + Type: schema.TypeSet, + Optional: true, + Description: `The full names of the Google Compute Engine networks the domain instance is connected to. The domain is only available on networks listed in authorizedNetworks. +If CIDR subnets overlap between networks, domain creation will fail.`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Set: schema.HashString, + }, + "labels": { + Type: schema.TypeMap, + Optional: true, + Description: `Resource labels that can contain user-provided metadata`, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "fqdn": { + Type: schema.TypeString, + Computed: true, + Description: `The fully-qualified domain name of the exposed domain used by clients to connect to the service. +Similar to what would be chosen for an Active Directory set up on an internal network.`, + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: `The unique name of the domain using the format: 'projects/{project}/locations/global/domains/{domainName}'.`, + }, + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + } +} + +func resourceActiveDirectoryDomainCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + obj := make(map[string]interface{}) + labelsProp, err := expandActiveDirectoryDomainLabels(d.Get("labels"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("labels"); !isEmptyValue(reflect.ValueOf(labelsProp)) && (ok || !reflect.DeepEqual(v, labelsProp)) { + obj["labels"] = labelsProp + } + authorizedNetworksProp, err := expandActiveDirectoryDomainAuthorizedNetworks(d.Get("authorized_networks"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("authorized_networks"); !isEmptyValue(reflect.ValueOf(authorizedNetworksProp)) && (ok || !reflect.DeepEqual(v, authorizedNetworksProp)) { + obj["authorizedNetworks"] = authorizedNetworksProp + } + reservedIpRangeProp, err := expandActiveDirectoryDomainReservedIpRange(d.Get("reserved_ip_range"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("reserved_ip_range"); !isEmptyValue(reflect.ValueOf(reservedIpRangeProp)) && (ok || !reflect.DeepEqual(v, reservedIpRangeProp)) { + obj["reservedIpRange"] = reservedIpRangeProp + } + locationsProp, err := expandActiveDirectoryDomainLocations(d.Get("locations"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("locations"); !isEmptyValue(reflect.ValueOf(locationsProp)) && (ok || !reflect.DeepEqual(v, locationsProp)) { + obj["locations"] = locationsProp + } + adminProp, err := expandActiveDirectoryDomainAdmin(d.Get("admin"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("admin"); !isEmptyValue(reflect.ValueOf(adminProp)) && (ok || !reflect.DeepEqual(v, adminProp)) { + obj["admin"] = adminProp + } + + url, err := replaceVars(d, config, "{{ActiveDirectoryBasePath}}projects/{{project}}/locations/global/domains?domainName={{domain_name}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating new Domain: %#v", obj) + project, err := getProject(d, config) + if err != nil { + return err + } + res, err := sendRequestWithTimeout(config, "POST", project, url, obj, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return fmt.Errorf("Error creating Domain: %s", err) + } + + // Store the ID now + id, err := replaceVars(d, config, "{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + // Use the resource in the operation response to populate + // identity fields and d.Id() before read + var opRes map[string]interface{} + err = activeDirectoryOperationWaitTimeWithResponse( + config, res, &opRes, project, "Creating Domain", + d.Timeout(schema.TimeoutCreate)) + if err != nil { + // The resource didn't actually create + d.SetId("") + return fmt.Errorf("Error waiting to create Domain: %s", err) + } + + if err := d.Set("name", flattenActiveDirectoryDomainName(opRes["name"], d, config)); err != nil { + return err + } + + // This may have caused the ID to update - update it if so. + id, err = replaceVars(d, config, "{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + log.Printf("[DEBUG] Finished creating Domain %q: %#v", d.Id(), res) + + return resourceActiveDirectoryDomainRead(d, meta) +} + +func resourceActiveDirectoryDomainRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + url, err := replaceVars(d, config, "{{ActiveDirectoryBasePath}}{{name}}") + if err != nil { + return err + } + + project, err := getProject(d, config) + if err != nil { + return err + } + res, err := sendRequest(config, "GET", project, url, nil) + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("ActiveDirectoryDomain %q", d.Id())) + } + + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error reading Domain: %s", err) + } + + if err := d.Set("name", flattenActiveDirectoryDomainName(res["name"], d, config)); err != nil { + return fmt.Errorf("Error reading Domain: %s", err) + } + if err := d.Set("labels", flattenActiveDirectoryDomainLabels(res["labels"], d, config)); err != nil { + return fmt.Errorf("Error reading Domain: %s", err) + } + if err := d.Set("authorized_networks", flattenActiveDirectoryDomainAuthorizedNetworks(res["authorizedNetworks"], d, config)); err != nil { + return fmt.Errorf("Error reading Domain: %s", err) + } + if err := d.Set("reserved_ip_range", flattenActiveDirectoryDomainReservedIpRange(res["reservedIpRange"], d, config)); err != nil { + return fmt.Errorf("Error reading Domain: %s", err) + } + if err := d.Set("locations", flattenActiveDirectoryDomainLocations(res["locations"], d, config)); err != nil { + return fmt.Errorf("Error reading Domain: %s", err) + } + if err := d.Set("admin", flattenActiveDirectoryDomainAdmin(res["admin"], d, config)); err != nil { + return fmt.Errorf("Error reading Domain: %s", err) + } + if err := d.Set("fqdn", flattenActiveDirectoryDomainFqdn(res["fqdn"], d, config)); err != nil { + return fmt.Errorf("Error reading Domain: %s", err) + } + + return nil +} + +func resourceActiveDirectoryDomainUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + labelsProp, err := expandActiveDirectoryDomainLabels(d.Get("labels"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("labels"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, labelsProp)) { + obj["labels"] = labelsProp + } + authorizedNetworksProp, err := expandActiveDirectoryDomainAuthorizedNetworks(d.Get("authorized_networks"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("authorized_networks"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, authorizedNetworksProp)) { + obj["authorizedNetworks"] = authorizedNetworksProp + } + locationsProp, err := expandActiveDirectoryDomainLocations(d.Get("locations"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("locations"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, locationsProp)) { + obj["locations"] = locationsProp + } + + url, err := replaceVars(d, config, "{{ActiveDirectoryBasePath}}{{name}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Updating Domain %q: %#v", d.Id(), obj) + updateMask := []string{} + + if d.HasChange("labels") { + updateMask = append(updateMask, "labels") + } + + if d.HasChange("authorized_networks") { + updateMask = append(updateMask, "authorizedNetworks") + } + + if d.HasChange("locations") { + updateMask = append(updateMask, "locations") + } + // updateMask is a URL parameter but not present in the schema, so replaceVars + // won't set it + url, err = addQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) + if err != nil { + return err + } + res, err := sendRequestWithTimeout(config, "PATCH", project, url, obj, d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return fmt.Errorf("Error updating Domain %q: %s", d.Id(), err) + } + + err = activeDirectoryOperationWaitTime( + config, res, project, "Updating Domain", + d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return err + } + + return resourceActiveDirectoryDomainRead(d, meta) +} + +func resourceActiveDirectoryDomainDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + url, err := replaceVars(d, config, "{{ActiveDirectoryBasePath}}{{name}}") + if err != nil { + return err + } + + var obj map[string]interface{} + log.Printf("[DEBUG] Deleting Domain %q", d.Id()) + + res, err := sendRequestWithTimeout(config, "DELETE", project, url, obj, d.Timeout(schema.TimeoutDelete)) + if err != nil { + return handleNotFoundError(err, d, "Domain") + } + + err = activeDirectoryOperationWaitTime( + config, res, project, "Deleting Domain", + d.Timeout(schema.TimeoutDelete)) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Finished deleting Domain %q: %#v", d.Id(), res) + return nil +} + +func resourceActiveDirectoryDomainImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + + config := meta.(*Config) + + // current import_formats can't import fields with forward slashes in their value + if err := parseImportId([]string{"(?P[^ ]+) (?P[^ ]+)", "(?P[^ ]+)"}, d, config); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} + +func flattenActiveDirectoryDomainName(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenActiveDirectoryDomainLabels(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenActiveDirectoryDomainAuthorizedNetworks(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return v + } + return schema.NewSet(schema.HashString, v.([]interface{})) +} + +func flattenActiveDirectoryDomainReservedIpRange(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenActiveDirectoryDomainLocations(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenActiveDirectoryDomainAdmin(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenActiveDirectoryDomainFqdn(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func expandActiveDirectoryDomainLabels(v interface{}, d TerraformResourceData, config *Config) (map[string]string, error) { + if v == nil { + return map[string]string{}, nil + } + m := make(map[string]string) + for k, val := range v.(map[string]interface{}) { + m[k] = val.(string) + } + return m, nil +} + +func expandActiveDirectoryDomainAuthorizedNetworks(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + v = v.(*schema.Set).List() + return v, nil +} + +func expandActiveDirectoryDomainReservedIpRange(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandActiveDirectoryDomainLocations(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandActiveDirectoryDomainAdmin(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} diff --git a/google/resource_active_directory_domain_generated_test.go b/google/resource_active_directory_domain_generated_test.go new file mode 100644 index 00000000000..02af25b5fee --- /dev/null +++ b/google/resource_active_directory_domain_generated_test.go @@ -0,0 +1,86 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestAccActiveDirectoryDomain_activeDirectoryDomainBasicExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckActiveDirectoryDomainDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccActiveDirectoryDomain_activeDirectoryDomainBasicExample(context), + }, + { + ResourceName: "google_active_directory_domain.ad-domain", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"domain_name"}, + }, + }, + }) +} + +func testAccActiveDirectoryDomain_activeDirectoryDomainBasicExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_active_directory_domain" "ad-domain" { + domain_name = "mydomain.org.com" + locations = ["us-central1"] + reserved_ip_range = "192.168.255.0/24" +} +`, context) +} + +func testAccCheckActiveDirectoryDomainDestroyProducer(t *testing.T) func(s *terraform.State) error { + return func(s *terraform.State) error { + for name, rs := range s.RootModule().Resources { + if rs.Type != "google_active_directory_domain" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + config := googleProviderConfig(t) + + url, err := replaceVarsForTest(config, rs, "{{ActiveDirectoryBasePath}}{{name}}") + if err != nil { + return err + } + + _, err = sendRequest(config, "GET", "", url, nil) + if err == nil { + return fmt.Errorf("ActiveDirectoryDomain still exists at %s", url) + } + } + + return nil + } +} diff --git a/google/resource_active_directory_domain_sweeper_test.go b/google/resource_active_directory_domain_sweeper_test.go new file mode 100644 index 00000000000..ec4ed3886e8 --- /dev/null +++ b/google/resource_active_directory_domain_sweeper_test.go @@ -0,0 +1,124 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "context" + "log" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func init() { + resource.AddTestSweepers("ActiveDirectoryDomain", &resource.Sweeper{ + Name: "ActiveDirectoryDomain", + F: testSweepActiveDirectoryDomain, + }) +} + +// At the time of writing, the CI only passes us-central1 as the region +func testSweepActiveDirectoryDomain(region string) error { + resourceName := "ActiveDirectoryDomain" + log.Printf("[INFO][SWEEPER_LOG] Starting sweeper for %s", resourceName) + + config, err := sharedConfigForRegion(region) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error getting shared config for region: %s", err) + return err + } + + err = config.LoadAndValidate(context.Background()) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error loading: %s", err) + return err + } + + t := &testing.T{} + billingId := getTestBillingAccountFromEnv(t) + + // Setup variables to replace in list template + d := &ResourceDataMock{ + FieldsInSchema: map[string]interface{}{ + "project": config.Project, + "region": region, + "location": region, + "zone": "-", + "billing_account": billingId, + }, + } + + listTemplate := strings.Split("https://managedidentities.googleapis.com/v1/projects/{{project}}/locations/global/domains?domainName={{domain_name}}", "?")[0] + listUrl, err := replaceVars(d, config, listTemplate) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error preparing sweeper list url: %s", err) + return nil + } + + res, err := sendRequest(config, "GET", config.Project, listUrl, nil) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error in response from request %s: %s", listUrl, err) + return nil + } + + resourceList, ok := res["domains"] + if !ok { + log.Printf("[INFO][SWEEPER_LOG] Nothing found in response.") + return nil + } + + rl := resourceList.([]interface{}) + + log.Printf("[INFO][SWEEPER_LOG] Found %d items in %s list response.", len(rl), resourceName) + // Keep count of items that aren't sweepable for logging. + nonPrefixCount := 0 + for _, ri := range rl { + obj := ri.(map[string]interface{}) + if obj["name"] == nil { + log.Printf("[INFO][SWEEPER_LOG] %s resource name was nil", resourceName) + return nil + } + + name := GetResourceNameFromSelfLink(obj["name"].(string)) + // Skip resources that shouldn't be sweeped + if !isSweepableTestResource(name) { + nonPrefixCount++ + continue + } + + deleteTemplate := "https://managedidentities.googleapis.com/v1/{{name}}" + deleteUrl, err := replaceVars(d, config, deleteTemplate) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error preparing delete url: %s", err) + return nil + } + deleteUrl = deleteUrl + name + + // Don't wait on operations as we may have a lot to delete + _, err = sendRequest(config, "DELETE", config.Project, deleteUrl, nil) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error deleting for url %s : %s", deleteUrl, err) + } else { + log.Printf("[INFO][SWEEPER_LOG] Sent delete request for %s resource: %s", resourceName, name) + } + } + + if nonPrefixCount > 0 { + log.Printf("[INFO][SWEEPER_LOG] %d items were non-sweepable and skipped.", nonPrefixCount) + } + + return nil +} diff --git a/google/resource_active_directory_domain_update_test.go b/google/resource_active_directory_domain_update_test.go new file mode 100644 index 00000000000..9a6bfcc369c --- /dev/null +++ b/google/resource_active_directory_domain_update_test.go @@ -0,0 +1,78 @@ +package google + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccActiveDirectoryDomain_update(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "domain": "mydomain.org1.com", + "resource_name": "ad-domain", + } + + resourceName := Nprintf("google_active_directory_domain.%{resource_name}", context) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckActiveDirectoryDomainDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccADDomainBasic(context), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"domain_name"}, + }, + { + Config: testAccADDomainUpdate(context), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"domain_name"}, + }, + { + Config: testAccADDomainBasic(context), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"domain_name"}, + }, + }, + }) +} + +func testAccADDomainBasic(context map[string]interface{}) string { + + return Nprintf(` + resource "google_active_directory_domain" "%{resource_name}" { + domain_name = "%{domain}" + locations = ["us-central1"] + reserved_ip_range = "192.168.255.0/24" + } + `, context) +} + +func testAccADDomainUpdate(context map[string]interface{}) string { + return Nprintf(` + resource "google_active_directory_domain" "%{resource_name}" { + domain_name = "%{domain}" + locations = ["us-central1", "us-west1"] + reserved_ip_range = "192.168.255.0/24" + labels = { + env = "test" + } + } + `, context) + +} diff --git a/google/validation.go b/google/validation.go index 0fa957c9120..2b145de95c4 100644 --- a/google/validation.go +++ b/google/validation.go @@ -30,6 +30,9 @@ const ( // https://cloud.google.com/iam/docs/understanding-custom-roles#naming_the_role IAMCustomRoleIDRegex = "^[a-zA-Z0-9_\\.]{3,64}$" + + // https://cloud.google.com/managed-microsoft-ad/reference/rest/v1/projects.locations.global.domains/create#query-parameters + ADDomainNameRegex = "^[a-z][a-z0-9-]{0,14}\\.[a-z0-9-\\.]*[a-z]+[a-z0-9]*$" ) var ( @@ -311,3 +314,15 @@ func validateRFC3339Date(v interface{}, k string) (warnings []string, errors []e } return } + +func validateADDomainName() schema.SchemaValidateFunc { + return func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + if len(value) > 64 || !regexp.MustCompile(ADDomainNameRegex).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q (%q) doesn't match regexp %q, domain_name must be 2 to 64 with lowercase letters, digits, hyphens, dots and start with a letter", k, value, ADDomainNameRegex)) + } + return + } +} diff --git a/website/docs/r/active_directory_domain.html.markdown b/website/docs/r/active_directory_domain.html.markdown new file mode 100644 index 00000000000..2310be83b9c --- /dev/null +++ b/website/docs/r/active_directory_domain.html.markdown @@ -0,0 +1,128 @@ +--- +# ---------------------------------------------------------------------------- +# +# *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +# +# ---------------------------------------------------------------------------- +# +# This file is automatically generated by Magic Modules and manual +# changes will be clobbered when the file is regenerated. +# +# Please read more about how to change this file in +# .github/CONTRIBUTING.md. +# +# ---------------------------------------------------------------------------- +subcategory: "Managed Microsoft Active Directory" +layout: "google" +page_title: "Google: google_active_directory_domain" +sidebar_current: "docs-google-active-directory-domain" +description: |- + Creates a Microsoft AD domain +--- + +# google\_active\_directory\_domain + +Creates a Microsoft AD domain + + +To get more information about Domain, see: + +* [API documentation](https://cloud.google.com/managed-microsoft-ad/reference/rest/v1/projects.locations.global.domains) +* How-to Guides + * [Managed Microsoft Active Directory Quickstart](https://cloud.google.com/managed-microsoft-ad/docs/quickstarts) + + +## Example Usage - Active Directory Domain Basic + + +```hcl +resource "google_active_directory_domain" "ad-domain" { + domain_name = "mydomain.org.com" + locations = ["us-central1"] + reserved_ip_range = "192.168.255.0/24" +} +``` + +## Argument Reference + +The following arguments are supported: + + +* `reserved_ip_range` - + (Required) + The CIDR range of internal addresses that are reserved for this domain. Reserved networks must be /24 or larger. + Ranges must be unique and non-overlapping with existing subnets in authorizedNetworks + +* `locations` - + (Required) + Locations where domain needs to be provisioned. [regions][compute/docs/regions-zones/] + e.g. us-west1 or us-east4 Service supports up to 4 locations at once. Each location will use a /26 block. + +* `domain_name` - + (Required) + The fully qualified domain name. e.g. mydomain.myorganization.com, with the restrictions, + https://cloud.google.com/managed-microsoft-ad/reference/rest/v1/projects.locations.global.domains. + + +- - - + + +* `labels` - + (Optional) + Resource labels that can contain user-provided metadata + +* `authorized_networks` - + (Optional) + The full names of the Google Compute Engine networks the domain instance is connected to. The domain is only available on networks listed in authorizedNetworks. + If CIDR subnets overlap between networks, domain creation will fail. + +* `admin` - + (Optional) + The name of delegated administrator account used to perform Active Directory operations. + If not specified, setupadmin will be used. + +* `project` - (Optional) The ID of the project in which the resource belongs. + If it is not provided, the provider project is used. + + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are exported: + +* `id` - an identifier for the resource with format `{{name}}` + +* `name` - + The unique name of the domain using the format: `projects/{project}/locations/global/domains/{domainName}`. + +* `fqdn` - + The fully-qualified domain name of the exposed domain used by clients to connect to the service. + Similar to what would be chosen for an Active Directory set up on an internal network. + + +## Timeouts + +This resource provides the following +[Timeouts](/docs/configuration/resources.html#timeouts) configuration options: + +- `create` - Default is 60 minutes. +- `update` - Default is 60 minutes. +- `delete` - Default is 60 minutes. + +## Import + +Domain can be imported using any of these accepted formats: + +``` +$ terraform import google_active_directory_domain.default {{name}} +``` + +-> If you're importing a resource with beta features, make sure to include `-provider=google-beta` +as an argument so that Terraform uses the correct provider to import your resource. + +## User Project Overrides + +This resource supports [User Project Overrides](https://www.terraform.io/docs/providers/google/guides/provider_reference.html#user_project_override). diff --git a/website/google.erb b/website/google.erb index cea70e46ce8..8be2487651f 100644 --- a/website/google.erb +++ b/website/google.erb @@ -1890,6 +1890,22 @@ +
  • + Managed Microsoft Active Directory + +
  • +
  • Memorystore (Redis)