From d91b1cf20983cc4ccc6ce2b83d3045e86ee9eaf1 Mon Sep 17 00:00:00 2001 From: The Magician Date: Tue, 18 Feb 2020 10:31:25 -0800 Subject: [PATCH] Terraform Data Source to get DNSKEY records of DNSSEC-signed managed zones (#3117) (#1768) * DNSSEC Keys * update schema * Define as a data source * add into data sources map * remove unused import * add tests * No fill dns keys of zone is not dnssec enabled * add docs * add ds record to ksk * fix string templating * improve doc description * improve ds record generation * Update third_party/terraform/website/docs/d/datasource_dns_key.html.markdown Co-Authored-By: Sam Levenick * rename data source in plural * rename file, add comment on maps * rename doc file Co-authored-by: Sam Levenick Signed-off-by: Modular Magician Co-authored-by: Sam Levenick --- .changelog/3117.txt | 3 + google-beta/data_source_dns_key_test.go | 92 ++++++++ google-beta/data_source_dns_keys.go | 219 ++++++++++++++++++ google-beta/provider.go | 1 + .../docs/d/datasource_dns_keys.html.markdown | 70 ++++++ website/google.erb | 3 + 6 files changed, 388 insertions(+) create mode 100644 .changelog/3117.txt create mode 100644 google-beta/data_source_dns_key_test.go create mode 100644 google-beta/data_source_dns_keys.go create mode 100644 website/docs/d/datasource_dns_keys.html.markdown diff --git a/.changelog/3117.txt b/.changelog/3117.txt new file mode 100644 index 0000000000..43187dab00 --- /dev/null +++ b/.changelog/3117.txt @@ -0,0 +1,3 @@ +```release-note:new-datasource +google_dns_keys +``` diff --git a/google-beta/data_source_dns_key_test.go b/google-beta/data_source_dns_key_test.go new file mode 100644 index 0000000000..c7bc960540 --- /dev/null +++ b/google-beta/data_source_dns_key_test.go @@ -0,0 +1,92 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestAccDataSourceDNSKeys_basic(t *testing.T) { + t.Parallel() + + dnsZoneName := fmt.Sprintf("data-dnskey-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDNSManagedZoneDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceDNSKeysConfig(dnsZoneName, "on"), + Check: resource.ComposeTestCheckFunc( + testAccDataSourceDNSKeysDSRecordCheck("data.google_dns_keys.foo_dns_key"), + resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key", "key_signing_keys.#", "1"), + resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key", "zone_signing_keys.#", "1"), + resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key_id", "key_signing_keys.#", "1"), + resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key_id", "zone_signing_keys.#", "1"), + ), + }, + }, + }) +} + +func TestAccDataSourceDNSKeys_noDnsSec(t *testing.T) { + t.Parallel() + + dnsZoneName := fmt.Sprintf("data-dnskey-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDNSManagedZoneDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceDNSKeysConfig(dnsZoneName, "off"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key", "key_signing_keys.#", "0"), + resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key", "zone_signing_keys.#", "0"), + ), + }, + }, + }) +} + +func testAccDataSourceDNSKeysDSRecordCheck(datasourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + ds, ok := s.RootModule().Resources[datasourceName] + if !ok { + return fmt.Errorf("root module has no resource called %s", datasourceName) + } + + if ds.Primary.Attributes["key_signing_keys.0.ds_record"] == "" { + return fmt.Errorf("DS record not found in data source") + } + + return nil + } +} + +func testAccDataSourceDNSKeysConfig(dnsZoneName, dnssecStatus string) string { + return fmt.Sprintf(` +resource "google_dns_managed_zone" "foo" { + name = "%s" + dns_name = "dnssec.tf-test.club." + + dnssec_config { + state = "%s" + non_existence = "nsec3" + } +} + +data "google_dns_keys" "foo_dns_key" { + managed_zone = google_dns_managed_zone.foo.name +} + +data "google_dns_keys" "foo_dns_key_id" { + managed_zone = google_dns_managed_zone.foo.id +} +`, dnsZoneName, dnssecStatus) +} diff --git a/google-beta/data_source_dns_keys.go b/google-beta/data_source_dns_keys.go new file mode 100644 index 0000000000..7248c65ab3 --- /dev/null +++ b/google-beta/data_source_dns_keys.go @@ -0,0 +1,219 @@ +package google + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "google.golang.org/api/dns/v1" +) + +// DNSSEC Algorithm Numbers: https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml +// The following are algorithms that are supported by Cloud DNS +var dnssecAlgoNums = map[string]int{ + "rsasha1": 5, + "rsasha256": 8, + "rsasha512": 10, + "ecdsap256sha256": 13, + "ecdsap384sha384": 14, +} + +// DS RR Digest Types: https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml +// The following are digests that are supported by Cloud DNS +var dnssecDigestType = map[string]int{ + "sha1": 1, + "sha256": 2, + "sha384": 4, +} + +func dataSourceDNSKeys() *schema.Resource { + return &schema.Resource{ + Read: dataSourceDNSKeysRead, + + Schema: map[string]*schema.Schema{ + "managed_zone": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: compareSelfLinkOrResourceName, + }, + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "key_signing_keys": { + Type: schema.TypeList, + Computed: true, + Elem: kskResource(), + }, + "zone_signing_keys": { + Type: schema.TypeList, + Computed: true, + Elem: dnsKeyResource(), + }, + }, + } +} + +func dnsKeyResource() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "algorithm": { + Type: schema.TypeString, + Computed: true, + }, + "creation_time": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "digests": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "digest": { + Type: schema.TypeString, + Optional: true, + }, + "type": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "id": { + Type: schema.TypeString, + Computed: true, + }, + "is_active": { + Type: schema.TypeBool, + Computed: true, + }, + "key_length": { + Type: schema.TypeInt, + Computed: true, + }, + "key_tag": { + Type: schema.TypeInt, + Computed: true, + }, + "public_key": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func kskResource() *schema.Resource { + resource := dnsKeyResource() + + resource.Schema["ds_record"] = &schema.Schema{ + Type: schema.TypeString, + Computed: true, + } + + return resource +} + +func generateDSRecord(signingKey *dns.DnsKey) (string, error) { + algoNum, found := dnssecAlgoNums[signingKey.Algorithm] + if !found { + return "", fmt.Errorf("DNSSEC Algorithm number for %s not found", signingKey.Algorithm) + } + + digestType, found := dnssecDigestType[signingKey.Digests[0].Type] + if !found { + return "", fmt.Errorf("DNSSEC Digest type for %s not found", signingKey.Digests[0].Type) + } + + return fmt.Sprintf("%d %d %d %s", + signingKey.KeyTag, + algoNum, + digestType, + signingKey.Digests[0].Digest), nil +} + +func flattenSigningKeys(signingKeys []*dns.DnsKey, keyType string) []map[string]interface{} { + var keys []map[string]interface{} + + for _, signingKey := range signingKeys { + if signingKey != nil && signingKey.Type == keyType { + data := map[string]interface{}{ + "algorithm": signingKey.Algorithm, + "creation_time": signingKey.CreationTime, + "description": signingKey.Description, + "digests": flattenDigests(signingKey.Digests), + "id": signingKey.Id, + "is_active": signingKey.IsActive, + "key_length": signingKey.KeyLength, + "key_tag": signingKey.KeyTag, + "public_key": signingKey.PublicKey, + } + + if signingKey.Type == "keySigning" && len(signingKey.Digests) > 0 { + dsRecord, err := generateDSRecord(signingKey) + if err == nil { + data["ds_record"] = dsRecord + } + } + + keys = append(keys, data) + } + } + + return keys +} + +func flattenDigests(dnsKeyDigests []*dns.DnsKeyDigest) []map[string]interface{} { + var digests []map[string]interface{} + + for _, dnsKeyDigest := range dnsKeyDigests { + if dnsKeyDigest != nil { + data := map[string]interface{}{ + "digest": dnsKeyDigest.Digest, + "type": dnsKeyDigest.Type, + } + + digests = append(digests, data) + } + } + + return digests +} + +func dataSourceDNSKeysRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + fv, err := parseProjectFieldValue("managedZones", d.Get("managed_zone").(string), "project", d, config, false) + if err != nil { + return err + } + project := fv.Project + managedZone := fv.Name + + d.Set("project", project) + d.SetId(fmt.Sprintf("projects/%s/managedZones/%s", project, managedZone)) + + log.Printf("[DEBUG] Fetching DNS keys from managed zone %s", managedZone) + + response, err := config.clientDns.DnsKeys.List(project, managedZone).Do() + if err != nil && !isGoogleApiErrorWithCode(err, 404) { + return fmt.Errorf("error retrieving DNS keys: %s", err) + } else if isGoogleApiErrorWithCode(err, 404) { + return nil + } + + log.Printf("[DEBUG] Fetched DNS keys from managed zone %s", managedZone) + + d.Set("key_signing_keys", flattenSigningKeys(response.DnsKeys, "keySigning")) + d.Set("zone_signing_keys", flattenSigningKeys(response.DnsKeys, "zoneSigning")) + + return nil +} diff --git a/google-beta/provider.go b/google-beta/provider.go index 02c82d8596..7c8681cac0 100644 --- a/google-beta/provider.go +++ b/google-beta/provider.go @@ -510,6 +510,7 @@ func Provider() terraform.ResourceProvider { "google_container_engine_versions": dataSourceGoogleContainerEngineVersions(), "google_container_registry_image": dataSourceGoogleContainerImage(), "google_container_registry_repository": dataSourceGoogleContainerRepo(), + "google_dns_keys": dataSourceDNSKeys(), "google_dns_managed_zone": dataSourceDnsManagedZone(), "google_iam_policy": dataSourceGoogleIamPolicy(), "google_iam_role": dataSourceGoogleIamRole(), diff --git a/website/docs/d/datasource_dns_keys.html.markdown b/website/docs/d/datasource_dns_keys.html.markdown new file mode 100644 index 0000000000..aec8608293 --- /dev/null +++ b/website/docs/d/datasource_dns_keys.html.markdown @@ -0,0 +1,70 @@ +--- +subcategory: "Cloud DNS" +layout: "google" +page_title: "Google: google_dns_keys" +sidebar_current: "docs-google-datasource-dns-keys" +description: |- + Get DNSKEY and DS records of DNSSEC-signed managed zones. +--- + +# google\_dns\_keys + +Get the DNSKEY and DS records of DNSSEC-signed managed zones. For more information see the +[official documentation](https://cloud.google.com/dns/docs/dnskeys/) +and [API](https://cloud.google.com/dns/docs/reference/v1/dnsKeys). + + +## Example Usage + +```hcl +resource "google_dns_managed_zone" "foo" { + name = "foobar" + dns_name = "foo.bar." + + dnssec_config { + state = "on" + non_existence = "nsec3" + } +} + +data "google_dns_keys" "foo_dns_keys" { + managed_zone = google_dns_managed_zone.foo.id +} + +output "foo_dns_ds_record" { + description = "DS record of the foo subdomain." + value = data.google_dns_keys.foo_dns_keys.key_signing_keys[0].ds_record +} +``` + +## Argument Reference + +The following arguments are supported: + +* `managed_zone` - (Required) The name or id of the Cloud DNS managed zone. + +* `project` - (Optional) The ID of the project in which the resource belongs. If `project` is not provided, the provider project is used. + +## Attributes Reference + +The following attributes are exported: + +* `key_signing_keys` - A list of Key-signing key (KSK) records. Structure is documented below. Additionally, the DS record is provided: + * `ds_record` - The DS record based on the KSK record. This is used when [delegating](https://cloud.google.com/dns/docs/dnssec-advanced#subdelegation) DNSSEC-signed subdomains. + +* `zone_signing_keys` - A list of Zone-signing key (ZSK) records. Structure is documented below. + +--- + +The `key_signing_keys` and `zone_signing_keys` block supports: + * `algorithm` - String mnemonic specifying the DNSSEC algorithm of this key. Immutable after creation time. Possible values are `ecdsap256sha256`, `ecdsap384sha384`, `rsasha1`, `rsasha256`, and `rsasha512`. + * `creation_time` - The time that this resource was created in the control plane. This is in RFC3339 text format. + * `description` - A mutable string of at most 1024 characters associated with this resource for the user's convenience. + * `digests` - A list of cryptographic hashes of the DNSKEY resource record associated with this DnsKey. These digests are needed to construct a DS record that points at this DNS key. Each contains: + - `digest` - The base-16 encoded bytes of this digest. Suitable for use in a DS resource record. + - `type` - Specifies the algorithm used to calculate this digest. Possible values are `sha1`, `sha256` and `sha384` + * `id` - Unique identifier for the resource; defined by the server. + * `is_active` - Active keys will be used to sign subsequent changes to the ManagedZone. Inactive keys will still be present as DNSKEY Resource Records for the use of resolvers validating existing signatures. + * `key_length` - Length of the key in bits. Specified at creation time then immutable. + * `key_tag` - The key tag is a non-cryptographic hash of the a DNSKEY resource record associated with this DnsKey. The key tag can be used to identify a DNSKEY more quickly (but it is not a unique identifier). In particular, the key tag is used in a parent zone's DS record to point at the DNSKEY in this child ManagedZone. The key tag is a number in the range [0, 65535] and the algorithm to calculate it is specified in RFC4034 Appendix B. + * `public_key` - Base64 encoded public half of this key. diff --git a/website/google.erb b/website/google.erb index f0d56d9711..dade3d500c 100644 --- a/website/google.erb +++ b/website/google.erb @@ -126,6 +126,9 @@ > google_container_registry_repository + > + google_dns_keys + > google_dns_managed_zone