Skip to content

About

Terraform module: terraform-dynatrace-direct-shares

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

🔷 Dynatrace Direct Shares Terraform Module

Manages a direct-share grant for a single Dynatrace Platform document (dashboard, notebook, or launchpad) via dynatrace_direct_shares, targeting the dynatrace-oss/dynatrace provider ~> 1.98. This is a composite module: one keystone resource per document-share grant, owning a caller-keyed collection of recipients rendered as a nested block inside that same resource.

Terraform Dynatrace Provider Module Version Module Type Resources Posture


🧩 Overview

  • 🎯 Manages exactly one dynatrace_direct_shares.this keystone resource per module instance — one document-share grant.
  • 🧷 Owns a caller-keyed map(object({ id, type })) of recipients (var.recipients), rendered as a single dynamic "recipients" wrapper block containing a nested dynamic "recipient" block — not a separate child resource. The underlying schema's own recipients wrapper is Max: 1; its inner recipient set is Max: 1000.
  • 🚫 Produces an inert share (grants access to nobody) when var.recipients is left at its default empty map — the caller must explicitly add recipients to share the document with anyone.
  • 🔒 Defaults access to "read" — the narrower of the resource's two legal values — as an explicit house override; the provider itself documents no default for this field (see § Schema notes that bite).
  • 🔗 Consumes document_id from a sibling terraform-dynatrace-document instance's id output. This module does not create or manage the document itself, and does not create the recipient users/groups it references — both are owned elsewhere.
  • ☁️ Dynatrace SaaS only, and OAuth-only — this resource has no legacy API-token fallback (see § Required OAuth Scopes below), unlike most Settings 2.0 resources in this provider.

💡 Why it matters: direct shares are how an individual dashboard or notebook gets handed to a specific person or group without changing that document's environment-wide visibility. Modeling access with a secure, explicit default and modeling recipients as a caller-keyed map (never a positional list) means a reviewer can see, line by line in a terraform plan diff, exactly who is gaining or losing access to exactly which document — and a newly-authored module call never accidentally grants write access or shares with an unintended recipient by omission.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


🗺️ Where this fits

flowchart LR
 Doc["terraform-dynatrace-document<br/>(upstream module)"]:::sibling
 Recip["IAM users / groups<br/>(pre-existing recipients)"]:::sibling
 ThisMod["terraform-dynatrace-direct-shares<br/>(this module)"]:::thismodule
 Keystone["dynatrace_direct_shares<br/>(keystone)"]:::keystone

 Doc -- "emits `id`" --> ThisMod
 ThisMod -- "sets `document_id` on" --> Keystone
 Recip -- "referenced by `id` as" --> Keystone

 classDef thismodule fill:#1496FF,color:#FFFFFF,stroke:#0A2540,stroke-width:2px;
 classDef keystone fill:#0A2540,color:#FFFFFF,stroke:#0A2540,stroke-width:2px;
 classDef sibling fill:#E8EEF3,color:#0A2540,stroke:#94A3B8,stroke-width:1px;
Loading

Validated via the Mermaid Chart MCP (validate_and_render_mermaid_diagram) — valid: true.

  • terraform-dynatrace-document (light neutral, upstream sibling) owns the target dynatrace_document this module shares. Its id output feeds this module's document_id input — an implicit cross-module reference, never a hardcoded document ID.
  • IAM users / groups (light neutral, external) are pre-existing entities this module references by id inside each recipients map entry. This module does not create or manage users or groups.
  • This module (#1496FF, white text) renders the one keystone resource, dynatrace_direct_shares.this (#0A2540), which is this domain's target/keystone.

🧬 What this builds

flowchart TB
 Inputs["var.document_id
var.access
var.recipients"]:::io

 subgraph Keystone["dynatrace_direct_shares.this — keystone"]
 direction TB
 Attrs["document_id
access"]:::attr
 subgraph RecipWrap["recipients (Block List, Max: 1) — owned, nested"]
 direction TB
 RecipItem["recipient (Block Set, Max: 1000)
id, type — one per var.recipients entry"]:::nested
 end
 end

 Outputs["id
recipient_keys"]:::io

 Inputs -- "renders into" --> Keystone
 Keystone -- "emits" --> Outputs

 classDef io fill:#E8EEF3,color:#0A2540,stroke:#94A3B8,stroke-width:1px;
 classDef attr fill:#0A2540,color:#FFFFFF,stroke:#0A2540,stroke-width:1px;
 classDef nested fill:#1496FF,color:#FFFFFF,stroke:#0A2540,stroke-width:2px;
Loading

Validated via the Mermaid Chart MCP (validate_and_render_mermaid_diagram) — valid: true.

Resource inventory:

Resource Count Notes
dynatrace_direct_shares.this 1 (keystone, single instance per module call) Top-level document_id and access attributes; owns a nested recipients wrapper (dynamic block, Max: 1 in the live schema) containing a nested recipient set (dynamic block, Max: 1000), driven by for_each = var.recipients. No separate child resource — the entire recipient collection lives inside this one resource.

✅ Provider / Versions

Requirement Value
Terraform >= 1.12.0
dynatrace-oss/dynatrace provider ~> 1.98
provider {} block in this module None. The caller configures auth (OAuth client credentials — mandatory for this specific resource, see § Required OAuth Scopes) and dt_env_url once, outside this module.

Schema notes that bite:

  • ⚠️ Both closed-enum fields use lowercase values — confirmed directly against the live schema, and unusual for this provider. access accepts read / read-write, and recipient.type accepts group / user — both lowercase, per the resource's own live schema documentation text ("Access grants. Possible values are read and read-write"; "Type of the recipient. Possible values are group' and user'"). Many other resources in this provider use uppercase enum conventions (e.g. dynatrace_mgmz_permission.permissions's VIEWER, LOG_VIEWER, etc.) — do not "correct" this module's lowercase validation {} blocks to uppercase by analogy to sibling modules; the casing here is exact and confirmed, and this module's variables.tf matches it verbatim.
  • 🔒 access = "read" is an house secure default, not a provider default. The live schema documents access as optional with no documented default value at all. Left genuinely unset at the provider level, the field's behavior is undefined by the schema. Per this module suite's secure-by-default posture ("the empty call must produce the safe resource"), this module fills that gap explicitly with default = "read" — the narrower of the two legal values — so a caller who calls this module without specifying access gets a read-only share, never an unreviewed write grant. Set access = "read-write" explicitly to opt into broader access.
  • OAuth-only — no legacy API-token fallback. Unlike most Settings 2.0 resources in this provider catalog, dynatrace_direct_shares's own documentation names only the OAuth client-credentials mechanism (DT_CLIENT_ID / DT_CLIENT_SECRET / DT_ACCOUNT_ID); no API-token alternative is documented. See § Required OAuth Scopes below — do not assume the usual OAuth-or-API-token choice applies here.
  • Dynatrace SaaS only. The resource's own documentation states this explicitly; it has no Managed cluster equivalent.
  • recipients is a single-instance wrapper, not itself repeatable. The live schema caps the recipients block at Max: 1 — this module's main.tf reflects that by guarding the wrapper's dynamic block with length(var.recipients) > 0 ? [var.recipients]: [], producing zero or one recipients { } blocks, never more than one. The repeatability lives one level down, in the nested recipient set (Max: 1000), which is what var.recipients' map entries actually drive.
  • No ForceNew/immutability is called out anywhere on this resource's documented schema page — the doc page does not flag document_id, access, or recipients as immutable. Treat this as "not confirmed either way from the documentation," not as confirmed mutability; verify against the raw JSON schema (terraform providers schema -json) or a live plan if immutable-field behavior matters for a specific change (e.g. re-pointing document_id at a different document).
  • This resource is excluded by default from the provider's own export utility — per its documentation, terraform-provider-dynatrace -export dynatrace_direct_shares must name the resource explicitly to retrieve existing configuration. Not relevant to authoring this module (export tooling is out of scope for this module), but worth knowing if reconciling against a live environment later.

🔑 Required OAuth Scopes / API Token Permissions

Confirmed directly against the live provider schema (provider dynatrace-oss/dynatrace v1.100.0). The resource's own documentation states, verbatim:

Dynatrace SaaS only.

To utilize this resource, please define the environment variables DT_CLIENT_ID, DT_CLIENT_SECRET, DT_ACCOUNT_ID with an OAuth client including the following permissions: Read direct-shares (document:direct-shares:read), Write direct-shares (document:direct-shares:write), and Delete direct-shares (document:direct-shares:delete).

This resource is OAuth-only — a legacy API token is not accepted at all. Unlike most Settings 2.0 resources in this provider (which typically accept either OAuth or a legacy API token), this resource's own schema documentation names only the OAuth client-credentials mechanism, consistent with this module suite's auth-model conventions for Platform/Automation-family resources: this resource backs a Grail-native Document, part of the platform Document Service, not a classic environment-scoped Settings 2.0 object.

  • OAuth (required — mandatory, not merely preferred): grant the OAuth client's IAM policy document:direct-shares:read, document:direct-shares:write, and document:direct-shares:delete.
  • Platform token: presumed viable as OAuth's long-lived equivalent (same scope-grant model, mutually exclusive with OAuth client credentials at the provider-config level, per this module suite's authentication model) — but this is not separately confirmed in the resource's own documentation page, which names only the OAuth-client-credentials env vars. Verify against your environment before relying on it.
  • Legacy API token: not accepted. Do not provision a legacy API token as this module's sole auth path — it will not authenticate this resource, and the failure will present as an opaque authentication error unrelated to the token's actual permissions (see § Troubleshooting).
  • This module does not touch any other IAM or Automation resource, so no broader account-idm-*/automation:* scopes are needed for this module specifically.

Dynatrace Prerequisites

  • Target environment is Dynatrace SaaS — this resource has no Managed-cluster equivalent.
  • The target document must already exist — either created out of band or in the same plan via an implicit reference to terraform-dynatrace-document's id output.
  • The target recipients (users/groups) must already exist. This module does not create them; the recipients[*].id field is a plain identifier reference, not a name that resolves automatically.
  • An OAuth client (DT_CLIENT_ID / DT_CLIENT_SECRET / DT_ACCOUNT_ID) provisioned with document:direct-shares:read, document:direct-shares:write, and document:direct-shares:delete, bound via an IAM policy. A legacy API token alone is not sufficient for this module.

📁 Module Structure

terraform-dynatrace-direct-shares/
├── providers.tf # required_version + dynatrace-oss/dynatrace ~> 1.98 pin — no provider {} block
├── variables.tf # document_id, access (default "read"), recipients (default {})
├── main.tf # dynatrace_direct_shares.this, dynamic "recipients" wrapping dynamic "recipient"
├── outputs.tf # id, recipient_keys
├── SCOPE.md # cross-module Consumes/Emits contract + confirmed auth model
├── examples/ # runnable Quick Start snippet(s)
│ └── quickstart/main.tf
└── README.md # this file

⚙️ Quick Start

module "direct_share" {
  source = "git::https://github.com/microsoftexpert/terraform-dynatrace-direct-shares.git?ref=v1.0.0"

  document_id = "d3f4a5b6-0001-4a2b-8c3d-000000000001" # terraform-dynatrace-document's `id` output

  recipients = {
    finance_readers = {
      id   = "7f6e9c2a-1111-4a5b-9c3d-000000000001"
      type = "group"
    }
  }

  # access omitted — defaults to "read", the house secure default
}

ℹ️ The caller configures the dynatrace provider (OAuth client credentials — mandatory for this resource, see § Required OAuth Scopes above — plus dt_env_url) once, outside this module. This module never declares a provider {} block or an auth-related variable.

A full standalone, directly-runnable version of this snippet (with its own terraform/provider block) is in examples/quickstart/main.tf.


🔌 Cross-Module Contract

Consumes

Input Type Source module
document_id string, required terraform-dynatrace-document (its id output)

Emits

Output Description Consumed by
id Direct-share grant ID Reference only — no sibling module in this catalog consumes this by id as of this writing
recipient_keys The caller-chosen keys of the configured recipients map Reference only

📚 Example Library

1 · Minimal single-recipient share — read access, the secure default
module "direct_share" {
  source = "git::https://github.com/microsoftexpert/terraform-dynatrace-direct-shares.git?ref=v1.0.0"

  document_id = "d3f4a5b6-0001-4a2b-8c3d-000000000001"

  recipients = {
    jane_doe = {
      id   = "1a2b3c4d-0000-4a2b-8c3d-000000000010"
      type = "user"
    }
  }
}

💡 The smallest real call: one document, one recipient, access left at its "read" default.

2 · Read-write share — explicit broader-access opt-out
module "direct_share" {
  source = "git::https://github.com/microsoftexpert/terraform-dynatrace-direct-shares.git?ref=v1.0.0"

  document_id = "d3f4a5b6-0002-4a2b-8c3d-000000000002"
  access      = "read-write"

  recipients = {
    dashboard_editor = {
      id   = "2b3c4d5e-0001-4a2b-8c3d-000000000011"
      type = "user"
    }
  }
}

⚠️ access = "read-write" grants the recipient edit rights on the shared document. There is no module default that grants this silently — the caller must set it explicitly.

3 · Group-recipient share
module "direct_share" {
  source = "git::https://github.com/microsoftexpert/terraform-dynatrace-direct-shares.git?ref=v1.0.0"

  document_id = "d3f4a5b6-0003-4a2b-8c3d-000000000003"

  recipients = {
    platform_engineering = {
      id   = "3c4d5e6f-0001-4a2b-8c3d-000000000012"
      type = "group"
    }
  }
}

ℹ️ type = "group" — confirmed lowercase per the live schema. Grants the entire group read access (the default) to this one document.

4 · Multi-recipient at scale — 6 recipients mixing users and groups via for_each
module "direct_share" {
  source = "git::https://github.com/microsoftexpert/terraform-dynatrace-direct-shares.git?ref=v1.0.0"

  document_id = "d3f4a5b6-0004-4a2b-8c3d-000000000004"
  access      = "read"

  recipients = {
    finance_group    = { id = "4d5e6f70-0001-4a2b-8c3d-000000000013", type = "group" }
    risk_group       = { id = "5e6f7081-0002-4a2b-8c3d-000000000014", type = "group" }
    audit_group      = { id = "6f708192-0003-4a2b-8c3d-000000000015", type = "group" }
    jane_doe         = { id = "708192a3-0004-4a2b-8c3d-000000000016", type = "user" }
    john_smith       = { id = "8192a3b4-0005-4a2b-8c3d-000000000017", type = "user" }
    service_reporter = { id = "92a3b4c5-0006-4a2b-8c3d-000000000018", type = "user" }
  }
}

💡 Six independent recipients in one grant, each keyed by role. Removing service_reporter later touches only that map entry's plan — for_each (never a positional list) keys each nested recipient block by the caller's own map key, so unrelated recipients are untouched.

5 · Access-level variant — same document, read vs. read-write compared side by side
module "direct_share_viewers" {
  source = "git::https://github.com/microsoftexpert/terraform-dynatrace-direct-shares.git?ref=v1.0.0"

  document_id = "d3f4a5b6-0005-4a2b-8c3d-000000000005"
  access      = "read"

  recipients = {
    ops_readers = { id = "a3b4c5d6-0001-4a2b-8c3d-000000000019", type = "group" }
  }
}

module "direct_share_editors" {
  source = "git::https://github.com/microsoftexpert/terraform-dynatrace-direct-shares.git?ref=v1.0.0"

  document_id = "d3f4a5b6-0005-4a2b-8c3d-000000000005" # same document
  access      = "read-write"

  recipients = {
    ops_editors = { id = "b4c5d6e7-0002-4a2b-8c3d-00000000001a", type = "group" }
  }
}

🔒 Two separate dynatrace_direct_shares grants against the same document, deliberately split by access level — a common segregation-of-duties shape where a smaller editor group gets read-write and a broader readership gets read, rather than one grant with mixed access.

6 · Empty recipients — secure-by-default inert share
module "direct_share" {
  source = "git::https://github.com/microsoftexpert/terraform-dynatrace-direct-shares.git?ref=v1.0.0"

  document_id = "d3f4a5b6-0006-4a2b-8c3d-000000000006"

  # recipients intentionally omitted — defaults to {}
}

🔒 var.recipients defaults to {}. The resulting dynatrace_direct_shares.this resource still exists (with access = "read"), but grants access to nobody until the caller populates at least one recipient — the safe, inert default.

7 · Single-user recipient only
module "direct_share" {
  source = "git::https://github.com/microsoftexpert/terraform-dynatrace-direct-shares.git?ref=v1.0.0"

  document_id = "d3f4a5b6-0007-4a2b-8c3d-000000000007"

  recipients = {
    on_call_engineer = {
      id   = "c5d6e7f8-0001-4a2b-8c3d-00000000001b"
      type = "user"
    }
  }
}

ℹ️ A single named individual, rather than a group — useful for a one-off share ahead of an incident review or audit, without changing the document's broader visibility.

8 · Explicit mixed-type pair — one user, one group, in a single grant
module "direct_share" {
  source = "git::https://github.com/microsoftexpert/terraform-dynatrace-direct-shares.git?ref=v1.0.0"

  document_id = "d3f4a5b6-0008-4a2b-8c3d-000000000008"

  recipients = {
    sample_service_user = { id = "d6e7f809-0001-4a2b-8c3d-00000000001c", type = "user" }
    sample_group        = { id = "e7f8091a-0002-4a2b-8c3d-00000000001d", type = "group" }
  }
}

💡 Mirrors the provider's own documented resource example (a service user plus a group in one dynatrace_direct_shares grant), rendered through this module's caller-keyed map instead of two positional recipient { } blocks.

9 · Recipient with omitted type — type left unset
module "direct_share" {
  source = "git::https://github.com/microsoftexpert/terraform-dynatrace-direct-shares.git?ref=v1.0.0"

  document_id = "d3f4a5b6-0009-4a2b-8c3d-000000000009"

  recipients = {
    unclassified_recipient = {
      id = "f8091a2b-0001-4a2b-8c3d-00000000001e"
      # type intentionally omitted — optional per the live schema
    }
  }
}

ℹ️ type is optional on the underlying recipient block; this module's main.tf renders try(recipient.value.type, null), so an omitted type passes null through to the provider rather than failing at plan. Confirm with your environment whether the platform can resolve an untyped recipient id before relying on this in a production grant.

10 · Locals-driven for_each construction — DRY at scale
locals {
  finance_group_ids = {
    finance_readers  = "091a2b3c-0001-4a2b-8c3d-00000000001f"
    finance_leads    = "1a2b3c4d-0002-4a2b-8c3d-000000000020"
    finance_auditors = "2b3c4d5e-0003-4a2b-8c3d-000000000021"
  }
}

module "direct_share" {
  source = "git::https://github.com/microsoftexpert/terraform-dynatrace-direct-shares.git?ref=v1.0.0"

  document_id = "d3f4a5b6-000a-4a2b-8c3d-00000000000a"

  recipients = {
    for role, group_id in local.finance_group_ids : role => {
      id   = group_id
      type = "group"
    }
  }
}

ℹ️ Building recipients from a for expression over a locals map keeps a repetitive, same-shape recipient list DRY while still producing a stable, caller-keyed map for for_each — the resulting keys are still role, not a numeric index.

11 · Multiple documents, one share pattern each — caller-level for_each over module calls
locals {
  shared_documents = {
    exec_dashboard   = "d3f4a5b6-000b-4a2b-8c3d-00000000000b"
    incident_runbook = "d3f4a5b6-000c-4a2b-8c3d-00000000000c"
  }
}

module "direct_share" {
  for_each = local.shared_documents
  source   = "git::https://github.com/microsoftexpert/terraform-dynatrace-direct-shares.git?ref=v1.0.0"

  document_id = each.value

  recipients = {
    leadership_group = {
      id   = "3c4d5e6f-0001-4a2b-8c3d-00000000000d"
      type = "group"
    }
  }
}

💡 This module itself has one keystone per instance, not a top-level for_each over documents — the caller supplies the fan-out across documents by putting for_each on the module block itself, one dynatrace_direct_shares.this per document.

12 · Segregation of duties — narrow read grant reviewed separately from a write grant
module "direct_share_read_only" {
  source = "git::https://github.com/microsoftexpert/terraform-dynatrace-direct-shares.git?ref=v1.0.0"

  document_id = "d3f4a5b6-000d-4a2b-8c3d-00000000000e"
  access      = "read"

  recipients = {
    broad_readership = { id = "4d5e6f70-0001-4a2b-8c3d-00000000000f", type = "group" }
  }
}

🔒 Keeping the read-only grant in its own module call (rather than merging it with a hypothetical write grant) keeps each grant's terraform plan diff scoped to one access level and one recipient set — easier for a reviewer to reason about during a periodic access review.

13 · Narrow single-group read-write grant for a small maintenance team
module "direct_share" {
  source = "git::https://github.com/microsoftexpert/terraform-dynatrace-direct-shares.git?ref=v1.0.0"

  document_id = "d3f4a5b6-000e-4a2b-8c3d-00000000000f"
  access      = "read-write"

  recipients = {
    dashboard_maintainers = {
      id   = "5e6f7081-0001-4a2b-8c3d-000000000010"
      type = "group"
    }
  }
}

⚠️ Reserve read-write for a genuinely small, named group responsible for maintaining this specific document — not a general operations or platform-wide group.

14 · Custom document + narrow scoped share, wired via explicit local
locals {
  quarterly_review_document_id = "d3f4a5b6-000f-4a2b-8c3d-000000000011"
}

module "direct_share" {
  source = "git::https://github.com/microsoftexpert/terraform-dynatrace-direct-shares.git?ref=v1.0.0"

  document_id = local.quarterly_review_document_id
  access      = "read"

  recipients = {
    board_reviewers = {
      id   = "6f708192-0001-4a2b-8c3d-000000000012"
      type = "group"
    }
  }
}

ℹ️ A named local for the document id (rather than a bare literal) makes the intent — "this share targets the quarterly review document" — legible in the plan without requiring a lookup elsewhere.

15 · 🏗️ End-to-end composition — document + direct share
module "document" {
  source = "git::https://github.com/microsoftexpert/terraform-dynatrace-document.git?ref=v1.0.0"

  name = "Payments Platform Reliability Overview"
  type = "dashboard"
  content = jsonencode({
    "version" : 1,
    "variables" : [],
    "tiles" : {
      "0" : {
        "type" : "markdown",
        "title" : "",
        "content" : "Payments platform reliability overview"
      }
    },
    "layouts" : {
      "0" : { "x" : 0, "y" : 0, "w" : 24, "h" : 14 }
    }
  })
  # private left at its default (true) -- visible only to the document's owner until shared below
}

module "direct_share" {
  source = "git::https://github.com/microsoftexpert/terraform-dynatrace-direct-shares.git?ref=v1.0.0"

  document_id = module.document.id # implicit reference -- terraform-dynatrace-document's `id` output

  recipients = {
    payments_platform_viewers = {
      id   = "7f6e9c2a-1111-4a5b-9c3d-000000000001"
      type = "group"
    }
    payments_platform_owner = {
      id   = "8091a2b3-2222-4a5b-9c3d-000000000002"
      type = "user"
    }
  }

  # access omitted -- defaults to "read"
}

output "document_id" {
  value = module.document.id
}

output "direct_share_id" {
  value = module.direct_share.id
}

💡 This is the module's mandatory end-to-end shape: terraform-dynatrace-document owns the document and emits id; terraform-dynatrace-direct-shares consumes that id as document_id and owns the recipient collection for that one document. Terraform's implicit dependency graph orders the document before the share automatically — no depends_on is needed or used.


📥 Inputs

Variable Type Default Required
document_id string — Yes
access string ("read" | "read-write") "read" (house secure default) No
recipients map(object({ id = string, type = optional(string) })) {} No (empty map shares with nobody)
Full object schemas
variable "document_id" {
  type = string
}

variable "access" {
  type    = string
  default = "read"

  validation {
    condition     = contains(["read", "read-write"], var.access)
    error_message = "var.access must be one of: \"read\", \"read-write\"."
  }
}

variable "recipients" {
  type = map(object({
    id   = string
    type = optional(string)
  }))
  default = {}

  validation {
    condition = alltrue([
      for k, v in var.recipients :
      v.type == null || contains(["group", "user"], v.type)
    ])
    error_message = "Each recipients[*].type, when set, must be one of: \"group\", \"user\" (lowercase, per the live schema)."
  }
}

Per-field notes:

Field Type Notes
document_id string Required. The target document's id. Source from terraform-dynatrace-document's id output; never hardcode.
access string Optional. Closed enum read / read-write (lowercase, confirmed against the live schema). No provider-documented default; this module's default = "read" is an house secure-default override.
recipients map(object({ id = string, type = optional(string) })) Optional. Default {} — an inert grant with no recipients. Each entry's id is the recipient user/group's own id; type, when set, must be group or user (lowercase, confirmed against the live schema).

🧾 Outputs

Output Description Sensitive
id Direct-share grant ID (dynatrace_direct_shares.this.id) No
recipient_keys The caller-chosen keys of the configured recipients map (keys(var.recipients)) No

🧠 Architecture Notes

  • One keystone, no separate child resource. main.tf renders exactly one resource, dynatrace_direct_shares.this. The entire recipient collection lives inside that same resource as a nested dynamic "recipients" block wrapping a nested dynamic "recipient" block — there is no second resource type in this module, unlike composite modules whose children are independently addressable resources.
  • The recipients wrapper is guarded to render zero or one blocks, never more. main.tf uses for_each = length(var.recipients) > 0 ? [var.recipients]: [] on the outer dynamic "recipients" block specifically because the live schema caps that wrapper at Max: 1 — this is not an arbitrary stylistic choice, it mirrors a real schema constraint.
  • for_each key stability for the inner recipient set. Each recipient block is driven by for_each = recipients.value (the caller's var.recipients map), so removing one recipient from var.recipients never re-plans any other recipient's block — consistent with the house rule of never using count for a collection of relationship-shaped records.
  • try(recipient.value.type, null) lets type be genuinely optional. A recipients map entry that omits type still plans cleanly; the provider receives null for that attribute rather than the module failing at plan time.
  • No secrets, no sensitive = true fields. document_id, access, and every recipients[*] field are plain identifiers/enum values, not credential material — the house secure-by-default credential-handling pattern does not apply to any field in this specific module.
  • OAuth-only auth surface. Unlike most composite modules in this catalog that document an OAuth-or-API-token choice, this module's underlying resource accepts OAuth client credentials only (see § Required OAuth Scopes) — there is no fallback path to document or code around.

🧱 Design Principles

Concern Safe default in this module Opt-out (caller must set explicitly)
access Defaults to "read" — an house override of the provider's undocumented default, per this module suite's secure-by-default posture Caller sets access = "read-write" explicitly
recipients Defaults to {} — an inert share with no recipients configured Caller must populate at least one map entry to grant access to anyone
for_each vs. positional lists Every recipient is keyed by the caller's own map key, never a numeric index N/A — the module never exposes an index-based collection to opt into
Closed-enum validation Both access and recipients[*].type are checked at terraform validate/plan against their exact, confirmed-lowercase legal values via this module's own validation {} blocks N/A — not a default to opt out of; this is a correctness guard, not a policy choice
Auth mechanism This module's permissions section documents OAuth as the only accepted mechanism for this specific resource — no ambiguity about a fallback that does not exist N/A — provider/resource-level constraint, not a module-configurable default

🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check

Pin the module source to an explicit tag — never a branch:

source = "git::https://github.com/microsoftexpert/terraform-dynatrace-direct-shares.git?ref=v1.0.0"

This library is plan-only. No terraform apply is run as part of authoring or reviewing this module — a human runs apply from CI after reviewing the plan.


🧪 Testing

What validate/fmt cover offline, without any live Dynatrace environment:

  • document_id, access, and every recipients[*] entry's shape match variables.tf's type — a missing document_id, or a mistyped recipients entry (e.g. a missing id), fails at validate, not apply.
  • access and recipients[*].type are checked against their closed, confirmed-lowercase enum values via this module's own validation {} blocks — an unsupported value (e.g. "READ" or "Group") fails at validate/plan, not as an opaque API-side error against a live tenant.
  • fmt -check enforces canonical HCL formatting.

What only a real plan against a live environment can additionally catch — this module's validate cannot verify these:

  • Whether document_id actually corresponds to an existing document in the target environment.
  • Whether each recipients[*].id actually corresponds to an existing user or group.
  • Whether the configured OAuth client actually holds document:direct-shares:read/write/delete.
  • Whether the target environment is genuinely SaaS (this resource has no Managed equivalent, and a Managed-targeted apply will fail at the API, not at plan).

💬 Example Output

Representative terraform output for Example 15 (end-to-end composition):

direct_share_id = "9a1c4e70-aaaa-4b6c-9d4e-000000000099"
document_id = "d3f4a5b6-bbbb-4a2b-8c3d-000000000098"

🔍 Troubleshooting

Symptom Cause Fix
apply fails with an authentication error even though a legacy API token is configured This resource is OAuth-only — a legacy API token does not authenticate dynatrace_direct_shares at all Provision an OAuth client (DT_CLIENT_ID/DT_CLIENT_SECRET/DT_ACCOUNT_ID) scoped document:direct-shares:read, document:direct-shares:write, document:direct-shares:delete; a token alone will not work regardless of the permissions granted to it
terraform validate rejects access even though the value "looks right" The value was cased like "Read" or "READ" instead of the confirmed-lowercase "read" Use the exact lowercase legal values read / read-write
terraform validate rejects a recipients[*].type entry The value was cased like "Group"/"User" instead of the confirmed-lowercase "group"/"user" Use the exact lowercase legal values group / user
A direct share applies but the recipient reports no visible access change recipients[*].id was populated with a display name instead of the recipient's actual user/group id Replace the value with the recipient's real id, not a human-readable name
apply fails with "resource not found" for the document document_id does not correspond to an existing document, or the document was created in a different environment than the provider is configured against Confirm document_id was sourced from terraform-dynatrace-document's id output in the same plan/environment, not a hardcoded value from another environment
A share was applied with broader access than intended access was set to "read-write" explicitly by a prior change without review There is no accidental default to broader access in this module — review the specific access value in the offending module call; the default is always "read" unless explicitly overridden
Recipient count in the plan looks wrong after adding/removing one entry An unrelated recipients map entry appears to change alongside the intended edit Confirm var.recipients uses stable map keys, not a for expression that regenerates numeric-like keys; for_each keys must stay stable across plans for unrelated entries to remain untouched
apply succeeds against a Dynatrace Managed cluster but the resource behaves unexpectedly (or provisioning fails entirely) This resource is documented as Dynatrace SaaS only Confirm the target environment is SaaS before using this module; there is no Managed equivalent for dynatrace_direct_shares

🔗 Related Docs

  • Dynatrace provider registry docs — dynatrace_direct_shares (verify current argument reference and authentication support against the live schema for your pinned provider version, not this README alone).
  • Dynatrace provider registry docs — dynatrace_document (source of the id attribute this module depends on).
  • Sibling module: terraform-dynatrace-document — owns the target document; see its outputs.tf for the id output this module's document_id input consumes.
  • This module's own SCOPE.md — the authoritative Consumes/Emits contract this README is derived from, including the confirmed OAuth-only auth model.

💙 "Infrastructure as Code should be standardized, consistent, and secure."

About

Terraform module: terraform-dynatrace-direct-shares

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages