diff --git a/.github/workflows/tfresource-pull.yaml b/.github/workflows/tfresource-pull.yaml new file mode 100644 index 00000000..bffec6bd --- /dev/null +++ b/.github/workflows/tfresource-pull.yaml @@ -0,0 +1,29 @@ +name: tfresource-pull +on: + pull_request: + paths: + - tfresource/** +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + sparse-checkout: tfresource + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 18.x + registry-url: https://registry.npmjs.org + - name: Install winglang + run: npm i -g winglang + - name: Install dependencies + run: npm install --include=dev + working-directory: tfresource + - name: Test + run: wing test + working-directory: tfresource + - name: Pack + run: wing pack + working-directory: tfresource diff --git a/.github/workflows/tfresource-release.yaml b/.github/workflows/tfresource-release.yaml new file mode 100644 index 00000000..22a472ae --- /dev/null +++ b/.github/workflows/tfresource-release.yaml @@ -0,0 +1,37 @@ +name: tfresource-release +on: + push: + branches: + - main + paths: + - tfresource/** +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + sparse-checkout: tfresource + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 18.x + registry-url: https://registry.npmjs.org + - name: Install winglang + run: npm i -g winglang + - name: Install dependencies + run: npm install --include=dev + working-directory: tfresource + - name: Test + run: wing test + working-directory: tfresource + - name: Pack + run: wing pack + working-directory: tfresource + - name: Publish + run: npm publish --access=public --registry https://registry.npmjs.org --tag + latest *.tgz + working-directory: tfresource + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/LICENSE b/LICENSE index a875f479..44b89064 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Wing +Copyright (c) 2023 Wing Cloud, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/mklib.sh b/mklib.sh index f732bd1d..df883b93 100755 --- a/mklib.sh +++ b/mklib.sh @@ -47,21 +47,25 @@ cat > $1/README.md < { + fs.writeFile(path, data); + return this.state(path, data); + }); + + r.onDelete(inflight (state) => { + let s = FileState.fromJson(state); + fs.remove(s.path); + }); + + r.onRead(inflight (state) => { + return this.state(path, data); + }); + } + + inflight state(path: str, data: str): FileState { + return { + hash: util.sha256(data), + path: path, + }; + } +} +``` + +Now, we can use this new resource like this: + +```js +new File("hello.txt", "my file") as "hello"; +new File("world.txt", "your file") as "world"; +``` + +If we compile this to `tf-*` and apply: + +```sh +wing compile -t tf-aws main.w +cd target/main.tfaws +terraform init +terraform apply +``` + +You'll notice two new files: + +```sh +$ cat hello.txt +my file +$ cat world.txt +your file +``` + +Now, let's change our code to: + +```js +new File("hello1.txt", "my file") as "hello"; +new File("world.txt", "your file 2") as "world"; +``` + +Notice that we've changed the *name* of the first file and the *contents* of the 2nd file. + +Compile and apply: + +```sh +wing compile -t tf-aws main.w +cd target/main.tfaws +terraform apply +``` + +Now, you'll see that `hello.txt` was *renamed* to `hello1.txt` and `world.txt` includes the new +content. How cool is that? + +## Maintainers + +* [@eladb](https://github.com/eladb) + +## License + +This library is licensed under the [MIT License](./LICENSE). diff --git a/tfresource/examples/file.main.w b/tfresource/examples/file.main.w new file mode 100644 index 00000000..c7daa26b --- /dev/null +++ b/tfresource/examples/file.main.w @@ -0,0 +1,38 @@ +bring "../lib.w" as tfr; +bring fs; +bring util; + +struct FileState { + hash: str; + path: str; +} + +class File { + new(path: str, data: str) { + let r = new tfr.TerraformResource(); + + r.onCreate(inflight () => { + fs.writeFile(path, data); + return this.state(path, data); + }); + + r.onDelete(inflight (state) => { + let s = FileState.fromJson(state); + fs.remove(s.path); + }); + + r.onRead(inflight (state) => { + return this.state(path, data); + }); + } + + inflight state(path: str, data: str): FileState { + return { + hash: util.sha256(data), + path: path, + }; + } +} + +new File("hello1.txt", "my file") as "hello"; +new File("world.txt", "your file2") as "world"; diff --git a/tfresource/lib.test.w b/tfresource/lib.test.w new file mode 100644 index 00000000..dfba8343 --- /dev/null +++ b/tfresource/lib.test.w @@ -0,0 +1,22 @@ +bring fs; +bring expect; +bring "./lib.w" as tfr; + +struct ShellOpts { + cwd: str?; +} + +class Util { + pub static inflight extern "./util.js" shell(cmd: str, opts: ShellOpts?): str; +} + +test "weird way to test this" { + Util.shell("wing compile -t tf-aws examples/file.main.w"); + + let cwd = "examples/target/file.main.tfaws"; + Util.shell("terraform init", cwd: cwd); + Util.shell("terraform apply -auto-approve", cwd: cwd); + + expect.equal(fs.readFile("{cwd}/hello1.txt"), "my file"); + expect.equal(fs.readFile("{cwd}/world.txt"), "your file2"); +} \ No newline at end of file diff --git a/tfresource/lib.w b/tfresource/lib.w new file mode 100644 index 00000000..79f9ff9a --- /dev/null +++ b/tfresource/lib.w @@ -0,0 +1,109 @@ +bring "cdktf" as cdktf; +bring fs; +bring cloud; +bring util; + +pub class TerraformResource { + commands: MutJson; + + new() { + this.setupProvider(); + + let commands = MutJson {}; + + this.commands = commands; + + class Shell extends cdktf.TerraformResource { + new() { + super(terraformResourceType: "shell_script"); + } + + pub synthesizeAttributes(): Json { + return { + lifecycle_commands: Json.deepCopy(commands), + }; + } + } + + new Shell(); + } + + pub onCreate(handler: inflight (): Json) { + let prog = this.toExecutable("create", handler); + this.commands.set("create", "node {prog}"); + } + + pub onRead(handler: inflight (Json): Json) { + let prog = this.toExecutable("read", handler); + this.commands.set("read", "node {prog}"); + } + + pub onUpdate(handler: inflight (Json): Json) { + let prog = this.toExecutable("update", handler); + this.commands.set("update", "node {prog}"); + } + + pub onDelete(handler: inflight (Json): Json?) { + let prog = this.toExecutable("delete", handler); + this.commands.set("delete", "node {prog}"); + } + + toExecutable(name: str, handler: inflight (Json): Json?): str { + let workdir = std.Node.of(this).app.workdir; + + // TODO: this is a hack calling the internal API. I am wondering if we should expose this + // capability through some other utility API (`std.toInflightJavaScript(handler)`?). eventually + // this returns some leaky JavaScript code (for inflight closures it's a class that has a + // `handle` method), so I am not sure how nice we can make this. + let code: str = unsafeCast(handler)?._toInflight(); + + // TODO: wondering if we can generalize this... + // + // package the handle as a node.js executable. input is read from STDIN as JSON and return value + // is printed to STDOUT as JSON. + let path = fs.join(workdir, "{name}-{this.node.addr}.js"); + let wrapper = " + const fs = require('fs'); + const data = fs.readFileSync(0, 'utf-8'); + const input = data ? JSON.parse(data) : \{}; + console.log = console.error; + const invoke = async () => ({code}).handle(input); + invoke().then(output => \{ + output = output ?? \{}; + process.stdout.write(JSON.stringify(output)); + }).catch(e => \{ + console.error(e); + process.exit(1); + }); + "; + fs.writeFile(path, wrapper); + return fs.relative(fs.dirname(fs.absolute(workdir)), path); + } + + setupProvider() { + let uid = "TerraformProvider:scottwinkler-shell"; + let root = std.Node.of(this).root; + let rootNode = std.Node.of(root); + + if rootNode.tryFindChild(uid)? { + // already installed + return; + } + + class ShellProvider extends cdktf.TerraformProvider { + new() { + super( + terraformResourceType: "shell", + terraformProviderSource: "scottwinkler/shell", + terraformGeneratorMetadata: { + providerName: "shell", + providerVersion: "1.7.10", + providerVersionConstraint: "~> 1.0" + }, + ); + } + } + + new ShellProvider() as uid in root; + } +} diff --git a/tfresource/package-lock.json b/tfresource/package-lock.json new file mode 100644 index 00000000..4f7a08af --- /dev/null +++ b/tfresource/package-lock.json @@ -0,0 +1,565 @@ +{ + "name": "@winglibs/tfresource", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@winglibs/tfresource", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "cdktf": "^0.19.1" + } + }, + "node_modules/cdktf": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/cdktf/-/cdktf-0.19.1.tgz", + "integrity": "sha512-scZhp2+FEgNUd+l5vaDCHABdwFApB1Lcknn2+dUw8aYwNsMoYT0tWs4AzPg22Z4jQFOIQLIXmBxifhr+RahdRg==", + "bundleDependencies": [ + "archiver", + "json-stable-stringify", + "semver" + ], + "dependencies": { + "archiver": "5.3.2", + "json-stable-stringify": "^1.0.2", + "semver": "^7.5.4" + }, + "peerDependencies": { + "constructs": "^10.0.25" + } + }, + "node_modules/cdktf/node_modules/archiver": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cdktf/node_modules/archiver-utils": { + "version": "2.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cdktf/node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.7", + "inBundle": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/cdktf/node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/cdktf/node_modules/async": { + "version": "3.2.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/cdktf/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/cdktf/node_modules/base64-js": { + "version": "1.5.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/cdktf/node_modules/bl": { + "version": "4.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/cdktf/node_modules/brace-expansion": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cdktf/node_modules/buffer": { + "version": "5.7.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "inBundle": true, + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/cdktf/node_modules/buffer-crc32": { + "version": "0.2.13", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/cdktf/node_modules/compress-commons": { + "version": "4.1.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cdktf/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/cdktf/node_modules/core-util-is": { + "version": "1.0.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/cdktf/node_modules/crc-32": { + "version": "1.2.2", + "inBundle": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cdktf/node_modules/crc32-stream": { + "version": "4.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cdktf/node_modules/end-of-stream": { + "version": "1.4.4", + "inBundle": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/cdktf/node_modules/fs-constants": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/cdktf/node_modules/fs.realpath": { + "version": "1.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/cdktf/node_modules/glob": { + "version": "7.2.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cdktf/node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cdktf/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/cdktf/node_modules/graceful-fs": { + "version": "4.2.10", + "inBundle": true, + "license": "ISC" + }, + "node_modules/cdktf/node_modules/ieee754": { + "version": "1.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/cdktf/node_modules/inflight": { + "version": "1.0.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/cdktf/node_modules/inherits": { + "version": "2.0.4", + "inBundle": true, + "license": "ISC" + }, + "node_modules/cdktf/node_modules/isarray": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/cdktf/node_modules/json-stable-stringify": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "jsonify": "^0.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cdktf/node_modules/jsonify": { + "version": "0.0.1", + "inBundle": true, + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cdktf/node_modules/lazystream": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/cdktf/node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.7", + "inBundle": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/cdktf/node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/cdktf/node_modules/lodash.defaults": { + "version": "4.2.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/cdktf/node_modules/lodash.difference": { + "version": "4.5.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/cdktf/node_modules/lodash.flatten": { + "version": "4.4.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/cdktf/node_modules/lodash.isplainobject": { + "version": "4.0.6", + "inBundle": true, + "license": "MIT" + }, + "node_modules/cdktf/node_modules/lodash.union": { + "version": "4.6.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/cdktf/node_modules/lru-cache": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cdktf/node_modules/minimatch": { + "version": "5.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cdktf/node_modules/normalize-path": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cdktf/node_modules/once": { + "version": "1.4.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/cdktf/node_modules/path-is-absolute": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cdktf/node_modules/process-nextick-args": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/cdktf/node_modules/readable-stream": { + "version": "3.6.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cdktf/node_modules/readdir-glob": { + "version": "1.1.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/cdktf/node_modules/safe-buffer": { + "version": "5.1.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/cdktf/node_modules/semver": { + "version": "7.5.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cdktf/node_modules/string_decoder": { + "version": "1.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/cdktf/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/cdktf/node_modules/tar-stream": { + "version": "2.2.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/cdktf/node_modules/util-deprecate": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/cdktf/node_modules/wrappy": { + "version": "1.0.2", + "inBundle": true, + "license": "ISC" + }, + "node_modules/cdktf/node_modules/yallist": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/cdktf/node_modules/zip-stream": { + "version": "4.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "compress-commons": "^4.1.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/constructs": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.3.0.tgz", + "integrity": "sha512-vbK8i3rIb/xwZxSpTjz3SagHn1qq9BChLEfy5Hf6fB3/2eFbrwt2n9kHwQcS0CPTRBesreeAcsJfMq2229FnbQ==", + "peer": true, + "engines": { + "node": ">= 16.14.0" + } + } + } +} diff --git a/tfresource/package.json b/tfresource/package.json new file mode 100644 index 00000000..651b4d07 --- /dev/null +++ b/tfresource/package.json @@ -0,0 +1,18 @@ +{ + "name": "@winglibs/tfresource", + "description": "Wing library for building custom Terraform resources", + "author": { + "name": "Elad Ben-Israel", + "email": "eladb@wing.cloud" + }, + "version": "0.0.1", + "repository": { + "type": "git", + "url": "https://github.com/winglang/winglibs.git", + "directory": "tfresource" + }, + "license": "MIT", + "dependencies": { + "cdktf": "^0.19.1" + } +} \ No newline at end of file diff --git a/tfresource/util.js b/tfresource/util.js new file mode 100644 index 00000000..6c78b221 --- /dev/null +++ b/tfresource/util.js @@ -0,0 +1,6 @@ +const { execSync } = require("child_process"); + +exports.shell = (cmd, opts) => { + const result = execSync(cmd, { shell: true, stdio: ["pipe", "pipe", "inherit"], ...opts }); + return result.toString("utf8"); +} \ No newline at end of file