@hiiretail/gcp-infra-generators 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generators/common-resources/firebase/README.md +70 -0
- package/dist/generators/common-resources/firebase/generator.json +4 -0
- package/dist/generators/common-resources/firebase/index.js +102 -0
- package/dist/generators/common-resources/firebase/templates/firebase/apps/terragrunt.hcl +38 -0
- package/dist/generators/common-resources/firebase/templates/firebase/firebase.yaml +17 -0
- package/dist/generators/common-resources/firebase/templates/firebase/terragrunt.hcl +38 -0
- package/dist/generators/common-resources/firestore/README.md +246 -0
- package/dist/generators/common-resources/firestore/index.js +211 -63
- package/dist/generators/common-resources/firestore/templates/firestore/databases.yaml +26 -0
- package/dist/generators/common-resources/firestore/templates/firestore/terragrunt.hcl +101 -13
- package/dist/generators/common-resources/firestore/validators.js +22 -0
- package/dist/node_modules/.package-lock.json +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
- package/dist/generators/common-resources/firestore/templates/firestore/indexes/indexes.yaml +0 -12
- package/dist/generators/common-resources/firestore/templates/firestore/indexes/terragrunt.hcl +0 -27
- package/dist/generators/common-resources/firestore/templates/firestore/spec.hcl +0 -31
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Firebase Generator
|
|
2
|
+
|
|
3
|
+
This generator scaffolds Firebase infrastructure configuration for use with Terragrunt and the [terraform-google-firebase](https://github.com/GoogleCloudPlatform/terraform-google-firebase) module (v0.2.3).
|
|
4
|
+
|
|
5
|
+
## Generated Files
|
|
6
|
+
|
|
7
|
+
Located at `infra/{env}/firebase/`:
|
|
8
|
+
|
|
9
|
+
1. **firebase.yaml** - Platform configuration
|
|
10
|
+
2. **terragrunt.hcl** - Firebase multi-platform app registration
|
|
11
|
+
|
|
12
|
+
## File Architecture
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
infra/
|
|
16
|
+
prod/firebase/
|
|
17
|
+
firebase.yaml
|
|
18
|
+
terragrunt.hcl
|
|
19
|
+
staging/firebase/
|
|
20
|
+
firebase.yaml
|
|
21
|
+
terragrunt.hcl
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## firebase.yaml Format
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
apps:
|
|
28
|
+
android:
|
|
29
|
+
package_name: "com.example.app"
|
|
30
|
+
ios:
|
|
31
|
+
bundle_id: "com.example.app"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- **android.package_name**: Android application ID in reverse-domain format
|
|
35
|
+
- **ios.bundle_id**: iOS bundle identifier in reverse-domain format
|
|
36
|
+
|
|
37
|
+
Either platform section can be removed to stop managing that platform. At least one must remain.
|
|
38
|
+
|
|
39
|
+
## Apps Module Outputs (FCM Config Files)
|
|
40
|
+
|
|
41
|
+
After `terragrunt apply`, the platform SDK config files are available as Terraform outputs:
|
|
42
|
+
|
|
43
|
+
| Output | Platform | File |
|
|
44
|
+
| ---------------- | -------- | ------------------------------------------- |
|
|
45
|
+
| `android_config` | Android | `google-services.json` (base64-encoded) |
|
|
46
|
+
| `apple_config` | iOS | `GoogleService-Info.plist` (base64-encoded) |
|
|
47
|
+
|
|
48
|
+
Decode and save the files from `infra/{env}/firebase/`:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Android
|
|
52
|
+
terragrunt output -raw android_config | base64 --decode > google-services.json
|
|
53
|
+
|
|
54
|
+
# iOS
|
|
55
|
+
terragrunt output -raw apple_config | base64 --decode > GoogleService-Info.plist
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Terragrunt Configuration
|
|
59
|
+
|
|
60
|
+
The `terragrunt.hcl` reads from the `firebase.yaml` in the same directory:
|
|
61
|
+
|
|
62
|
+
```hcl
|
|
63
|
+
locals {
|
|
64
|
+
config = yamldecode(file("${get_terragrunt_dir()}/firebase.yaml"))
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
terragrunt apply --terragrunt-working-dir infra/prod/firebase
|
|
70
|
+
```
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const BaseGenerator = require('../../../src/BaseGenerator');
|
|
4
|
+
const { required } = require('../../../src/validators');
|
|
5
|
+
|
|
6
|
+
module.exports = class extends BaseGenerator {
|
|
7
|
+
prompting() {
|
|
8
|
+
const prompts = [
|
|
9
|
+
{
|
|
10
|
+
type: 'input',
|
|
11
|
+
name: 'appDisplayName',
|
|
12
|
+
message: 'App display name (shared across all platforms):',
|
|
13
|
+
validate: required,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
type: 'checkbox',
|
|
17
|
+
name: 'platforms',
|
|
18
|
+
message: 'Which platforms do you want to register Firebase apps for?',
|
|
19
|
+
choices: [
|
|
20
|
+
{ name: 'Android', value: 'android', checked: true },
|
|
21
|
+
{ name: 'iOS', value: 'ios', checked: true },
|
|
22
|
+
],
|
|
23
|
+
validate: (selected) =>
|
|
24
|
+
selected.length > 0 || 'Select at least one platform.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
when: (response) => response.platforms.includes('android'),
|
|
28
|
+
type: 'input',
|
|
29
|
+
name: 'androidPackageName',
|
|
30
|
+
message:
|
|
31
|
+
'Android package name - letters, digits, underscores, and dots only, no hyphens (e.g. com.example_app.checkout):',
|
|
32
|
+
validate: required,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
when: (response) => response.platforms.includes('ios'),
|
|
36
|
+
type: 'input',
|
|
37
|
+
name: 'iosBundleId',
|
|
38
|
+
message:
|
|
39
|
+
'iOS bundle ID - letters, digits, hyphens, and dots only, no underscores (e.g. com.example-app.checkout):',
|
|
40
|
+
validate: required,
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
return this.prompt(prompts).then((props) => {
|
|
45
|
+
this.answers = props;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
writing() {
|
|
50
|
+
const {
|
|
51
|
+
appDisplayName,
|
|
52
|
+
platforms = [],
|
|
53
|
+
androidPackageName,
|
|
54
|
+
iosBundleId,
|
|
55
|
+
} = this.answers;
|
|
56
|
+
|
|
57
|
+
const yamlContext = {
|
|
58
|
+
appDisplayName,
|
|
59
|
+
platforms,
|
|
60
|
+
androidPackageName,
|
|
61
|
+
iosBundleId,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
['prod', 'staging'].forEach((env) => {
|
|
65
|
+
const firebaseDest = path.join('infra', env, 'firebase');
|
|
66
|
+
|
|
67
|
+
this.fs.copyTpl(
|
|
68
|
+
this.templatePath('firebase', 'firebase.yaml'),
|
|
69
|
+
this.destinationPath(firebaseDest, 'firebase.yaml'),
|
|
70
|
+
yamlContext,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
this.fs.copy(
|
|
74
|
+
this.templatePath('firebase', 'terragrunt.hcl'),
|
|
75
|
+
this.destinationPath(firebaseDest, 'terragrunt.hcl'),
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
end() {
|
|
81
|
+
const header = chalk.green(
|
|
82
|
+
'Your Firebase configuration has been created. Review the generated files:',
|
|
83
|
+
);
|
|
84
|
+
let message = header;
|
|
85
|
+
let i = 1;
|
|
86
|
+
|
|
87
|
+
message += `\n${chalk.green(`${i++}.`)} ${chalk.cyan(path.join('firebase', 'firebase.yaml'))} - Shared Firebase configuration`;
|
|
88
|
+
message += `\n${chalk.green(`${i++}.`)} ${chalk.cyan(path.join('firebase', 'terragrunt.hcl'))} - Firebase apps module`;
|
|
89
|
+
|
|
90
|
+
message += chalk.yellow(
|
|
91
|
+
'\n\nAfter applying, retrieve the FCM config files from infra/{env}/firebase/:',
|
|
92
|
+
);
|
|
93
|
+
message += chalk.cyan(
|
|
94
|
+
'\n terragrunt output -raw android_config | base64 --decode > google-services.json',
|
|
95
|
+
);
|
|
96
|
+
message += chalk.cyan(
|
|
97
|
+
'\n terragrunt output -raw apple_config | base64 --decode > GoogleService-Info.plist',
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
this.log(`\n${message}\n`);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Terragrunt configuration for Firebase multi-platform application
|
|
2
|
+
# https://github.com/GoogleCloudPlatform/terraform-google-firebase
|
|
3
|
+
|
|
4
|
+
terraform {
|
|
5
|
+
source = "git::https://github.com/extenda/terraform-google-firebase//modules/firebase_multi_platform_application/apps?ref=feat/Add-wrapper-apps-around-multi-application-module"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
include {
|
|
9
|
+
path = find_in_parent_folders("terragrunt_root.hcl")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
generate "firebase_providers" {
|
|
13
|
+
path = "firebase_providers.tf"
|
|
14
|
+
if_exists = "overwrite_terragrunt"
|
|
15
|
+
contents = <<EOF
|
|
16
|
+
provider "google" {
|
|
17
|
+
project = var.project_id
|
|
18
|
+
billing_project = var.project_id
|
|
19
|
+
user_project_override = true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
provider "google-beta" {
|
|
23
|
+
project = var.project_id
|
|
24
|
+
billing_project = var.project_id
|
|
25
|
+
user_project_override = true
|
|
26
|
+
}
|
|
27
|
+
EOF
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
locals {
|
|
31
|
+
project_vars = read_terragrunt_config(find_in_parent_folders("project.hcl"))
|
|
32
|
+
config = yamldecode(file("${get_terragrunt_dir()}/../firebase.yaml"))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
inputs = {
|
|
36
|
+
project_id = local.project_vars.locals.project_id
|
|
37
|
+
apps = local.config.apps
|
|
38
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Firebase configuration
|
|
2
|
+
#
|
|
3
|
+
# Naming conventions per platform:
|
|
4
|
+
# android.package_name — Java package notation: letters, digits, underscores, and dots only.
|
|
5
|
+
# No hyphens. Example: com.example_app.checkout
|
|
6
|
+
# ios.bundle_id — Reverse DNS notation: letters, digits, hyphens, and dots only.
|
|
7
|
+
# No underscores. Example: com.example-app.checkout
|
|
8
|
+
apps:
|
|
9
|
+
- display_name: "<%- appDisplayName %>"
|
|
10
|
+
<% if (platforms.includes('android')) { -%>
|
|
11
|
+
android:
|
|
12
|
+
package_name: <%- androidPackageName %>
|
|
13
|
+
<% } -%>
|
|
14
|
+
<% if (platforms.includes('ios')) { -%>
|
|
15
|
+
ios:
|
|
16
|
+
bundle_id: <%- iosBundleId %>
|
|
17
|
+
<% } -%>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Terragrunt configuration for Firebase multi-platform application
|
|
2
|
+
# https://github.com/GoogleCloudPlatform/terraform-google-firebase
|
|
3
|
+
|
|
4
|
+
terraform {
|
|
5
|
+
source = "git::https://github.com/extenda/terraform-google-firebase//modules/firebase_multi_platform_application/apps?ref=v1.0.0"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
include {
|
|
9
|
+
path = find_in_parent_folders("terragrunt_root.hcl")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
generate "firebase_providers" {
|
|
13
|
+
path = "firebase_providers.tf"
|
|
14
|
+
if_exists = "overwrite_terragrunt"
|
|
15
|
+
contents = <<EOF
|
|
16
|
+
provider "google" {
|
|
17
|
+
project = var.project_id
|
|
18
|
+
billing_project = var.project_id
|
|
19
|
+
user_project_override = true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
provider "google-beta" {
|
|
23
|
+
project = var.project_id
|
|
24
|
+
billing_project = var.project_id
|
|
25
|
+
user_project_override = true
|
|
26
|
+
}
|
|
27
|
+
EOF
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
locals {
|
|
31
|
+
project_vars = read_terragrunt_config(find_in_parent_folders("project.hcl"))
|
|
32
|
+
config = yamldecode(file("${get_terragrunt_dir()}/firebase.yaml"))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
inputs = {
|
|
36
|
+
project_id = local.project_vars.locals.project_id
|
|
37
|
+
apps = local.config.apps
|
|
38
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Firestore Generator
|
|
2
|
+
|
|
3
|
+
This generator scaffolds a Firestore database configuration for use with Terragrunt and the [terraform-google-firestore](https://github.com/extenda/terraform-google-firestore) module (via wrapper supporting multi-database deployments).
|
|
4
|
+
|
|
5
|
+
## Generated Files
|
|
6
|
+
|
|
7
|
+
The generator creates files at the firestore level per environment (prod/staging), supporting **multiple databases** deployed in a single `terragrunt plan`:
|
|
8
|
+
|
|
9
|
+
Located at `infra/{env}/firestore/`:
|
|
10
|
+
1. **databases.yaml** - Central configuration for all databases with YAML anchors for safe defaults
|
|
11
|
+
2. **schema/*.yaml** - Individual collection schemas (fields and indexes)
|
|
12
|
+
3. **terragrunt.hcl** - Multi-database orchestration that calls the wrapper module via `for_each`
|
|
13
|
+
|
|
14
|
+
## File Architecture
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
infra/
|
|
18
|
+
prod/firestore/
|
|
19
|
+
databases.yaml ← All databases + backup schedules with YAML anchors
|
|
20
|
+
terragrunt.hcl ← Multi-database orchestration (calls wrapper)
|
|
21
|
+
schema/
|
|
22
|
+
users.yaml ← Fields and indexes for users collection
|
|
23
|
+
posts.yaml ← Fields and indexes for posts collection
|
|
24
|
+
staging/firestore/
|
|
25
|
+
databases.yaml
|
|
26
|
+
terragrunt.hcl
|
|
27
|
+
schema/
|
|
28
|
+
users.yaml
|
|
29
|
+
posts.yaml
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Databases YAML Format
|
|
33
|
+
|
|
34
|
+
The `databases.yaml` file defines all Firestore databases and backup schedules using YAML anchors for reusable defaults.
|
|
35
|
+
|
|
36
|
+
### Basic Structure
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
# YAML anchors define reusable backup schedules
|
|
40
|
+
backup_schedule_configuration: &default_backup_schedule
|
|
41
|
+
weekly_recurrence:
|
|
42
|
+
day: "MONDAY"
|
|
43
|
+
retention: "8w"
|
|
44
|
+
|
|
45
|
+
# All databases in one place
|
|
46
|
+
databases:
|
|
47
|
+
- name: "database1"
|
|
48
|
+
backup_schedule_configuration: *default_backup_schedule
|
|
49
|
+
collections:
|
|
50
|
+
- schema: schema/users.yaml
|
|
51
|
+
- schema: schema/posts.yaml
|
|
52
|
+
|
|
53
|
+
- name: "database2"
|
|
54
|
+
backup_schedule_configuration: *default_backup_schedule
|
|
55
|
+
collections:
|
|
56
|
+
- schema: schema/users.yaml
|
|
57
|
+
- schema: schema/posts.yaml
|
|
58
|
+
# Optional: override delete protection for this database only
|
|
59
|
+
delete_protection_state: "DELETE_PROTECTION_ENABLED"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Databases Properties
|
|
63
|
+
|
|
64
|
+
- **name**: Unique database identifier (required)
|
|
65
|
+
- **backup_schedule_configuration**: Backup schedule using YAML anchors (required)
|
|
66
|
+
- **daily_recurrence**: For daily backups with retention (e.g., "30d")
|
|
67
|
+
- **weekly_recurrence**: For weekly backups with day and retention (e.g., "8w")
|
|
68
|
+
- **collections**: List of schema references (required)
|
|
69
|
+
- **schema**: Path to schema file relative to firestore folder (e.g., `schema/users.yaml`)
|
|
70
|
+
- **delete_protection_state**: Optional override (`DELETE_PROTECTION_ENABLED` or `DELETE_PROTECTION_DISABLED`)
|
|
71
|
+
|
|
72
|
+
### Example: Multiple Databases
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
backup_schedule_configuration: &daily
|
|
76
|
+
daily_recurrence:
|
|
77
|
+
retention: "30d"
|
|
78
|
+
|
|
79
|
+
backup_schedule_configuration: &weekly
|
|
80
|
+
weekly_recurrence:
|
|
81
|
+
day: "FRIDAY"
|
|
82
|
+
retention: "12w"
|
|
83
|
+
|
|
84
|
+
databases:
|
|
85
|
+
- name: "production-db"
|
|
86
|
+
backup_schedule_configuration: *weekly
|
|
87
|
+
collections:
|
|
88
|
+
- schema: schema/users.yaml
|
|
89
|
+
- schema: schema/posts.yaml
|
|
90
|
+
- schema: schema/comments.yaml
|
|
91
|
+
|
|
92
|
+
- name: "analytics-db"
|
|
93
|
+
backup_schedule_configuration: *daily
|
|
94
|
+
collections:
|
|
95
|
+
- schema: schema/events.yaml
|
|
96
|
+
- schema: schema/metrics.yaml
|
|
97
|
+
delete_protection_state: "DELETE_PROTECTION_DISABLED"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Schema YAML Format
|
|
101
|
+
|
|
102
|
+
Each schema file in `schema/` defines the TTL field and composite indexes for a collection.
|
|
103
|
+
|
|
104
|
+
### Basic Structure
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
name: users
|
|
108
|
+
description: "User profiles with indexes"
|
|
109
|
+
|
|
110
|
+
ttl_field: expires_at
|
|
111
|
+
|
|
112
|
+
indexes:
|
|
113
|
+
- name: email_created_index
|
|
114
|
+
fields:
|
|
115
|
+
- field_path: "email"
|
|
116
|
+
order: "ASCENDING"
|
|
117
|
+
array_config: null
|
|
118
|
+
- field_path: "created_at"
|
|
119
|
+
order: "ASCENDING"
|
|
120
|
+
array_config: null
|
|
121
|
+
- field_path: "__name__"
|
|
122
|
+
order: "ASCENDING"
|
|
123
|
+
array_config: null
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### TTL Field
|
|
127
|
+
|
|
128
|
+
- **ttl_field**: The name of the field that has Time To Live enabled (optional scalar, omit if no TTL). Only one field per collection can have TTL.
|
|
129
|
+
|
|
130
|
+
### Index Properties
|
|
131
|
+
|
|
132
|
+
- **name**: Index name (required, must be unique within the database)
|
|
133
|
+
- **fields**: List of fields in the composite index
|
|
134
|
+
- **field_path**: Field name (required, quoted string)
|
|
135
|
+
- **order**: `"ASCENDING"` or `"DESCENDING"` for ordered fields; `null` for array fields
|
|
136
|
+
- **array_config**: `"CONTAINS"` for array-contains queries; `null` for ordered fields
|
|
137
|
+
|
|
138
|
+
### Example: Complete Schema
|
|
139
|
+
|
|
140
|
+
```yaml
|
|
141
|
+
name: posts
|
|
142
|
+
description: "Blog posts with advanced indexing"
|
|
143
|
+
|
|
144
|
+
ttl_field: expires_at
|
|
145
|
+
|
|
146
|
+
indexes:
|
|
147
|
+
- name: posts_by_author_created
|
|
148
|
+
fields:
|
|
149
|
+
- field_path: "author_id"
|
|
150
|
+
order: "ASCENDING"
|
|
151
|
+
array_config: null
|
|
152
|
+
- field_path: "created_at"
|
|
153
|
+
order: "ASCENDING"
|
|
154
|
+
array_config: null
|
|
155
|
+
- field_path: "__name__"
|
|
156
|
+
order: "ASCENDING"
|
|
157
|
+
array_config: null
|
|
158
|
+
|
|
159
|
+
- name: posts_by_tags
|
|
160
|
+
fields:
|
|
161
|
+
- field_path: "tags"
|
|
162
|
+
array_config: "CONTAINS"
|
|
163
|
+
order: null
|
|
164
|
+
- field_path: "created_at"
|
|
165
|
+
order: "ASCENDING"
|
|
166
|
+
array_config: null
|
|
167
|
+
- field_path: "__name__"
|
|
168
|
+
order: "ASCENDING"
|
|
169
|
+
array_config: null
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Terragrunt Configuration
|
|
173
|
+
|
|
174
|
+
The `terragrunt.hcl` file automatically handles multi-database orchestration with safe defaults.
|
|
175
|
+
|
|
176
|
+
### Safe Defaults
|
|
177
|
+
|
|
178
|
+
The terragrunt.hcl applies safe defaults to all databases:
|
|
179
|
+
|
|
180
|
+
```hcl
|
|
181
|
+
default_database_config = {
|
|
182
|
+
location = "europe-west1"
|
|
183
|
+
database_type = "FIRESTORE_NATIVE"
|
|
184
|
+
concurrency_mode = "OPTIMISTIC"
|
|
185
|
+
delete_protection_state = "DELETE_PROTECTION_ENABLED" # ← Prevents accidental deletion
|
|
186
|
+
deletion_policy = "DELETE"
|
|
187
|
+
point_in_time_recovery_enablement = "POINT_IN_TIME_RECOVERY_DISABLED"
|
|
188
|
+
kms_key_name = null
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
These defaults are merged with database-specific overrides in `databases.yaml`. Each database inherits all defaults unless explicitly overridden.
|
|
193
|
+
|
|
194
|
+
### Multi-Database Deployment
|
|
195
|
+
|
|
196
|
+
When you run `terragrunt plan`, it:
|
|
197
|
+
1. Loads all databases from `databases.yaml`
|
|
198
|
+
2. Loads all schemas from `schema/*.yaml`
|
|
199
|
+
3. Merges each database config with safe defaults
|
|
200
|
+
4. Calls the wrapper module via `for_each` for each database
|
|
201
|
+
5. Shows all resources for all databases in one plan
|
|
202
|
+
|
|
203
|
+
## Editing Configurations
|
|
204
|
+
|
|
205
|
+
After generation, you can:
|
|
206
|
+
|
|
207
|
+
1. **Modify databases.yaml** to:
|
|
208
|
+
- Add/remove databases
|
|
209
|
+
- Adjust backup schedules using YAML anchors
|
|
210
|
+
- Override delete_protection_state per database
|
|
211
|
+
- Add/remove collections per database
|
|
212
|
+
- Reference existing or new schema files
|
|
213
|
+
|
|
214
|
+
2. **Modify schema/*.yaml** to:
|
|
215
|
+
- Add/remove fields
|
|
216
|
+
- Add/remove composite indexes
|
|
217
|
+
- Adjust field ordering and indexing
|
|
218
|
+
|
|
219
|
+
3. **Leave terragrunt.hcl as-is** - it orchestrates the wrapper module automatically with safe defaults
|
|
220
|
+
|
|
221
|
+
## Using Different Configurations Per Environment
|
|
222
|
+
|
|
223
|
+
The generator creates identical files for prod and staging. You can customize each environment by editing:
|
|
224
|
+
- `infra/prod/firestore/databases.yaml`
|
|
225
|
+
- `infra/staging/firestore/databases.yaml`
|
|
226
|
+
|
|
227
|
+
To use existing schemas across multiple databases, reference the same schema files in multiple database entries in `databases.yaml`:
|
|
228
|
+
|
|
229
|
+
```yaml
|
|
230
|
+
databases:
|
|
231
|
+
- name: "db1"
|
|
232
|
+
collections:
|
|
233
|
+
- schema: schema/users.yaml # Reuse schema
|
|
234
|
+
- name: "db2"
|
|
235
|
+
collections:
|
|
236
|
+
- schema: schema/users.yaml # Same schema in different database
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Firestore Constraints
|
|
240
|
+
|
|
241
|
+
- **TTL Limit**: Only ONE field per database can have TTL enabled (mark with `ttl_enabled: true`)
|
|
242
|
+
- **Single-Field Indexes**: Use `order` for non-array fields to enable efficient sorting/filtering
|
|
243
|
+
- **Array Indexes**: Use `array_config: CONTAINS` for array fields to enable array-contains queries
|
|
244
|
+
- **Composite Indexes**: Automatically include `__name__` field at the end for document ID ordering
|
|
245
|
+
- **Collections**: Implicitly created when documents are added (no Terraform resource for collection itself)
|
|
246
|
+
- **Schema Reuse**: Multiple databases can reference the same schema file
|
|
@@ -3,6 +3,10 @@ const chalk = require('chalk');
|
|
|
3
3
|
const BaseGenerator = require('../../../src/BaseGenerator');
|
|
4
4
|
const { required } = require('../../../src/validators');
|
|
5
5
|
const getTribeAndClanName = require('../../init/clan-infra/tribe-clan-repo');
|
|
6
|
+
const {
|
|
7
|
+
validateTtlField,
|
|
8
|
+
validateCompositeIndexName,
|
|
9
|
+
} = require('./validators');
|
|
6
10
|
|
|
7
11
|
module.exports = class extends BaseGenerator {
|
|
8
12
|
prompting() {
|
|
@@ -24,32 +28,31 @@ module.exports = class extends BaseGenerator {
|
|
|
24
28
|
},
|
|
25
29
|
{
|
|
26
30
|
type: 'list',
|
|
27
|
-
name: '
|
|
31
|
+
name: 'concurrencyMode',
|
|
28
32
|
message: 'Select the database concurrency mode.',
|
|
29
|
-
default: '
|
|
33
|
+
default: 'PESSIMISTIC',
|
|
30
34
|
choices: ['OPTIMISTIC', 'PESSIMISTIC'],
|
|
31
35
|
},
|
|
32
36
|
{
|
|
33
|
-
type: '
|
|
34
|
-
name: '
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
type: 'input',
|
|
38
|
+
name: 'location',
|
|
39
|
+
default: 'europe-west1',
|
|
40
|
+
message:
|
|
41
|
+
'Please provide the database location (e.g., europe-west1, us-central1).',
|
|
42
|
+
validate: required,
|
|
38
43
|
},
|
|
39
44
|
{
|
|
40
|
-
when: (response) => response.databaseBackupEnabled === 'true',
|
|
41
45
|
type: 'list',
|
|
42
|
-
name: '
|
|
43
|
-
message: '
|
|
46
|
+
name: 'backupSchedule',
|
|
47
|
+
message: 'Enable database backups?',
|
|
44
48
|
default: 'daily-backup',
|
|
45
|
-
choices: ['daily-backup', 'weekly-backup'],
|
|
49
|
+
choices: ['daily-backup', 'weekly-backup', 'none'],
|
|
46
50
|
},
|
|
47
51
|
{
|
|
48
|
-
when: (response) => response.
|
|
52
|
+
when: (response) => response.backupSchedule === 'weekly-backup',
|
|
49
53
|
type: 'list',
|
|
50
|
-
name: '
|
|
51
|
-
message:
|
|
52
|
-
'Please provide the day of the week the backup must be created.',
|
|
54
|
+
name: 'backupWeeklyDay',
|
|
55
|
+
message: 'Select the day for weekly backups.',
|
|
53
56
|
default: 'SUNDAY',
|
|
54
57
|
choices: [
|
|
55
58
|
'MONDAY',
|
|
@@ -62,93 +65,238 @@ module.exports = class extends BaseGenerator {
|
|
|
62
65
|
],
|
|
63
66
|
},
|
|
64
67
|
{
|
|
65
|
-
when: (response) => response.
|
|
68
|
+
when: (response) => response.backupSchedule !== 'none',
|
|
66
69
|
type: 'input',
|
|
67
|
-
name: '
|
|
68
|
-
default:
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
name: 'backupRetention',
|
|
71
|
+
default: (response) =>
|
|
72
|
+
response.backupSchedule === 'weekly-backup' ? '1209600s' : '259200s',
|
|
73
|
+
message: (response) => {
|
|
74
|
+
const days =
|
|
75
|
+
response.backupSchedule === 'weekly-backup' ? '14 days' : '3 days';
|
|
76
|
+
const seconds =
|
|
77
|
+
response.backupSchedule === 'weekly-backup'
|
|
78
|
+
? '1209600s'
|
|
79
|
+
: '259200s';
|
|
80
|
+
return `Please provide backup retention in seconds (e.g., "${seconds}" is ${days}).`;
|
|
81
|
+
},
|
|
71
82
|
validate: required,
|
|
72
83
|
},
|
|
73
84
|
{
|
|
74
85
|
type: 'input',
|
|
75
|
-
name: '
|
|
76
|
-
message: 'Please provide the name of the collection
|
|
86
|
+
name: 'collectionName',
|
|
87
|
+
message: 'Please provide the name of the collection.',
|
|
77
88
|
validate: required,
|
|
78
89
|
},
|
|
79
90
|
{
|
|
80
91
|
type: 'input',
|
|
81
|
-
name: '
|
|
82
|
-
message: 'Please provide
|
|
83
|
-
|
|
92
|
+
name: 'collectionDescription',
|
|
93
|
+
message: 'Please provide a description for this collection.',
|
|
94
|
+
default: '',
|
|
84
95
|
},
|
|
85
96
|
{
|
|
86
97
|
type: 'input',
|
|
87
|
-
name: '
|
|
88
|
-
message: '
|
|
89
|
-
|
|
98
|
+
name: 'ttlField',
|
|
99
|
+
message: 'Which field should have TTL enabled? (leave blank for none)',
|
|
100
|
+
default: '',
|
|
101
|
+
validate: validateTtlField,
|
|
90
102
|
},
|
|
91
103
|
{
|
|
92
|
-
type: '
|
|
93
|
-
name: '
|
|
94
|
-
message:
|
|
95
|
-
|
|
96
|
-
|
|
104
|
+
type: 'input',
|
|
105
|
+
name: 'compositeIndexFields',
|
|
106
|
+
message:
|
|
107
|
+
'Which fields should be in composite indexes? (comma-separated, leave blank for none)',
|
|
108
|
+
default: '',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
when: (response) => response.compositeIndexFields.trim().length > 0,
|
|
112
|
+
type: 'input',
|
|
113
|
+
name: 'compositeIndexName',
|
|
114
|
+
message:
|
|
115
|
+
'Please provide a name for the composite index.\n (the name must be unique within the database, e.g., "<collection>-index-1")',
|
|
116
|
+
validate: validateCompositeIndexName,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
when: (response) => response.compositeIndexFields.trim().length > 0,
|
|
120
|
+
type: 'input',
|
|
121
|
+
name: 'arrayFieldsInComposite',
|
|
122
|
+
message:
|
|
123
|
+
'Which fields in the composite index are array fields? (comma-separated, leave blank for none)',
|
|
124
|
+
default: '',
|
|
97
125
|
},
|
|
98
126
|
];
|
|
99
127
|
|
|
100
128
|
return this.prompt(prompts).then((props) => {
|
|
101
129
|
this.answers = props;
|
|
130
|
+
|
|
131
|
+
// Check for existing files (databases.yaml and schema files)
|
|
132
|
+
const existingFiles = [];
|
|
133
|
+
['prod', 'staging'].forEach((env) => {
|
|
134
|
+
const databasesPath = this.destinationPath(
|
|
135
|
+
path.join('infra', env, 'firestore', 'databases.yaml'),
|
|
136
|
+
);
|
|
137
|
+
const schemaDir = this.destinationPath(
|
|
138
|
+
path.join('infra', env, 'firestore', 'schema'),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (this.fs.exists(databasesPath)) {
|
|
142
|
+
existingFiles.push(`infra/${env}/firestore/databases.yaml`);
|
|
143
|
+
}
|
|
144
|
+
if (this.fs.exists(schemaDir)) {
|
|
145
|
+
existingFiles.push(`infra/${env}/firestore/schema/`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (existingFiles.length > 0) {
|
|
150
|
+
return this.prompt([
|
|
151
|
+
{
|
|
152
|
+
type: 'list',
|
|
153
|
+
name: 'existingFilesAction',
|
|
154
|
+
message: `Found existing firestore files: ${existingFiles.join(', ')}. What do you want to do?`,
|
|
155
|
+
choices: [
|
|
156
|
+
{ name: 'Use existing schema', value: 'use' },
|
|
157
|
+
{ name: 'Overwrite with new configuration', value: 'overwrite' },
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
]).then((action) => {
|
|
161
|
+
this.answers.existingFilesAction = action.existingFilesAction;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
102
164
|
});
|
|
103
165
|
}
|
|
104
166
|
|
|
105
167
|
writing() {
|
|
106
|
-
const {
|
|
168
|
+
const {
|
|
169
|
+
databaseName,
|
|
170
|
+
collectionName,
|
|
171
|
+
collectionDescription,
|
|
172
|
+
ttlField,
|
|
173
|
+
compositeIndexFields,
|
|
174
|
+
compositeIndexName,
|
|
175
|
+
arrayFieldsInComposite,
|
|
176
|
+
...rest
|
|
177
|
+
} = this.answers;
|
|
178
|
+
|
|
179
|
+
const ttlFields = ttlField ? [ttlField.trim()] : [];
|
|
180
|
+
const compositeFields = compositeIndexFields
|
|
181
|
+
? compositeIndexFields
|
|
182
|
+
.split(',')
|
|
183
|
+
.map((f) => f.trim())
|
|
184
|
+
.filter(Boolean)
|
|
185
|
+
: [];
|
|
186
|
+
const compositeArrayFields = arrayFieldsInComposite
|
|
187
|
+
? arrayFieldsInComposite
|
|
188
|
+
.split(',')
|
|
189
|
+
.map((f) => f.trim())
|
|
190
|
+
.filter(Boolean)
|
|
191
|
+
: [];
|
|
192
|
+
|
|
193
|
+
// Build schema file content
|
|
194
|
+
let schemaYaml =
|
|
195
|
+
'# The last field entry is always for the field path __name__.\n' +
|
|
196
|
+
'# If, on creation, __name__ was not specified as the last field,\n' +
|
|
197
|
+
'# it will be added automatically with the same direction as that of the last field defined.\n' +
|
|
198
|
+
'# If the final field in a composite index is not directional, the __name__ will be ordered "ASCENDING".\n' +
|
|
199
|
+
'# Index names must be unique within the database, e.g., "<collection name>-index-1"\n\n';
|
|
200
|
+
schemaYaml += `name: ${collectionName}\n`;
|
|
201
|
+
schemaYaml += `description: "${collectionDescription || collectionName}"\n\n`;
|
|
202
|
+
|
|
203
|
+
if (ttlFields.length > 0) {
|
|
204
|
+
schemaYaml += `ttl_field: ${ttlFields[0]}\n`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (compositeFields.length > 0) {
|
|
208
|
+
schemaYaml += 'indexes:\n';
|
|
209
|
+
schemaYaml += ` - name: ${compositeIndexName}\n`;
|
|
210
|
+
schemaYaml += ' fields:\n';
|
|
211
|
+
|
|
212
|
+
compositeFields.forEach((field) => {
|
|
213
|
+
schemaYaml += ` - field_path: ${field}\n`;
|
|
214
|
+
if (compositeArrayFields.includes(field)) {
|
|
215
|
+
schemaYaml += ` array_config: "CONTAINS"\n`;
|
|
216
|
+
schemaYaml += ` order: null\n`;
|
|
217
|
+
} else {
|
|
218
|
+
schemaYaml += ` order: "ASCENDING"\n`;
|
|
219
|
+
schemaYaml += ` array_config: null\n`;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Add __name__ field at the end
|
|
224
|
+
schemaYaml += ` - field_path: "__name__"\n`;
|
|
225
|
+
schemaYaml += ` order: "ASCENDING"\n`;
|
|
226
|
+
schemaYaml += ` array_config: null\n`;
|
|
227
|
+
}
|
|
107
228
|
|
|
108
229
|
['prod', 'staging'].forEach((env) => {
|
|
109
230
|
const context = {
|
|
110
231
|
...rest,
|
|
111
232
|
databaseName,
|
|
112
|
-
|
|
233
|
+
databaseType: this.answers.databaseType,
|
|
234
|
+
collectionName,
|
|
113
235
|
env,
|
|
114
236
|
};
|
|
115
|
-
const
|
|
116
|
-
const collectionDest = path.join(
|
|
117
|
-
baseDest,
|
|
118
|
-
'indexes',
|
|
119
|
-
databaseCollectionName,
|
|
120
|
-
);
|
|
237
|
+
const firestoreDest = path.join('infra', env, 'firestore');
|
|
121
238
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
this.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
context,
|
|
239
|
+
// Only write databases.yaml and schema if not using existing
|
|
240
|
+
if (this.answers.existingFilesAction !== 'use') {
|
|
241
|
+
const databasesPath = this.destinationPath(
|
|
242
|
+
firestoreDest,
|
|
243
|
+
'databases.yaml',
|
|
128
244
|
);
|
|
129
|
-
|
|
245
|
+
const schemaDir = this.destinationPath(firestoreDest, 'schema');
|
|
246
|
+
const schemaFileName = `${collectionName.toLowerCase()}.yaml`;
|
|
247
|
+
const schemaPath = this.destinationPath(schemaDir, schemaFileName);
|
|
248
|
+
|
|
249
|
+
// Delete existing files first to avoid Yeoman conflict prompts
|
|
250
|
+
if (this.fs.exists(databasesPath)) {
|
|
251
|
+
this.fs.delete(databasesPath);
|
|
252
|
+
}
|
|
253
|
+
if (this.fs.exists(schemaPath)) {
|
|
254
|
+
this.fs.delete(schemaPath);
|
|
255
|
+
}
|
|
130
256
|
|
|
131
|
-
|
|
132
|
-
['indexes.yaml', 'terragrunt.hcl'].forEach((file) => {
|
|
257
|
+
// Copy databases.yaml to firestore folder
|
|
133
258
|
this.fs.copyTpl(
|
|
134
|
-
this.templatePath('firestore
|
|
135
|
-
|
|
259
|
+
this.templatePath('firestore', 'databases.yaml'),
|
|
260
|
+
databasesPath,
|
|
136
261
|
context,
|
|
137
262
|
);
|
|
138
|
-
|
|
263
|
+
|
|
264
|
+
// Write schema file to schema/ subdirectory
|
|
265
|
+
this.fs.write(schemaPath, schemaYaml);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Copy terragrunt.hcl to firestore folder (multi-database wrapper)
|
|
269
|
+
this.fs.copyTpl(
|
|
270
|
+
this.templatePath('firestore', 'terragrunt.hcl'),
|
|
271
|
+
this.destinationPath(firestoreDest, 'terragrunt.hcl'),
|
|
272
|
+
context,
|
|
273
|
+
);
|
|
139
274
|
});
|
|
140
275
|
}
|
|
141
276
|
|
|
142
277
|
end() {
|
|
143
|
-
const {
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
278
|
+
const { existingFilesAction } = this.answers;
|
|
279
|
+
|
|
280
|
+
const header = chalk.green(
|
|
281
|
+
'Your Firestore database has now been created. Review the generated configuration:',
|
|
282
|
+
);
|
|
283
|
+
let message = header;
|
|
284
|
+
|
|
285
|
+
if (existingFilesAction === 'use') {
|
|
286
|
+
const databasesPreserved = `${chalk.green('1.')} ${chalk.cyan(path.join('firestore', 'databases.yaml'))} - ${chalk.yellow('(preserved existing file)')}`;
|
|
287
|
+
const schemaPreserved = `${chalk.green('2.')} ${chalk.cyan(path.join('firestore', 'schema', '*'))} - ${chalk.yellow('(preserved existing files)')}`;
|
|
288
|
+
message += `\n${databasesPreserved}`;
|
|
289
|
+
message += `\n${schemaPreserved}`;
|
|
290
|
+
} else {
|
|
291
|
+
const databasesNew = `${chalk.green('1.')} ${chalk.cyan(path.join('firestore', 'databases.yaml'))} - Database configuration with defaults`;
|
|
292
|
+
const schemaNew = `${chalk.green('2.')} ${chalk.cyan(path.join('firestore', 'schema', '*'))} - Collection schema files`;
|
|
293
|
+
message += `\n${databasesNew}`;
|
|
294
|
+
message += `\n${schemaNew}`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const terragruntFile = `${chalk.green('3.')} ${chalk.cyan(path.join('firestore', 'terragrunt.hcl'))} - Multi-database orchestration`;
|
|
298
|
+
message += `\n${terragruntFile}`;
|
|
299
|
+
|
|
300
|
+
this.log(`\n${message}\n`);
|
|
153
301
|
}
|
|
154
302
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Firestore Databases Configuration
|
|
2
|
+
#
|
|
3
|
+
# Defaults are configured in terragrunt.hcl
|
|
4
|
+
# Each database only needs:
|
|
5
|
+
# - name: unique identifier
|
|
6
|
+
# - collections: list of schema references or inline definitions
|
|
7
|
+
# - backup_schedule_configuration: backup schedule
|
|
8
|
+
# - (optional) any field overrides
|
|
9
|
+
|
|
10
|
+
backup_schedule_configuration: &default_backup_schedule
|
|
11
|
+
<% if (backupSchedule === 'daily-backup') { %>
|
|
12
|
+
daily_recurrence:
|
|
13
|
+
retention: "<%- backupRetention %>"
|
|
14
|
+
<% } else if (backupSchedule === 'weekly-backup') { %>
|
|
15
|
+
weekly_recurrence:
|
|
16
|
+
day: "<%- backupWeeklyDay %>"
|
|
17
|
+
retention: "<%- backupRetention %>"
|
|
18
|
+
<% } else { %>
|
|
19
|
+
null
|
|
20
|
+
<% } %>
|
|
21
|
+
|
|
22
|
+
databases:
|
|
23
|
+
- name: "<%- databaseName %>"
|
|
24
|
+
backup_schedule_configuration: *default_backup_schedule
|
|
25
|
+
collections:
|
|
26
|
+
- schema: schema/<%= collectionName.toLowerCase() %>.yaml
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
# Terragrunt
|
|
2
|
-
#
|
|
1
|
+
# Terragrunt configuration for Firestore databases using the multi-database wrapper
|
|
2
|
+
# https://github.com/extenda/terraform-google-firestore
|
|
3
|
+
|
|
3
4
|
terraform {
|
|
4
|
-
source = "git::https://github.com/extenda/
|
|
5
|
+
source = "git::https://github.com/extenda/terraform-google-firestore//databases?ref=v1.0.0"
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
# Include all settings from the root terragrunt.hcl file
|
|
@@ -10,16 +11,103 @@ include {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
locals {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
# Load project variables
|
|
15
|
+
project_vars = read_terragrunt_config(find_in_parent_folders("project.hcl"))
|
|
16
|
+
|
|
17
|
+
# Default configuration for all databases
|
|
18
|
+
default_database_config = {
|
|
19
|
+
location = "<%- location %>"
|
|
20
|
+
database_type = "<%- databaseType %>"
|
|
21
|
+
concurrency_mode = "<%- concurrencyMode %>"
|
|
22
|
+
delete_protection_state = "DELETE_PROTECTION_ENABLED"
|
|
23
|
+
deletion_policy = "DELETE"
|
|
24
|
+
point_in_time_recovery_enablement = "POINT_IN_TIME_RECOVERY_DISABLED"
|
|
25
|
+
kms_key_name = null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Load database configuration from databases.yaml
|
|
29
|
+
database_config = yamldecode(file("${get_terragrunt_dir()}/databases.yaml"))
|
|
30
|
+
|
|
31
|
+
# Get all databases from databases.yaml and merge with defaults
|
|
32
|
+
all_databases = [
|
|
33
|
+
for db in try(local.database_config.databases, []) :
|
|
34
|
+
merge(local.default_database_config, db)
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# Build field and index configurations for each database
|
|
38
|
+
databases_collections = {
|
|
39
|
+
for db in local.all_databases : db.name => try(db.collections, [])
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Load collection schemas from referenced files OR inline definitions
|
|
43
|
+
collection_schemas = {
|
|
44
|
+
for db_name, collections in local.databases_collections : db_name => {
|
|
45
|
+
for collection in collections :
|
|
46
|
+
(try(collection.schema, null) != null ? yamldecode(file("${get_terragrunt_dir()}/${collection.schema}")).name : collection.name) => merge(
|
|
47
|
+
{
|
|
48
|
+
name = try(collection.schema, null) != null ? yamldecode(file("${get_terragrunt_dir()}/${collection.schema}")).name : collection.name
|
|
49
|
+
description = try(collection.schema, null) != null ? yamldecode(file("${get_terragrunt_dir()}/${collection.schema}")).description : try(collection.description, "")
|
|
50
|
+
ttl_field = try(collection.schema, null) != null ? try(yamldecode(file("${get_terragrunt_dir()}/${collection.schema}")).ttl_field, null) : try(collection.ttl_field, null)
|
|
51
|
+
indexes = try(collection.schema, null) != null ? try(yamldecode(file("${get_terragrunt_dir()}/${collection.schema}")).indexes, []) : try(collection.indexes, [])
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Build field configuration for each database (TTL fields only)
|
|
58
|
+
databases_field_configuration = {
|
|
59
|
+
for db_name, schemas in local.collection_schemas : db_name => flatten([
|
|
60
|
+
for schema_name, schema in schemas :
|
|
61
|
+
try(schema.ttl_field, null) != null ? [
|
|
62
|
+
{
|
|
63
|
+
collection = schema_name
|
|
64
|
+
field = schema.ttl_field
|
|
65
|
+
ttl_enabled = true
|
|
66
|
+
}
|
|
67
|
+
] : []
|
|
68
|
+
])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Build composite index configuration for each database
|
|
72
|
+
databases_composite_index_configuration = {
|
|
73
|
+
for db_name, schemas in local.collection_schemas : db_name => flatten([
|
|
74
|
+
for schema_name, schema in schemas : [
|
|
75
|
+
for idx, index in try(schema.indexes, []) :
|
|
76
|
+
{
|
|
77
|
+
index_id = index.name
|
|
78
|
+
collection = schema_name
|
|
79
|
+
query_scope = try(index.query_scope, "COLLECTION")
|
|
80
|
+
api_scope = try(index.api_scope, "ANY_API")
|
|
81
|
+
fields = [
|
|
82
|
+
for field_config in try(index.fields, []) : {
|
|
83
|
+
field_path = field_config.field_path
|
|
84
|
+
order = try(field_config.order, "ASCENDING")
|
|
85
|
+
array_config = try(field_config.array_config, null)
|
|
86
|
+
vector_config = try(field_config.vector_config, null)
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
])
|
|
92
|
+
}
|
|
16
93
|
}
|
|
17
94
|
|
|
18
|
-
#
|
|
19
|
-
inputs =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
95
|
+
# Inputs to pass to the multi-database wrapper
|
|
96
|
+
inputs = {
|
|
97
|
+
project_id = local.project_vars.locals.project_id
|
|
98
|
+
databases = {
|
|
99
|
+
for db in local.all_databases : db.name => {
|
|
100
|
+
database_id = db.name
|
|
101
|
+
location = db.location
|
|
102
|
+
database_type = db.database_type
|
|
103
|
+
concurrency_mode = db.concurrency_mode
|
|
104
|
+
delete_protection_state = db.delete_protection_state
|
|
105
|
+
backup_schedule_configuration = db.backup_schedule_configuration
|
|
106
|
+
point_in_time_recovery_enablement = db.point_in_time_recovery_enablement
|
|
107
|
+
deletion_policy = db.deletion_policy
|
|
108
|
+
kms_key_name = db.kms_key_name
|
|
109
|
+
field_configuration = local.databases_field_configuration[db.name]
|
|
110
|
+
composite_index_configuration = local.databases_composite_index_configuration[db.name]
|
|
24
111
|
}
|
|
25
|
-
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
validateTtlField: (input) => {
|
|
3
|
+
if (input.trim() === '') {
|
|
4
|
+
return true;
|
|
5
|
+
}
|
|
6
|
+
if (input.includes(',')) {
|
|
7
|
+
return 'Please enter only one field for TTL. Multiple fields are not supported.';
|
|
8
|
+
}
|
|
9
|
+
return true;
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
validateCompositeIndexName: (input) => {
|
|
13
|
+
const trimmedInput = input.trim();
|
|
14
|
+
if (!trimmedInput) {
|
|
15
|
+
return 'Composite index name is required';
|
|
16
|
+
}
|
|
17
|
+
if (trimmedInput.includes(',')) {
|
|
18
|
+
return 'Please enter only one name for the composite index';
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -11528,7 +11528,7 @@
|
|
|
11528
11528
|
},
|
|
11529
11529
|
"packages/generators": {
|
|
11530
11530
|
"name": "@hiiretail/gcp-infra-generators",
|
|
11531
|
-
"version": "1.
|
|
11531
|
+
"version": "1.11.0",
|
|
11532
11532
|
"license": "MIT",
|
|
11533
11533
|
"dependencies": {
|
|
11534
11534
|
"@google-cloud/storage": "^7.18.0",
|
package/dist/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# The last field entry is always for the field path __name__.
|
|
2
|
-
# If, on creation, __name__ was not specified as the last field,
|
|
3
|
-
# it will be added automatically with the same direction as that of the last field defined.
|
|
4
|
-
# If the final field in a composite index is not directional, the __name__ will be ordered "ASCENDING".
|
|
5
|
-
indexes:
|
|
6
|
-
<%-databaseCollectionIndexName%>:
|
|
7
|
-
- field_path: "<%-databaseCollectionFieldPath%>"
|
|
8
|
-
array_config: "CONTAINS"
|
|
9
|
-
order: "<%-databaseCollectionFieldorder%>"
|
|
10
|
-
- field_path: "__name__"
|
|
11
|
-
order: "<%-databaseCollectionFieldorder%>"
|
|
12
|
-
array_config: null
|
package/dist/generators/common-resources/firestore/templates/firestore/indexes/terragrunt.hcl
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# Terragrunt will copy the Terraform configurations specified by the source parameter, along with any files in the
|
|
2
|
-
# working directory, into a temporary folder, and execute your Terraform commands in that folder.
|
|
3
|
-
terraform {
|
|
4
|
-
source = "git::https://github.com/extenda/tf-module-gcp-firestore//modules/index?ref=v1.0.0"
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
# Include all settings from the root terragrunt.hcl file
|
|
8
|
-
include {
|
|
9
|
-
path = find_in_parent_folders("terragrunt_root.hcl")
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
locals {
|
|
13
|
-
project_vars = read_terragrunt_config(find_in_parent_folders("project.hcl"))
|
|
14
|
-
collection = basename(get_terragrunt_dir())
|
|
15
|
-
database_name = basename(dirname(dirname(get_terragrunt_dir())))
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
# These are the variables we have to pass in to use the module specified in the terragrunt configuration above
|
|
19
|
-
inputs = merge(
|
|
20
|
-
yamldecode(
|
|
21
|
-
file("${get_terragrunt_dir()}/indexes.yaml")),
|
|
22
|
-
{
|
|
23
|
-
project_id = local.project_vars.locals.project_id
|
|
24
|
-
database_name = local.database_name
|
|
25
|
-
collection = local.collection
|
|
26
|
-
}
|
|
27
|
-
)
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
locals {
|
|
2
|
-
###################
|
|
3
|
-
# REQUIRED INPUTS #
|
|
4
|
-
###################
|
|
5
|
-
|
|
6
|
-
database_location_id = "europe-west1"
|
|
7
|
-
database_type = "<%- databaseType %>"
|
|
8
|
-
database_concurrency_mode = "<%- databaseConcurrencyMode %>"
|
|
9
|
-
|
|
10
|
-
###################
|
|
11
|
-
# OPTIONAL INPUTS #
|
|
12
|
-
###################
|
|
13
|
-
|
|
14
|
-
# Description: Enables protection of a Firestore database from accidental deletion across all surfaces (API, gcloud, Cloud Console, and Terraform).
|
|
15
|
-
#Type: String
|
|
16
|
-
#Default: "DELETE_PROTECTION_DISABLED"
|
|
17
|
-
database_delete_protection_state = "DELETE_PROTECTION_ENABLED"
|
|
18
|
-
|
|
19
|
-
<% if (databaseBackupEnabled === 'true') { %>
|
|
20
|
-
#Type: Bool
|
|
21
|
-
#Default: true
|
|
22
|
-
# Description: Firestore backup settings
|
|
23
|
-
database_backup_enabled = true
|
|
24
|
-
database_backup_schedule = "<%- databaseBackupSchedule %>"
|
|
25
|
-
<% if (databaseBackupSchedule === 'weekly-backup') { %>
|
|
26
|
-
database_backup_weekly_recurrence = "<%- databaseBackupWeeklyRecurrence %>"
|
|
27
|
-
<% } %>
|
|
28
|
-
database_point_in_time_recovery_enablement = "POINT_IN_TIME_RECOVERY_DISABLED"
|
|
29
|
-
database_backup_retention = "<%- databaseBackupRetention %>"
|
|
30
|
-
<% } %>
|
|
31
|
-
}
|