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 a CNAME recursive resolving datasource #390

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

hightoxicity
Copy link

@hightoxicity hightoxicity commented Dec 16, 2023

This PR replaces #389 where @bflad mentioned about offering a new datasource that would allow to get the CNAME chained list and in term of design would avoid any confusion and also it would not imply extra resolutions some other do not need.

Feature description

Offer a new dns_recursive_cname_record_set datasource allowing to recursively resolve some chained CNAMEs.

Example:

dig A test2.tony.docusign.dev

; <<>> DiG 9.10.6 <<>> A test2.tony.docusign.dev
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 36583
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4000
;; QUESTION SECTION:
;test2.tony.docusign.dev.       IN      A

;; ANSWER SECTION:
test2.tony.docusign.dev. 1707   IN      CNAME   stage.services.docusign.net.
stage.services.docusign.net. 30 IN      CNAME   stage.services.docusign.net.akadns.net.
stage.services.docusign.net.akadns.net. 30 IN A 20.118.144.188

;; Query time: 69 msec
;; SERVER: 10.180.65.74#53(10.180.65.74)
;; WHEN: Tue Dec 19 08:57:59 CET 2023
;; MSG SIZE  rcvd: 158

We would like a way to get all recursive CNAMEs pieces traversed by above A request.

Data structure

data "dns_recursive_cname_record_set" "test" {
  host = "test2.tony.docusign.dev"
}

Output fields:

cnames (List of strings): ["stage.services.docusign.net", "stage.services.docusign.net.akadns.net."]
last_name (string): "stage.services.docusign.net.akadns.net."

Where cnames is an ordered list respecting CNAME precedence (higher index means more proximity to the last CNAME leaf) and where last_cname is a string containing the CNAME at the end of the chain.

Why to propose such orphan datasource (without a matching resource)?

When CNAME are chained, we have currently no easy way to unstack CNAMES using the existing data sources (dns_cname_record_set) because the chained CNAMEs tree depth is unknown and recursivity is not the Terraform HCL best ally, we currently do not see the way to get all the pieces, other data sources like dns_aaaa_record_set and dns_a_record_set allow to get the final value but we are not able to get the traversed CNAMEs to get up to the record value.
Unlike that, if we want to create such tree, it is as convenient as creating the end record set (with dns_a_record_set) and create some dns_cname_record the number of times we chain CNAMEs (from the one the closest of the leaf to the further one), we have no need to create a new resource for this.

The usecase where we (Docusign) need such datasource

We are using the Akamai terraform provider (https://registry.terraform.io/providers/akamai/akamai/latest/docs) to describe and provision traffic flows up to our datacenters.
This provider offers a akamai_gtm_property resource (https://techdocs.akamai.com/terraform/docs/gtm-rc-gtm-rc-property) that allows us to describe some datacenters as traffic target, for those traffic targets we can define some servers (using IPs or FQDNs).
In the case of FQDNs, if the user set one that has a prefix in akadns.net, then the API will reject it, it is the same if the FQDNs is a CNAME that goes at some point through a akamdns.net suffixed hostname (chained CNAMEs case).
Since we offer a self service to our developers, they may not know this particularity, currently they open a PR creating some properties or update some ones, a terraform plan is done in CI and everything is technically good for merging.
The problems are triggered later after merge/terraform apply when we face the above described GTM business rule and since we had no way to get all the pieces to resolve traversed CNAMEs of the provided FQDN, then we were not able to use a lifecycle precondition on the resource to make things fail on the PR into the CI.

The proposed feature would allows us to write something like this:

locals {
  expected_types = ["static", "failover", "ranked-failover", "geographic", "cidrmapping", "weighted-hashed", "weighted-round-robin", "weighted-round-robin-load-feedback", "qtr", "performance"]

  properties = concat(
    flatten([
      for dname, props in try(module.yaml_config_properties.map_configs.properties, {}) : [
        for prop_name, prop_specs in props : merge({
          domain_name      = dname
          property_name    = prop_name
        },
        prop_specs)
      ]
    ])
  )

  tt_server_hostnames = distinct(sort(flatten([ for prop in local.properties: [ for tt in prop.traffic_targets: [ for srv in tt.servers: srv if (length(try(regex("^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", srv), [])) == 0) && (length(try(regex("^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$"), [])) == 0) ] ] ])))
}

data "dns_recursive_cname_record_set" "tt_servers_rec_cnames" {
  for_each   = { for hostname in local.tt_server_hostnames : "${hostname}" => hostname }

  host = each.value
}

resource "akamai_gtm_property" "ak-gtm-properties" {
  for_each   = { for prop in local.properties : "${prop.domain_name}#${prop.property_name}" => prop }

  domain                      = akamai_gtm_domain.ak-gtm-domains[each.value.domain_name].name
  name                        = each.value.property_name
  cname                       = try(each.value.cname, null)
  comments                    = try(each.value.comments, null)
  type                        = each.value.type
  ipv6                        = try(each.value.ipv6, false)
  score_aggregation_type      = each.value.score_aggregation_type
  stickiness_bonus_percentage = try(each.value.stickiness_bonus_percentage, 0)
  stickiness_bonus_constant   = try(each.value.stickiness_bonus_constant, 0)
  use_computed_targets        = try(each.value.use_computed_targets, false)
  backup_ip                   = try(each.value.backup_ip, null)
  backup_cname                = try(each.value.backup_cname, null)
  balance_by_download_score   = try(each.value.balance_by_download_score, false)
  dynamic_ttl                 = try(each.value.dynamic_ttl, 30)
  handout_limit               = try(each.value.handout_limit, 8)
  handout_mode                = try(each.value.handout_mode, "normal")
  failover_delay              = try(each.value.failover_delay, 0)
  failback_delay              = try(each.value.failback_delay, 0)
  ghost_demand_reporting      = try(each.value.ghost_demand_reporting, false)
  map_name                    = try(each.value.map_name, null)

  dynamic "traffic_target" {
    for_each = try(each.value.traffic_targets, null) == null ? [] : each.value.traffic_targets
    content {
      datacenter_id = try(traffic_target.value["datacenter"], null) == null ? null : akamai_gtm_datacenter.ak-gtm-datacenters[format("%s#%s", each.value.domain_name, traffic_target.value["datacenter"])].datacenter_id
      enabled       = try(traffic_target.value["enabled"], false)
      weight        = try(traffic_target.value["weight"], null)
      servers       = try(traffic_target.value["servers"], [])
      name          = try(traffic_target.value["name"], null)
      handout_cname = try(traffic_target.value["handout_cname"], null)
    }
  }

  dynamic "liveness_test" {
    for_each = concat(try(each.value.liveness_tests, null) == null ? [] : each.value.liveness_tests, try(each.value.common_liveness_tests, null) == null ? [] : [for v in each.value.common_liveness_tests: (contains(keys(module.yaml_config_liveness_tests.map_configs.liveness_tests[v]), "name") ? module.yaml_config_liveness_tests.map_configs.liveness_tests[v] : merge(module.yaml_config_liveness_tests.map_configs.liveness_tests[v], {name = v}))])
    content {
      name                             = liveness_test.value.name
      peer_certificate_verification    = try(liveness_test.value.peer_certificate_verification, false)
      test_interval                    = try(liveness_test.value.test_interval, 10)
      test_object                      = try(liveness_test.value.test_object, null) == null ? "" : liveness_test.value.test_object
      http_error3xx                    = try(liveness_test.value.http_error3xx, false)
      http_error4xx                    = try(liveness_test.value.http_error4xx, false)
      http_error5xx                    = try(liveness_test.value.http_error5xx, false)
      disabled                         = try(liveness_test.value.disabled, false)
      test_object_protocol             = try(liveness_test.value.test_object_protocol, "HTTPS")
      test_object_port                 = try(liveness_test.value.test_object_port, 443)
      disable_nonstandard_port_warning = try(liveness_test.value.disable_nonstandard_port_warning, false)
      test_timeout                     = try(liveness_test.value.test_timeout, 10.0)
      answers_required                 = try(liveness_test.value.answers_required, false)
      recursion_requested              = try(liveness_test.value.recursion_requested, false)
      resource_type                    = try(liveness_test.value.resource_type, null)
      request_string                   = try(liveness_test.value.request_string, null)
      response_string                  = try(liveness_test.value.response_string, null)

      dynamic "http_header" {
        for_each = concat(try(liveness_test.value.http_headers, []),
          try(
            liveness_test.value.host_header == null ? [] : [{
              name  = "Host"
              value = liveness_test.value.host_header
            }],
            []
          )
        )

        content {
          name  = http_header.value.name
          value = http_header.value.value
        }
      }
    }
  }

  lifecycle {
    precondition {
      condition     = contains(local.expected_types, each.value.type)
      error_message = format("You tried to set an invalid type value of %s on %s.%s property, the type should be one of [%s] values", each.value.type, each.value.property_name, akamai_gtm_domain.ak-gtm-domains[each.value.domain_name].name, join(",", local.expected_types))
    }

    precondition {
      condition     = alltrue([for v in (try(each.value.traffic_targets, null) == null ? [] : each.value.traffic_targets): (alltrue([for s in (try(v.servers, null) == null ? [] : v.servers): (length(try(regex("^(.+\\.akadns\\.net\\.?)$", s), [])) == 0)]))])
      error_message = format("You tried to set an unsupported hostname in the servers list of one traffic target (%s.%s property), .akadns.net suffix is not supported here, you may want to use a handout CNAME instead...", each.value.property_name, akamai_gtm_domain.ak-gtm-domains[each.value.domain_name].name)
    }

   precondition {
     condition     = alltrue([for v in (try(each.value.traffic_targets, null) == null ? [] : each.value.traffic_targets): (alltrue([for s in (try(v.servers, null) == null ? [] : v.servers): alltrue([for c in try(data.dns_recursive_cname_record_set.tt_servers_rec_cnames[s].cnames, []): (length(try(regex("^(.+\\.akadns\\.net\\.?)$", c), [])) == 0)])]))])
     error_message = format("You tried to set a CNAME that leads to an akadns.net hostname as a traffic target server into the %s.%s property: %#v, it is forbidden by GTM", each.value.property_name, akamai_gtm_domain.ak-gtm-domains[each.value.domain_name].name, flatten([for v in (try(each.value.traffic_targets, null) == null ? [] : each.value.traffic_targets): [for s in (try(v.servers, null) == null ? [] : v.servers): [for c in try(data.dns_recursive_cname_record_set.tt_servers_rec_cnames[s].cnames, []): format("%s leads to %s", s, c) if (length(try(regex("^(.+\\.akadns\\.net\\.?)$", c), [])) > 0)]]]))
   }
  }
}

We would be able to make a lookup into the CNAMEs pieces of all provided servers for a property and make any plan fail when akadns.net prefixed hostname is traversed at some point.

@hightoxicity hightoxicity force-pushed the feat-add-cname-recursive-resolution-datasource branch from 3895045 to b699e96 Compare August 30, 2024 08:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant