@c6fc/spellcraft-gcp-terraform 1.0.1
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/LICENSE +21 -0
- package/README.md +199 -0
- package/module.js +189 -0
- package/module.libsonnet +132 -0
- package/package.json +37 -0
- package/test.jsonnet +34 -0
- package/utils/cli-test.js +78 -0
- package/utils/test.js +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Brad Woodward
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# SpellCraft AWS Integration
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@c6fc/spellcraft-gcp-terraform)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
This module exposes common constructs for using [SpellCraft](https://github.com/@c6fc/spellcraft) SpellFrames to deploy infrastructure to AWS using Terraform.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install --save @c6fc/spellcraft-gcp-terraform
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
This module exposes the concept of a bootstrap bucket (functionally a terraform backend), and artifacts which can contain arbitrary data and are stored alongside the terraform state in the bootstrap bucket. The former allows for dynamic configuration of Terraform providers within different environments, while the latter simplifies the storage and use of dynamic configuration details that might be environment dependent.
|
|
15
|
+
|
|
16
|
+
<!-- SPELLCRAFT_DOCS_CLI_START -->
|
|
17
|
+
|
|
18
|
+
<!-- SPELLCRAFT_DOCS_CLI_END -->
|
|
19
|
+
|
|
20
|
+
## SpellFrame 'init()' features
|
|
21
|
+
|
|
22
|
+
This plugin does not perform any distinct 'init' operations, other than to initialize credentials within the dependant plugin `@c6fc/terraform-gcp-auth`.
|
|
23
|
+
|
|
24
|
+
## JavaScript context features
|
|
25
|
+
|
|
26
|
+
Extends the JavaScript function context with an `awsterraform` object containing the following keys:
|
|
27
|
+
|
|
28
|
+
```JSON
|
|
29
|
+
{
|
|
30
|
+
"projectName": "<contains the name of the project specified by bootstrap()>",
|
|
31
|
+
"bootstrapBucket": "<contains the ARN for the bootstrap bucket>",
|
|
32
|
+
"bootstrapLocation": "<contains the region where the bootstrap bucket is located>"
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
<!-- SPELLCRAFT_DOCS_API_START -->
|
|
37
|
+
## API Reference
|
|
38
|
+
|
|
39
|
+
### `bootstrap(project)`
|
|
40
|
+
|
|
41
|
+
Creates a Terraform backend bucket if one doesn't already exist, then
|
|
42
|
+
returns a 'backend' object referencing this bucket and a unique path
|
|
43
|
+
for this project's state and artifacts.
|
|
44
|
+
|
|
45
|
+
- param {string} project
|
|
46
|
+
- returns {object} backend
|
|
47
|
+
|
|
48
|
+
**Examples:**
|
|
49
|
+
|
|
50
|
+
```jsonnet
|
|
51
|
+
local gcp = import "@c6fc/spellcraft-gcp-terraform";
|
|
52
|
+
|
|
53
|
+
gcp.bootstrap("myBootstrapTest");
|
|
54
|
+
|
|
55
|
+
// Returns:
|
|
56
|
+
{
|
|
57
|
+
"terraform": {
|
|
58
|
+
"backend": {
|
|
59
|
+
"gcs": {
|
|
60
|
+
"bucket": "spellcraft-random-0123456789",
|
|
61
|
+
"key": "spellcraft/myBootstrapTest/terraform.tfstate",
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
### `getArtifact(name)`
|
|
70
|
+
|
|
71
|
+
Obtains the contents of a named artifact stored alongside this project in the bootstrap
|
|
72
|
+
bucket. This artifact is created with 'putArtifact';
|
|
73
|
+
|
|
74
|
+
- param {string} name
|
|
75
|
+
- returns {object} backend
|
|
76
|
+
|
|
77
|
+
**Examples:**
|
|
78
|
+
|
|
79
|
+
```jsonnet
|
|
80
|
+
local gcp = import "@c6fc/spellcraft-gcp-terraform";
|
|
81
|
+
|
|
82
|
+
gcp.getArtifact("myArtifact");
|
|
83
|
+
|
|
84
|
+
// Returns:
|
|
85
|
+
<contents of your artifact>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
### `getBootstrapBucket()`
|
|
90
|
+
|
|
91
|
+
Attempts to discover the bucket created through bootstrap(), returning the
|
|
92
|
+
bucket name if present.
|
|
93
|
+
|
|
94
|
+
- returns {string} bucketArn
|
|
95
|
+
|
|
96
|
+
**Examples:**
|
|
97
|
+
|
|
98
|
+
```jsonnet
|
|
99
|
+
local gcp = import "@c6fc/spellcraft-gcp-terraform";
|
|
100
|
+
|
|
101
|
+
gcp.getBootstrapBucket();
|
|
102
|
+
|
|
103
|
+
// Returns:
|
|
104
|
+
spellcraft-terraform-<project-id>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
### `getRemoteState(project)`
|
|
109
|
+
|
|
110
|
+
Read the Terraform state for an adjacent SpellCraft project in the same GCP account
|
|
111
|
+
|
|
112
|
+
- param {string} project
|
|
113
|
+
- returns {object} state
|
|
114
|
+
|
|
115
|
+
**Examples:**
|
|
116
|
+
|
|
117
|
+
```jsonnet
|
|
118
|
+
local gcp = import "@c6fc/spellcraft-gcp-terraform";
|
|
119
|
+
|
|
120
|
+
gcp.getRemoteState("mySecondProject");
|
|
121
|
+
|
|
122
|
+
// Returns:
|
|
123
|
+
{ full remote state object }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
### `putArtifact(name, content)`
|
|
128
|
+
|
|
129
|
+
Stores the JSON-encoded balue of 'contents' as a file in the GCS backend bucket using
|
|
130
|
+
the project prefix.
|
|
131
|
+
|
|
132
|
+
- param {string} name
|
|
133
|
+
- param {*} contents
|
|
134
|
+
- returns {boolean} true
|
|
135
|
+
|
|
136
|
+
**Examples:**
|
|
137
|
+
|
|
138
|
+
```jsonnet
|
|
139
|
+
local gcp = import "@c6fc/spellcraft-gcp-terraform";
|
|
140
|
+
|
|
141
|
+
gcp.putArtifact("myArtifact", { someData: someValue });
|
|
142
|
+
|
|
143
|
+
// Returns:
|
|
144
|
+
true
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
### `providerAliases(default, filter="")`
|
|
149
|
+
|
|
150
|
+
Stores the JSON-encoded value of 'contents' as a file in the GCS backend bucket using
|
|
151
|
+
the project prefix. If 'filter' is provided, only region names that string match
|
|
152
|
+
will be included.
|
|
153
|
+
|
|
154
|
+
- param {string} default
|
|
155
|
+
- param {string} filter
|
|
156
|
+
|
|
157
|
+
**Examples:**
|
|
158
|
+
|
|
159
|
+
```jsonnet
|
|
160
|
+
local gcp = import "@c6fc/spellcraft-gcp-terraform";
|
|
161
|
+
|
|
162
|
+
gcp.providerAliases("us-west2");
|
|
163
|
+
|
|
164
|
+
// Returns:
|
|
165
|
+
[{ google: {
|
|
166
|
+
region: "us-west2"
|
|
167
|
+
}}, { google: {
|
|
168
|
+
region: "us-east1",
|
|
169
|
+
alias: "us-east1"
|
|
170
|
+
}}, ...]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
<!-- SPELLCRAFT_DOCS_API_END -->
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
## Installation
|
|
179
|
+
|
|
180
|
+
Install the plugin as a dependency in your SpellCraft project:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
npm install --save @c6fc/spellcraft-gcp-terraform
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Once installed, you can load the module into your JSonnet files.
|
|
187
|
+
|
|
188
|
+
```jsonnet
|
|
189
|
+
local aws = import "@c6fc/spellcraft-gcp-terraform";
|
|
190
|
+
|
|
191
|
+
{
|
|
192
|
+
'backend.tf.json': aws.bootstrap("myProjectName"),
|
|
193
|
+
|
|
194
|
+
// Generate provider list defaulting to 'us-west2' but including all 'us-' regions
|
|
195
|
+
'provider.tf.json': {
|
|
196
|
+
provider: aws.providerAliases("us-west2", "us-")
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
package/module.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE=1
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
|
|
8
|
+
// Nab the authenticated AWS instantiation from gcp-auth
|
|
9
|
+
const gcpauth = require("@c6fc/spellcraft-gcp-auth");
|
|
10
|
+
const { google } = gcpauth._spellcraft_metadata.functionContext;
|
|
11
|
+
const storage = google.storage('v1');
|
|
12
|
+
const compute = google.compute('v1');
|
|
13
|
+
|
|
14
|
+
let cachedProject = null;
|
|
15
|
+
|
|
16
|
+
// Initialize caches
|
|
17
|
+
const artifacts = {};
|
|
18
|
+
const gcpterraform = { projectName: null, bootstrapBucket: null };
|
|
19
|
+
const remoteStates = {};
|
|
20
|
+
|
|
21
|
+
exports._spellcraft_metadata = {
|
|
22
|
+
functionContext: { gcpterraform }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
exports.bootstrap = [async function (project) {
|
|
26
|
+
return await bootstrap(project);
|
|
27
|
+
}, "project"];
|
|
28
|
+
|
|
29
|
+
exports.getArtifact = [async function (name) {
|
|
30
|
+
return await getArtifact(name);
|
|
31
|
+
}, "name"];
|
|
32
|
+
|
|
33
|
+
exports.getBootstrapBucket = [async function () {
|
|
34
|
+
return await getBootstrapBucket();
|
|
35
|
+
}];
|
|
36
|
+
|
|
37
|
+
exports.getRemoteState = [async function (project) {
|
|
38
|
+
return await getRemoteState(project);
|
|
39
|
+
}, "project"];
|
|
40
|
+
|
|
41
|
+
exports.putArtifact = [async function (name, content) {
|
|
42
|
+
return await putArtifact(name, content);
|
|
43
|
+
}, "name", "content"];
|
|
44
|
+
|
|
45
|
+
async function bootstrap(projectName) {
|
|
46
|
+
cachedProject = await gcpauth.getProjectId[0]();
|
|
47
|
+
const targetBucket = `spellcraft-terraform-${cachedProject}`;
|
|
48
|
+
|
|
49
|
+
if (!await getBootstrapBucket()) {
|
|
50
|
+
try {
|
|
51
|
+
console.log(`[+] Creating GCS Bootstrap Bucket: ${targetBucket}`);
|
|
52
|
+
await storage.buckets.insert({
|
|
53
|
+
project: cachedProject,
|
|
54
|
+
requestBody: {
|
|
55
|
+
name: targetBucket,
|
|
56
|
+
location: 'US', // Defaulting to US multi-region for high availability
|
|
57
|
+
storageClass: 'STANDARD',
|
|
58
|
+
versioning: { enabled: true },
|
|
59
|
+
iamConfiguration: {
|
|
60
|
+
uniformBucketLevelAccess: { enabled: true }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
} catch(e) {
|
|
65
|
+
throw new Error(`Failed to discover/create GCS bucket: ${e.message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
gcpterraform.bootstrapBucket = targetBucket;
|
|
70
|
+
gcpterraform.projectName = projectName;
|
|
71
|
+
|
|
72
|
+
// Return the Terraform backend configuration object
|
|
73
|
+
return {
|
|
74
|
+
terraform: {
|
|
75
|
+
backend: {
|
|
76
|
+
gcs: {
|
|
77
|
+
bucket: gcpterraform.bootstrapBucket,
|
|
78
|
+
prefix: `spellcraft/${projectName}`
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
async function getBootstrapBucket() {
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if (!!gcpterraform.bootstrapBucket) {
|
|
89
|
+
return gcpterraform.bootstrapBucket;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!cachedProject) {
|
|
93
|
+
cachedProject = await gcpauth.getProjectId[0]();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// 1. Try to find existing bucket
|
|
99
|
+
await storage.buckets.get({ bucket: `spellcraft-terraform-${cachedProject}` });
|
|
100
|
+
gcpterraform.bootstrapBucket = `spellcraft-terraform-${cachedProject}`;
|
|
101
|
+
|
|
102
|
+
return gcpterraform.bootstrapBucket;
|
|
103
|
+
} catch (e) {
|
|
104
|
+
return false;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function getRemoteState(project) {
|
|
111
|
+
|
|
112
|
+
if (!!!remoteStates[project]) {
|
|
113
|
+
if (!gcpterraform.bootstrapBucket) throw new Error("Module not bootstrapped. Call bootstrap() first.");
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const res = await storage.objects.get({
|
|
117
|
+
bucket: gcpterraform.bootstrapBucket,
|
|
118
|
+
object: `spellcraft/${project}/default.tfstate`,
|
|
119
|
+
alt: 'media'
|
|
120
|
+
});
|
|
121
|
+
return res.data;
|
|
122
|
+
} catch (e) {
|
|
123
|
+
throw new Error(`Could not find remote state for project: ${project}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const state = JSON.parse(stateJson.Body);
|
|
127
|
+
|
|
128
|
+
const resources = state.resources.reduce((a, c) => {
|
|
129
|
+
let path;
|
|
130
|
+
|
|
131
|
+
if (c.mode == "data") {
|
|
132
|
+
a.data = (!a.data) ? {} : a.data;
|
|
133
|
+
a.data[c.type] = (!a.data[c.type]) ? {} : a.data[c.type];
|
|
134
|
+
|
|
135
|
+
path = a.data[c.type];
|
|
136
|
+
} else {
|
|
137
|
+
a[c.type] = (!a[c.type]) ? {} : a[c.type];
|
|
138
|
+
|
|
139
|
+
path = a[c.type];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
path[c.name] = (c.instances.length == 1) ? c.instances[0].attributes : c.instances.map(e => e.attributes);
|
|
143
|
+
|
|
144
|
+
return a;
|
|
145
|
+
}, {});
|
|
146
|
+
|
|
147
|
+
resources.outputs = Object.keys(state.outputs).reduce((a, c) => {
|
|
148
|
+
a[c] = state.outputs[c].value;
|
|
149
|
+
|
|
150
|
+
return a;
|
|
151
|
+
}, {});
|
|
152
|
+
|
|
153
|
+
// console.log(resources.outputs);
|
|
154
|
+
|
|
155
|
+
remoteStates[project] = resources;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return resources;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
exports.getArtifact = async function(name) {
|
|
162
|
+
if (!gcpterraform.bootstrapBucket) throw new Error("Module not bootstrapped. Call bootstrap() first.");
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const res = await storage.objects.get({
|
|
166
|
+
bucket: gcpterraform.bootstrapBucket,
|
|
167
|
+
object: `spellcraft/${gcpterraform.projectName}/artifacts/${name}.json`,
|
|
168
|
+
alt: 'media'
|
|
169
|
+
});
|
|
170
|
+
return res.data;
|
|
171
|
+
} catch (e) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
exports.putArtifact = async function(name, content) {
|
|
177
|
+
if (!gcpterraform.bootstrapBucket) throw new Error("Module not bootstrapped. Call bootstrap() first.");
|
|
178
|
+
|
|
179
|
+
const res = await storage.objects.insert({
|
|
180
|
+
bucket: gcpterraform.bootstrapBucket,
|
|
181
|
+
name: `spellcraft/${gcpterraform.projectName}/artifacts/${name}.json`,
|
|
182
|
+
media: {
|
|
183
|
+
mimeType: 'application/json',
|
|
184
|
+
body: JSON.stringify(content, null, 2)
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return !!res.data;
|
|
189
|
+
};
|
package/module.libsonnet
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Don't try to 'import' your spellcraft native functions here.
|
|
2
|
+
// Use std.native(function)(..args) instead
|
|
3
|
+
|
|
4
|
+
{
|
|
5
|
+
// JS Native functions are already documented in spellcraft_modules/foo.js
|
|
6
|
+
// but need to be specified here to expose them through the import
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a Terraform backend bucket if one doesn't already exist, then
|
|
10
|
+
* returns a 'backend' object referencing this bucket and a unique path
|
|
11
|
+
* for this project's state and artifacts.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} project
|
|
14
|
+
* @returns {object} backend
|
|
15
|
+
* @example
|
|
16
|
+
* local gcp = import "@c6fc/spellcraft-gcp-terraform";
|
|
17
|
+
*
|
|
18
|
+
* gcp.bootstrap("myBootstrapTest");
|
|
19
|
+
*
|
|
20
|
+
* // Returns:
|
|
21
|
+
* {
|
|
22
|
+
* "terraform": {
|
|
23
|
+
* "backend": {
|
|
24
|
+
* "gcs": {
|
|
25
|
+
* "bucket": "spellcraft-random-0123456789",
|
|
26
|
+
* "key": "spellcraft/myBootstrapTest/terraform.tfstate",
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
bootstrap(project):: std.native("@c6fc/spellcraft-gcp-terraform:bootstrap")(project),
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Obtains the contents of a named artifact stored alongside this project in the bootstrap
|
|
36
|
+
* bucket. This artifact is created with 'putArtifact';
|
|
37
|
+
*
|
|
38
|
+
* @param {string} name
|
|
39
|
+
* @returns {object} backend
|
|
40
|
+
* @example
|
|
41
|
+
* local gcp = import "@c6fc/spellcraft-gcp-terraform";
|
|
42
|
+
*
|
|
43
|
+
* gcp.getArtifact("myArtifact");
|
|
44
|
+
*
|
|
45
|
+
* // Returns:
|
|
46
|
+
* <contents of your artifact>
|
|
47
|
+
*/
|
|
48
|
+
getArtifact(name):: std.native("@c6fc/spellcraft-gcp-terraform:getArtifact")(name),
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Attempts to discover the bucket created through bootstrap(), returning the
|
|
52
|
+
* bucket name if present.
|
|
53
|
+
*
|
|
54
|
+
* @returns {string} bucketArn
|
|
55
|
+
* @example
|
|
56
|
+
* local gcp = import "@c6fc/spellcraft-gcp-terraform";
|
|
57
|
+
*
|
|
58
|
+
* gcp.getBootstrapBucket();
|
|
59
|
+
*
|
|
60
|
+
* // Returns:
|
|
61
|
+
* spellcraft-terraform-<project-id>
|
|
62
|
+
*/
|
|
63
|
+
getBootstrapBucket():: std.native("@c6fc/spellcraft-gcp-terraform:getBootstrapBucket")(),
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Read the Terraform state for an adjacent SpellCraft project in the same GCP account
|
|
67
|
+
*
|
|
68
|
+
* @param {string} project
|
|
69
|
+
* @returns {object} state
|
|
70
|
+
* @example
|
|
71
|
+
* local gcp = import "@c6fc/spellcraft-gcp-terraform";
|
|
72
|
+
*
|
|
73
|
+
* gcp.getRemoteState("mySecondProject");
|
|
74
|
+
*
|
|
75
|
+
* // Returns:
|
|
76
|
+
* { full remote state object }
|
|
77
|
+
*/
|
|
78
|
+
getRemoteState(project):: std.native("@c6fc/spellcraft-gcp-terraform:getRemoteState")(project),
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Stores the JSON-encoded balue of 'contents' as a file in the GCS backend bucket using
|
|
82
|
+
* the project prefix.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} name
|
|
85
|
+
* @param {*} contents
|
|
86
|
+
* @returns {boolean} true
|
|
87
|
+
* @example
|
|
88
|
+
* local gcp = import "@c6fc/spellcraft-gcp-terraform";
|
|
89
|
+
*
|
|
90
|
+
* gcp.putArtifact("myArtifact", { someData: someValue });
|
|
91
|
+
*
|
|
92
|
+
* // Returns:
|
|
93
|
+
* true
|
|
94
|
+
*/
|
|
95
|
+
putArtifact(name, content):: std.native("@c6fc/spellcraft-gcp-terraform:putArtifact")(name, content),
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Stores the JSON-encoded value of 'contents' as a file in the GCS backend bucket using
|
|
99
|
+
* the project prefix. If 'filter' is provided, only region names that string match
|
|
100
|
+
* will be included.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} default
|
|
103
|
+
* @param {string} filter
|
|
104
|
+
* @example
|
|
105
|
+
* local gcp = import "@c6fc/spellcraft-gcp-terraform";
|
|
106
|
+
*
|
|
107
|
+
* gcp.providerAliases("us-west2");
|
|
108
|
+
*
|
|
109
|
+
* // Returns:
|
|
110
|
+
* [{ google: {
|
|
111
|
+
* region: "us-west2"
|
|
112
|
+
* }}, { google: {
|
|
113
|
+
* region: "us-east1",
|
|
114
|
+
alias: "us-east1"
|
|
115
|
+
* }}, ...]
|
|
116
|
+
*/
|
|
117
|
+
providerAliases(default, filter=""):: [{
|
|
118
|
+
google: {
|
|
119
|
+
alias: region,
|
|
120
|
+
region: region
|
|
121
|
+
}
|
|
122
|
+
} for region in std.filterMap(
|
|
123
|
+
function(x) std.length(filter) < 1 || std.length(std.findSubstr(filter, x.name)) > 0,
|
|
124
|
+
function(x) x.name,
|
|
125
|
+
std.native("@c6fc/spellcraft-gcp-auth:api")('compute.v1.regions.list', '{"project":"%s"}' % std.native("@c6fc/spellcraft-gcp-auth:getProjectId")()).items
|
|
126
|
+
)] + [{
|
|
127
|
+
google: {
|
|
128
|
+
region: default
|
|
129
|
+
}
|
|
130
|
+
}]
|
|
131
|
+
|
|
132
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@c6fc/spellcraft-gcp-terraform",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"main": "module.js",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"cli": "utils/cli-test.js",
|
|
7
|
+
"test": "node utils/test.js"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/c6fc/spellcraft-gcp-terraform.git"
|
|
12
|
+
},
|
|
13
|
+
"spellcraft": true,
|
|
14
|
+
"keywords": [
|
|
15
|
+
"spellcraft",
|
|
16
|
+
"aws",
|
|
17
|
+
"terraform"
|
|
18
|
+
],
|
|
19
|
+
"author": "Brad Woodward (brad@bradwoodward.io)",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/c6fc/spellcraft-gcp-terraform/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/c6fc/spellcraft-gcp-terraform#readme",
|
|
25
|
+
"description": "A plugin to empower @c6fc/spellcraft with GCP and Terraform",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@c6fc/spellcraft-gcp-auth": "^1.0.1",
|
|
28
|
+
"@c6fc/spellcraft-terraform": "^1.1.3"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@c6fc/spellcraft": "~0.1.0",
|
|
32
|
+
"yargs": "^18.0.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@c6fc/spellcraft": "~0.1.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/test.jsonnet
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This file should manifest all the exposed features of your module
|
|
3
|
+
so users can see examples of how they are used, and the output they
|
|
4
|
+
generate.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
local gcp = import "module.libsonnet";
|
|
8
|
+
|
|
9
|
+
{
|
|
10
|
+
"bootstrap.tf.json": gcp.bootstrap("spellcraft-gcp-terraform-module-test"),
|
|
11
|
+
"test.tf.json": {
|
|
12
|
+
output: {
|
|
13
|
+
putArtifact: {
|
|
14
|
+
value: gcp.putArtifact("putArtifactTest", "mytest2")
|
|
15
|
+
},
|
|
16
|
+
getBootstrapBucket: {
|
|
17
|
+
value: gcp.getBootstrapBucket()
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
/* These can only be used after the project is created and the contents populated.
|
|
21
|
+
getArtifact: {
|
|
22
|
+
value: gcp.getArtifact("putArtifactTest")
|
|
23
|
+
},
|
|
24
|
+
getRemoteState: {
|
|
25
|
+
value: std.parseJson(gcp.getRemoteState("spellcraft-gcp-terraform-module-test"))
|
|
26
|
+
}
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
'providers.tf.json': {
|
|
32
|
+
provider: gcp.providerAliases("us-west2", "us-")
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const yargs = require('yargs');
|
|
8
|
+
const { hideBin } = require('yargs/helpers');
|
|
9
|
+
const { SpellFrame } = require('@c6fc/spellcraft');
|
|
10
|
+
|
|
11
|
+
const spellframe = new SpellFrame();
|
|
12
|
+
|
|
13
|
+
// 1. Resolve local package info to simulate a real plugin load
|
|
14
|
+
const packageDir = path.resolve(__dirname, '..'); // Assuming script is in /utils
|
|
15
|
+
const packageJsonPath = path.join(packageDir, 'package.json');
|
|
16
|
+
|
|
17
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
18
|
+
throw new Error(`[!] Could not find package.json at ${packageJsonPath}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
22
|
+
const jsEntry = path.join(packageDir, pkg.main || 'index.js');
|
|
23
|
+
|
|
24
|
+
console.log(`[*] Loading local plugin context: ${pkg.name}`);
|
|
25
|
+
|
|
26
|
+
// 2. Load the plugin into SpellFrame
|
|
27
|
+
// This registers native functions (namespaced) and populates cliExtensions
|
|
28
|
+
// automatically via the _spellcraft_metadata export in module.js
|
|
29
|
+
spellframe.loadPlugin(pkg.name, jsEntry);
|
|
30
|
+
|
|
31
|
+
(async () => {
|
|
32
|
+
|
|
33
|
+
let cli = yargs(hideBin(process.argv))
|
|
34
|
+
.usage("Syntax: $0 <command> [options]")
|
|
35
|
+
.scriptName("spellcraft")
|
|
36
|
+
|
|
37
|
+
.command("generate <filename>", "Generates files from a configuration", (yargsInstance) => {
|
|
38
|
+
return yargsInstance.positional('filename', {
|
|
39
|
+
describe: 'Jsonnet configuration file to consume',
|
|
40
|
+
type: 'string',
|
|
41
|
+
demandOption: true,
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
async (argv) => {
|
|
45
|
+
try {
|
|
46
|
+
await spellframe.init();
|
|
47
|
+
console.log(`[+] Rendering configuration from: ${argv.filename}`);
|
|
48
|
+
await spellframe.render(argv.filename);
|
|
49
|
+
await spellframe.write();
|
|
50
|
+
console.log("[+] Generation complete.");
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(`[!] Error during generation: ${error.message}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// 3. Register CLI extensions found in the loaded plugin
|
|
58
|
+
// These were populated by loadPlugin() reading _spellcraft_metadata
|
|
59
|
+
if (spellframe.cliExtensions && spellframe.cliExtensions.length > 0) {
|
|
60
|
+
spellframe.cliExtensions.forEach((extensionFn) => {
|
|
61
|
+
if (typeof extensionFn === 'function') {
|
|
62
|
+
extensionFn(cli, spellframe);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
cli
|
|
68
|
+
.demandCommand(1, 'You need to specify a command.')
|
|
69
|
+
.recommendCommands()
|
|
70
|
+
.strict()
|
|
71
|
+
.showHelpOnFail(true)
|
|
72
|
+
.help("help")
|
|
73
|
+
.alias('h', 'help')
|
|
74
|
+
.version()
|
|
75
|
+
.alias('v', 'version')
|
|
76
|
+
.epilogue('For more information, consult the SpellCraft documentation.')
|
|
77
|
+
.argv;
|
|
78
|
+
})();
|
package/utils/test.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
This is a pre-written test-case wrapper
|
|
5
|
+
for local module development. Populate
|
|
6
|
+
../test.jsonnet with tests of your
|
|
7
|
+
module capabilities.
|
|
8
|
+
|
|
9
|
+
Run with `npm run test`
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { SpellFrame } = require('@c6fc/spellcraft');
|
|
15
|
+
|
|
16
|
+
const spellframe = new SpellFrame();
|
|
17
|
+
|
|
18
|
+
(async () => {
|
|
19
|
+
try {
|
|
20
|
+
// 1. Read the local package.json
|
|
21
|
+
const packageJsonPath = path.resolve('./package.json');
|
|
22
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
23
|
+
throw new Error("Could not find package.json in current directory.");
|
|
24
|
+
}
|
|
25
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
26
|
+
|
|
27
|
+
// 2. Identify the entry point
|
|
28
|
+
const jsEntry = path.resolve(pkg.main || 'index.js');
|
|
29
|
+
|
|
30
|
+
console.log(`[*] Manually loading plugin: ${pkg.name}`);
|
|
31
|
+
|
|
32
|
+
// 3. Register the plugin using the current package name.
|
|
33
|
+
// This ensures the native functions are registered as "@scope/pkg:func",
|
|
34
|
+
// matching the std.native() calls in your libsonnet.
|
|
35
|
+
spellframe.loadPlugin(pkg.name, jsEntry);
|
|
36
|
+
|
|
37
|
+
// 4. Initialize
|
|
38
|
+
await spellframe.init();
|
|
39
|
+
|
|
40
|
+
// 5. Render
|
|
41
|
+
// Note: Ensure your test.jsonnet imports "./module.libsonnet" directly
|
|
42
|
+
console.log("[*] Rendering test.jsonnet...");
|
|
43
|
+
await spellframe.render("test.jsonnet");
|
|
44
|
+
|
|
45
|
+
console.log(JSON.stringify(spellframe.lastRender, null, 4));
|
|
46
|
+
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error(`[!] Test failed: ${error.message}`);
|
|
49
|
+
if (error.stack) console.error(error.stack);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
})();
|