-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvariables.tf
More file actions
312 lines (259 loc) · 15.3 KB
/
Copy pathvariables.tf
File metadata and controls
312 lines (259 loc) · 15.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
variable "name" {
type = string
description = <<-EOT
The notification hub's name. Required and force-new.
A HUB MAPS ROUGHLY TO ONE APPLICATION, where the namespace holding it maps to one purpose -- development, test,
production. It holds one credential per push platform for that app.
EOT
validation {
condition = length(trimspace(var.name)) > 0
error_message = "name must not be empty."
}
validation {
condition = !startswith(trimspace(var.name), "/subscriptions/")
error_message = "name looks like a Resource ID, but this argument takes the hub's NAME. The namespace and its resource group are supplied separately, as `namespace_name` and `resource_group_name`."
}
}
variable "namespace_name" {
type = string
description = <<-EOT
The name of the Notification Hubs namespace this hub is created in. Required and force-new.
```hcl
namespace_name = module.notification_hub_namespace.name
```
THIS TAKES A NAME, NOT AN ID. Pass the namespace module's `name` output rather than a literal: the attribute reference is
what tells Terraform to destroy this hub before the namespace, and it keeps the two from drifting apart.
THE NAMESPACE MUST BE OF TYPE `NotificationHub`. A namespace created with the legacy `Messaging` type is not what a hub
expects to live in, and the namespace's type is force-new -- so that is a decision made before this module runs, and one
this module cannot see.
EOT
validation {
condition = length(trimspace(var.namespace_name)) > 0
error_message = "namespace_name must not be empty."
}
validation {
condition = !startswith(trimspace(var.namespace_name), "/subscriptions/")
error_message = "namespace_name looks like a Resource ID, but this argument takes the namespace's NAME. Use `module.notification_hub_namespace.name` rather than `.id` -- and note that passing the attribute rather than a literal is also what orders the destroy correctly."
}
}
variable "resource_group_name" {
type = string
description = <<-EOT
The name of the resource group the **namespace** exists in. Required and force-new.
NOTE the provider's own wording: this is the resource group "in which the Notification Hub Namespace exists". A hub is a
child of the namespace, so it has no resource group of its own to choose -- this argument locates the parent.
This module does not create the resource group.
EOT
validation {
condition = length(trimspace(var.resource_group_name)) > 0
error_message = "resource_group_name must not be empty."
}
validation {
condition = !startswith(trimspace(var.resource_group_name), "/subscriptions/")
error_message = "resource_group_name looks like a Resource ID, but this argument takes the resource group's NAME. NOTE it locates the NAMESPACE rather than placing this hub."
}
}
variable "location" {
type = string
description = <<-EOT
The Azure region. Required and force-new.
NOTE the provider describes this as the region "in which this Notification Hub Namespace exists" -- so it should match the
namespace's own location. A hub is not independently placeable, and this module cannot read the namespace to check.
This library targets US Azure regions; `eastus`, `eastus2`, `westus2` and `centralus` are the conventional values.
EOT
validation {
condition = length(trimspace(var.location)) > 0
error_message = "location must not be empty."
}
}
###############################################################################
# Push platform credentials.
#
# NONE of these variables is marked `sensitive`, deliberately, and the reason is
# NOT the for_each reason that applies elsewhere in this library -- these are
# single blocks, so a sensitive variable would render fine. The reason is
# CONTAGION: marking the object sensitive would make `application_mode` and
# `vapid_public_key` sensitive too, and outputs.tf derives reportable flags from
# them. Terraform refuses to emit a sensitive bool, so those flags would break.
#
# Instead, `sensitive()` is applied to the individual secret fields at the point
# of use in main.tf -- which the provider already marks sensitive in its schema.
###############################################################################
variable "apns_credential" {
type = object({
application_mode = string
bundle_id = string
key_id = string
team_id = string
token = string
})
default = null
description = <<-EOT
Apple Push Notification service credentials, for iOS and macOS. Token-based authentication.
```hcl
apns_credential = {
application_mode = "Production"
bundle_id = "com.contoso.exampleapp"
key_id = "ABC123DEFG"
team_id = "HIJ456KLMN"
token = var.apns_signing_key # the .p8 CONTENTS, without the PEM markers
}
```
`application_mode` DECIDES WHICH APPLE GATEWAY IS USED, and getting it wrong fails silently. `Sandbox` is Apple's
development gateway: a production build's device tokens are not valid there, so notifications are simply not delivered
rather than rejected with anything useful. outputs.tf reports which gateway was chosen.
`token` IS THE CONTENTS OF THE `.p8` KEY, WITHOUT THE PEM MARKERS. The provider is explicit: it is what sits *between*
the `-----BEGIN PRIVATE KEY-----` and `-----END PRIVATE KEY-----` lines. Including those markers is the predictable
mistake, so the module rejects a value containing them.
THIS IS A REAL SECRET AND IT WILL BE IN TERRAFORM STATE IN PLAINTEXT. `sensitive` marking redacts plan output and does
nothing to state, so the control that matters is an encrypted, access-controlled backend -- never a local state file in a
repository. Source the value from a secret store, not from a committed variable file.
AND REMOVING THIS BLOCK LATER REPLACES THE HUB. The provider documents an Azure SDK for Go bug that forces recreation
when an `apns_credential` block is removed -- which destroys every device registration on the hub. Changing the values in
place is fine; taking the block away is not.
EOT
validation {
condition = var.apns_credential == null ? true : contains(["Production", "Sandbox"], var.apns_credential.application_mode)
error_message = "apns_credential.application_mode must be exactly \"Production\" or \"Sandbox\" -- both PascalCase. `Sandbox` is Apple's DEVELOPMENT gateway: production device tokens are not valid there and notifications are silently undelivered, so choose it only for a development hub."
}
# The documented probable mistake: pasting the whole PEM file rather than its contents.
# strcontains(), not a regex -- these are literals, and a regex here would risk the
# can()-swallows-a-bad-pattern trap.
validation {
condition = var.apns_credential == null ? true : !(
strcontains(var.apns_credential.token, "BEGIN PRIVATE KEY") ||
strcontains(var.apns_credential.token, "END PRIVATE KEY")
)
error_message = "apns_credential.token must be the CONTENTS of the .p8 key, without the PEM markers -- the provider documents it as the text BETWEEN the -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY----- lines. Strip those lines and any surrounding blank lines."
}
validation {
condition = var.apns_credential == null ? true : !endswith(trimspace(var.apns_credential.token), ".p8")
error_message = "apns_credential.token looks like a FILE NAME rather than a key. Pass the file's contents -- for example `file(\"$${path.module}/AuthKey.p8\")` with the PEM markers stripped, or better, a value read from a secret store."
}
validation {
condition = var.apns_credential == null ? true : length(trimspace(var.apns_credential.token)) > 0
error_message = "apns_credential.token must not be empty."
}
# A bundle ID is reverse-DNS. This is a HEURISTIC, and the message says so.
validation {
condition = var.apns_credential == null ? true : strcontains(var.apns_credential.bundle_id, ".")
error_message = "apns_credential.bundle_id does not contain a dot, which suggests a team ID or a key ID was passed instead of the application's bundle identifier. This check is a HEURISTIC, not a documented rule -- a bundle ID is conventionally reverse-DNS, such as \"com.contoso.exampleapp\", and the provider's own example follows that form."
}
validation {
condition = var.apns_credential == null ? true : (
length(trimspace(var.apns_credential.key_id)) > 0 &&
length(trimspace(var.apns_credential.team_id)) > 0
)
error_message = "apns_credential.key_id and team_id must not be empty. Both come from the Apple Developer portal, and they are different values -- key_id identifies the signing key, team_id identifies your developer team."
}
}
variable "browser_credential" {
type = object({
subject = string
vapid_private_key = string
vapid_public_key = string
})
default = null
description = <<-EOT
Web Push credentials, for browsers. VAPID key pair.
```hcl
browser_credential = {
subject = "mailto:push-admin@contoso.com"
vapid_public_key = var.vapid_public_key # published to browsers
vapid_private_key = var.vapid_private_key # a real secret
}
```
NOTE THIS WHOLE BLOCK IS FORCE-NEW, unlike the other two. The provider documents that changing `browser_credential`
forces a new resource -- so rotating a VAPID key pair through Terraform destroys the hub and every device registration on
it. That asymmetry is not obvious from three similar-looking blocks, and outputs.tf states it.
THE PUBLIC KEY IS NOT A SECRET AND THE PROVIDER AGREES. `vapid_public_key` is not marked sensitive in the schema, because
a VAPID public key exists to be handed to browsers; `vapid_private_key` is. This module follows the provider's own split
rather than redacting both.
`subject` IS A VAPID CONTACT URI, conventionally `mailto:` or an `https://` URL, so a push service can reach whoever
operates the application. The provider documents it only as "the subject name of web push", so the module does not
enforce a scheme -- it flags the one likely mistake and says the check is a heuristic.
AND THE PRIVATE KEY WILL BE IN STATE IN PLAINTEXT. Same caveat as the other credentials: `sensitive` redacts plan output,
not state.
EOT
validation {
condition = var.browser_credential == null ? true : (
length(trimspace(var.browser_credential.subject)) > 0 &&
length(trimspace(var.browser_credential.vapid_private_key)) > 0 &&
length(trimspace(var.browser_credential.vapid_public_key)) > 0
)
error_message = "browser_credential requires a non-empty subject, vapid_private_key and vapid_public_key -- the provider marks all three required."
}
# A HEURISTIC, and the message says so: VAPID subjects are URIs by convention.
validation {
condition = var.browser_credential == null ? true : !(
strcontains(var.browser_credential.subject, "@") &&
!startswith(lower(trimspace(var.browser_credential.subject)), "mailto:")
)
error_message = "browser_credential.subject looks like a bare email address. A VAPID subject is conventionally a contact URI -- \"mailto:push-admin@contoso.com\" rather than \"push-admin@contoso.com\". This check is a HEURISTIC, not a documented provider rule: the provider describes the field only as \"the subject name of web push\", so an https:// URL or another URI form is accepted without comment."
}
# VAPID keys are base64url, not PEM. Pasting a PEM file is the predictable mistake.
validation {
condition = var.browser_credential == null ? true : !(
strcontains(var.browser_credential.vapid_private_key, "BEGIN") ||
strcontains(var.browser_credential.vapid_public_key, "BEGIN")
)
error_message = "a VAPID key contains PEM markers, but VAPID keys are base64url-encoded values rather than PEM blocks. Pass the encoded key itself -- the output of a VAPID key generator -- not the contents of a .pem file."
}
validation {
condition = var.browser_credential == null ? true : var.browser_credential.vapid_private_key != var.browser_credential.vapid_public_key
error_message = "browser_credential.vapid_private_key and vapid_public_key are identical, so one of them is wrong. They are the two halves of a key pair."
}
}
variable "gcm_credential" {
type = object({
api_key = string
})
default = null
description = <<-EOT
Google credentials for Android. **This targets an API Google has already retired.**
GOOGLE STOPPED SUPPORTING FCM LEGACY HTTP ON 20 JUNE 2024, and Microsoft's own documentation states that the FCM legacy
APIs "are no longer supported and are retired". This block carries a single `api_key`, which is the FCM **legacy**
credential. FCM v1 -- the supported protocol -- needs three different values: a private key, a client email and a project
ID.
AND THIS PROVIDER VERSION EXPOSES NO FCM v1 ARGUMENT AT ALL. There is no `fcm_v1_credential` block in the schema, so
Android push cannot be configured through this resource. It has to be configured out of band -- the Azure portal's
**Google (FCM v1)** blade, the REST API, or an Azure SDK -- and that configuration will then sit outside Terraform's
view. outputs.tf states both halves of this so the gap is visible rather than inferred from a missing argument.
THE BLOCK IS STILL EXPOSED HERE, DELIBERATELY. The provider accepts it and Azure still stores it, so refusing it would
reject input that is legal today and would break a caller migrating away from it. This module reports the retirement
instead of enforcing a judgement -- but a new hub should not be configured with it.
IF YOU DO SET IT: the value is a real secret, it will be in Terraform state in plaintext, and removing the block later
forces the hub to be recreated -- see the Azure SDK bug the provider documents.
EOT
validation {
condition = var.gcm_credential == null ? true : length(trimspace(var.gcm_credential.api_key)) > 0
error_message = "gcm_credential.api_key must not be empty. NOTE this is the FCM LEGACY server key, and Google retired that API on 20 June 2024 -- see this variable's description before using it at all."
}
}
###############################################################################
# Universal tail
###############################################################################
variable "tags" {
type = map(string)
default = {}
description = <<-EOT
Tags to assign to the hub.
SUPPORTED HERE, unlike on the authorization rules beneath it -- so a hub is the lowest level in this family a tag policy
can reach. Tag the namespace too: it is the billed unit.
EOT
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = null
description = <<-EOT
Operation timeouts. All four operations exist.
NOTE OBJECT-TYPE CONVERSION SILENTLY DISCARDS UNKNOWN KEYS, so a misspelled key vanishes with no error at all.
RAISE `delete` IF THE HUB MATTERS. Deleting a hub removes every device registration on it, and those registrations were
created by client applications at runtime -- they are not in Terraform state and nothing recreates them.
EOT
}