diff --git a/.github/workflows/draft-release.yaml b/.github/workflows/draft-release.yaml deleted file mode 100644 index 4780d357..00000000 --- a/.github/workflows/draft-release.yaml +++ /dev/null @@ -1,96 +0,0 @@ -name: Bump Package Versions, Tag and Draft Release - -on: - pull_request: - types: [closed] - branches: - - 'main' - -jobs: - version: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - token: ${{ secrets.STANDARD_VERSION_BUMP_PAT }} - - name: Cache Yarn Cache - uses: actions/cache@v2 - with: - path: '.yarn/cache' - key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} - - name: Use Node 14 - uses: actions/setup-node@v1 - with: - node-version: 14 - - name: Git Identity - run: | - git config --global user.name 'github-actions[bot]' - git config --global user.email 'github-actions[bot]@users.noreply.github.com' - - - name: Install Dependencies - run: yarn --immutable - - - name: Bump and Tag - run: | - yarn bump - echo "::set-output name=TAG_REF::`git describe --abbrev=0`" - id: bump_tag - - - name: Push version bump & tags - run: | - git fetch - git checkout main - git push origin main --follow-tags - - - name: Create Draft Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ steps.bump_tag.outputs.TAG_REF }} - release_name: Release ${{ steps.bump_tag.outputs.TAG_REF }} - body: See CHANGELOG.md for changes - draft: true - prerelease: false - - - name: Build Binaries - id: build_binaries - run: | - yarn build:common - yarn workspace @nitric/cli package:all - - - name: Upload Linux Binary - id: upload-linux-binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./packages/base/dist/cli-linux - asset_name: nitric-linux-x64 - asset_content_type: application/octet-stream - - - name: Upload MacOS Binary - id: upload-macos-binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./packages/base/dist/cli-macos - asset_name: nitric-macos-x64 - asset_content_type: application/octet-stream - - - name: Upload Windows Binary - id: upload-windows-binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./packages/base/dist/cli-win.exe - asset_name: nitric-windows-x64 - asset_content_type: application/octet-stream \ No newline at end of file diff --git a/.github/workflows/publish-on-release.yaml b/.github/workflows/publish-on-release.yaml index 1bc6781c..1f9bcd39 100644 --- a/.github/workflows/publish-on-release.yaml +++ b/.github/workflows/publish-on-release.yaml @@ -25,28 +25,32 @@ jobs: registry-url: 'https://registry.npmjs.org' always-auth: true + - name: Normalize version string + run: | + version="${{ github.event.release.tag_name }}" + echo "::set-output name=VERSION::`echo ${version:1}`" + id: normalize_version + - name: Install Dependencies run: yarn --immutable + # Update version metadata to match tag version + - name: Update Versions + run: yarn version:all ${{ steps.normalize_version.outputs.VERSION }} + - name: Build Common run: yarn build:common - - name: Publish to NPM + # Release Production + - name: Publish latest to NPM + if: "!github.event.release.prerelease" run: yarn publish:all env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Normalize version string - run: | - version="${{ github.event.release.tag_name }}" - echo "::set-output name=VERSION::`echo ${version:1}`" - id: normalize_version - - # Signal the releases repostory to prepare a release - - name: Chocolatey Release - uses: peter-evans/repository-dispatch@v1 - with: - token: ${{ secrets.RELEASE_TOKEN }} - repository: nitrictech/cli-chocolatey - event-type: choco-release - client-payload: '{"version": "${{ steps.normalize_version.outputs.VERSION }}"}' \ No newline at end of file + # release RC + - name: Publish latest RC to NPM + if: "github.event.release.prerelease" + run: yarn publish:all --tag rc-latest + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/rc-release.yaml b/.github/workflows/rc-release.yaml new file mode 100644 index 00000000..bd868135 --- /dev/null +++ b/.github/workflows/rc-release.yaml @@ -0,0 +1,38 @@ +name: Bump Package Versions, Tag and Draft Release + +on: + pull_request: + types: [closed] + branches: + - 'develop' + +jobs: + version: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Bump version and push tag + id: tag_version + uses: mathieudutour/github-tag-action@v5.6 + with: + dry_run: true + release_branches: main,develop + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Add SHORT_SHA to outputs + id: vars + run: echo "::set-output name=sha_short::$(echo ${GITHUB_SHA} | cut -c1-8)" + + - name: Create a GitHub release + uses: actions/create-release@v1 + env: + # Use nitric bot token to trigger release actions + GITHUB_TOKEN: ${{ secrets.NITRIC_BOT_TOKEN }} + with: + prerelease: true + tag_name: ${{ steps.tag_version.outputs.new_tag }}-rc.${{ steps.vars.outputs.sha_short }} + release_name: Release ${{ steps.tag_version.outputs.new_tag }}-rc.${{ steps.vars.outputs.sha_short }} + body: ${{ steps.tag_version.outputs.changelog }} + \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 00000000..da755103 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,32 @@ +name: Bump Package Versions, Tag and Draft Release + +on: + pull_request: + types: [closed] + branches: + - 'main' + +jobs: + version: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Bump version and push tag + id: tag_version + uses: mathieudutour/github-tag-action@v5.6 + with: + release_branches: main + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Create a GitHub release + uses: actions/create-release@v1 + env: + # use nitric bot token to trigger release actions + GITHUB_TOKEN: ${{ secrets.NITRIC_BOT_TOKEN }} + with: + tag_name: ${{ steps.tag_version.outputs.new_tag }} + release_name: Release ${{ steps.tag_version.outputs.new_tag }} + body: ${{ steps.tag_version.outputs.changelog }} + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4312d9fa..94dfc0fa 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,6 @@ lib/ !**/src/**/* oclif.manifest.json + +# Autogenerated change log +CHANGELOG.md \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e1b9021c..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,629 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. - -### [0.0.86](https://github.com/nitrictech/cli/compare/v0.0.85...v0.0.86) (2021-09-09) - - -### Features - -* Add .packrc support for env variable and service context. ([0e79b57](https://github.com/nitrictech/cli/commit/0e79b570851d3c91bebe7629e6731d5d1d0a24d2)) -* buildpacks implementation. ([0da1cc2](https://github.com/nitrictech/cli/commit/0da1cc2d5a5feae37b469704620996ffe7766fd0)) -* make:stack to create new Nitric Stacks from templates ([fbdd02a](https://github.com/nitrictech/cli/commit/fbdd02a738ac92053b2f78501ea32ca3548a5006)) -* Support APIs as separate files. ([4ef10da](https://github.com/nitrictech/cli/commit/4ef10da4a752b72f7663c081de07e070f3c391a7)) -* support functions and custom containers ([745e852](https://github.com/nitrictech/cli/commit/745e852f7ff519f4acd4b5c2aa075a72ecc51c4c)) - - -### Bug Fixes - -* Add env for nitric run NITRIC_DEV_VOLUME. ([889888f](https://github.com/nitrictech/cli/commit/889888fdc2cca4244644a3a1b1ec677ca9bfe98f)) -* cleanup pack containers. ([427fc06](https://github.com/nitrictech/cli/commit/427fc06099acf457ab6c6f4f7ea024b23040a435)) -* setup nitric run volume in .nitric/run relative to cwd. ([eebf817](https://github.com/nitrictech/cli/commit/eebf817a429ce328c1217af9bfdc3632eb528a8f)) -* update stack name for template stacks ([0006ab9](https://github.com/nitrictech/cli/commit/0006ab9f375caff8fa95a6e4b2687702164c8d7e)) - -### [0.0.85](https://github.com/nitrictech/cli/compare/v0.0.84...v0.0.85) (2021-09-03) - -### [0.0.84](https://github.com/nitrictech/cli/compare/v0.0.82...v0.0.84) (2021-08-26) - - -### Features - -* **plugins/gcp:** Add initial service enablement to gcp deployments. ([6bfeaf4](https://github.com/nitrictech/cli/commit/6bfeaf4090e1bb7d682eb0a505f4a2227dc37cec)) -* **plugins/gcp:** Add new component resources for configuring project permissions. ([fa244a9](https://github.com/nitrictech/cli/commit/fa244a96dc579541eb63a68e8ea99b546df82959)) - - -### Bug Fixes - -* **plugins/gcp:** Add authentication config for APIs to invoke services. ([a85f53f](https://github.com/nitrictech/cli/commit/a85f53f2a77d4f13c80214a7a3d1c67771275e30)) -* **plugins/gcp:** Add missing permission for pubsub push subscriptions. ([aa56042](https://github.com/nitrictech/cli/commit/aa56042cc238dc97e46ddda220887de1b20c8ff0)) -* preserve stack file comments ([cbefc2d](https://github.com/nitrictech/cli/commit/cbefc2d8559537ecb7962938ba5cab933fa3e8cd)) -* print the friendly error message to user ([18a4bb7](https://github.com/nitrictech/cli/commit/18a4bb79d2d43fa74a0a0d62db99842643460362)) - -### [0.0.83](https://github.com/nitrictech/cli/compare/v0.0.82...v0.0.83) (2021-08-26) - - -### Bug Fixes - -* preserve stack file comments ([cbefc2d](https://github.com/nitrictech/cli/commit/cbefc2d8559537ecb7962938ba5cab933fa3e8cd)) -* print the friendly error message to user ([18a4bb7](https://github.com/nitrictech/cli/commit/18a4bb79d2d43fa74a0a0d62db99842643460362)) - -### [0.0.82](https://github.com/nitrictech/cli/compare/v0.0.81...v0.0.82) (2021-08-11) - -### Bug Fixes - -- modify logging names to be agnostic. ([6789577](https://github.com/nitrictech/cli/commit/67895777ac860042c6bdc8902dab8f2e3fe8824b)) - -### [0.0.81](https://github.com/nitrictech/cli/compare/v0.0.80...v0.0.81) (2021-08-10) - -### Bug Fixes - -- correct example references ([9012be9](https://github.com/nitrictech/cli/commit/9012be9ddc0cc7075deb0a88d30ac95b081c1be7)) - -### [0.0.80](https://github.com/nitrictech/cli/compare/v0.0.79...v0.0.80) (2021-08-06) - -### Features - -- add nitric.yaml file validation ([9eaf54d](https://github.com/nitrictech/cli/commit/9eaf54dfce1aa6e00a0917f44b7234b81832d34e)) - -### [0.0.79](https://github.com/nitrictech/cli/compare/v0.0.78...v0.0.79) (2021-08-04) - -### Features - -- support collection deployment in AWS ([c5b39d7](https://github.com/nitrictech/cli/commit/c5b39d7edc77f7ae3f0a83790ad8787c5d993e50)) - -### [0.0.78](https://github.com/nitrictech/cli/compare/v0.0.77...v0.0.78) (2021-08-04) - -### Bug Fixes - -- Ensure resource names are unique. ([0f9637b](https://github.com/nitrictech/cli/commit/0f9637b2c3906d89fbf385620cc5ed9f728f29f7)) - -### [0.0.77](https://github.com/nitrictech/cli/compare/v0.0.76...v0.0.77) (2021-08-03) - -### Bug Fixes - -- **cli-common:** Fix template runtime extraction exclusion path. ([c4d0acd](https://github.com/nitrictech/cli/commit/c4d0acd8d74f9987a959f7742428bfb149c649d6)) - -### [0.0.76](https://github.com/nitrictech/cli/compare/v0.0.75...v0.0.76) (2021-08-03) - -### Bug Fixes - -- **GCP:** preserve paths from API gateway to services ([c3259f8](https://github.com/nitrictech/cli/commit/c3259f8d10ac528f5415405158f4e0f635faaac0)) - -### [0.0.75](https://github.com/nitrictech/cli/compare/v0.0.74...v0.0.75) (2021-07-29) - -### Bug Fixes - -- ensure make:service creates service in correct directory relative to config ([7cb585a](https://github.com/nitrictech/cli/commit/7cb585a8ac8f6af3da9f5c8c0dabc8b6f6fc7807)) - -### [0.0.74](https://github.com/nitrictech/cli/compare/v0.0.73...v0.0.74) (2021-07-26) - -### Bug Fixes - -- **plugins/do:** Fix stack names for digital ocean plugins. ([3ceeb1e](https://github.com/nitrictech/cli/commit/3ceeb1e6ff30a52fd451f32369c8afe1b2b03588)) - -### [0.0.73](https://github.com/nitrictech/cli/compare/v0.0.72...v0.0.73) (2021-07-01) - -### Bug Fixes - -- add execStream wrapper function to fix windows site run issue ([844b41d](https://github.com/nitrictech/cli/commit/844b41d60a257f4a6a60a30115ecd90ffb376277)) -- use path.join for staging directory ([0b0ad95](https://github.com/nitrictech/cli/commit/0b0ad9503e754761166389d96fb5d2b35086805e)) - -### [0.0.72](https://github.com/nitrictech/cli/compare/v0.0.71...v0.0.72) (2021-06-30) - -### Bug Fixes - -- use node readline instead of keypress and fix windows exit issues ([16ff5e2](https://github.com/nitrictech/cli/commit/16ff5e2aaa6df39c552914dae5132e85b3a00712)) - -### [0.0.71](https://github.com/nitrictech/cli/compare/v0.0.70...v0.0.71) (2021-06-28) - -### Bug Fixes - -- **plugins/aws:** Add missing s3 permission. ([f02d812](https://github.com/nitrictech/cli/commit/f02d812fb6e3b2617516ff539ef6034314419284)) - -### [0.0.70](https://github.com/nitrictech/cli/compare/v0.0.69...v0.0.70) (2021-06-27) - -### Bug Fixes - -- **plugins/aws:** Add s3 permissions to nitric services ([4e0f1ec](https://github.com/nitrictech/cli/commit/4e0f1ec9ac277240f4c4a9ddcdd412dad6371df6)) - -### [0.0.69](https://github.com/nitrictech/cli/compare/v0.0.68...v0.0.69) (2021-06-24) - -### Bug Fixes - -- Disable bytecode generation. ([9a5fe91](https://github.com/nitrictech/cli/commit/9a5fe912ba37e81067586781604e927341ec354f)) -- Include @oclif/plugin-plugins. ([208516d](https://github.com/nitrictech/cli/commit/208516d4a33c9ecb8a2a238a30a44cee83b2b114)) - -### [0.0.68](https://github.com/nitrictech/cli/compare/v0.0.67...v0.0.68) (2021-06-23) - -### Bug Fixes - -- Fix issue where errors weren't converted to strings before logging ([5aedf27](https://github.com/nitrictech/cli/commit/5aedf2735e468b2617430ecb3b43459cf2c09da0)) - -### [0.0.67](https://github.com/nitrictech/cli/compare/v0.0.66...v0.0.67) (2021-06-23) - -### Bug Fixes - -- Handle promise rejections from pulumi tasks. ([c15696b](https://github.com/nitrictech/cli/commit/c15696b8dabe46785ab50f6be6de0c2d8b685d48)) - -### [0.0.66](https://github.com/nitrictech/cli/compare/v0.0.65...v0.0.66) (2021-06-23) - -### Bug Fixes - -- **run:** Fix failure to cleanup running docker resources ([95b36f8](https://github.com/nitrictech/cli/commit/95b36f8c011f2fb0c7e9f56cf4e592973efd5b1e)) -- **run:** improve docker error status code logging ([8008567](https://github.com/nitrictech/cli/commit/80085673b0f7fb3be9dc47d84ca3429c1608d4ae)) - -### [0.0.65](https://github.com/nitrictech/cli/compare/v0.0.64...v0.0.65) (2021-06-21) - -### Bug Fixes - -- don't collapse errors on run ([3401796](https://github.com/nitrictech/cli/commit/34017968961db704e8730333982cddd0d919827c)) -- runIds for gateways and api names ([259af75](https://github.com/nitrictech/cli/commit/259af75dbeeb1b2201ce56c8e1813342375ca068)) - -### [0.0.64](https://github.com/nitrictech/cli/compare/v0.0.63...v0.0.64) (2021-06-20) - -### Features - -- add unique run id to local run resources ([cc553f9](https://github.com/nitrictech/cli/commit/cc553f9d07eb99afad59aa8fab7c718fd29e0d65)) - -### [0.0.63](https://github.com/nitrictech/cli/compare/v0.0.62...v0.0.63) (2021-06-15) - -### Bug Fixes - -- add exiting please wait message ([30a5c0d](https://github.com/nitrictech/cli/commit/30a5c0dafb1e5b752f414c010915725e682bb95b)) - -### [0.0.62](https://github.com/nitrictech/cli/compare/v0.0.61...v0.0.62) (2021-06-11) - -### [0.0.61](https://github.com/nitrictech/cli/compare/v0.0.60...v0.0.61) (2021-06-11) - -### [0.0.60](https://github.com/nitrictech/cli/compare/v0.0.59...v0.0.60) (2021-06-11) - -### [0.0.59](https://github.com/nitrictech/cli/compare/v0.0.58...v0.0.59) (2021-06-11) - -### [0.0.58](https://github.com/nitrictech/cli/compare/v0.0.57...v0.0.58) (2021-06-10) - -### Bug Fixes - -- check that docker daemon is running for run and build commands ([4413c06](https://github.com/nitrictech/cli/commit/4413c060a3c844e1d3229420e7e7cf04d244532a)) - -### [0.0.57](https://github.com/nitrictech/cli/compare/v0.0.56...v0.0.57) (2021-06-10) - -### [0.0.56](https://github.com/nitrictech/cli/compare/v0.0.55...v0.0.56) (2021-06-10) - -### Bug Fixes - -- check for git when installing template store ([f40610f](https://github.com/nitrictech/cli/commit/f40610f19b28d18f53ef7e4edab2cd659201a0a2)) -- use correct slashes for windows powershell pulumi install ([ed30e33](https://github.com/nitrictech/cli/commit/ed30e338c37bcb1d5a8f53711c6b8ea2a571b4c6)) - -### [0.0.55](https://github.com/nitrictech/cli/compare/v0.0.54...v0.0.55) (2021-06-10) - -### Bug Fixes - -- improve error handling when Git is missing ([75502d9](https://github.com/nitrictech/cli/commit/75502d90ae258c0602843ce1c714e1c36a557bb0)) -- remove literal path separator from repo file path ([fe77865](https://github.com/nitrictech/cli/commit/fe77865f7a255ddde1fecb854e6ccb393aca928b)) - -### [0.0.54](https://github.com/nitrictech/cli/compare/v0.0.53...v0.0.54) (2021-06-10) - -### Bug Fixes - -- add repo name to add repo task output ([7c76e2e](https://github.com/nitrictech/cli/commit/7c76e2e3856823c478b107fbd7369630f749792d)) -- **make:project:** install the correct repos when they're missing ([7582808](https://github.com/nitrictech/cli/commit/75828088b0fd9008a70fb97ba768780bdac7f6a8)) - -### [0.0.53](https://github.com/nitrictech/cli/compare/v0.0.52...v0.0.53) (2021-06-09) - -### Bug Fixes - -- Scope stack deployments with stackname & provider. ([0889758](https://github.com/nitrictech/cli/commit/08897580607d1eb6cb9c2155396a934a40be145d)) - -### [0.0.52](https://github.com/nitrictech/cli/compare/v0.0.51...v0.0.52) (2021-06-08) - -### Features - -- Enable buildkit. ([a5b6e40](https://github.com/nitrictech/cli/commit/a5b6e40a56725272de3c8d9b009aadd96736fc6a)) -- **common:** Add imageDigest as output for NitricServiceImage. ([7173e51](https://github.com/nitrictech/cli/commit/7173e519ff8b524e0d122e8a824c71f3dfe4067c)) -- **plugin-do:** Add image digest as tag for service spec. ([55e6fad](https://github.com/nitrictech/cli/commit/55e6fade229939d247fc873fcbd0c2c2ac2ed9af)) - -### [0.0.51](https://github.com/nitrictech/cli/compare/v0.0.50...v0.0.51) (2021-06-08) - -### Bug Fixes - -- **common:** Remove superfluous makeDirectory in stack.pullTemplate ([80e8ab1](https://github.com/nitrictech/cli/commit/80e8ab16aa5871772c57228da8a3f1e24046311d)) - -### [0.0.50](https://github.com/nitrictech/cli/compare/v0.0.49...v0.0.50) (2021-06-07) - -### Features - -- Add certificate creation and validation for custom domains. ([c501dbe](https://github.com/nitrictech/cli/commit/c501dbe38be45dce5476841a0a4690dd74f1bc12)) -- add custom domains support for DO entrypoints ([fde501c](https://github.com/nitrictech/cli/commit/fde501cca7edbb42e930db59812ecf7ea17f16dd)) -- Add extensions for all stack items. ([87e4995](https://github.com/nitrictech/cli/commit/87e4995c295777758e5119e591dafd12f0f84fbb)) -- Add generic extensions to resources classes. ([8950c13](https://github.com/nitrictech/cli/commit/8950c1376501f833cd98cbc34ef959fa0b86aebb)) -- Add output results and cleanup to digital ocean deploy. ([bc7c123](https://github.com/nitrictech/cli/commit/bc7c123443014086c6d10b8e0e8024a0bcc4a54f)) -- Add outputs and instructions for AWS dns configurations. ([e881ef2](https://github.com/nitrictech/cli/commit/e881ef2a947183caec2969de651b50f4d3a4b5d4)) -- Make domains and paths extensible. ([65538c0](https://github.com/nitrictech/cli/commit/65538c02e0f7595b7e35089ab42ebe41cfc2dc9b)) -- support multiple entrypoints for local run ([00ca5e4](https://github.com/nitrictech/cli/commit/00ca5e45bda76e616713a21a6338c5929575f3b4)) -- support multiple entrypoints in gcp ([e0d5a9f](https://github.com/nitrictech/cli/commit/e0d5a9fbcb1873d6001f0d20f27ab0dbb51475cf)) -- Update AWS and GCP entrypoint outputs. ([b976ab2](https://github.com/nitrictech/cli/commit/b976ab24b8d19f221b9eff8b9ec49cc1f6ff187a)) -- Update aws entrypoints definitions. ([1169c9c](https://github.com/nitrictech/cli/commit/1169c9c95a145becbfc54056d28e309bd8627a0c)) -- Update types to simplify domains. ([e57e4ae](https://github.com/nitrictech/cli/commit/e57e4aeee1378603657c98db9817c4579be79077)) - -### Bug Fixes - -- add mistakenly removed imports ([ae6a3f8](https://github.com/nitrictech/cli/commit/ae6a3f8b25952105ac31bdcbcf515ba086526bec)) -- **gcp:** Fix incorrect backend name. ([ab5ebe3](https://github.com/nitrictech/cli/commit/ab5ebe30e7febb08186c8e7f1d7d494721a55317)) -- **gcp:** fix incorrect resource names. ([92999da](https://github.com/nitrictech/cli/commit/92999da09b8d44ce1d2bda82f8bfef74d9df9cb9)) -- **gcp:** Fix non-default entrypoint normalization. ([b31594e](https://github.com/nitrictech/cli/commit/b31594eab8ddb1659c10dcf56fbdb2576ec4aa77)) -- **gcp:** fix target filtering. ([e73bb50](https://github.com/nitrictech/cli/commit/e73bb50031abcceea90cf4f275fc993cba973dd0)) -- **gcp:** Fix type error. ([64efe3e](https://github.com/nitrictech/cli/commit/64efe3eb5fcc380498a70d3cf79dc4d715dee21c)) -- add entrypoint suffix to do app name ([b4a1fc7](https://github.com/nitrictech/cli/commit/b4a1fc72be26b3409ce2bb5956ac4b3e000a33f8)) -- Await the deployment. ([291bb47](https://github.com/nitrictech/cli/commit/291bb47eff0753c04d401c0eb2b479f07a1fc7f6)) -- AWS cloudfront deploys to use pre-provisioned certificates. ([e6ab460](https://github.com/nitrictech/cli/commit/e6ab460d38634b6235fbb85a71095714d66c24eb)) -- don't use BuildKit for image builds ([f7a4c23](https://github.com/nitrictech/cli/commit/f7a4c2379df6084d8eb0e3d4cf48f99e579957f5)) - -### [0.0.49](https://github.com/nitrictech/cli/compare/v0.0.48...v0.0.49) (2021-06-06) - -### Features - -- Remove references to stage stack task. ([8ba641c](https://github.com/nitrictech/cli/commit/8ba641c66b130333bde19f4b7c42170c70abde85)) -- Stage runtime as workaround for lack of external dockerfile support for dockerode. ([1c63ea7](https://github.com/nitrictech/cli/commit/1c63ea740e6d821c741ba7168ab6d83242d8cff6)) -- WIP templates pulling into nitric project stacks. ([26f0c20](https://github.com/nitrictech/cli/commit/26f0c2033563baf3a3d3e222831a7ac58950d5a9)) - -### Bug Fixes - -- Add ignore for dockerode build. ([4ec2fc3](https://github.com/nitrictech/cli/commit/4ec2fc3622f6eb636211ce88d9b8f6e48a84af10)) - -### [0.0.48](https://github.com/nitrictech/cli/compare/v0.0.47...v0.0.48) (2021-06-03) - -### [0.0.47](https://github.com/nitrictech/cli/compare/v0.0.46...v0.0.47) (2021-06-02) - -### Bug Fixes - -- Remove autopull of official nitric template repository. ([96faf3f](https://github.com/nitrictech/cli/commit/96faf3f09958a020f14aa911217f6d500d416c7d)) - -### [0.0.46](https://github.com/nitrictech/cli/compare/v0.0.45...v0.0.46) (2021-05-28) - -### [0.0.45](https://github.com/nitrictech/cli/compare/v0.0.44...v0.0.45) (2021-05-25) - -### Bug Fixes - -- allow unhandled exceptions to bubble up and print ([f07f24f](https://github.com/nitrictech/cli/commit/f07f24f97f7c33b16193c6bc6ccfea7c6b966516)) - -### [0.0.44](https://github.com/nitrictech/cli/compare/v0.0.43...v0.0.44) (2021-05-25) - -### Features - -- Add additional output results for gcp deployment. ([d7931e2](https://github.com/nitrictech/cli/commit/d7931e23caef19b664367211f346634c735dae0c)) - -### [0.0.43](https://github.com/nitrictech/cli/compare/v0.0.42...v0.0.43) (2021-05-24) - -### Bug Fixes - -- Add missing bucket deployments. ([5971b07](https://github.com/nitrictech/cli/commit/5971b0722c03f0943283c1541f51a8dab3efde38)) - -### [0.0.42](https://github.com/nitrictech/cli/compare/v0.0.41...v0.0.42) (2021-05-20) - -### Bug Fixes - -- Remove default root object. ([9dbf818](https://github.com/nitrictech/cli/commit/9dbf81883ef67cbe5fad3138c770a53b1f87008f)) -- Working deployment. ([ede610e](https://github.com/nitrictech/cli/commit/ede610e2dd299984510e9878f249685b5f875fc4)) - -### [0.0.41](https://github.com/nitrictech/cli/compare/v0.0.40...v0.0.41) (2021-05-17) - -### Bug Fixes - -- **nit-368:** remove spaces before message ([db3aa50](https://github.com/nitrictech/cli/commit/db3aa500d0c0a4f979d33a2643ac9f19335c1923)) - -### [0.0.40](https://github.com/nitrictech/cli/compare/v0.0.39...v0.0.40) (2021-05-13) - -### Bug Fixes - -- Ensure NITRIC_HOME directory created before preferences is written. ([6b78bb3](https://github.com/nitrictech/cli/commit/6b78bb37ea462923aacbb2e7f284e3374d38e4cb)) -- resolve unknown arguments error for base command parsing. ([6be0182](https://github.com/nitrictech/cli/commit/6be0182f6027ba67ba4029092e03be524d3ca627)) - -### [0.0.39](https://github.com/nitrictech/cli/compare/v0.0.38...v0.0.39) (2021-05-10) - -### Bug Fixes - -- catch more errors in run command, cleanup remaining commands ([a7c3eb5](https://github.com/nitrictech/cli/commit/a7c3eb560081319aa2b614df1141943985c84c3b)) - -### [0.0.38](https://github.com/nitrictech/cli/compare/v0.0.37...v0.0.38) (2021-05-10) - -### Features - -- Add ci mode to the CLI to enable a non-interactive DX. ([0fda8d6](https://github.com/nitrictech/cli/commit/0fda8d6d045eef787fb0bcd0e3d1452c95559da6)) -- Finalize first cut of GA CLI integration. ([bd3cae5](https://github.com/nitrictech/cli/commit/bd3cae5b9bb6adf8ac058827af0f62ed42a85dc8)) -- WIP Analytics integration for oclif commands. ([9c83777](https://github.com/nitrictech/cli/commit/9c8377759e904846e7ca7721f351fa9c991fbea6)) -- WIP start for google analytics CLI integration. ([937d753](https://github.com/nitrictech/cli/commit/937d75359a8d5699af1039e118ab68309ba8855d)) -- Wrap commands in BaseCommand. ([2483022](https://github.com/nitrictech/cli/commit/2483022f3dafa07dc3c7139e3cd82faec255d478)) - -### Bug Fixes - -- Add missing peer dependency for cli-common. ([8cf883c](https://github.com/nitrictech/cli/commit/8cf883c7eb7f939270c8e44c3d3ad3b0c072e240)) -- Common typings. ([da7ae00](https://github.com/nitrictech/cli/commit/da7ae002ae0c5a2ecce6234682d50860410008c3)) -- Fix static method references in Preferences. ([ca03c2f](https://github.com/nitrictech/cli/commit/ca03c2f09c726405a36de935296a0f2198bf3545)) -- **azure:** use new ci flag to mark non-interactive. ([d435dd4](https://github.com/nitrictech/cli/commit/d435dd41f522f577a9aff634a46dab5f98b74acf)) -- replace remaining 'run' functions with 'do' ([f4ab4f9](https://github.com/nitrictech/cli/commit/f4ab4f90d237e3f6bfc869f690230e612dcf7fba)) - -### [0.0.37](https://github.com/nitrictech/cli/compare/v0.0.36...v0.0.37) (2021-05-08) - -### Bug Fixes - -- fix empty arrays default to null in stack definition ([4de0dba](https://github.com/nitrictech/cli/commit/4de0dbaec3ef67dbe5b245fd766557fed215c7a7)) - -### [0.0.36](https://github.com/nitrictech/cli/compare/v0.0.35...v0.0.36) (2021-05-07) - -### Features - -- Update repostory download. ([22bda35](https://github.com/nitrictech/cli/commit/22bda35f47d020c0581c982e8917f78e67ffacda)) -- WIP stack definition refactor. ([f68c84b](https://github.com/nitrictech/cli/commit/f68c84b84f968ecfaa0fed3689a515a48bd6b3a2)) - -### Bug Fixes - -- Ensure DO App creation waits on image push. ([4dd69a3](https://github.com/nitrictech/cli/commit/4dd69a36f537ae3ce2984d9ca037cf29f5df573e)) -- Fix syntax error in method call. ([8414d4c](https://github.com/nitrictech/cli/commit/8414d4c46af55b48825875518623f2e8a24cc537)) -- Missing NamedObject API type. ([98e2399](https://github.com/nitrictech/cli/commit/98e23991518f614b931dd0574eaa68e3ee97665e)) -- Type error in azure plugin. ([bc0f394](https://github.com/nitrictech/cli/commit/bc0f3947e211aebd823399a37cbfa42a44ca663f)) - -### [0.0.35](https://github.com/nitrictech/cli/compare/v0.0.34...v0.0.35) (2021-05-04) - -### [0.0.34](https://github.com/nitrictech/cli/compare/v0.0.33...v0.0.34) (2021-04-30) - -### Features - -- Upgrade AWS plugin dependencies. ([3b02f89](https://github.com/nitrictech/cli/commit/3b02f89aaf1ded81756b37105a9684add9bcec18)) -- Upgrade libraries to pulumi 3. ([11b4a9b](https://github.com/nitrictech/cli/commit/11b4a9bd1edf1876ecdd32cb6183ba036a1f5b1d)) - -### Bug Fixes - -- Fix deployable region subset for gcp plugin. ([e343966](https://github.com/nitrictech/cli/commit/e3439664306721f22137f91f2da61defcbcebbb4)) -- setup all topics on 'run', even without subscribers ([66646ee](https://github.com/nitrictech/cli/commit/66646eec69a5e09b30d346e0cccd1461fccd1cc9)) -- update pulumi api references in azure plugin ([6a98939](https://github.com/nitrictech/cli/commit/6a98939238c669f1172ebd677a6efe462af1c854)) - -### [0.0.33](https://github.com/nitrictech/cli/compare/v0.0.32...v0.0.33) (2021-04-27) - -### [0.0.32](https://github.com/nitrictech/cli/compare/v0.0.31...v0.0.32) (2021-04-27) - -### Features - -- Increase docker build shm sizes. ([11b65b5](https://github.com/nitrictech/cli/commit/11b65b5e3c5b959dcb6c426f804465486f325128)) -- Initial Digital Ocean plugin. ([78cc98d](https://github.com/nitrictech/cli/commit/78cc98d3815a8bf9c64a0403408ea3e592ea32bf)) - -### [0.0.31](https://github.com/nitrictech/cli/compare/v0.0.30...v0.0.31) (2021-04-27) - -### Bug Fixes - -- add repository and store check to doctor ([d5cd4c3](https://github.com/nitrictech/cli/commit/d5cd4c3ffb72dc029cc34a3b505174cf9633ec3d)) -- **nit-298:** ensure the checkout directory exists before checkout ([8d398b1](https://github.com/nitrictech/cli/commit/8d398b1174b24506ffb761d0f81acddd2d707af9)) - -### [0.0.30](https://github.com/nitrictech/cli/compare/v0.0.29...v0.0.30) (2021-04-20) - -### [0.0.29](https://github.com/nitrictech/cli/compare/v0.0.28...v0.0.29) (2021-04-20) - -### [0.0.28](https://github.com/nitrictech/cli/compare/v0.0.27...v0.0.28) (2021-04-20) - -### Bug Fixes - -- use file path from stack file found for working directory ([1c123eb](https://github.com/nitrictech/cli/commit/1c123eb8d2ab4beb7574dbc17163e8d6c254d769)) - -### [0.0.27](https://github.com/nitrictech/cli/compare/v0.0.26...v0.0.27) (2021-04-20) - -### Features - -- Add function as an entrypoint type. ([e22a1be](https://github.com/nitrictech/cli/commit/e22a1bed11e2b1857456e14071716d6738871bd8)) -- Connect functions to entrypoint proxy for nitric run. ([536e7a6](https://github.com/nitrictech/cli/commit/536e7a635fa07a78c77c6be5248ea01be246cb47)) -- Connect gcp deployed functions to entrypoints. ([f9b215a](https://github.com/nitrictech/cli/commit/f9b215ad7e5c637ac7aa2768364127b23d691f1f)) -- Map AWS deployed functions to entrypoints. ([b84fa38](https://github.com/nitrictech/cli/commit/b84fa38e128366ba029da7df80288b0c2de53663)) - -### Bug Fixes - -- Fix AWS lambda gateway deployment. ([5052004](https://github.com/nitrictech/cli/commit/5052004666bb63eab1611c65fe8dd3a523e9c45f)) -- Fix GCP subscription account name length. ([127eb94](https://github.com/nitrictech/cli/commit/127eb94da65d2025870c3cf28ee7fe41a582dee2)) -- Have deployment output more useful errors. ([0f2b6a5](https://github.com/nitrictech/cli/commit/0f2b6a5b009a04147088d215c691542db6b192de)) -- Incorrect hostname for local function entrypoint mapping. ([fb6217e](https://github.com/nitrictech/cli/commit/fb6217e990fa5a7c5f4a0bd41f17fb3491d15e72)) -- Respect .dockerignore files specified in function templates. ([f78cdf0](https://github.com/nitrictech/cli/commit/f78cdf03ab8e1ca1f813c789e314036cc4195494)) - -### [0.0.26](https://github.com/nitrictech/cli/compare/v0.0.25...v0.0.26) (2021-04-11) - -### Bug Fixes - -- improve error message when function path can't be found ([fb215fb](https://github.com/nitrictech/cli/commit/fb215fb7d1354481bf1d24c414207ccb75571e87)) - -### [0.0.25](https://github.com/nitrictech/cli/compare/v0.0.24...v0.0.25) (2021-04-09) - -### Features - -- move from listr to listr2 for cli UX ([26e965a](https://github.com/nitrictech/cli/commit/26e965a805bae83a1af1c43d97267102586563a3)) -- update gcp plugin commands to listr2 ([53e571f](https://github.com/nitrictech/cli/commit/53e571f5f46c014f3069575159b4c819e9b7f564)) - -### Bug Fixes - -- Exit on build error and display more useful info. ([a197ef7](https://github.com/nitrictech/cli/commit/a197ef7fcde55fb312c9d3eb51c33afa9e89909c)) -- improve error logging when stopping already stopped containers ([a328fb3](https://github.com/nitrictech/cli/commit/a328fb36934801a42fe22c4ec06ac77478737d46)) -- reduce noise of aws:deploy and down ([9672873](https://github.com/nitrictech/cli/commit/96728730927ce6da787c0daf2079c1850c4e5fdf)) -- update listr import in remaining commands ([535b43b](https://github.com/nitrictech/cli/commit/535b43bc795d992c8825622f1481ec78228d6793)) - -### [0.0.24](https://github.com/nitrictech/cli/compare/v0.0.23...v0.0.24) (2021-04-09) - -### Bug Fixes - -- Fix awaits on dockerode image pulls. ([af1b111](https://github.com/nitrictech/cli/commit/af1b11190e2a63fe8d25b28845a84f9ee0ec8bed)) - -### [0.0.23](https://github.com/nitrictech/cli/compare/v0.0.22...v0.0.23) (2021-04-06) - -### Bug Fixes - -- add default dir for deploy:aws cmd ([5e309f5](https://github.com/nitrictech/cli/commit/5e309f5714d6ae25d31ab6aed1e44144a945ad0b)) -- deployment failures due to pulumi version ([be5aa0a](https://github.com/nitrictech/cli/commit/be5aa0ae58a87eb9e2db55b9e89199f786df5b4c)) -- remove unused import ([152d586](https://github.com/nitrictech/cli/commit/152d5862572c34891646d26ada6fa6fc0bc7f5a3)) -- Update gcp and aws pulumi dependencies. ([c28e187](https://github.com/nitrictech/cli/commit/c28e187be447c2fad61550cbbd89a1ff2f34fa1c)) -- Update pulumi. ([2cbe552](https://github.com/nitrictech/cli/commit/2cbe552f604d7210b87dd0d5fdc34382e68700a6)) - -### [0.0.22](https://github.com/nitrictech/cli/compare/v0.0.21...v0.0.22) (2021-04-01) - -### Bug Fixes - -- add provider scope to down commands ([ecee9be](https://github.com/nitrictech/cli/commit/ecee9be14bd419a79b923fa0108e7947910339db)) - -### [0.0.21](https://github.com/nitrictech/cli/compare/v0.0.20...v0.0.21) (2021-04-01) - -### Bug Fixes - -- Add missing phantom dependencies. ([794fbe9](https://github.com/nitrictech/cli/commit/794fbe978df54d95749b8a9eac9401cf541eeb6d)) - -### [0.0.20](https://github.com/nitrictech/cli/compare/v0.0.19...v0.0.20) (2021-04-01) - -### [0.0.19](https://github.com/nitrictech/cli/compare/v0.0.18...v0.0.19) (2021-04-01) - -### Features - -- Add deployment logging for AWS, GCP & Azure. ([798fb43](https://github.com/nitrictech/cli/commit/798fb43943c86d79211ae4eff08a68b0dcaaeccf)) -- **common:** Add utilities for stack operation logging. ([c5e219d](https://github.com/nitrictech/cli/commit/c5e219d3e2c8cb260b237a2bdb4aa9ecad2725a1)) - -### Bug Fixes - -- Fix missing common dependencies. ([6b8b305](https://github.com/nitrictech/cli/commit/6b8b305bd9f054bc6af43356a684c50c32a3c551)) - -### [0.0.18](https://github.com/nitrictech/cli/compare/v0.0.17...v0.0.18) (2021-03-30) - -### Bug Fixes - -- Scope stack names to providers for now. ([cc7de2a](https://github.com/nitrictech/cli/commit/cc7de2a5e319d353e14fa525d2d1e4ea183c97d6)) - -### [0.0.17](https://github.com/nitrictech/cli/compare/v0.0.16...v0.0.17) (2021-03-30) - -### Features - -- Add a quick link via the cli output. ([bb68b6d](https://github.com/nitrictech/cli/commit/bb68b6da86719355f0ef6fce26c1f81424edc216)) -- Add custom type provider for cloud scheduler. ([6226e0b](https://github.com/nitrictech/cli/commit/6226e0b0fb058dcede2511f37aa29922900a3f5e)) -- Add doctor:gcp command and tasks to install GCP pulumi plugin. ([528dab2](https://github.com/nitrictech/cli/commit/528dab26e8523d25de746cc05d8407413f3f7aeb)) -- Add static site building before deploy. ([02541e1](https://github.com/nitrictech/cli/commit/02541e1ceac2328d61811d4eb3b7a94f3277984a)) -- Add static site definition to NitricStack. ([530c2d6](https://github.com/nitrictech/cli/commit/530c2d6feaa59b901e6d4adb1f753065d207fc23)) -- Add template for creating an event bus rule for scheduled events. ([5f70bf9](https://github.com/nitrictech/cli/commit/5f70bf9e5b0e50b9a911939217e9a5ca670a100c)) -- add working aws api gateway deployment ([5b170d4](https://github.com/nitrictech/cli/commit/5b170d4a8ebe848c624f59cb83d0ecf7ca5feb7c)) -- Connect up new site creation and entrypoint creation functions. ([628d58c](https://github.com/nitrictech/cli/commit/628d58ca547a0037f537d9dfaaf6f2d041fdc89c)) -- HTTPs support for gcloud load balancer. ([2450d73](https://github.com/nitrictech/cli/commit/2450d731d1d990d390e672e8e96079a6afb349e7)) -- Preliminary local run task, for nitric entrypoints. ([a6857de](https://github.com/nitrictech/cli/commit/a6857de93ba68e069626052819e6bb70c005776b)) -- WIP commands for template repository download/listing. ([60bba71](https://github.com/nitrictech/cli/commit/60bba716af151111e8c2b01777930c2cf2f7c1be)) -- WIP doctor command to install pre-requisite software. ([f9bbf03](https://github.com/nitrictech/cli/commit/f9bbf03017739ec9cc81ac77f1cc57c9482583fc)) -- WIP entrypoints for AWS cloudfront/s3 buckets. ([68db125](https://github.com/nitrictech/cli/commit/68db125f46e08dbfe125462f4e4917916b4c63cc)) -- WIP GCP entrypoints support. ([ebb90b6](https://github.com/nitrictech/cli/commit/ebb90b675b638b34633f232c12ff37e014e38fdb)) -- WIP google pulumi implementation. ([2a8a823](https://github.com/nitrictech/cli/commit/2a8a823ef8034fffe120b638f28ad91882753bfa)) -- WIP implementation of schedule deployment on google cloud. ([595821c](https://github.com/nitrictech/cli/commit/595821c532ef7b22eb3c7ba24a41df7ba43a6977)) -- Working API/Site deploy for AWS. ([5edd67f](https://github.com/nitrictech/cli/commit/5edd67ff188f6546d5b02eab4fcb93ffcb230987)) -- Working Azure AppService deployment for nitric functions. ([0ea6237](https://github.com/nitrictech/cli/commit/0ea62376df237d1b01ba2265e65eb2533ee7931a)) -- Working GCP pulumi deployment. ([7ceb282](https://github.com/nitrictech/cli/commit/7ceb282b77b04f4191dcf2064ea271bf6c28d690)) -- Working local entrypoints deployment. ([9855ce1](https://github.com/nitrictech/cli/commit/9855ce15ab8213b978c2a3871d517aa92723ae99)) -- Working static site deployment. ([7a45fd4](https://github.com/nitrictech/cli/commit/7a45fd4a052617eda1eb20f506a1942a12614dfe)) -- **azure:** Wrap up prototype deployment scripts for azure. ([8219a01](https://github.com/nitrictech/cli/commit/8219a01a324d1497dc0cce17c8f1d5a71e3db78f)) -- **cli:** WIP GCP API Gateway implementation. ([e5019d8](https://github.com/nitrictech/cli/commit/e5019d897f0f8f5d4ec2c7135de15ff42dee9d95)) -- **cli-common:** Add API definitions for NitricStack ([a76ed94](https://github.com/nitrictech/cli/commit/a76ed94273b8292a96f133280ce16a3fca78e310)) -- **cli/gcp:** Working GCP APIGateway Deployment. ([3d727c6](https://github.com/nitrictech/cli/commit/3d727c633a67916c630ecbe39f6de1caafdb1ef9)) -- **gcp:** Working inline docker build/push ([0c75d18](https://github.com/nitrictech/cli/commit/0c75d18a7d7e07ae3de8e36dd81efeb76d02daf1)) -- **run:** Add API port mapping to output. ([14ea3cb](https://github.com/nitrictech/cli/commit/14ea3cbeffd64767d96ae2e9e76c108bd2c783f1)) -- **run:** WIP local API gateway. ([2ca0fee](https://github.com/nitrictech/cli/commit/2ca0fee8bf062a5c2ec03a988b2b200e5a729148)) -- **run:** Working API gateway startup on run. ([2b7f63e](https://github.com/nitrictech/cli/commit/2b7f63e4c1ce234ecf0d4e434ee0d3f321148233)) -- Working API gateway deployment. ([61ce682](https://github.com/nitrictech/cli/commit/61ce6823f2a276622f5cf88227a983f57461ea91)) -- Working schedule deployments for GCP. ([050711b](https://github.com/nitrictech/cli/commit/050711bd9588b1ac5f5640609c7313a3f8f5b042)) - -### Bug Fixes - -- add check for valid project names in make:project ([c2ee442](https://github.com/nitrictech/cli/commit/c2ee4421e32913f3cdad0fbdd6d7b9659876d8fe)) -- build task splits repo and template names to find each individually ([480622d](https://github.com/nitrictech/cli/commit/480622dc50708a1866c020cc565c37b4eb579e19)) -- check if at least one repo is available when listing templates ([885980a](https://github.com/nitrictech/cli/commit/885980a034a02c82ab2ebc7a66655b0b934acc59)) -- clear container start timeout on successful start ([c4c997b](https://github.com/nitrictech/cli/commit/c4c997bb8a6314224b9fd25dbec539f394c9ef7e)) -- copy the full template directory during build staging ([f32fcad](https://github.com/nitrictech/cli/commit/f32fcad284e6e05848171abd2be9ccf50775dd8e)) -- correct make project task classname ([3532329](https://github.com/nitrictech/cli/commit/35323296ca50494fc55fd46a72ef41ced0d29e11)) -- delete placeholder create command ([86108a5](https://github.com/nitrictech/cli/commit/86108a5586e40a1bf1ee3428b9a4cc28d2994ea0)) -- ignore legacy or superfluous template repo directories ([9b6390d](https://github.com/nitrictech/cli/commit/9b6390d19d7c84ed088d94563dfac496068070bb)) -- improve handling of failing to set docker network ([77533e2](https://github.com/nitrictech/cli/commit/77533e2a3cb1ce14c972f588e56bd1f8d3390ae1)) -- improve plugin load performance ([916f7a0](https://github.com/nitrictech/cli/commit/916f7a05ca25301cf4292edca9aee42c419aa907)) -- make port optional for RunFunctionTask ([ad01135](https://github.com/nitrictech/cli/commit/ad01135dd7a03db5e502508cdfc2ac5a2707c981)) -- minor issues in utils functions ([bbd68d5](https://github.com/nitrictech/cli/commit/bbd68d5c5187461e56a747724ccecfcfa12f0551)) -- move project name validation before prompts ([dbf12bc](https://github.com/nitrictech/cli/commit/dbf12bc88cfc8919a151692bfa9080a7b46f1aac)) -- passthrough stdio for doctor installs ([8e7cf4a](https://github.com/nitrictech/cli/commit/8e7cf4ac7f3c7c08eed7e2126ec5acc9807a4c01)) -- set default example func name if none provide for new project ([e7c8342](https://github.com/nitrictech/cli/commit/e7c8342da378d5c30ad04d11af186403538dc490)) -- stop warning about default network when no custom network set ([cdba178](https://github.com/nitrictech/cli/commit/cdba17823605778d2d7394adb19fc57219ca2bba)) -- typo in docker container hostconfig ([a6942c1](https://github.com/nitrictech/cli/commit/a6942c134b66c25b19f9ffa01d8f954ba0dc2244)) - -### [0.0.16](https://github.com/nitric-dev/cli/compare/v0.0.15...v0.0.16) (2020-12-20) - -### Features - -- Add --guided mode to the gcp:down command. ([b60eb51](https://github.com/nitric-dev/cli/commit/b60eb516f639d0ee6a32a30e586927e59161fe25)) -- Add a --guided CLI mode for the gcp:deploy command. ([e05c795](https://github.com/nitric-dev/cli/commit/e05c795948128776b8ea8ad523b1f2872acfedfb)) -- add container health check and scaling ([e8135c8](https://github.com/nitric-dev/cli/commit/e8135c829d641cade6da6513f3ff819e52b30454)) -- Add optional for externally invokable, unauthenticated functions. ([61ee2d0](https://github.com/nitric-dev/cli/commit/61ee2d082b092a20bd135ce6b3744b6ca16f8c23)) -- Add run container policy updates to allow unauthenticated access. ([b3de1a1](https://github.com/nitric-dev/cli/commit/b3de1a1acc8a4f5031625895936b2af5f8ffc1c4)) -- basic aws deploy with load balancer ([bfe88d2](https://github.com/nitric-dev/cli/commit/bfe88d2cb71ae5a1fd48d64bc68af586af1af3bc)) -- run functions on lambda with container ([20f6f61](https://github.com/nitric-dev/cli/commit/20f6f61b55d42f61615273dd5eaa7df8e25a0093)) - -### Bug Fixes - -- improve topic name normalization ([c86d2e8](https://github.com/nitric-dev/cli/commit/c86d2e87d756fad9e2d672b7897e0d39aa96947f)) -- incorrect topic property setting ([d23f9e0](https://github.com/nitric-dev/cli/commit/d23f9e0b387a205a311871d9252e08ead40ac161)) - -### [0.0.15](https://github.com/nitric-dev/cli/compare/v0.0.14...v0.0.15) (2020-11-26) - -### Bug Fixes - -- broken project make, due to new function make ([970253d](https://github.com/nitric-dev/cli/commit/970253df81442127bcbf0e79b4ca6ecbca8770b2)) - -### [0.0.14](https://github.com/nitric-dev/cli/compare/v0.0.13...v0.0.14) (2020-11-26) - -### Bug Fixes - -- catch error when no templates installed ([8f7ec46](https://github.com/nitric-dev/cli/commit/8f7ec46da220e8a9e2ae94db0fa7774b7085c1f5)) - -### [0.0.13](https://github.com/nitric-dev/cli/compare/v0.0.12...v0.0.13) (2020-11-26) - -### Features - -- add prompts for missing make:function args ([0e07682](https://github.com/nitric-dev/cli/commit/0e07682fff7eb413d6c1c3f34dfd58b37fda65b6)) -- assign function ports by alphabetical name ([bce4ab5](https://github.com/nitric-dev/cli/commit/bce4ab54e3604dbbd278673545684df41e6bbd0c)) -- auto detect function templates on make:project ([71fa07f](https://github.com/nitric-dev/cli/commit/71fa07fd8e5071394c9f14ffb72f9584cd754ae5)) - -### Bug Fixes - -- Add try/finally to container stop/remove. ([cea0a7c](https://github.com/nitric-dev/cli/commit/cea0a7c734be284c94155d35c8258415cd1a7281)) -- remove unused variable ([6cd1bef](https://github.com/nitric-dev/cli/commit/6cd1bef5d8a82540369c6eeb08d48fc3a9bc3d8c)) - -### [0.0.12](https://github.com/nitric-dev/cli/compare/v0.0.11...v0.0.12) (2020-11-20) - -### [0.0.11](https://github.com/nitric-dev/cli/compare/v0.0.10...v0.0.11) (2020-11-20) - -### Features - -- add local dev volumes ([d17c4dc](https://github.com/nitric-dev/cli/commit/d17c4dc59f16ddbfbd8ab30d2328d2cf755b488a)) -- add local run networking ([c461368](https://github.com/nitric-dev/cli/commit/c461368d89d05a6665399b3fcbddae27ded8a3e1)) - -### Bug Fixes - -- add basic error handling to container run ([5523411](https://github.com/nitric-dev/cli/commit/552341126cd2d1d340f428189b1da3027dc20118)) -- failing gcp plugin test ([5466225](https://github.com/nitric-dev/cli/commit/5466225bf6d6b4f11094c91a39b53035f0ed186c)) -- handle image id better ([eefb510](https://github.com/nitric-dev/cli/commit/eefb510702e72610d5f2d50be088acbf3e37354b)) -- remove gateway host requirement ([8a32e56](https://github.com/nitric-dev/cli/commit/8a32e567c1863b07131cf116cbde38269bf003a3)) -- respect template .dockerignore. ([dcf03aa](https://github.com/nitric-dev/cli/commit/dcf03aa0ddbae2c57b03f755d33390ab9082fd10)) - -### [0.0.10](https://github.com/nitric-dev/cli/compare/v0.0.9...v0.0.10) (2020-11-19) - -### [0.0.9](https://github.com/nitric-dev/cli/compare/v0.0.8...v0.0.9) (2020-10-29) - -### Bug Fixes - -- Fix incorrect MakeFunctionOpts key from refactor. ([990f2df](https://github.com/nitric-dev/cli/commit/990f2df085a8a1563f8a5cebbf2d3580ff7641f8)) - -### [0.0.8](https://github.com/nitric-dev/cli/compare/v0.0.7...v0.0.8) (2020-10-29) - -### Features - -- Allow forcing project creation if dir already exists. ([57d4b3f](https://github.com/nitric-dev/cli/commit/57d4b3f4a0aa2b6f0e084b97c26072dc5d9d1528)) -- **cli:** Add a make:project functionality. ([f1c47f6](https://github.com/nitric-dev/cli/commit/f1c47f6a0f7f46c80aa696ffda3e732ae742e6d0)) - -### Bug Fixes - -- improve command descriptions in cli ([17d9f01](https://github.com/nitric-dev/cli/commit/17d9f01e8eaee785462e89a4108fff21a44b0e8e)) - -### 0.0.7 (2020-10-28) - -### Bug Fixes - -- minor typing issue ([d6a5d20](https://github.com/nitric-dev/cli/commit/d6a5d203ae02e31a270b48d562788fab133328b6)) -- type error in make function task ([1e83a7b](https://github.com/nitric-dev/cli/commit/1e83a7be57d11cd34fe5c148e9b20fd5e82cbf4a)) diff --git a/package.json b/package.json index b93f34d5..bce8f40c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,5 @@ { "name": "nitric-cli", - "version": "0.0.86", "description": "CLI monorepo for the Nitric framework", "author": "Nitric ", "scripts": { @@ -20,7 +19,8 @@ "license:add": "license-check-and-add add -f ./licenseconfig.json", "license:remove": "license-check-and-add remove -f ./licenseconfig.json", "publish:all": "yarn workspaces foreach --no-private run npm:publish", - "nitric": "yarn workspace @nitric/cli nitric" + "nitric": "yarn workspace @nitric/cli nitric", + "version:all": "yarn workspaces foreach --no-private run set:version" }, "contributors": [ "Jye Cusch ", @@ -66,6 +66,7 @@ "./**/*.{ts,js}": "eslint --fix-dry-run" }, "dependencies": { + "axios": "^0.21.4", "legally": "^3.5.10", "rimraf": "^3.0.2" }, diff --git a/packages/base/package.json b/packages/base/package.json index f6cbced3..27165684 100644 --- a/packages/base/package.json +++ b/packages/base/package.json @@ -1,7 +1,6 @@ { "name": "@nitric/cli", "description": "CLI tool for nitric applications", - "version": "0.0.86", "author": "Nitric ", "bin": { "nitric": "./bin/run" @@ -73,14 +72,8 @@ "oclif": { "commands": "./lib/commands", "topics": { - "templates": { - "description": "Commands for template management" - }, - "templates:repos": { - "description": "Commands for managing template repositories" - }, "make": { - "description": "Commands to make new projects and assets." + "description": "Commands to make new stacks and assets." }, "deploy": { "description": "Plugin commands to deploy on supported providers. E.g. `$ nitric deploy:aws`" @@ -101,15 +94,15 @@ "directory": "packages/base" }, "scripts": { - "prepackage": "rm -rf lib && tsc -p tsconfig.build.json && oclif-dev manifest && oclif-dev readme", - "postpackage": "rm -f oclif.manifest.json", + "prepackage": "rimraf -rf lib && tsc -p tsconfig.build.json && oclif-dev manifest && oclif-dev readme", + "postpackage": "rimraf -f oclif.manifest.json", "package:all": "yarn run prepackage && pkg . --out-path ./dist/ --no-bytecode --public-packages '*' --public", "package": "yarn run prepackage && yarn run package:all && yarn run postpackage", - "postpack": "rm -f oclif.manifest.json", - "prepack": "rm -rf lib && tsc --project tsconfig.build.json && oclif-dev manifest && oclif-dev readme", + "postpack": "rimraf -f oclif.manifest.json", + "prepack": "rimraf -rf lib && tsc --project tsconfig.build.json && oclif-dev manifest && oclif-dev readme", "test": "tsc --emitDeclarationOnly && jest", "npm:publish": "yarn npm publish --access public --tolerate-republish", - "version": "oclif-dev readme && git add README.md", + "set:version": "npm version --version-git-tag false", "nitric": "./bin/run" }, "pkg": { diff --git a/packages/base/src/commands/doctor.test.ts b/packages/base/src/commands/doctor.test.ts index 42e900fb..4a3e127a 100644 --- a/packages/base/src/commands/doctor.test.ts +++ b/packages/base/src/commands/doctor.test.ts @@ -55,7 +55,7 @@ describe('Doctor Command:', () => { let whichSpy: jest.SpyInstance; beforeAll(() => { - whichSpy = jest.spyOn(which, 'sync').mockReturnValue(true); // Installed. + whichSpy = jest.spyOn(which, 'sync').mockReturnValue(['/usr/bin/test']); // Installed. }); afterAll(() => { @@ -91,7 +91,7 @@ describe('Doctor Command:', () => { let confirmSpy: jest.SpyInstance; beforeAll(() => { - whichSpy = jest.spyOn(which, 'sync').mockReturnValue(false); // Not installed. + whichSpy = jest.spyOn(which, 'sync').mockReturnValue(null); // Not installed. fsSpy = jest.spyOn(fs, 'existsSync').mockReturnValue(false); confirmSpy = jest.spyOn(cli, 'confirm'); }); diff --git a/packages/base/src/commands/make/stack.ts b/packages/base/src/commands/make/stack.ts index beee2bca..309dc33f 100644 --- a/packages/base/src/commands/make/stack.ts +++ b/packages/base/src/commands/make/stack.ts @@ -55,7 +55,7 @@ export default class Stack extends BaseCommand { { name: 'template', required: false, - description: 'Service template', + description: 'Stack template', // TODO: Handle case where no templates are available. Prompt to install template(s). // Present available templates from locally installed repositories choices: async (): Promise => { diff --git a/packages/base/src/commands/run.ts b/packages/base/src/commands/run.ts index 822c9932..f462b732 100644 --- a/packages/base/src/commands/run.ts +++ b/packages/base/src/commands/run.ts @@ -25,7 +25,7 @@ import { import { Listr, ListrTask, ListrContext, ListrRenderer } from 'listr2'; import path from 'path'; import execa from 'execa'; -import Docker, { Container, Network } from 'dockerode'; +import Docker, { Network } from 'dockerode'; import getPort from 'get-port'; import readline from 'readline'; @@ -37,6 +37,9 @@ import { RunGatewayTaskOptions, RunContainerTask, RunContainerTaskOptions, + RunStorageServiceTask, + RunStorageServiceTaskOptions, + RunContainerResult, } from '../tasks'; import { TaskWrapper } from 'listr2/dist/lib/task-wrapper'; import crypto from 'crypto'; @@ -149,6 +152,14 @@ export function createEntrypointTasks( ); } +export function createStorageServiceTask( + opts: RunStorageServiceTaskOptions, + docker: Docker, + network: Docker.Network, +): ListrTask { + return wrapTaskForListr(new RunStorageServiceTask({ ...opts, network }, docker)); +} + /** * Get tasks to creat a local API Gateway for set of APIs * @param stackName of the project stack @@ -231,12 +242,24 @@ export function createEntrypointContainerRunTasks( }; } +export function createStorageRunTask(storage: RunStorageServiceTaskOptions, docker: Docker): (ctx, task) => Listr { + return (ctx, task: TaskWrapper): Listr => { + return task.newListr(createStorageServiceTask(storage, docker, ctx.network), { + // Don't fail all on a single function failure... + exitOnError: true, + // Added to allow custom handling of SIGINT for run cmd cleanup. + registerSignalListeners: false, + }); + }; +} + /** * Top level listr run task displayed to the user * Will display tasks for creating docker resources */ export function createRunTasks( stack: Stack, + storage: RunStorageServiceTaskOptions, containers: RunContainerTaskOptions[], apis: RunGatewayTaskOptions[], entrypoints: RunEntrypointTaskOptions[], @@ -249,7 +272,10 @@ export function createRunTasks( wrapTaskForListr(new CreateNetworkTask({ name: `${stack.getName()}-net-${runId}`, docker }), 'network'), // Run the functions & containers, attached to the network { - title: 'Running Functions & Containers', + title: 'Running Storage Service', + task: createStorageRunTask(storage, docker), + }, + { task: createContainerRunTasks(stack, containers, docker), }, // Start the APIs, to route requests to the containers @@ -325,8 +351,9 @@ export default class Run extends BaseCommand { */ runContainers = async (stack: Stack, runId: string): Promise => { const nitricStack = stack.asNitricStack(); - const { entrypoints = {} } = nitricStack; + const { entrypoints = {}, buckets = {} } = nitricStack; const namedEntrypoints = Object.entries(entrypoints).map(([name, entrypoint]) => ({ name, ...entrypoint })); + const namedBuckets = Object.entries(buckets).map(([name, bucket]) => ({ name, ...bucket })); cli.action.stop(); @@ -375,9 +402,16 @@ export default class Run extends BaseCommand { }), ); + const runStorageOptions = { + buckets: namedBuckets, + stack, + runId, + } as RunStorageServiceTaskOptions; + // Capture the results of running tasks to setup docker network, functions and containers const runTaskResults = await createRunTasks( stack, + runStorageOptions, runContainersTaskOptions, runGatewayOptions, runEntrypointOptions, @@ -386,35 +420,56 @@ export default class Run extends BaseCommand { ).run(); // Capture created docker resources for cleanup on run termination (see cleanup()) - const { network: newNetwork, ...results }: { [key: string]: Container } & { network: Network } = runTaskResults; + const { network: newNetwork, ...results }: { [key: string]: RunContainerResult } & { network: Network } = + runTaskResults; this.network = newNetwork; - this.runningContainers = results; + this.runningContainers = Object.keys(results).reduce( + (acc, k) => ({ + ...acc, + [k]: results[k].container, + }), + {}, + ); // Present a list of containers (inc. functions and APIs) and their ports on the cli + const runningContainers = Object.keys(results) + .filter((k) => results[k].type === 'container') + .map((k) => results[k].name); cli.table(runContainersTaskOptions, { function: { get: (row): string => row.image && row.image.name, }, - port: {}, + port: { + get: (row): string | number => (runningContainers.includes(row.image.name) && row.port) || 'Failed to start', + }, }); if (nitricStack.apis) { + const runningApis = Object.keys(results) + .filter((k) => results[k].type === 'api') + .map((k) => results[k].name); cli.table(runGatewayOptions, { api: { get: (row): string => row.api && row.api.name, }, - port: {}, + port: { + get: (row): string | number => (runningApis.includes(row.api.name) && row.port) || 'Failed to start', + }, }); } if (nitricStack.entrypoints) { + const runningEntrypoints = Object.keys(results) + .filter((k) => results[k].type === 'entrypoint') + .map((k) => results[k].name); cli.table(runEntrypointOptions, { entrypoint: { get: (row): string => row.entrypoint.name, }, url: { - get: (row): string => `http://localhost:${row.port}`, + get: (row): string => + (runningEntrypoints.includes(row.entrypoint.name) && `http://localhost:${row.port}`) || 'Failed to start', }, }); } @@ -518,8 +573,10 @@ export default class Run extends BaseCommand { }); }); - process.stdin.setRawMode!(true); - process.stdin.resume(); + if (process.stdin.setRawMode) { + process.stdin.setRawMode(true); + process.stdin.resume(); + } await cleanUp; diff --git a/packages/base/src/tasks/make/stack.test.ts b/packages/base/src/tasks/make/stack.test.ts index 2586e310..f2fd32e8 100644 --- a/packages/base/src/tasks/make/stack.test.ts +++ b/packages/base/src/tasks/make/stack.test.ts @@ -22,7 +22,7 @@ describe('MakeStackTask: ', () => { describe("When the stack folder doesn't exist", () => { test('The new stack directory is created', async () => { await new MakeStackTask({ name: 'test-stack', force: true }).do(); - expect(fs.mkdirSync).toBeCalledWith('./test-stack'); + expect(fs.mkdirSync).toBeCalledWith('./test-stack', { recursive: true }); }); }); diff --git a/packages/base/src/tasks/make/stack.ts b/packages/base/src/tasks/make/stack.ts index f3adacdb..e2ecc2ed 100644 --- a/packages/base/src/tasks/make/stack.ts +++ b/packages/base/src/tasks/make/stack.ts @@ -53,9 +53,15 @@ export class MakeStackTask extends Task { const stackPath = `./${stackName}`; + if (fs.existsSync(stackPath) && fs.readdirSync(stackPath).length && !this.force) { + throw new Error( + "Stack directory already exists and isn't empty, choose a different name or use the --force flag to create in a non-empty directory.", + ); + } + // Create new folder relative to current directory for the new project try { - fs.mkdirSync(stackPath); + fs.mkdirSync(stackPath, { recursive: true }); } catch (error) { if (error.message.includes('file already exists')) { if (!this.force) { diff --git a/packages/base/src/tasks/run/constants.ts b/packages/base/src/tasks/run/constants.ts index cf110277..f77472d9 100644 --- a/packages/base/src/tasks/run/constants.ts +++ b/packages/base/src/tasks/run/constants.ts @@ -13,3 +13,7 @@ // limitations under the License. export const NITRIC_DEV_VOLUME = '/nitric/'; + +export const NITRIC_RUN_DIR = './.nitric/run'; +// NOTE: octal notation is important here!!! +export const NITRIC_RUN_PERM = 0o777; diff --git a/packages/base/src/tasks/run/entrypoints.ts b/packages/base/src/tasks/run/entrypoints.ts index 41498718..8728126d 100644 --- a/packages/base/src/tasks/run/entrypoints.ts +++ b/packages/base/src/tasks/run/entrypoints.ts @@ -13,7 +13,7 @@ // limitations under the License. import { NamedObject, NitricEntrypoint, StackSite, Stack, Task } from '@nitric/cli-common'; -import Docker, { Container, ContainerCreateOptions, Network, NetworkInspectInfo } from 'dockerode'; +import Docker, { ContainerCreateOptions, Network, NetworkInspectInfo } from 'dockerode'; import tar from 'tar-fs'; import fs from 'fs'; import path from 'path'; @@ -21,6 +21,7 @@ import getPort from 'get-port'; import os from 'os'; import streamToPromise from 'stream-to-promise'; import { DOCKER_LABEL_RUN_ID } from '../../constants'; +import { RunContainerResult } from './types'; const HTTP_PORT = 80; const NGINX_CONFIG_FILE = 'nginx.conf'; @@ -147,7 +148,7 @@ export interface RunEntrypointTaskOptions { /** * Run local http entrypoint(s) as containers for developments/testing purposes. */ -export class RunEntrypointTask extends Task { +export class RunEntrypointTask extends Task { private entrypoint: NamedObject; private stack: Stack; private network?: Network; @@ -165,7 +166,7 @@ export class RunEntrypointTask extends Task { this.runId = runId; } - async do(): Promise { + async do(): Promise { const { stack, network, docker, runId } = this; if (!this.port) { @@ -262,6 +263,11 @@ export class RunEntrypointTask extends Task { }), ); - return container; + return { + container, + name: this.entrypoint.name, + type: 'entrypoint', + ports: [this.port!], + }; } } diff --git a/packages/base/src/tasks/run/function.ts b/packages/base/src/tasks/run/function.ts index a4bb3f46..ef38e722 100644 --- a/packages/base/src/tasks/run/function.ts +++ b/packages/base/src/tasks/run/function.ts @@ -20,6 +20,7 @@ import fs from 'fs'; import { DOCKER_LABEL_RUN_ID } from '../../constants'; import { NITRIC_DEV_VOLUME } from './constants'; import path from 'path'; +import { RunContainerResult } from './types'; const GATEWAY_PORT = 9001; @@ -42,7 +43,7 @@ export interface RunContainerTaskOptions { /** * Run a Nitric Function or Container locally for development or testing */ -export class RunContainerTask extends Task { +export class RunContainerTask extends Task { private stack: Stack; private image: ContainerImage; private port: number | undefined; @@ -62,7 +63,7 @@ export class RunContainerTask extends Task { this.runId = runId; } - async do(): Promise { + async do(): Promise { const { network, subscriptions, runId } = this; if (!this.port) { @@ -82,7 +83,15 @@ export class RunContainerTask extends Task { const dockerOptions = { name: `${this.image.name}-${runId}`, - Env: [`LOCAL_SUBSCRIPTIONS=${JSON.stringify(subscriptions)}`, `NITRIC_DEV_VOLUME=${NITRIC_DEV_VOLUME}`], + Env: [ + `LOCAL_SUBSCRIPTIONS=${JSON.stringify(subscriptions)}`, + `NITRIC_DEV_VOLUME=${NITRIC_DEV_VOLUME}`, + // ENV variables to connect the membrane to a local minio instance + `MINIO_ENDPOINT=http://minio-${runId}:9000`, + // TODO: Update these from defaults + `MINIO_ACCESS_KEY=minioadmin`, + `MINIO_SECRET_KEY=minioadmin`, + ], ExposedPorts: { [`${GATEWAY_PORT}/tcp`]: {}, }, @@ -170,7 +179,12 @@ export class RunContainerTask extends Task { runResult.on('container', (container: Container) => { clearTimeout(rejectTimeout); this.update(`Container ${container.id.substring(0, 12)} listening on: ${this.port}`); - res(container); + res({ + name: this.image.name, + type: 'container', + container, + ports: [this.port!], + }); }); }); } diff --git a/packages/base/src/tasks/run/gateway.test.ts b/packages/base/src/tasks/run/gateway.test.ts index 42126bc5..adf6439d 100644 --- a/packages/base/src/tasks/run/gateway.test.ts +++ b/packages/base/src/tasks/run/gateway.test.ts @@ -13,8 +13,8 @@ // limitations under the License. import 'jest'; -import { RunGatewayTask } from '.'; -import Docker, { Container } from 'dockerode'; +import { RunGatewayTask, RunContainerResult } from '.'; +import Docker from 'dockerode'; import getPort from 'get-port'; import { StackAPI } from '@nitric/cli-common/lib/stack/api'; import _ from 'stream-to-promise'; @@ -59,10 +59,10 @@ describe('GatewayRunTask', () => { }); describe('when minimal options are provided', () => { - let container: Container; + let result: RunContainerResult; beforeAll(async () => { - container = await new RunGatewayTask({ + result = await new RunGatewayTask({ stackName: 'test', api: MOCK_API, docker: new Docker(), @@ -84,11 +84,11 @@ describe('GatewayRunTask', () => { }); it('should start the created source', () => { - expect(container.start).toHaveBeenCalled(); + expect(result.container.start).toHaveBeenCalled(); }); it('should upload the api to the created source', () => { - expect(container.putArchive).toHaveBeenCalled(); + expect(result.container.putArchive).toHaveBeenCalled(); }); it('should use default bridge network', () => { diff --git a/packages/base/src/tasks/run/gateway.ts b/packages/base/src/tasks/run/gateway.ts index 73393fed..7ef078b3 100644 --- a/packages/base/src/tasks/run/gateway.ts +++ b/packages/base/src/tasks/run/gateway.ts @@ -13,11 +13,12 @@ // limitations under the License. import { Task, StackAPI, STAGING_API_DIR } from '@nitric/cli-common'; -import Docker, { Container, ContainerCreateOptions, Network, NetworkInspectInfo } from 'dockerode'; +import Docker, { ContainerCreateOptions, Network, NetworkInspectInfo } from 'dockerode'; import fs from 'fs'; import getPort from 'get-port'; import streamToPromise from 'stream-to-promise'; import tar from 'tar-fs'; +import { RunContainerResult } from './types'; import { DOCKER_LABEL_RUN_ID } from '../../constants'; const GATEWAY_PORT = 8080; @@ -40,7 +41,7 @@ export interface RunGatewayTaskOptions { */ export function createAPIStagingDirectory(): string { if (!fs.existsSync(`${STAGING_API_DIR}`)) { - fs.mkdirSync(`${STAGING_API_DIR}`); + fs.mkdirSync(`${STAGING_API_DIR}`, { recursive: true }); } return `${STAGING_API_DIR}`; @@ -52,7 +53,7 @@ export function createAPIStagingDirectory(): string { export function createAPIDirectory(apiName: string): string { const stagingDir = createAPIStagingDirectory(); if (!fs.existsSync(`${stagingDir}/${apiName}`)) { - fs.mkdirSync(`${stagingDir}/${apiName}`); + fs.mkdirSync(`${stagingDir}/${apiName}`, { recursive: true }); } return `${stagingDir}/${apiName}`; @@ -61,7 +62,7 @@ export function createAPIDirectory(apiName: string): string { /** * Run local API Gateways for development/testing */ -export class RunGatewayTask extends Task { +export class RunGatewayTask extends Task { private stackName: string; private api: StackAPI; private port?: number; @@ -79,7 +80,7 @@ export class RunGatewayTask extends Task { this.runId = runId; } - async do(): Promise { + async do(): Promise { const { stackName, api, network, runId } = this; if (!this.port) { @@ -146,6 +147,11 @@ export class RunGatewayTask extends Task { await container.start(); - return container; + return { + name: api.name, + type: 'api', + container, + ports: [this.port!], + }; } } diff --git a/packages/base/src/tasks/run/index.ts b/packages/base/src/tasks/run/index.ts index 6c2c1848..8ffb1939 100644 --- a/packages/base/src/tasks/run/index.ts +++ b/packages/base/src/tasks/run/index.ts @@ -16,3 +16,5 @@ export * from './network'; export * from './function'; export * from './gateway'; export * from './entrypoints'; +export * from './storage'; +export * from './types'; diff --git a/packages/base/src/tasks/run/storage.ts b/packages/base/src/tasks/run/storage.ts new file mode 100644 index 00000000..d10d6918 --- /dev/null +++ b/packages/base/src/tasks/run/storage.ts @@ -0,0 +1,182 @@ +// Copyright 2021, Nitric Technologies Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NamedObject, NitricBucket, Stack, Task } from '@nitric/cli-common'; +import Docker, { ContainerCreateOptions, Network, NetworkInspectInfo } from 'dockerode'; +import fs from 'fs'; +import path from 'path'; +import getPort from 'get-port'; +import streamToPromise from 'stream-to-promise'; +import { DOCKER_LABEL_RUN_ID } from '../../constants'; +import { NITRIC_RUN_DIR, NITRIC_RUN_PERM, NITRIC_DEV_VOLUME } from './constants'; +import { RunContainerResult } from './types'; + +const MINIO_PORT = 9000; +// TODO: Determine if we would like to expose the console +const MINIO_CONSOLE_PORT = 9001; + +/** + * Starts an exec stream and Wraps the docker.modem.followProgress function on non-windows platforms + * For windows a workaround using exec.inspect is used to ensure we can capture the end of the stream + * @param exec + * @param docker + */ +//export async function execStream(exec: Docker.Exec, docker: Docker): Promise { +// const execStream = await exec.start({}); + +// await new Promise((resolve) => { +// if (os.platform() == 'win32') { +// let output = ''; +// execStream.on('data', (chunk) => (output += chunk)); + +// const interval = setInterval(async () => { +// const { Running } = await exec.inspect(); + +// if (!Running) { +// clearInterval(interval); +// execStream.destroy(); +// resolve(); +// } +// }, 100); +// } else { +// docker.modem.followProgress(execStream, resolve); +// } +// }); +//} + +/** + * Options when running entrypoints for local testing + */ +export interface RunStorageServiceTaskOptions { + stack: Stack; + buckets: NamedObject[]; + network?: Network; + port?: number; + consolePort?: number; + runId: string; +} + +/** + * Run local http entrypoint(s) as containers for developments/testing purposes. + */ +export class RunStorageServiceTask extends Task { + private stack: Stack; + private buckets: NamedObject[]; + private network?: Network; + private docker: Docker; + private port?: number; + private consolePort?: number; + private runId: string; + + constructor({ stack, buckets, network, port, consolePort, runId }: RunStorageServiceTaskOptions, docker: Docker) { + super(`minio server`); + this.stack = stack; + this.buckets = buckets; + this.network = network; + this.docker = docker; + this.port = port; + this.consolePort = consolePort; + this.runId = runId; + } + + async do(): Promise { + const { buckets, network, docker, runId } = this; + + if (!this.port) { + // Find any open port if none provided. + this.port = await getPort(); + } + + if (!this.consolePort) { + this.consolePort = await getPort(); + } + + let networkName = 'bridge'; + if (network) { + try { + networkName = ((await network?.inspect()) as NetworkInspectInfo).Name; + } catch (error) { + console.warn(`Failed to set custom docker network, defaulting to bridge network`); + } + } + + // Ensure the buckets directory exists + const nitricRunDir = path.join(this.stack.getDirectory(), NITRIC_RUN_DIR); + fs.mkdirSync(nitricRunDir, { recursive: true }); + fs.chmodSync(nitricRunDir, NITRIC_RUN_PERM); + await Promise.all( + buckets.map((b) => fs.promises.mkdir(path.join(nitricRunDir, `./buckets/${b.name}`), { recursive: true })), + ); + + // Add storage volume, if configured + const MOUNT_POINT = `${NITRIC_DEV_VOLUME}`; + + // Pull nginx + await streamToPromise(await docker.pull('minio/minio')); + + const containerName = `minio-${runId}`; + + const dockerOptions = { + name: containerName, + // Pull nginx + Image: 'minio/minio', + ExposedPorts: { + [`${MINIO_PORT}/tcp`]: {}, + [`${MINIO_CONSOLE_PORT}/tcp`]: {}, + }, + Volumes: { + [MOUNT_POINT]: {}, + }, + Labels: { + [DOCKER_LABEL_RUN_ID]: runId, + }, + Cmd: ['server', '/nitric/buckets', '--console-address', `:${MINIO_CONSOLE_PORT}`], + HostConfig: { + NetworkMode: networkName, + PortBindings: { + [`${MINIO_PORT}/tcp`]: [ + { + HostPort: `${this.port}/tcp`, + }, + ], + [`${MINIO_CONSOLE_PORT}/tcp`]: [ + { + HostPort: `${this.consolePort}/tcp`, + }, + ], + }, + Mounts: [ + { + Target: MOUNT_POINT, + Source: nitricRunDir, // volume.name, + Type: 'bind', + }, + ], + }, + } as ContainerCreateOptions; + + // Create the nginx source first + const container = await docker.createContainer(dockerOptions); + + // Start the entrypoint source + await container.start(); + + return { + name: containerName, + container, + type: 'container', + ports: [this.port!], + }; + } +} diff --git a/packages/base/src/tasks/run/types.ts b/packages/base/src/tasks/run/types.ts new file mode 100644 index 00000000..e0ad48e5 --- /dev/null +++ b/packages/base/src/tasks/run/types.ts @@ -0,0 +1,22 @@ +// Copyright 2021, Nitric Technologies Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Container } from 'dockerode'; + +export interface RunContainerResult { + name: string; + type: 'container' | 'api' | 'entrypoint'; + container: Container; + ports: number[]; +} diff --git a/packages/base/src/utils/index.ts b/packages/base/src/utils/index.ts index 3a910281..97eb9a8d 100644 --- a/packages/base/src/utils/index.ts +++ b/packages/base/src/utils/index.ts @@ -21,7 +21,7 @@ import path from 'path'; */ export function createNitricLogDir(): void { if (!fs.existsSync(LOG_DIR)) { - fs.mkdirSync(LOG_DIR); + fs.mkdirSync(LOG_DIR, { recursive: true }); } } diff --git a/packages/common/package.json b/packages/common/package.json index a1e49710..eecce61b 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,5 @@ { "name": "@nitric/cli-common", - "version": "0.0.86", "description": "Common Nitric typings and utility functions", "author": "Nitric ", "bugs": "https://github.com/nitrictech/cli/issues", @@ -16,7 +15,8 @@ "build": "yarn run clean && yarn run build:sources", "build:sources": "tsc --project tsconfig.build.json --outDir ./lib/", "test": "tsc --emitDeclarationOnly && jest", - "npm:publish": "yarn npm publish --access public --tolerate-republish" + "npm:publish": "yarn npm publish --access public --tolerate-republish", + "set:version": "npm version --version-git-tag false" }, "contributors": [ "Jye Cusch ", @@ -24,6 +24,7 @@ ], "license": "Apache-2.0", "dependencies": { + "@iarna/toml": "^2.2.5", "@oclif/command": "^1", "@oclif/config": "^1", "@pulumi/docker": "^3.0.0", @@ -46,6 +47,7 @@ "tslib": "^1", "universal-analytics": "^0.4.23", "uuid": "^8.3.2", + "which": "^2.0.2", "yaml": "^2.0.0-7" }, "files": [ @@ -58,6 +60,7 @@ "@types/stream-to-promise": "^2.2.1", "@types/universal-analytics": "^0.4.4", "@types/uuid": "^8.3.0", + "@types/which": "^2.0.1", "jest": "^26.6.1", "openapi-types": "^7.2.3", "ts-jest": "^26.4.3", diff --git a/packages/common/src/constants/index.ts b/packages/common/src/constants/index.ts index ba1253d0..2657bd8b 100644 --- a/packages/common/src/constants/index.ts +++ b/packages/common/src/constants/index.ts @@ -12,11 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. +import path from 'path'; import { ListrBaseClassOptions, ListrDefaultRendererValue, ListrFallbackRendererValue } from 'listr2'; +export const DEFAULT_NITRIC_DIR = '.nitric/'; +export const DEFAULT_BUILD_DIR = path.join(DEFAULT_NITRIC_DIR, 'build'); + export const DEFAULT_LISTR_OPTIONS: ListrBaseClassOptions = { rendererOptions: { collapseErrors: false, }, }; + +export const OAI_NITRIC_TARGET_EXT = 'x-nitric-target'; diff --git a/packages/common/src/stack/container.ts b/packages/common/src/stack/container.ts index b244c848..1a05cfaf 100644 --- a/packages/common/src/stack/container.ts +++ b/packages/common/src/stack/container.ts @@ -89,7 +89,7 @@ export class StackContainer> { ); } - return origPath; + return fullPath; } /** diff --git a/packages/common/src/stack/function.ts b/packages/common/src/stack/function.ts index 79649c9a..79b2f4b3 100644 --- a/packages/common/src/stack/function.ts +++ b/packages/common/src/stack/function.ts @@ -58,6 +58,14 @@ export class StackFunction> { return this.name; } + /** + * + * @returns the nitric framework version used by this function + */ + getVersion(): string { + return this.descriptor.version || this.getStack().getVersion(); + } + /** * Get the build context of the function * @returns diff --git a/packages/common/src/stack/schema.ts b/packages/common/src/stack/schema.ts index eb7c7d5d..ba546edd 100644 --- a/packages/common/src/stack/schema.ts +++ b/packages/common/src/stack/schema.ts @@ -24,6 +24,16 @@ import { StackAPIDocument } from './api'; * Pattern for stack resources names, e.g. func names, topic names, buckets, etc. */ export const resourceNamePattern = /^\w+([.\\-]\w+)*$/.toString().slice(1, -1); + +/** + * Pattern for nitric framework versions + * taken from: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + */ +export const versionPattern = + /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ + .toString() + .slice(1, -1); + /** * CRON expression string pattern */ @@ -1541,6 +1551,11 @@ export const STACK_SCHEMA: JSONSchema7 = { type: 'string', pattern: resourceNamePattern, }, + version: { + title: 'nitric framework version', + type: 'string', + pattern: versionPattern, + }, functions: { title: 'functions', type: 'object', @@ -1550,8 +1565,19 @@ export const STACK_SCHEMA: JSONSchema7 = { description: 'A nitric compute func, such as a serverless function', type: 'object', properties: { + version: { + title: 'nitric framework version', + type: 'string', + pattern: versionPattern, + }, handler: { type: 'string' }, context: { type: 'string' }, + excludes: { + type: 'array', + items: { + type: 'string', + }, + }, triggers: { title: 'func triggers', type: 'object', @@ -1586,6 +1612,10 @@ export const STACK_SCHEMA: JSONSchema7 = { properties: { dockerfile: { type: 'string' }, context: { type: 'string' }, + args: { + type: 'object', + additionalProperties: { type: 'string' }, + }, triggers: { title: 'func triggers', type: 'object', @@ -2010,10 +2040,13 @@ export const validateStack = (potentialStack: any, filePath: string): void => { return; } const target = op['x-nitric-target']; - if (!(potentialStack[`${target.type}s`] && potentialStack[`${target.type}s`][target.name])) { - logicErrors.push( - `Invalid target for apis.${apiName}.${pathName}.${opName}, target ${target.type} "${target.name}" doesn't exist`, - ); + // Only validate if a x-nitric-target property is provided + if (target) { + if (!(potentialStack[`${target.type}s`] && potentialStack[`${target.type}s`][target.name])) { + logicErrors.push( + `Invalid target for apis.${apiName}.${pathName}.${opName}, target ${target.type} "${target.name}" doesn't exist`, + ); + } } }); }); diff --git a/packages/common/src/stack/stack.ts b/packages/common/src/stack/stack.ts index 777a1827..2e4e3239 100644 --- a/packages/common/src/stack/stack.ts +++ b/packages/common/src/stack/stack.ts @@ -129,6 +129,14 @@ export class Stack< this.descriptor.name = name; } + /** + * + * @returns the nitric framework version used by this stack + */ + getVersion(): string { + return this.descriptor.version || 'latest'; + } + /** * Return the descriptor for the stack * @param noUndefined if true, removes undefined top level properties from the description. diff --git a/packages/common/src/task/build-container.ts b/packages/common/src/task/build-container.ts index 386d0f37..2cf2efc3 100644 --- a/packages/common/src/task/build-container.ts +++ b/packages/common/src/task/build-container.ts @@ -15,11 +15,8 @@ import { Task } from './task'; import { ContainerImage } from '../types'; import { StackContainer } from '../stack'; -import { dockerodeEvtToString } from '../index'; -// import rimraf from 'rimraf'; - -import tar from 'tar-fs'; -import Docker from 'dockerode'; +import { oneLine } from 'common-tags'; +import execa from 'execa'; interface BuildContainerTaskOptions { baseDir: string; @@ -29,74 +26,52 @@ interface BuildContainerTaskOptions { export class BuildContainerTask extends Task { private container: StackContainer; - // private readonly stack: Stack; private readonly provider: string; constructor({ container, provider = 'local' }: BuildContainerTaskOptions) { super(`${container.getName()}`); this.container = container; - // this.stack = stack; this.provider = provider; } async do(): Promise { - const docker = new Docker(); - - // const ignoreFiles = await Template.getDockerIgnoreFiles(template); - - const pack = tar.pack(this.container.getContext(), { - // TODO: support ignore again - // ignore: (name) => - // // Simple filter before more complex multimatch - // ignoreFiles.filter((f) => name.includes(f)).length > 0 || match(name, ignoreFiles).length > 0, - }); + const imageId = this.container.getImageTagName(this.provider); + const { args = {} } = this.container.getDescriptor(); - // FIXME: Currently dockerode does not support dockerfiles specified outside of build context - const dockerfile = this.container.getDockerfile(); - - const options = { - buildargs: { - PROVIDER: this.provider, - }, - t: this.container.getImageTagName(this.provider), - dockerfile, - }; + const cmd = oneLine` + docker build ${this.container.getContext()} + -f ${this.container.getDockerfile()} + -t ${imageId} + --progress plain + --build-arg PROVIDER=${this.provider} + ${Object.keys(args) + .map((k) => `--build-arg ${k}=${args[k]}`) + .join(' ')} + `; - let stream: NodeJS.ReadableStream; try { - stream = await docker.buildImage(pack, options); - } catch (error) { - if (error.errno && error.errno === -61) { - throw new Error('Unable to connect to docker, is it running locally?'); - } - throw error; - } - - // Get build updates - const buildResults = await new Promise((resolve, reject) => { - docker.modem.followProgress( - stream, - (errorInner: Error, resolveInner: Record[]) => - errorInner ? reject(errorInner) : resolve(resolveInner), - (event: any) => { - try { - this.update(dockerodeEvtToString(event)); - } catch (error) { - reject(new Error(error.message.replace(/\n/g, ''))); - } + const dockerProcess = execa.command(cmd, { + // Enable buildkit for out of context dockerfile + env: { + DOCKER_BUILDKIT: '1', }, - ); - }); + }); + + // Only outputs on stderr + dockerProcess.stderr.on('data', (data) => { + // fs.writeFileSync('debug.txt', data); + this.update(data.toString()); + }); - const filteredResults = buildResults.filter((obj) => 'aux' in obj && 'ID' in obj['aux']); - if (filteredResults.length > 0) { - const imageId = filteredResults[filteredResults.length - 1]['aux'].ID.split(':').pop() as string; - return { id: imageId, name: this.container.getName() } as ContainerImage; - } else { - const { - errorDetail: { message }, - } = buildResults.pop() as any; - throw new Error(message); + // wait for the process to finalize + await dockerProcess; + } catch (e) { + throw new Error(e.message); } + + return { + id: imageId, + name: this.container.getName(), + }; } } diff --git a/packages/common/src/task/build-function.ts b/packages/common/src/task/build-function.ts index 842939ef..74968812 100644 --- a/packages/common/src/task/build-function.ts +++ b/packages/common/src/task/build-function.ts @@ -13,10 +13,15 @@ // limitations under the License. import execa from 'execa'; +import path from 'path'; import { oneLine } from 'common-tags'; import { Task } from './task'; import { ContainerImage } from '../types'; import { StackFunction } from '../stack'; +import { DEFAULT_NITRIC_DIR, DEFAULT_BUILD_DIR } from '../constants'; +import which from 'which'; +import TOML from '@iarna/toml'; +import fs from 'fs'; interface BuildFunctionTaskOptions { baseDir: string; @@ -24,9 +29,15 @@ interface BuildFunctionTaskOptions { provider?: string; } -const PACK_IMAGE = 'buildpacksio/pack:0.13.1'; +const PACK_IMAGE = 'buildpacksio/pack:0.21.1'; const BUILDER_IMAGE = 'nitrictech/bp-builder-base'; +const DEFAULT_PROJECT_CONFIG = { + build: { + exclude: [DEFAULT_NITRIC_DIR], + }, +}; + export class BuildFunctionTask extends Task { private service: StackFunction; private readonly provider: string; @@ -40,26 +51,52 @@ export class BuildFunctionTask extends Task { async do(): Promise { const imageId = this.service.getImageTagName(this.provider); + // Create a temporary default ignore file + // and delete it when we're done + const contextDirectory = this.service.getDescriptor().context || '.'; + const contextBuildDirectory = `./${path.join(contextDirectory, DEFAULT_BUILD_DIR)}`; + + await fs.promises.mkdir(contextBuildDirectory, { + recursive: true, + }); + await fs.promises.writeFile(`./${contextBuildDirectory}/${imageId}.toml`, TOML.stringify(DEFAULT_PROJECT_CONFIG)); + + let baseCmd = oneLine` + build ${imageId} + --builder ${BUILDER_IMAGE} + --trust-builder + ${Object.entries(this.service.getPackEnv()) + .map(([k, v]) => `--env ${k}=${v}`) + .join(' ')} + -d ./.nitric/build/${imageId}.toml + --env BP_MEMBRANE_VERSION=${this.service.getVersion()} + --env BP_MEMBRANE_PROVIDER=${this.provider} + --env BP_NITRIC_SERVICE_HANDLER=${this.service.getContextRelativeDirectory()} + --pull-policy if-not-present + --default-process membrane + `; + + const packInstalled = which.sync('pack', { nothrow: true }); + + if (!packInstalled) { + baseCmd = oneLine` + docker run + --rm + --privileged=true + -u root + -v /var/run/docker.sock:/var/run/docker.sock + -v ${this.service.getContext()}:/workspace -w /workspace + ${PACK_IMAGE} ${baseCmd} + `; + } else { + baseCmd = oneLine`pack ${baseCmd} --path ${this.service.getContext()}`; + } + // Run docker // TODO: This will need to be updated for mono repo support // FIXME: Need to confirm docker sock mounting will work on windows try { - const packProcess = execa.command(oneLine` - docker run - --rm - --privileged=true - -v /var/run/docker.sock:/var/run/docker.sock - -v ${this.service.getContext()}:/workspace -w /workspace - ${PACK_IMAGE} build ${imageId} - --builder ${BUILDER_IMAGE} - ${Object.entries(this.service.getPackEnv()) - .map(([k, v]) => `--env ${k}=${v}`) - .join(' ')} - --env BP_MEMBRANE_PROVIDER=${this.provider} - --env BP_NITRIC_SERVICE_HANDLER=${this.service.getContextRelativeDirectory()} - --pull-policy if-not-present - --default-process membrane - `); + const packProcess = execa.command(baseCmd); // pipe build to stdout packProcess.stdout.on('data', (data) => { @@ -68,6 +105,14 @@ export class BuildFunctionTask extends Task { // wait for the process to finalize await packProcess; + + // clean build files + await fs.promises.unlink(`./${contextBuildDirectory}/${imageId}.toml`); + + // remove build directory if empty + if (fs.existsSync(contextBuildDirectory) && fs.readdirSync(contextBuildDirectory).length === 0) { + await fs.promises.rmdir(contextBuildDirectory); + } } catch (e) { throw new Error(e.message); } diff --git a/packages/common/src/types/compute/container.ts b/packages/common/src/types/compute/container.ts index 6e76b64a..39a4b470 100644 --- a/packages/common/src/types/compute/container.ts +++ b/packages/common/src/types/compute/container.ts @@ -18,4 +18,5 @@ export interface NitricContainer extends NitricComputeUnit { // The path to the Dockerfile to use to build this source // relative to context dockerfile: string; + args?: Record; } diff --git a/packages/common/src/types/compute/function.ts b/packages/common/src/types/compute/function.ts index ffb319f8..5970f2b4 100644 --- a/packages/common/src/types/compute/function.ts +++ b/packages/common/src/types/compute/function.ts @@ -18,11 +18,12 @@ export interface NitricFunction extends NitricComputeUnit { // The location of the function handler // relative to context handler: string; + // The build pack version of the membrane used for the function build + version?: string; // Scripts that will be executed by the nitric // build process before beginning the docker build buildScripts?: string[]; // files to exclude from final build - // can be globs excludes?: string[]; // The most requests a single function instance should handle maxRequests?: number; diff --git a/packages/common/src/types/schedule.ts b/packages/common/src/types/schedule.ts index 0e2e3528..e6dc9b77 100644 --- a/packages/common/src/types/schedule.ts +++ b/packages/common/src/types/schedule.ts @@ -23,12 +23,15 @@ export interface NitricScheduleEvent { export interface NitricScheduleTarget { type: 'topic'; // ; | "queue" - id: string; + name: string; } /** * A Nitric Schedule definition */ + +// TODO: resolve following +// eslint-disable-next-line @typescript-eslint/ban-types export interface NitricSchedule = {}> { expression: string; // The Topic to be targeted for schedule diff --git a/packages/common/src/types/stack.ts b/packages/common/src/types/stack.ts index 51ea51aa..64e99cca 100644 --- a/packages/common/src/types/stack.ts +++ b/packages/common/src/types/stack.ts @@ -37,6 +37,8 @@ export interface NitricStack< > { // Name of the Nitric Stack name: string; + // Nitric Framework Version + version?: string; // Functions that will be deployed functions?: { [name: string]: NitricFunction; diff --git a/packages/common/src/utils/check-docker-daemon.ts b/packages/common/src/utils/check-docker-daemon.ts new file mode 100644 index 00000000..ecf7de78 --- /dev/null +++ b/packages/common/src/utils/check-docker-daemon.ts @@ -0,0 +1,24 @@ +// Copyright 2021, Nitric Technologies Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import execa from 'execa'; + +export function checkDockerDaemon(doctorCommand = 'doctor'): void { + try { + execa.sync('docker', ['ps']); + } catch { + throw new Error( + `Docker daemon was not found!\nTry using 'nitric ${doctorCommand}' to confirm it is correctly installed, and check that the service is running.`, + ); + } +} diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts index f33b7a29..075d4dd7 100644 --- a/packages/common/src/utils/index.ts +++ b/packages/common/src/utils/index.ts @@ -16,3 +16,4 @@ export * from './dir'; export * from './stack'; export * from './tagged-templates'; export * from './map-object'; +export * from './check-docker-daemon'; diff --git a/packages/plugins/aws/package.json b/packages/plugins/aws/package.json index e8a434c1..4859a2ef 100644 --- a/packages/plugins/aws/package.json +++ b/packages/plugins/aws/package.json @@ -1,7 +1,6 @@ { "name": "@nitric/plugin-aws", "description": "An AWS plugin for Nitric", - "version": "0.0.86", "author": "Nitric ", "bugs": "https://github.com/nitrictech/cli/issues", "dependencies": { @@ -66,10 +65,10 @@ "directory": "packages/plugins/aws" }, "scripts": { - "postpack": "rm -f oclif.manifest.json", - "prepack": "rm -rf lib && tsc -p tsconfig.build.json && oclif-dev manifest && oclif-dev readme", + "postpack": "rimraf -f oclif.manifest.json", + "prepack": "rimraf -rf lib && tsc -p tsconfig.build.json && oclif-dev manifest && oclif-dev readme", "test": "tsc --emitDeclarationOnly && jest --passWithNoTests", - "version": "oclif-dev readme && git add README.md", + "set:version": "npm version --version-git-tag false", "npm:publish": "yarn npm publish --access public --tolerate-republish" } } diff --git a/packages/plugins/aws/src/commands/deploy/aws.ts b/packages/plugins/aws/src/commands/deploy/aws.ts index 193bc979..51ab8859 100644 --- a/packages/plugins/aws/src/commands/deploy/aws.ts +++ b/packages/plugins/aws/src/commands/deploy/aws.ts @@ -14,7 +14,14 @@ import { flags } from '@oclif/command'; import { Deploy, DEPLOY_TASK_KEY, DeployResult } from '../../tasks/deploy'; -import { BaseCommand, wrapTaskForListr, Stack, constants, createBuildListrTask } from '@nitric/cli-common'; +import { + BaseCommand, + wrapTaskForListr, + Stack, + constants, + createBuildListrTask, + checkDockerDaemon, +} from '@nitric/cli-common'; import { Listr } from 'listr2'; import path from 'path'; import AWS from 'aws-sdk'; @@ -63,8 +70,12 @@ export default class AwsDeploy extends BaseCommand { static args = [{ name: 'dir', default: '.' }]; async do(): Promise { + // Check docker daemon is running + checkDockerDaemon('doctor:aws'); + const { args, flags } = this.parse(AwsDeploy); const { dir } = args; + const sts = new AWS.STS(); const { Account: derivedAccountId } = await sts.getCallerIdentity({}).promise(); diff --git a/packages/plugins/aws/src/commands/down/aws.ts b/packages/plugins/aws/src/commands/down/aws.ts index dcfa2a3f..15fcc58e 100644 --- a/packages/plugins/aws/src/commands/down/aws.ts +++ b/packages/plugins/aws/src/commands/down/aws.ts @@ -29,6 +29,10 @@ export default class AwsDown extends BaseCommand { char: 'f', description: 'file containing the stack definition of the stack to be torn down', }), + destroy: flags.boolean({ + char: 'd', + description: 'destroy all resources, including buckets, secrets, and collections', + }), }; static args = [ @@ -41,13 +45,13 @@ export default class AwsDown extends BaseCommand { async do(): Promise { const { args, flags } = this.parse(AwsDown); const { dir = '.' } = args; - const { file = 'nitric.yaml' } = flags; + const { file = 'nitric.yaml', destroy } = flags; const stackDefinitionPath = path.join(dir, file); const stack = (await Stack.fromFile(stackDefinitionPath)).asNitricStack(); try { - await new Listr([wrapTaskForListr(new Down({ stack }))], constants.DEFAULT_LISTR_OPTIONS).run(); + await new Listr([wrapTaskForListr(new Down({ stack, destroy }))], constants.DEFAULT_LISTR_OPTIONS).run(); } catch (error) { // eat this error to avoid duplicate console output. } diff --git a/packages/plugins/aws/src/resources/api.ts b/packages/plugins/aws/src/resources/api.ts index 615ac5ff..7adcf52c 100644 --- a/packages/plugins/aws/src/resources/api.ts +++ b/packages/plugins/aws/src/resources/api.ts @@ -15,7 +15,7 @@ import * as pulumi from '@pulumi/pulumi'; import * as aws from '@pulumi/aws'; import { OpenAPIV3 } from 'openapi-types'; import { uniq } from 'lodash'; -import { StackAPI } from '@nitric/cli-common'; +import { StackAPI, constants } from '@nitric/cli-common'; import { NitricComputeAWSLambda } from './compute'; type method = 'get' | 'post' | 'put' | 'patch' | 'delete'; @@ -63,7 +63,6 @@ export class NitricApiAwsApiGateway extends pulumi.ComponentResource { const defaultResourceOptions: pulumi.ResourceOptions = { parent: this }; const { api, lambdas } = args; - const { name: nitricName, ...rest } = api; this.name = name; @@ -76,10 +75,10 @@ export class NitricApiAwsApiGateway extends pulumi.ComponentResource { return [ ...acc, ...Object.keys(path) - .filter((k) => METHOD_KEYS.includes(k as method)) + .filter((k) => METHOD_KEYS.includes(k as method) && path[k][constants.OAI_NITRIC_TARGET_EXT]) .map((m) => { const method = path[m as method]!; - return method['x-nitric-target'].name; + return method[constants.OAI_NITRIC_TARGET_EXT].name; }), ]; }, [] as string[]), @@ -89,7 +88,7 @@ export class NitricApiAwsApiGateway extends pulumi.ComponentResource { .all(lambdas.map((s) => s.lambda.invokeArn.apply((arn) => `${s.name}||${arn}`))) .apply((nameArnPairs) => { const transformedApi = { - ...rest, + ...openapi, paths: Object.keys(openapi.paths).reduce((acc, pathKey) => { const path = openapi.paths[pathKey]!; const newMethods = Object.keys(path) @@ -98,31 +97,40 @@ export class NitricApiAwsApiGateway extends pulumi.ComponentResource { const p = path[method]; // The name of the function we want to target with this APIGateway - const targetName = p['x-nitric-target'].name; - const invokeArnPair = nameArnPairs.find((f) => f.split('||')[0] === targetName); - - if (!invokeArnPair) { - throw new Error(`Invalid nitric target ${targetName} defined in api: ${api.name}`); + if (p[constants.OAI_NITRIC_TARGET_EXT]) { + const targetName = p[constants.OAI_NITRIC_TARGET_EXT].name; + + const invokeArnPair = nameArnPairs.find((f) => f.split('||')[0] === targetName); + + if (!invokeArnPair) { + throw new Error(`Invalid nitric target ${targetName} defined in api: ${api.name}`); + } + + const invokeArn = invokeArnPair.split('||')[1]; + // Discard the old key on the transformed API + const { [constants.OAI_NITRIC_TARGET_EXT]: _, ...rest } = p; + + return { + ...acc, + [method]: { + ...(rest as OpenAPIV3.OperationObject), + 'x-amazon-apigateway-integration': { + type: 'aws_proxy', + httpMethod: 'POST', + payloadFormatVersion: '2.0', + // TODO: This might cause some trouble + // Need to determine if the body of the + uri: invokeArn, + }, + } as any, // OpenAPIV3.OperationObject + }; } - const invokeArn = invokeArnPair.split('||')[1]; - // Discard the old key on the transformed API - const { 'x-nitric-target': _, ...rest } = p; - + // return method without re-write if x-nitric-target not specified return { ...acc, - [method]: { - ...(rest as OpenAPIV3.OperationObject), - 'x-amazon-apigateway-integration': { - type: 'aws_proxy', - httpMethod: 'POST', - payloadFormatVersion: '2.0', - // TODO: This might cause some trouble - // Need to determine if the body of the - uri: invokeArn, - }, - } as any, // OpenAPIV3.OperationObject + [method]: p, }; }, {} as { [key: string]: OpenAPIV3.OperationObject }); diff --git a/packages/plugins/aws/src/resources/compute.ts b/packages/plugins/aws/src/resources/compute.ts index b00ce44c..f3694089 100644 --- a/packages/plugins/aws/src/resources/compute.ts +++ b/packages/plugins/aws/src/resources/compute.ts @@ -104,6 +104,34 @@ export class NitricComputeAWSLambda extends pulumi.ComponentResource { defaultResourceOptions, ); + new aws.iam.RolePolicy( + `${source.getName()}SQSAccess`, + { + role: lambdaRole.id, + policy: JSON.stringify({ + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: [ + 'sqs:ChangeMessageVisibility', + 'sqs:DeleteMessage', + 'sqs:GetQueueAttributes', + 'sqs:GetQueueUrl', + 'sqs:ListDeadLetterSourceQueues', + 'sqs:ListQueues', + 'sqs:ListQueueTags', + 'sqs:ReceiveMessage', + 'sqs:SendMessage', + ], + Resource: '*', + }, + ], + }), + }, + defaultResourceOptions, + ); + new aws.iam.RolePolicy( `${source.getName()}SecretsAccess`, { diff --git a/packages/plugins/aws/src/resources/index.ts b/packages/plugins/aws/src/resources/index.ts index c0514ce2..5572cea3 100644 --- a/packages/plugins/aws/src/resources/index.ts +++ b/packages/plugins/aws/src/resources/index.ts @@ -19,3 +19,4 @@ export * from './schedule'; export * from './compute'; export * from './site'; export * from './topic'; +export * from './queue'; diff --git a/packages/plugins/aws/src/resources/queue.ts b/packages/plugins/aws/src/resources/queue.ts new file mode 100644 index 00000000..671b019b --- /dev/null +++ b/packages/plugins/aws/src/resources/queue.ts @@ -0,0 +1,52 @@ +// Copyright 2021, Nitric Technologies Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import * as pulumi from '@pulumi/pulumi'; +import { NamedObject, NitricQueue } from '@nitric/cli-common'; +import * as aws from '@pulumi/aws'; + +interface NitricSQSQueueArgs { + queue: NamedObject; +} + +/** + * Nitric AWS SQS Queue based Queue + */ + +export class NitricSQSQueue extends pulumi.ComponentResource { + public readonly name: string; + public readonly queue: aws.sqs.Queue; + + constructor(name: string, args: NitricSQSQueueArgs, opts?: pulumi.ComponentResourceOptions) { + super('nitric:queue:SQSQueue', name, {}, opts); + const defaultResourceOptions: pulumi.ResourceOptions = { parent: this }; + const { queue } = args; + + this.name = queue.name; + this.queue = new aws.sqs.Queue( + this.name, + { + tags: { + 'x-nitric-name': queue.name, + }, + }, + defaultResourceOptions, + ); + + // Finalize the deployment + this.registerOutputs({ + name: this.name, + queue: this.queue, + }); + } +} diff --git a/packages/plugins/aws/src/resources/schedule.test.ts b/packages/plugins/aws/src/resources/schedule.test.ts new file mode 100644 index 00000000..83a65905 --- /dev/null +++ b/packages/plugins/aws/src/resources/schedule.test.ts @@ -0,0 +1,103 @@ +// Copyright 2021, Nitric Technologies Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { cronToAwsCron } from './schedule'; + +describe('Cron Expression Conversion', () => { + describe('Given an expression with more than 5 values', () => { + const exp = '*/1 * * * ? *'; + + it('Should throw an error', () => { + expect(() => { + cronToAwsCron(exp); + }).toThrow(); + }); + }); + + describe('Given a valid cron expression', () => { + const exp = '*/1 * * * *'; + + describe('When converting the expression', () => { + let awsExpValues: string[] = []; + beforeAll(() => { + // Expected result = '0/1 * * * ? *' + awsExpValues = cronToAwsCron(exp).split(' '); + }); + + it('Should replace the * with 0 for "every x unit" style values', () => { + expect(awsExpValues[0]).toEqual('0/1'); + }); + + it('Should replace * in Day of Week with ? if DOW and DOM are both *', () => { + expect(awsExpValues[4]).toEqual('?'); + }); + + it('Should output an expression with year added as a *', () => { + expect(awsExpValues.length).toEqual(6); + expect(awsExpValues[5]).toEqual('*'); + }); + }); + }); + + describe('Given a valid cron with a Day of Week value between 0-6', () => { + const exp = '*/1 * * * 3'; + + describe('When converting the expression', () => { + let awsExpValues: string[] = []; + beforeAll(() => { + // Expected result = '0/1 * ? * 3 *' + awsExpValues = cronToAwsCron(exp).split(' '); + }); + + it('increment the value by 1', () => { + expect(awsExpValues[4]).toEqual('4'); + }); + + it('Should replace * in Day of Month with ?', () => { + expect(awsExpValues[2]).toEqual('?'); + }); + }); + }); + + describe('Given a valid cron with a Day of Week value of 7 (Sunday)', () => { + const exp = '*/1 * * * 7'; + + describe('When converting the expression', () => { + let awsExpValues: string[] = []; + beforeAll(() => { + // Expected result = '0/1 * ? * 1 *' + awsExpValues = cronToAwsCron(exp).split(' '); + }); + + it('Should set the value to 1 (Sunday)', () => { + expect(awsExpValues[4]).toEqual('1'); + }); + }); + }); + + describe('Given a valid cron with a Day of Week value range', () => { + const exp = '*/1 * * * 1-3'; + + describe('When converting the expression', () => { + let awsExpValues: string[] = []; + beforeAll(() => { + // Expected result = '0/1 * ? * 2-4 *' + awsExpValues = cronToAwsCron(exp).split(' '); + }); + + it('Should increment both values by 1', () => { + expect(awsExpValues[4]).toEqual('2-4'); + }); + }); + }); +}); diff --git a/packages/plugins/aws/src/resources/schedule.ts b/packages/plugins/aws/src/resources/schedule.ts index 2b561978..4883a139 100644 --- a/packages/plugins/aws/src/resources/schedule.ts +++ b/packages/plugins/aws/src/resources/schedule.ts @@ -21,6 +21,77 @@ interface NitricScheduleEventBridgeArgs { topics: NitricSnsTopic[]; } +/** + * Converts a standard Crontab style cron expression to an AWS specific format. + * + * AWS appears to use a variation of the Quartz "Unix-like" Cron Expression Format. + * Notable changes include: + * - Removing the 'seconds' value (seconds are not supported) + * - Making the 'year' value mandatory + * - Providing a value for both Day of Month and Day of Year is not supported. + * + * Quartz CronExpression Docs: + * https://www.javadoc.io/doc/org.quartz-scheduler/quartz/1.8.2/org/quartz/CronExpression.html + * + * AWS Specific CronExpressions Doc: + * https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html#CronExpressions + * + * Crontab Expression Docs: + * https://man7.org/linux/man-pages/man5/crontab.5.html + * + * @param crontab the crontab style cron expression string + * @returns the input cron expression returned in the AWS specific format + */ +export const cronToAwsCron = (crontab: string): string => { + let parts = crontab.split(' '); + if (parts.length !== 5) { + throw new Error(`Invalid Expression. Expected 5 expression values, received ${parts.length}`); + } + + // Replace */x (i.e. "every x minutes") style inputs to the AWS equivalent + // AWS uses 0 instead of * for these expressions + parts = parts.map((part) => part.replace(/^\*(?=\/.*)/g, '0')); + + // Only day of week or day of month can be set with AWS, the other must be a ? char + // See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html#CronExpressions - Restrictions + const DAY_OF_MONTH = 2; + const DAY_OF_WEEK = 4; + if (parts[DAY_OF_WEEK] === '*') { + parts[DAY_OF_WEEK] = '?'; + } else { + if (parts[DAY_OF_MONTH] !== '*') { + // TODO: We can support both in future by creating two EventRules - one for DOW, another for DOM. + throw new Error('Invalid Expression. Day of Month and Day of Week expression component cannot both be set.'); + } + parts[DAY_OF_MONTH] = '?'; + } + + // We also need to adjust the Day of Week value + // crontab uses 0-7 (0 or 7 is Sunday) + // AWS uses 1-7 (Sunday-Saturday) + parts[DAY_OF_WEEK] = parts[DAY_OF_WEEK].split('') + .map((char) => { + let num = parseInt(char); + + if (!isNaN(num)) { + // Check for standard 0-6 day range and increment + if (num >= 0 && num <= 6) { + return num + 1; + } else { + // otherwise default to Sunday + return 1; + } + } else { + return char; + } + }) + .join(''); + + // Add the year component, this doesn't exist in crontab expressions, so we default it to * + parts = [...parts, '*']; + return parts.join(' '); +}; + /** * Nitric EventBridge based Schedule */ @@ -36,17 +107,24 @@ export class NitricScheduleEventBridge extends pulumi.ComponentResource { const defaultResourceOptions: pulumi.ResourceOptions = { parent: this }; const { schedule, topics } = args; - const topic = topics.find((t) => t.name === schedule.target.id); + const topic = topics.find((t) => t.name === schedule.target.name); this.name = schedule.name; + let awsCronValue = ''; + try { + awsCronValue = cronToAwsCron(schedule.expression?.replace(/['"]+/g, '')); + } catch (error) { + throw new Error(`Failed to process expression for schedule ${this.name}. Details: ${(error as Error).message}`); + } + if (topic) { const rule = new aws.cloudwatch.EventRule( `${schedule.name}Schedule`, { description: `Nitric schedule trigger for ${schedule.name}`, name: schedule.name, - scheduleExpression: `cron(${schedule.expression})`, + scheduleExpression: `cron(${awsCronValue})`, }, defaultResourceOptions, ); @@ -55,10 +133,39 @@ export class NitricScheduleEventBridge extends pulumi.ComponentResource { `${schedule.name}Target`, { arn: topic.sns.arn, - rule: rule.arn, + rule: rule.name, }, defaultResourceOptions, ); + + const snsTopicSchedulePolicy = topic.sns.arn.apply((arn) => + aws.iam.getPolicyDocument({ + // TODO: According to the docs, 'conditions' are not supported for a policy involving EventBridge + // See: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-use-resource-based.html#eb-sns-permissions + // "You can't use of Condition blocks in Amazon SNS topic policies for EventBridge." + // This means any EventBridge rule will be able to publish to this topic. + policyId: '__default_policy_ID', + statements: [ + { + sid: '__default_statement_ID', + effect: 'Allow', + actions: ['SNS:Publish'], + principals: [ + { + type: 'Service', + identifiers: ['events.amazonaws.com'], + }, + ], + resources: [arn], + }, + ], + }), + ); + + new aws.sns.TopicPolicy(`${schedule.name}Target${topic.name}Policy`, { + arn: topic.sns.arn, + policy: snsTopicSchedulePolicy.apply((snsTopicPolicy) => snsTopicPolicy.json), + }); } this.registerOutputs({ diff --git a/packages/plugins/aws/src/tasks/deploy/index.ts b/packages/plugins/aws/src/tasks/deploy/index.ts index 74afaa9c..ab1b881f 100644 --- a/packages/plugins/aws/src/tasks/deploy/index.ts +++ b/packages/plugins/aws/src/tasks/deploy/index.ts @@ -28,6 +28,7 @@ import { NitricBucketS3, NitricCollectionDynamo, NitricComputeAWSLambda, + NitricSQSQueue, } from '../../resources'; /** @@ -92,7 +93,14 @@ export class Deploy extends Task { async do(): Promise { const { stack, region } = this; - const { topics = {}, buckets = {}, collections = {}, schedules = {}, entrypoints } = stack.asNitricStack(); + const { + topics = {}, + queues = {}, + buckets = {}, + collections = {}, + schedules = {}, + entrypoints, + } = stack.asNitricStack(); // Use absolute path to log files, so it's easier for users to locate them if printed to the console. const logFile = path.resolve(await stack.getLoggingFile('deploy-aws')); const errorFile = path.resolve(await stack.getLoggingFile('error-aws')); @@ -128,6 +136,15 @@ export class Deploy extends Task { bucket, }), ); + + // Deploy Queues + mapObject(queues).forEach( + (queue) => + new NitricSQSQueue(queue.name, { + queue, + }), + ); + // Deploy Document Collections mapObject(collections).forEach( (collection) => diff --git a/packages/plugins/aws/src/tasks/down/index.ts b/packages/plugins/aws/src/tasks/down/index.ts index f66e7264..ebacbf35 100644 --- a/packages/plugins/aws/src/tasks/down/index.ts +++ b/packages/plugins/aws/src/tasks/down/index.ts @@ -14,14 +14,33 @@ import { NitricStack, Task } from '@nitric/cli-common'; import { LocalWorkspace } from '@pulumi/pulumi/automation'; +import Deployment from '../../types/deployment'; /** * Options when tearing down a nitric stack from AWS */ interface DownOptions { stack: NitricStack; + destroy: boolean; } +interface Target { + type: string; //e.g. bucket + pulumiTypes: string[]; +} + +//Map of the protected destroy targets +const protectedTargets: Target[] = [ + { + type: 'base', + pulumiTypes: ['pulumi:pulumi:Stack', 'pulumi:providers:aws'], + }, + { + type: 'bucket', + pulumiTypes: ['nitric:bucket:S3', 'aws:s3/bucket:Bucket'], + }, +]; + const NO_OP = async (): Promise => { return; }; @@ -31,10 +50,12 @@ const NO_OP = async (): Promise => { */ export class Down extends Task { private stack: NitricStack; + private destroy: boolean; - constructor({ stack }: DownOptions) { + constructor({ stack, destroy }: DownOptions) { super(`Tearing Down Stack: ${stack.name}`); this.stack = stack; + this.destroy = destroy; } async do(): Promise { @@ -48,13 +69,21 @@ export class Down extends Task { program: NO_OP, }); - const res = await pulumiStack.destroy({ onOutput: this.update.bind(this) }); + if (this.destroy) { + await pulumiStack.destroy({ onOutput: this.update.bind(this) }); + } else { + const deployment = (await pulumiStack.exportStack()).deployment as Deployment; + const nonTargets = protectedTargets //Possible to filter the protected targets in the future + .map((val) => val.pulumiTypes) + .reduce((acc, val) => acc.concat(val), []); - if (res.summary && res.summary.resourceChanges) { - const changes = Object.entries(res.summary.resourceChanges) - .map((entry) => entry.join(': ')) - .join(', '); - this.update(changes); + //List of targets that will be destroyed, filters out the ones that are protected + const targets = deployment.resources + .filter((resource) => !resource.urn.match('/nitric:site:S3/g') || !nonTargets.includes(resource.type)) + .map((resource) => resource.urn); + if (targets.length > 0) { + await pulumiStack.destroy({ onOutput: this.update.bind(this), target: targets }); + } } } catch (e) { console.log(e); diff --git a/packages/plugins/aws/src/types/deployment.ts b/packages/plugins/aws/src/types/deployment.ts new file mode 100644 index 00000000..80716d94 --- /dev/null +++ b/packages/plugins/aws/src/types/deployment.ts @@ -0,0 +1,28 @@ +// Copyright 2021, Nitric Technologies Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +interface Resource { + urn: string; + custom: boolean; + type: string; + outputs: string[]; + parent: string; + dependencies: string[]; +} + +interface Deployment { + resources: Resource[]; +} + +export default Deployment; diff --git a/packages/plugins/azure/package.json b/packages/plugins/azure/package.json index 1e6ba7ee..2e710c82 100644 --- a/packages/plugins/azure/package.json +++ b/packages/plugins/azure/package.json @@ -1,7 +1,6 @@ { "name": "@nitric/plugin-azure", "description": "Azure plugin for Nitric", - "version": "0.0.86", "author": "Nitric ", "bugs": "https://github.com/nitrictech/cli/issues", "dependencies": { @@ -19,12 +18,14 @@ "execa": "^5.0.0", "inquirer": "^8.0.0", "listr2": "^3.6.2", + "mime-types": "^2.1.32", "tslib": "^1" }, "devDependencies": { "@oclif/dev-cli": "^1", "@oclif/plugin-help": "^3", "@types/jest": "^26.0.15", + "@types/mime-types": "^2.1.1", "@types/node": "^10", "eslint": "^6.8.0", "eslint-config-oclif": "^3.1", @@ -63,10 +64,10 @@ "directory": "packages/plugins/azure" }, "scripts": { - "postpack": "rm -f oclif.manifest.json", - "prepack": "rm -rf lib && tsc -p tsconfig.build.json && oclif-dev manifest && oclif-dev readme", + "postpack": "rimraf -f oclif.manifest.json", + "prepack": "rimraf -rf lib && tsc -p tsconfig.build.json && oclif-dev manifest && oclif-dev readme", "test": "tsc --emitDeclarationOnly && jest --passWithNoTests", - "version": "oclif-dev readme && git add README.md", + "set:version": "npm version --version-git-tag false", "npm:publish": "yarn npm publish --access restricted --tolerate-republish" } } diff --git a/packages/plugins/azure/src/commands/deploy/azure.ts b/packages/plugins/azure/src/commands/deploy/azure.ts index efdb9320..18e5bf15 100644 --- a/packages/plugins/azure/src/commands/deploy/azure.ts +++ b/packages/plugins/azure/src/commands/deploy/azure.ts @@ -15,9 +15,17 @@ import { flags } from '@oclif/command'; import { Listr } from 'listr2'; import inquirer from 'inquirer'; -import { BaseCommand, Stack, wrapTaskForListr, constants, createBuildListrTask } from '@nitric/cli-common'; +import { + BaseCommand, + Stack, + wrapTaskForListr, + constants, + createBuildListrTask, + checkDockerDaemon, +} from '@nitric/cli-common'; import path from 'path'; import { Deploy } from '../../tasks/deploy'; +import { AppServicePlan } from '../../types'; const SUPPORTED_REGIONS = [ 'eastus', @@ -86,6 +94,41 @@ const SUPPORTED_REGIONS = [ 'brazilsoutheast', ]; +const APPSERVICE_PLANS: Record = { + 'Free-F1': { + tier: 'Free', + size: 'F1', + }, + 'Shared-D1': { + tier: 'Shared', + size: 'D1', + }, + 'Basic-B1': { + tier: 'Basic', + size: 'B1', + }, + 'Basic-B2': { + tier: 'Basic', + size: 'B2', + }, + 'Basic-B3': { + tier: 'Basic', + size: 'B3', + }, + 'Standard-S1': { + tier: 'Standard', + size: 'S1', + }, + 'Standard-S2': { + tier: 'Standard', + size: 'S2', + }, + 'Standard-S3': { + tier: 'Standard', + size: 'S3', + }, +}; + /** * Deploy a stack to Microsoft Azure * @@ -103,6 +146,11 @@ export default class AzureDeploy extends BaseCommand { char: 'r', description: 'azure region to deploy to', }), + plan: flags.enum({ + options: Object.keys(APPSERVICE_PLANS), + char: 'p', + description: 'azure appservice plan tier', + }), file: flags.string({ char: 'f', default: 'nitric.yaml', @@ -121,6 +169,9 @@ export default class AzureDeploy extends BaseCommand { ]; async do(): Promise { + // Check docker daemon is running + checkDockerDaemon('doctor:azure'); + const { args, flags } = this.parse(AzureDeploy); const { ci } = flags; const { dir = '.' } = args; @@ -146,7 +197,7 @@ export default class AzureDeploy extends BaseCommand { promptFlags = await inquirer.prompt(prompts); } - const { file, region, orgName, adminEmail } = { ...flags, ...promptFlags }; + const { file, region, orgName, adminEmail, plan } = { ...flags, ...promptFlags }; if (!region) { throw new Error('Region must be provided, for prompts use the --guided flag'); @@ -163,7 +214,18 @@ export default class AzureDeploy extends BaseCommand { const stack = await Stack.fromFile(path.join(dir, file)); new Listr( - [createBuildListrTask(stack, 'azure'), wrapTaskForListr(new Deploy({ stack, region, orgName, adminEmail }))], + [ + createBuildListrTask(stack, 'azure'), + wrapTaskForListr( + new Deploy({ + stack, + region, + orgName, + adminEmail, + servicePlan: APPSERVICE_PLANS[plan], + }), + ), + ], constants.DEFAULT_LISTR_OPTIONS, ).run(); } diff --git a/packages/plugins/azure/src/commands/down/azure.ts b/packages/plugins/azure/src/commands/down/azure.ts index 1482f6fd..ef122325 100644 --- a/packages/plugins/azure/src/commands/down/azure.ts +++ b/packages/plugins/azure/src/commands/down/azure.ts @@ -27,6 +27,10 @@ export default class DownCmd extends BaseCommand { static flags = { ...BaseCommand.flags, file: flags.string({ char: 'f' }), + destroy: flags.boolean({ + char: 'd', + description: 'destroy all resources, including buckets, secrets, and collections', + }), }; static args = [{ name: 'dir' }]; @@ -34,13 +38,13 @@ export default class DownCmd extends BaseCommand { async do(): Promise { const { args, flags } = this.parse(DownCmd); const { dir = '.' } = args; - const { file = 'nitric.yaml' } = flags; + const { file = 'nitric.yaml', destroy } = flags; const stackDefinitionPath = path.join(dir, file); const stack = (await Stack.fromFile(stackDefinitionPath)).asNitricStack(); try { - await new Listr([wrapTaskForListr(new Down({ stack }))], constants.DEFAULT_LISTR_OPTIONS).run(); + await new Listr([wrapTaskForListr(new Down({ stack, destroy }))], constants.DEFAULT_LISTR_OPTIONS).run(); } catch (error) { // eat this error to avoid duplicate console output. } diff --git a/packages/plugins/azure/src/resources/api.ts b/packages/plugins/azure/src/resources/api.ts index 130440c0..ade81872 100644 --- a/packages/plugins/azure/src/resources/api.ts +++ b/packages/plugins/azure/src/resources/api.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. import * as pulumi from '@pulumi/pulumi'; -import { NitricAPITarget, StackAPI } from '@nitric/cli-common'; +import { NitricAPITarget, StackAPI, constants } from '@nitric/cli-common'; import { resources, apimanagement } from '@pulumi/azure-native'; import { OpenAPIV3 } from 'openapi-types'; import { NitricComputeAzureAppService } from '.'; @@ -38,7 +38,7 @@ export class NitricApiAzureApiManagement extends pulumi.ComponentResource { public readonly resourceGroup: resources.ResourceGroup; constructor(name: string, args: NitricApiAzureApiManagementArgs, opts?: pulumi.ComponentResourceOptions) { - super('nitric:bucket:AzureStorage', name, {}, opts); + super('nitric:api:AzureApiManagement', name, {}, opts); const defaultResourceOptions: pulumi.ResourceOptions = { parent: this }; const { resourceGroup, orgName, adminEmail, api, services } = args; @@ -49,8 +49,6 @@ export class NitricApiAzureApiManagement extends pulumi.ComponentResource { name, { resourceGroupName: resourceGroup.name, - // TODO: Extract from API doc? - serviceName: `${api.name}-service`, publisherEmail: adminEmail, publisherName: orgName, // TODO: Add configuration for this @@ -67,10 +65,13 @@ export class NitricApiAzureApiManagement extends pulumi.ComponentResource { this.api = new apimanagement.Api( `${name}-api`, { + displayName: openapi.info?.title || `${name}-api`, + protocols: ['https'], apiId: name, - format: 'openapi-json', + format: 'openapi+json', path: '/', resourceGroupName: resourceGroup.name, + subscriptionRequired: false, serviceName: this.service.name, // XXX: Do we need to stringify this? // Not need to transform the original spec, @@ -90,25 +91,43 @@ export class NitricApiAzureApiManagement extends pulumi.ComponentResource { // Get the nitric target URL const pathMethod = path[m] as OpenAPIV3.OperationObject; - const func = services.find((f) => f.name === pathMethod['x-nitric-target'].name); + + const func = services.find( + (f) => + pathMethod[constants.OAI_NITRIC_TARGET_EXT] && + f.name === pathMethod[constants.OAI_NITRIC_TARGET_EXT].name, + ); if (func) { new apimanagement.ApiOperationPolicy( - '', + `${name}-api-${pathMethod.operationId}`, { resourceGroupName: resourceGroup.name, - apiId: this.api.id, + // this.api.id returns a URL path, which is the incorrect value here. + // We instead need the value passed to apiId in the api creation above. + // However, we want to maintain the pulumi dependency, so we need to keep the 'apply' call. + apiId: this.api.id.apply(() => name), serviceName: this.service.name, // TODO: Need to figure out how this is mapped to a real api operation entity operationId: pathMethod.operationId!, - policyId: pulumi.interpolate`${pathMethod.operationId!}Policy`, + // policyId must always be set to the static string 'policy' or Azure returns an error. + policyId: 'policy', + format: 'xml', value: pulumi.interpolate` - - - - - - + + + + + + + + + + + + + + `, }, diff --git a/packages/plugins/azure/src/resources/collection.ts b/packages/plugins/azure/src/resources/collection.ts index 08fc2e29..39109353 100644 --- a/packages/plugins/azure/src/resources/collection.ts +++ b/packages/plugins/azure/src/resources/collection.ts @@ -50,16 +50,7 @@ export class NitricCollectionCosmosMongo extends pulumi.ComponentResource { options: {}, resource: { id: collection.name, - indexes: [ - { - key: { - keys: [], - }, - options: { - unique: true, - }, - }, - ], + // _id index is created by default, add indexes here once we have them in stack }, }, defaultResourceOptions, diff --git a/packages/plugins/azure/src/resources/compute.ts b/packages/plugins/azure/src/resources/compute.ts index 14654d4c..421c4b4c 100644 --- a/packages/plugins/azure/src/resources/compute.ts +++ b/packages/plugins/azure/src/resources/compute.ts @@ -12,11 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. import * as pulumi from '@pulumi/pulumi'; -import { resources, web, containerregistry, eventgrid } from '@pulumi/azure-native'; -import { NitricContainerImage, StackFunction, StackContainer } from '@nitric/cli-common'; import { NitricEventgridTopic } from './topic'; +import { types, authorization, resources, web, containerregistry } from '@pulumi/azure-native'; +import { NitricContainerImage, StackFunction, StackContainer } from '@nitric/cli-common'; + +export interface NitricComputeAzureAppServiceEnvVariable { + name: string; + value: string | pulumi.Output; +} interface NitricComputeAzureAppServiceArgs { + /** + * SubscriptionId this resources is being deployed under + */ + subscriptionId: string | pulumi.Output; + /** * Azure resource group to deploy func to */ @@ -43,22 +53,38 @@ interface NitricComputeAzureAppServiceArgs { image: NitricContainerImage; /** - * Deployed Nitric Service Topics + * Deployed Nitric Topics that Trigger this Compute Resource */ topics: NitricEventgridTopic[]; + + /** + * Environment variables for this compute instance + */ + env?: NitricComputeAzureAppServiceEnvVariable[]; } +// Built in role definitions for Azure +// See below URL for mapping +// https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles +const ROLE_DEFINITION_MAP = { + KeyVaultSecretsOfficer: 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7', + StorageBlobDataContributor: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe', + StorageQueueDataContributor: '974c5e8b-45b9-4653-ba55-5f855dd0fb88', + EventGridDataSender: 'd5a91429-5739-47e2-a06b-3470a27159e7', +}; + /** * Azure App Service implementation of a Nitric Function or Custom Container */ export class NitricComputeAzureAppService extends pulumi.ComponentResource { public readonly name: string; public readonly webapp: web.WebApp; + public readonly subscriptions: NitricEventgridTopic[]; constructor(name: string, args: NitricComputeAzureAppServiceArgs, opts?: pulumi.ComponentResourceOptions) { super('nitric:func:AppService', name, {}, opts); const defaultResourceOptions: pulumi.ResourceOptions = { parent: this }; - const { source, resourceGroup, plan, registry, image, topics } = args; + const { source, subscriptionId, resourceGroup, plan, registry, image, topics, env = [] } = args; this.name = name; @@ -79,12 +105,16 @@ export class NitricComputeAzureAppService extends pulumi.ComponentResource { // So hopefully this should be as simple as including a gateway plugin for // Azure that utilizes that contract. // return new appservice.FunctionApp() + this.webapp = new web.WebApp( source.getName(), { serverFarmId: plan.id, name: `${source.getStack().getName()}-${source.getName()}`, resourceGroupName: resourceGroup.name, + identity: { + type: 'SystemAssigned', + }, siteConfig: { appSettings: [ { @@ -103,10 +133,20 @@ export class NitricComputeAzureAppService extends pulumi.ComponentResource { name: 'DOCKER_REGISTRY_SERVER_PASSWORD', value: adminPassword, }, + { + name: 'TOLERATE_MISSING_SERVICES', + value: 'true', + }, + { + name: 'AZURE_SUBSCRIPTION_ID', + value: subscriptionId, + }, { name: 'WEBSITES_PORT', value: '9001', }, + // Append additional env variables + ...env, ], // alwaysOn: true, linuxFxVersion: pulumi.interpolate`DOCKER|${image.imageUri}`, @@ -116,33 +156,36 @@ export class NitricComputeAzureAppService extends pulumi.ComponentResource { ); const { triggers = {} } = nitricContainer; - // Deploy an evengrid webhook subscription - (triggers.topics || []).forEach((s) => { - const topic = topics.find((t) => t.name === s); - - if (topic) { - new eventgrid.EventSubscription( - `${source.getName()}-${topic.name}-subscription`, + // Assign roles to the deployed app service + Object.entries(ROLE_DEFINITION_MAP).map( + ([name, id]) => + new authorization.RoleAssignment( + `${source.getName()}${name}`, { - eventSubscriptionName: `${source.getName()}-${topic.name}-subscription`, - scope: topic.eventgrid.id, - destination: { - endpointType: 'WebHook', - endpointUrl: this.webapp.defaultHostName, - // TODO: Reduce event chattiness here and handle internally in the Azure AppService HTTP Gateway? - maxEventsPerBatch: 1, - }, + principalId: this.webapp.identity.apply((t) => t!.principalId), + principalType: types.enums.authorization.PrincipalType.ServicePrincipal, + roleDefinitionId: pulumi.interpolate`/subscriptions/${subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/${id}`, + scope: pulumi.interpolate`subscriptions/${subscriptionId}/resourceGroups/${resourceGroup.name}`, }, defaultResourceOptions, - ); - } + ), + ); - // TODO: Throw error in case of misconfiguration? - }); + // Determine required subscriptions so they can be setup once the container starts + this.subscriptions = (triggers.topics || []) + .map((s) => { + const topic = topics.find((t) => t.name === s); + if (!topic) { + pulumi.log.error(`Failed to find matching Event Grid topic for name ${s}.`); + } + return topic; + }) + .filter((topic) => !!topic) as NitricEventgridTopic[]; this.registerOutputs({ wepapp: this.webapp, name: this.name, + subscriptions: this.subscriptions, }); } } diff --git a/packages/plugins/azure/src/resources/database-account.ts b/packages/plugins/azure/src/resources/database-account.ts new file mode 100644 index 00000000..5fe0d345 --- /dev/null +++ b/packages/plugins/azure/src/resources/database-account.ts @@ -0,0 +1,68 @@ +// Copyright 2021, Nitric Technologies Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import * as pulumi from '@pulumi/pulumi'; +import { resources, documentdb } from '@pulumi/azure-native'; + +interface NitricDatabaseAccountMongoDBArgs { + resourceGroup: resources.ResourceGroup; +} + +/** + * Nitric Azure CosmosDB based DatabaseAccount + */ +export class NitricDatabaseAccountMongoDB extends pulumi.ComponentResource { + public readonly name: string; + public readonly account: documentdb.DatabaseAccount; + public readonly resourceGroup: resources.ResourceGroup; + + constructor(name: string, args: NitricDatabaseAccountMongoDBArgs, opts?: pulumi.ComponentResourceOptions) { + super('nitric:database:Account', name, {}, opts); + const { resourceGroup } = args; + + this.name = name; + this.resourceGroup = resourceGroup; + + this.account = new documentdb.DatabaseAccount(`${name}-db-account`, { + resourceGroupName: resourceGroup.name, + // 24 character limit + accountName: `${name.replace(/-/g, '')}`, + kind: 'MongoDB', + apiProperties: { + serverVersion: '4.0', + }, + location: resourceGroup.location, + databaseAccountOfferType: 'Standard', + locations: [ + // what should we make these? TODO + { + failoverPriority: 0, + isZoneRedundant: false, + locationName: 'southcentralus', + }, + { + failoverPriority: 1, + isZoneRedundant: false, + locationName: 'eastus', + }, + ], + }); + + // Finalize the deployment + this.registerOutputs({ + resourceGroup: this.resourceGroup, + account: this.account, + name: this.name, + }); + } +} diff --git a/packages/plugins/azure/src/resources/database.ts b/packages/plugins/azure/src/resources/database.ts new file mode 100644 index 00000000..937e580e --- /dev/null +++ b/packages/plugins/azure/src/resources/database.ts @@ -0,0 +1,54 @@ +// Copyright 2021, Nitric Technologies Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import * as pulumi from '@pulumi/pulumi'; +import { resources, documentdb } from '@pulumi/azure-native'; + +interface NitricDatabaseCosmosDBMongoArgs { + account: documentdb.DatabaseAccount; + resourceGroup: resources.ResourceGroup; +} + +/** + * Nitric Azure CosmosDB based Database + */ +export class NitricDatabaseCosmosMongo extends pulumi.ComponentResource { + public readonly name: string; + public readonly database: documentdb.MongoDBResourceMongoDBDatabase; + public readonly resourceGroup: resources.ResourceGroup; + + constructor(name: string, args: NitricDatabaseCosmosDBMongoArgs, opts?: pulumi.ComponentResourceOptions) { + super('nitric:database:CosmosMongo', name, {}, opts); + const { account, resourceGroup } = args; + + this.name = name; + this.resourceGroup = resourceGroup; + + this.database = new documentdb.MongoDBResourceMongoDBDatabase(name, { + accountName: account.name, + databaseName: name, + location: resourceGroup.location, + resource: { + id: name, + }, + resourceGroupName: resourceGroup.name, + }); + + // Finalize the deployment + this.registerOutputs({ + resourceGroup: this.resourceGroup, + database: this.database, + name: this.name, + }); + } +} diff --git a/packages/plugins/azure/src/resources/entrypoint.ts b/packages/plugins/azure/src/resources/entrypoint.ts index fbfa77dd..5fb47fcd 100644 --- a/packages/plugins/azure/src/resources/entrypoint.ts +++ b/packages/plugins/azure/src/resources/entrypoint.ts @@ -13,10 +13,12 @@ // limitations under the License. import * as pulumi from '@pulumi/pulumi'; import { NamedObject, NitricEntrypoint } from '@nitric/cli-common'; -import { resources } from '@pulumi/azure-native'; +import { resources, network, types } from '@pulumi/azure-native'; import { NitricComputeAzureAppService, NitricAzureStorageSite, NitricApiAzureApiManagement } from '.'; interface NitricAzureStorageBucketArgs { + stackName: string; + subscriptionId: pulumi.Input; entrypoint: NamedObject; services: NitricComputeAzureAppService[]; sites: NitricAzureStorageSite[]; @@ -24,24 +26,227 @@ interface NitricAzureStorageBucketArgs { resourceGroup: resources.ResourceGroup; } +interface TranslatedRouteRules { + pool: types.input.network.BackendPoolArgs; + route: types.input.network.RoutingRuleArgs; +} + +type DefaultOdataType = pulumi.Input<'#Microsoft.Azure.FrontDoor.Models.FrontdoorForwardingConfiguration'>; + +const DEFAULT_ODATA_TYPE: DefaultOdataType = '#Microsoft.Azure.FrontDoor.Models.FrontdoorForwardingConfiguration'; + /** - * Nitric Azure Front Door based Entrypoing + * Nitric Azure Front Door based Entrypoint */ export class NitricEntrypointAzureFrontDoor extends pulumi.ComponentResource { public readonly name: string; + public readonly domains?: string[]; // TODO: Create resource - //public readonly frontdoor: network.FrontDoor; public readonly resourceGroup: resources.ResourceGroup; + public readonly frontdoor: network.FrontDoor; constructor(name: string, args: NitricAzureStorageBucketArgs, opts?: pulumi.ComponentResourceOptions) { - super('nitric:bucket:AzureStorage', name, {}, opts); - // const defaultResourceOptions: pulumi.ResourceOptions = { parent: this }; - const { resourceGroup } = args; + super('nitric:entrypoint:AzureFrontDoor', name, {}, opts); + const defaultResourceOptions: pulumi.ResourceOptions = { parent: this }; + const { resourceGroup, subscriptionId, entrypoint, stackName, sites, apis, services } = args; this.name = name; this.resourceGroup = resourceGroup; - // TODO: Implement entrypoint creation + this.domains = entrypoint.domains; + const frontDoorName = `${stackName}-${name}`; + + const rules = Object.keys(entrypoint.paths).map((key) => { + const { type, target } = entrypoint.paths[key]; + + const poolName = `${type}-${target}-pool`; + const routeName = `${type}-${target}-route`; + const routeConfig = { + backendPool: { + id: pulumi.interpolate`/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup.name}/providers/Microsoft.Network/frontDoors/${frontDoorName}/backendPools/${poolName}`, + }, + odataType: DEFAULT_ODATA_TYPE, + }; + const feEndpoints = [ + { + id: pulumi.interpolate`/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup.name}/providers/Microsoft.Network/frontDoors/${frontDoorName}/frontendEndpoints/default`, + }, + ]; + const healthProbeSettings = { + id: pulumi.interpolate`/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup.name}/providers/Microsoft.Network/frontDoors/${frontDoorName}/healthProbeSettings/healthProbeSettings1`, + }; + const loadBalancingSettings = { + id: pulumi.interpolate`/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup.name}/providers/Microsoft.Network/frontDoors/${frontDoorName}/loadBalancingSettings/loadBalancingSettings1`, + }; + + switch (type) { + case 'api': { + const deployedApi = apis.find((a) => a.name === target); + + if (!deployedApi) { + throw new Error(`Target API ${name} configured in entrypoints but does not exist`); + } + + const domainName = deployedApi.api.serviceUrl; + if (!domainName) { + throw new Error(`Target API ${name} does not have a defined serviceUrl`); + } + + return { + pool: { + name: poolName, + backends: [ + { + address: domainName as pulumi.Output, + backendHostHeader: domainName as pulumi.Output, + httpPort: 80, + httpsPort: 443, + priority: 1, + weight: 1, + }, + ], + loadBalancingSettings, + healthProbeSettings, + }, + route: { + name: routeName, + acceptedProtocols: ['Https'], + enabledState: 'Enabled', + patternsToMatch: [`${key}*`], + routeConfiguration: routeConfig, + frontendEndpoints: feEndpoints, + }, + }; + } + case 'site': { + const deployedSite = sites.find((a) => a.name === target); + + if (!deployedSite) { + throw new Error(`Target Site ${name} configured in entrypoints but does not exist`); + } + + const endpointOrigin = deployedSite.storageAccount.primaryEndpoints.apply((ep) => + ep.web.replace('https://', '').replace('/', ''), + ); + + return { + pool: { + name: poolName, + backends: [ + { + address: endpointOrigin, + backendHostHeader: endpointOrigin, + httpPort: 80, + httpsPort: 443, + priority: 1, + weight: 1, + }, + ], + loadBalancingSettings, + healthProbeSettings, + }, + route: { + name: routeName, + acceptedProtocols: ['Https'], + enabledState: 'Enabled', + patternsToMatch: [`${key}*`], + // TODO: Will probably change this to redirect instead of forwarding for sites + routeConfiguration: routeConfig, + frontendEndpoints: feEndpoints, + }, + }; + } + case 'container': + case 'function': { + const deployedService = services.find((a) => a.name === target); + + if (!deployedService) { + throw new Error(`Target container or function ${name} configured in entrypoints but does not exist`); + } + + return { + pool: { + name: poolName, + backends: [ + { + address: deployedService.webapp.defaultHostName, + backendHostHeader: deployedService.webapp.defaultHostName, + httpPort: 80, + httpsPort: 443, + priority: 1, + weight: 1, + }, + ], + healthProbeSettings, + loadBalancingSettings, + }, + route: { + name: routeName, + acceptedProtocols: ['Https'], + enabledState: 'Enabled', + patternsToMatch: [`${key}*`], + routeConfiguration: routeConfig, + frontendEndpoints: feEndpoints, + }, + }; + } + default: { + throw new Error(`Invalid entrypoint type:${type} defined`); + } + } + }); + + this.frontdoor = new network.FrontDoor( + this.name, + { + // Location MUST be global + location: 'global', + resourceGroupName: this.resourceGroup.name, + enabledState: 'Enabled', + frontDoorName, + // Create backend pools from stack configuration + backendPools: rules.map(({ pool }) => pool), + // Add default front door and any custom + // domain names + frontendEndpoints: [ + // TODO: Create domain mapped http endpoints based + // on user configeured domains + //{ + // hostName: 'www.contoso.com', + // name: 'frontendEndpoint1', + // sessionAffinityEnabledState: 'Enabled', + // sessionAffinityTtlSeconds: 60, + // webApplicationFirewallPolicyLink: { + // id: '/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/frontDoorWebApplicationFirewallPolicies/policy1', + // }, + //}, + { + hostName: `${frontDoorName}.azurefd.net`, + name: 'default', + }, + ], + // Create routing rules from load balancer path configs + routingRules: rules.map(({ route }) => route), + loadBalancingSettings: [ + { + name: 'loadBalancingSettings1', + sampleSize: 4, + successfulSamplesRequired: 2, + }, + ], + healthProbeSettings: [ + { + enabledState: 'Disabled', + healthProbeMethod: 'HEAD', + intervalInSeconds: 120, + name: 'healthProbeSettings1', + path: '/', + protocol: 'Http', + }, + ], + }, + defaultResourceOptions, + ); // Finalize the deployment this.registerOutputs({ diff --git a/packages/plugins/azure/src/resources/index.ts b/packages/plugins/azure/src/resources/index.ts index 2a74115a..2775989e 100644 --- a/packages/plugins/azure/src/resources/index.ts +++ b/packages/plugins/azure/src/resources/index.ts @@ -17,6 +17,8 @@ export * from './topic'; export * from './queue'; export * from './compute'; export * from './collection'; +export * from './database-account'; +export * from './database'; export * from './site'; export * from './api'; export * from './entrypoint'; diff --git a/packages/plugins/azure/src/resources/site.ts b/packages/plugins/azure/src/resources/site.ts index db6eb112..30d1004c 100644 --- a/packages/plugins/azure/src/resources/site.ts +++ b/packages/plugins/azure/src/resources/site.ts @@ -15,6 +15,7 @@ import * as pulumi from '@pulumi/pulumi'; import path from 'path'; import { crawlDirectorySync, StackSite } from '@nitric/cli-common'; import { resources, storage } from '@pulumi/azure-native'; +import mime from 'mime-types'; interface NitricAzureStorageBucketArgs { site: StackSite; @@ -29,6 +30,7 @@ export class NitricAzureStorageSite extends pulumi.ComponentResource { public readonly name: string; public readonly container: storage.StorageAccountStaticWebsite; public readonly resourceGroup: resources.ResourceGroup; + public readonly storageAccount: storage.StorageAccount; constructor(name: string, args: NitricAzureStorageBucketArgs, opts?: pulumi.ComponentResourceOptions) { super('nitric:site:AzureStorage', name, {}, opts); @@ -37,13 +39,14 @@ export class NitricAzureStorageSite extends pulumi.ComponentResource { this.name = name; this.resourceGroup = resourceGroup; + this.storageAccount = storageAcct; // TODO: Add additional config for index/error documents this.container = new storage.StorageAccountStaticWebsite( site.getName(), { resourceGroupName: resourceGroup.name, - accountName: storageAcct.name, + accountName: this.storageAccount.name, indexDocument: 'index.html', }, defaultResourceOptions, @@ -61,6 +64,7 @@ export class NitricAzureStorageSite extends pulumi.ComponentResource { accountName: storageAcct.name, containerName: this.container.containerName, source: new pulumi.asset.FileAsset(filePath), + contentType: mime.lookup(filePath) || undefined, }, defaultResourceOptions, ); @@ -69,6 +73,7 @@ export class NitricAzureStorageSite extends pulumi.ComponentResource { // Finalize the deployment this.registerOutputs({ resourceGroup: this.resourceGroup, + storgeAccount: this.storageAccount, container: this.container, name: this.name, }); diff --git a/packages/plugins/azure/src/resources/topic.ts b/packages/plugins/azure/src/resources/topic.ts index 1076de63..9812125d 100644 --- a/packages/plugins/azure/src/resources/topic.ts +++ b/packages/plugins/azure/src/resources/topic.ts @@ -16,6 +16,9 @@ import { NamedObject, NitricTopic } from '@nitric/cli-common'; import { resources, eventgrid } from '@pulumi/azure-native'; interface NitricEventgridTopicArgs { + /** + * Nitric Topic Definition which defines how to create this Event Grid Topic + */ topic: NamedObject; resourceGroup: resources.ResourceGroup; } @@ -25,7 +28,7 @@ interface NitricEventgridTopicArgs { */ export class NitricEventgridTopic extends pulumi.ComponentResource { public readonly name: string; - public readonly eventgrid: eventgrid.Topic; + public readonly eventGridTopic: eventgrid.Topic; public readonly resourceGroup: resources.ResourceGroup; constructor(name: string, args: NitricEventgridTopicArgs, opts?: pulumi.ComponentResourceOptions) { @@ -35,7 +38,7 @@ export class NitricEventgridTopic extends pulumi.ComponentResource { this.name = name; this.resourceGroup = resourceGroup; - this.eventgrid = new eventgrid.Topic( + this.eventGridTopic = new eventgrid.Topic( topic.name, { topicName: topic.name, @@ -47,23 +50,8 @@ export class NitricEventgridTopic extends pulumi.ComponentResource { // Finalize the deployment this.registerOutputs({ resourceGroup: this.resourceGroup, - eventgrid: this.eventgrid, + eventgrid: this.eventGridTopic, name: this.name, }); } - - /** - * - * @returns - */ - public getSasKeys = (): pulumi.Output<[string, string]> => { - const sasKeys = pulumi.all([this.resourceGroup.name, this.eventgrid.name]).apply(([resourceGroupName, topicName]) => - eventgrid.listTopicSharedAccessKeys({ - resourceGroupName, - topicName, - }), - ); - - return sasKeys.apply((k) => [k.key1!, k.key2!]) as pulumi.Output<[string, string]>; - }; } diff --git a/packages/plugins/azure/src/tasks/deploy/index.ts b/packages/plugins/azure/src/tasks/deploy/index.ts index 623e7ae8..d98112e1 100644 --- a/packages/plugins/azure/src/tasks/deploy/index.ts +++ b/packages/plugins/azure/src/tasks/deploy/index.ts @@ -14,7 +14,16 @@ import { Stack, Task, mapObject, NitricContainerImage } from '@nitric/cli-common'; import { LocalWorkspace } from '@pulumi/pulumi/automation'; -import { resources, storage, web, containerregistry } from '@pulumi/azure-native'; +import { + authorization, + resources, + storage, + web, + containerregistry, + keyvault, + eventgrid, + documentdb, +} from '@pulumi/azure-native'; import * as pulumi from '@pulumi/pulumi'; import fs from 'fs'; import path from 'path'; @@ -26,13 +35,20 @@ import { NitricAzureStorageSite, NitricApiAzureApiManagement, NitricEntrypointAzureFrontDoor, + NitricCollectionCosmosMongo, + NitricDatabaseCosmosMongo, + NitricDatabaseAccountMongoDB, + NitricComputeAzureAppServiceEnvVariable, } from '../../resources'; +import { AppServicePlan } from '../../types'; +import axios from 'axios'; interface DeployOptions { stack: Stack; region: string; orgName: string; adminEmail: string; + servicePlan: AppServicePlan; } export class Deploy extends Task { @@ -40,18 +56,27 @@ export class Deploy extends Task { private orgName: string; private adminEmail: string; private region: string; + private servicePlan: AppServicePlan; - constructor({ stack, orgName, adminEmail, region }: DeployOptions) { + constructor({ stack, orgName, adminEmail, region, servicePlan }: DeployOptions) { super('Deploying Infrastructure'); this.stack = stack; this.orgName = orgName; this.adminEmail = adminEmail; this.region = region; + this.servicePlan = servicePlan; } async do(): Promise { const { stack, orgName, adminEmail, region } = this; - const { buckets = {}, topics = {}, schedules = {}, queues = {}, entrypoints = {} } = stack.asNitricStack(); + const { + buckets = {}, + collections = {}, + topics = {}, + schedules = {}, + queues = {}, + entrypoints = {}, + } = stack.asNitricStack(); // Use absolute path to log files, so it's easier for users to locate them if printed to the console. const errorFile = path.resolve(await stack.getLoggingFile('error-azure')); @@ -67,6 +92,8 @@ export class Deploy extends Task { program: async () => { // Now we can start deploying with Pulumi try { + const clientConfig = await authorization.getClientConfig(); + // Create a new resource group for the nitric stack // This'll be used for basically everything we deploy in this stack const resourceGroup = new resources.ResourceGroup(stack.getName(), { @@ -74,20 +101,55 @@ export class Deploy extends Task { location: region, }); + // Create a stack level keyvault if secrets are enabled + // At the moment secrets have no config level setting + const kvault = new keyvault.Vault(`${stack.getName()}`.substring(0, 13), { + resourceGroupName: resourceGroup.name, + properties: { + enableSoftDelete: false, + enableRbacAuthorization: true, + sku: { + family: 'A', + name: 'standard', + }, + tenantId: clientConfig.tenantId, + }, + }); + + // Universal app service environment variables + let appServiceEnv: NitricComputeAzureAppServiceEnvVariable[] = [ + { + name: 'KVAULT_NAME', + value: kvault.name, + }, + ]; + // Create a new storage account for this stack // DEPLOY STORAGE BASED ASSETS let deployedSites: NitricAzureStorageSite[] = []; if (Object.keys(buckets).length || Object.keys(queues).length || stack.getSites().length) { - const account = new storage.StorageAccount(`${stack.getName()}-storage-account`, { + const account = new storage.StorageAccount(`${stack.getName()}`.replace(/-/g, '').substring(0, 13), { resourceGroupName: resourceGroup.name, - // 24 character limit - accountName: `${stack.getName().replace(/-/g, '')}`, - kind: 'Storage', + kind: storage.Kind.StorageV2, sku: { - name: 'Standard_LRS', + name: storage.SkuName.Standard_LRS, }, }); + // Ensure deployed app services are pointed to + // the created storage account endpoints + appServiceEnv = [ + ...appServiceEnv, + { + name: 'AZURE_STORAGE_ACCOUNT_BLOB_ENDPOINT', + value: account.primaryEndpoints.blob, + }, + { + name: 'AZURE_STORAGE_ACCOUNT_QUEUE_ENDPOINT', + value: account.primaryEndpoints.queue, + }, + ]; + // Not using refeschedulerrences produced currently, // but leaving as map in case we need to reference in future mapObject(buckets || {}).map( @@ -125,9 +187,63 @@ export class Deploy extends Task { }), ); + if (Object.keys(collections).length) { + // CREATE DB ACCOUNT + const { account } = new NitricDatabaseAccountMongoDB( + `${stack.getName()}`.replace(/-/g, '').substring(0, 13), + { + resourceGroup, + }, + ); + + // CREATE DB + const { database } = new NitricDatabaseCosmosMongo(stack.getName(), { + account, + resourceGroup, + }); + + // get connection string + const connectionStrings = pulumi + .all([resourceGroup.name, account.name]) + .apply(([resourceGroupName, accountName]) => + documentdb.listDatabaseAccountConnectionStrings({ resourceGroupName, accountName }), + ); + + const connectionString = connectionStrings.apply((cs) => cs.connectionStrings![0].connectionString); + + if (!connectionString) { + throw new Error('No connection strings found for azure database'); + } + + // TODO: Add connection string to app service instance + appServiceEnv = [ + ...appServiceEnv, + // Add the DB connection string and database name for the stack shared database + { + name: 'MONGODB_CONNECTION_STRING', + value: connectionString, + }, + { + name: 'MONGODB_DATABASE', + value: database.name, + }, + ]; + + // DEPLOY COLLECTIONS + mapObject(collections).map( + (coll) => + new NitricCollectionCosmosMongo(coll.name, { + collection: coll, + account, + database, + resourceGroup, + }), + ); + } + // DEPLOY SERVICES let deployedAzureApps: NitricComputeAzureAppService[] = []; - if (stack.getFunctions().length > 0) { + if (stack.getFunctions().length > 0 || stack.getContainers().length > 0) { // deploy a registry for deploying this stacks containers // TODO: We will want to prefer a pre-existing registry, supplied by the user const registry = new containerregistry.Registry(`${stack.getName()}-registry`, { @@ -148,14 +264,9 @@ export class Deploy extends Task { kind: 'Linux', reserved: true, sku: { - // for development only - // Will upgrade tiers/elasticity for different stack tiers e.g. dev/test/prod (prefab recipes) - //name: 'B1', - //tier: 'Basic', - //size: 'B1', - name: 'F1', - tier: 'Free', - size: 'F1', + name: this.servicePlan.size, + tier: this.servicePlan.tier, + size: this.servicePlan.size, }, }); @@ -186,11 +297,13 @@ export class Deploy extends Task { // Create a new Nitric azure app func instance return new NitricComputeAzureAppService(func.getName(), { source: func, + subscriptionId: clientConfig.subscriptionId, resourceGroup, plan, registry, topics: deployedTopics, image, + env: appServiceEnv, }); }), ...stack.getContainers().map((container) => { @@ -207,20 +320,94 @@ export class Deploy extends Task { // Create a new Nitric azure app func instance return new NitricComputeAzureAppService(container.getName(), { source: container, + subscriptionId: clientConfig.subscriptionId, resourceGroup, plan, registry, topics: deployedTopics, image, + env: appServiceEnv, }); }), ]; } + const maxWaitTime = 300000; // 5 minutes. + + // Setup subscriptions + await Promise.all( + deployedAzureApps + .filter((deployed) => deployed.subscriptions.length) + .map(async (deployed) => { + pulumi.log.info(`waiting for ${deployed.name} to start before creating subscriptions`); + // Get the full URL of the deployed container + const hostname = await new Promise((res) => deployed.webapp.defaultHostName.apply(res)); + const hostUrl = `https://${hostname}`; + + // Poll the URL until the host has started. + const start = Date.now(); + while (Date.now() - start <= maxWaitTime) { + pulumi.log.info(`attempting to contact container ${deployed.name} via ${hostUrl}`); + try { + // TODO: Implement a membrane health check handler in the Membrane and trigger that instead. + // Set event type header to simulate a subscription validation event. + // These events are automatically resolved by the Membrane and won't be processed by handlers. + const config = { + headers: { + 'aeg-event-type': 'SubscriptionValidation', + }, + }; + // Provide data in the expected shape. The content is current not important. + const data = [ + { + id: '', + topic: '', + subject: '', + eventType: '', + metadataVersion: '', + dataVersion: '', + data: { + validationCode: '', + validationUrl: '', + }, + }, + ]; + const resp = await axios.post(hostUrl, JSON.stringify(data), config); + pulumi.log.info( + `container ${deployed.name} is now available with status ${resp.status}, setting up subscription`, + ); + break; + } catch (err) { + console.log(err); + pulumi.log.info('failed to contact container'); + } + } + pulumi.log.info(`creating subscriptions for ${deployed.name}`); + + return deployed.subscriptions.map( + (sub) => + new eventgrid.EventSubscription(`${deployed.name}-${sub.name}-subscription`, { + eventSubscriptionName: `${deployed.name}-${sub.name}-subscription`, + scope: sub.eventGridTopic.id, + destination: { + endpointType: 'WebHook', + endpointUrl: hostUrl, + // TODO: Reduce event chattiness here and handle internally in the Azure AppService HTTP Gateway? + maxEventsPerBatch: 1, + }, + retryPolicy: { + maxDeliveryAttempts: 30, + eventTimeToLiveInMinutes: 5, + }, + }), + ); + }), + ); + // TODO: Add schedule support // NOTE: Currently CRONTAB support is required, we either need to revisit the design of - // our scheduled expressions or implement a workaround for request a feature. - if (schedules) { + // our scheduled expressions or implement a workaround or request a feature. + if (Object.keys(schedules).length) { pulumi.log.warn('Schedules are not currently supported for Azure deployments'); // schedules.map(s => createSchedule(resourceGroup, s)) } @@ -236,11 +423,13 @@ export class Deploy extends Task { }), ); - // FIXME: Implement front door deployment logic, + // FIXME: Implement Front Door deployment logic, // class is currently just a placeholder mapObject(entrypoints).map( (e) => new NitricEntrypointAzureFrontDoor(e.name, { + stackName: stack.getName(), + subscriptionId: clientConfig.subscriptionId, resourceGroup, entrypoint: e, services: deployedAzureApps, @@ -248,10 +437,10 @@ export class Deploy extends Task { apis: deployedApis, }), ); - } catch (e) { + } catch (err) { pulumi.log.error(`An error occurred, see latest azure:error log for details: ${errorFile}`); - fs.appendFileSync(errorFile, e.stack || e.toString()); - throw e; + fs.appendFileSync(errorFile, (err as Error).stack || (err as Error).toString()); + throw err; } }, }); @@ -266,9 +455,9 @@ export class Deploy extends Task { }, }); console.log(upRes); - } catch (e) { - fs.appendFileSync(errorFile, e.stack || e.toString()); - throw new Error(`An error occurred, see latest do:error log for details: ${errorFile}`); + } catch (err) { + fs.appendFileSync(errorFile, (err as Error).stack || (err as Error).toString()); + throw new Error(`An error occurred, see latest azure:error log for details: ${errorFile}`); } } } diff --git a/packages/plugins/azure/src/tasks/doctor/check-pulumi-plugin.ts b/packages/plugins/azure/src/tasks/doctor/check-pulumi-plugin.ts index da60290c..c73b9320 100644 --- a/packages/plugins/azure/src/tasks/doctor/check-pulumi-plugin.ts +++ b/packages/plugins/azure/src/tasks/doctor/check-pulumi-plugin.ts @@ -17,7 +17,7 @@ import execa from 'execa'; export class CheckPulumiPluginTask extends Task { constructor() { - super('Checking azure pulumi plugin installation'); + super('Checking azure-native pulumi plugin installation'); } async do(): Promise { @@ -25,7 +25,7 @@ export class CheckPulumiPluginTask extends Task { try { const result = execa.commandSync('pulumi plugin ls'); - if (result.stdout.includes('azure')) { + if (result.stdout.includes('azure-native')) { return true; } } catch (e) { diff --git a/packages/plugins/azure/src/tasks/doctor/install-pulumi-plugin.ts b/packages/plugins/azure/src/tasks/doctor/install-pulumi-plugin.ts index 06ad0d56..e99d2b58 100644 --- a/packages/plugins/azure/src/tasks/doctor/install-pulumi-plugin.ts +++ b/packages/plugins/azure/src/tasks/doctor/install-pulumi-plugin.ts @@ -17,11 +17,11 @@ import execa from 'execa'; export class InstallPulumiPluginTask extends Task { constructor() { - super('Installing azure pulumi plugin'); + super('Installing azure-native pulumi plugin'); } async do(): Promise { // Install the pulumi config - execa.commandSync('pulumi plugin install resource azure v3.46.0'); + execa.commandSync('pulumi plugin install resource azure-native v1.19.0'); } } diff --git a/packages/plugins/azure/src/tasks/down/index.ts b/packages/plugins/azure/src/tasks/down/index.ts index 08e55a10..fb97be73 100644 --- a/packages/plugins/azure/src/tasks/down/index.ts +++ b/packages/plugins/azure/src/tasks/down/index.ts @@ -14,21 +14,50 @@ import { NitricStack, Task } from '@nitric/cli-common'; import { LocalWorkspace } from '@pulumi/pulumi/automation'; +import Deployment from '../../types/deployment'; interface DownOptions { stack: NitricStack; + destroy: boolean; } +interface Target { + type: string; //e.g. bucket + pulumiTypes: string[]; +} + +//Map of the protect destroy targets +const protectedTargets: Target[] = [ + { + type: 'base', + pulumiTypes: ['pulumi:pulumi:Stack', 'pulumi:providers:azure-native', 'azure-native:resources:ResourceGroup'], + }, + { + type: 'bucket', + pulumiTypes: [ + 'azure-native:storage:StorageAccount', + 'azure-native:storage:BlobContainer', + 'nitric:bucket:AzureStorage', + ], + }, + { + type: 'secret', + pulumiTypes: ['azure-native:keyvault:Vault'], + }, +]; + const NO_OP = async (): Promise => { return; }; export class Down extends Task { private stack: NitricStack; + private destroy: boolean; - constructor({ stack }: DownOptions) { + constructor({ stack, destroy }: DownOptions) { super(`Tearing Down Stack: ${stack.name}`); this.stack = stack; + this.destroy = destroy; } async do(): Promise { @@ -42,7 +71,22 @@ export class Down extends Task { program: NO_OP, }); - const res = await pulumiStack.destroy({ onOutput: this.update.bind(this) }); + let res; + if (this.destroy) { + res = await pulumiStack.destroy({ onOutput: this.update.bind(this) }); + } else { + const deployment = (await pulumiStack.exportStack()).deployment as Deployment; + const nonTargets = protectedTargets //Possible to filter the protected targets in the future + .map((val) => val.pulumiTypes) + .reduce((acc, val) => acc.concat(val), []); + //List of targets that will be destroyed, filters out the ones that are protected + const targets = deployment.resources + .filter((resource) => !nonTargets.includes(resource.type)) + .map((resource) => resource.urn); + if (targets.length > 0) { + res = await pulumiStack.destroy({ onOutput: this.update.bind(this), target: targets }); + } + } console.log(res); } catch (e) { console.log(e); diff --git a/packages/plugins/azure/src/types/deployment.ts b/packages/plugins/azure/src/types/deployment.ts new file mode 100644 index 00000000..80716d94 --- /dev/null +++ b/packages/plugins/azure/src/types/deployment.ts @@ -0,0 +1,28 @@ +// Copyright 2021, Nitric Technologies Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +interface Resource { + urn: string; + custom: boolean; + type: string; + outputs: string[]; + parent: string; + dependencies: string[]; +} + +interface Deployment { + resources: Resource[]; +} + +export default Deployment; diff --git a/packages/plugins/azure/src/types/index.ts b/packages/plugins/azure/src/types/index.ts new file mode 100644 index 00000000..0b6986f1 --- /dev/null +++ b/packages/plugins/azure/src/types/index.ts @@ -0,0 +1,18 @@ +// Copyright 2021, Nitric Technologies Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export interface AppServicePlan { + tier: string; + size: string; +} diff --git a/packages/plugins/do/package.json b/packages/plugins/do/package.json index a4ceef35..0f918efa 100644 --- a/packages/plugins/do/package.json +++ b/packages/plugins/do/package.json @@ -1,7 +1,6 @@ { "name": "@nitric/plugin-do", "description": "Digital Ocean CLI plugin for Nitric", - "version": "0.0.86", "author": "Nitric ", "bugs": "https://github.com/nitrictech/cli/issues", "dependencies": { @@ -60,10 +59,10 @@ "directory": "packages/plugins/do" }, "scripts": { - "postpack": "rm -f oclif.manifest.json", - "prepack": "rm -rf lib && tsc -p tsconfig.build.json && oclif-dev manifest && oclif-dev readme", + "postpack": "rimraf -f oclif.manifest.json", + "prepack": "rimraf -rf lib && tsc -p tsconfig.build.json && oclif-dev manifest && oclif-dev readme", "test": "tsc --emitDeclarationOnly && jest --passWithNoTests", - "version": "oclif-dev readme && git add README.md", + "set:version": "npm version --version-git-tag false", "npm:publish": "yarn npm publish --access public --tolerate-republish" } } diff --git a/packages/plugins/do/src/commands/deploy/do.ts b/packages/plugins/do/src/commands/deploy/do.ts index e165fbb4..26ba9f30 100644 --- a/packages/plugins/do/src/commands/deploy/do.ts +++ b/packages/plugins/do/src/commands/deploy/do.ts @@ -14,7 +14,15 @@ import { flags } from '@oclif/command'; import { Deploy, DEPLOY_TASK_KEY, DeployResults } from '../../tasks/deploy'; -import { BaseCommand, wrapTaskForListr, Stack, block, constants, createBuildListrTask } from '@nitric/cli-common'; +import { + BaseCommand, + wrapTaskForListr, + Stack, + block, + constants, + createBuildListrTask, + checkDockerDaemon, +} from '@nitric/cli-common'; import { Listr } from 'listr2'; import path from 'path'; import inquirer from 'inquirer'; @@ -46,6 +54,9 @@ export default class DoDeploy extends BaseCommand { static args = [{ name: 'dir', default: '.' }]; async do(): Promise { + // Check docker daemon is running + checkDockerDaemon('doctor:do'); + const { args, flags } = this.parse(DoDeploy); const { dir } = args; diff --git a/packages/plugins/gcp/package.json b/packages/plugins/gcp/package.json index b4e08060..647f5dd0 100644 --- a/packages/plugins/gcp/package.json +++ b/packages/plugins/gcp/package.json @@ -1,7 +1,6 @@ { "name": "@nitric/plugin-gcp", "description": "GCP plugin for Nitric", - "version": "0.0.86", "author": "Nitric ", "bugs": "https://github.com/nitrictech/cli/issues", "dependencies": { @@ -65,10 +64,10 @@ "directory": "packages/plugins/gcp" }, "scripts": { - "postpack": "rm -f oclif.manifest.json", - "prepack": "rm -rf lib && tsc -p tsconfig.build.json && oclif-dev manifest && oclif-dev readme", + "postpack": "rimraf -f oclif.manifest.json", + "prepack": "rimraf -rf lib && tsc -p tsconfig.build.json && oclif-dev manifest && oclif-dev readme", "test": "tsc --emitDeclarationOnly && jest --passWithNoTests", - "version": "oclif-dev readme && git add README.md", + "set:version": "npm version --version-git-tag false", "npm:publish": "yarn npm publish --access public --tolerate-republish" } } diff --git a/packages/plugins/gcp/src/commands/deploy/gcp.ts b/packages/plugins/gcp/src/commands/deploy/gcp.ts index cd2ca9cb..66ed4147 100644 --- a/packages/plugins/gcp/src/commands/deploy/gcp.ts +++ b/packages/plugins/gcp/src/commands/deploy/gcp.ts @@ -14,7 +14,14 @@ import { flags } from '@oclif/command'; import { Deploy, DeployResult, DEPLOY_TASK_KEY } from '../../tasks/deploy'; -import { BaseCommand, wrapTaskForListr, Stack, constants, createBuildListrTask } from '@nitric/cli-common'; +import { + BaseCommand, + wrapTaskForListr, + Stack, + constants, + createBuildListrTask, + checkDockerDaemon, +} from '@nitric/cli-common'; import { Listr } from 'listr2'; import cli from 'cli-ux'; import path from 'path'; @@ -87,10 +94,13 @@ export default class GcpDeploy extends BaseCommand { static args = [{ name: 'dir' }]; async do(): Promise { + // Check docker daemon is running + checkDockerDaemon('doctor:gcp'); + const auth = new google.auth.GoogleAuth({ scopes: ['https://www.googleapis.com/auth/cloud-platform'], }); - const derivedProject = (await auth.getClient()).projectId; + const derivedProject = await auth.getProjectId(); const { args, flags } = this.parse(GcpDeploy); const { nonInteractive } = flags; const { dir = '.' } = args; @@ -116,9 +126,22 @@ export default class GcpDeploy extends BaseCommand { promptFlags = await inquirer.prompt(prompts); } - const { project = derivedProject, file, region } = { ...flags, ...promptFlags }; + const finalFlags = { ...flags, ...promptFlags }; + + let { project } = finalFlags; + const { file, region } = finalFlags; const stackDefinitionPath = path.join(dir, file); + if (!project && !derivedProject) { + throw new Error( + 'GCP project must be provided via the -p flag or configured locally (see GCP "Setting Credentials" documentation)', + ); + } + + if (!project?.length) { + project = derivedProject; + } + const stack = await Stack.fromFile(stackDefinitionPath); if (!region) { diff --git a/packages/plugins/gcp/src/commands/down/gcp.ts b/packages/plugins/gcp/src/commands/down/gcp.ts index 306330c7..04175523 100644 --- a/packages/plugins/gcp/src/commands/down/gcp.ts +++ b/packages/plugins/gcp/src/commands/down/gcp.ts @@ -24,6 +24,11 @@ const BaseFlags = { char: 'f', default: 'nitric.yaml', }), + destroy: flags.boolean({ + char: 'd', + default: false, + description: 'destroy all resources, including buckets, secrets, and collections', + }), }; /** @@ -78,7 +83,7 @@ export default class DownCmd extends BaseCommand { promptFlags = await inquirer.prompt(prompts); } - const { file } = { ...flags, ...promptFlags }; + const { file, destroy } = { ...flags, ...promptFlags }; const stackDefinitionPath = path.join(dir, file); const stack: NitricStack = await (await Stack.fromFile(stackDefinitionPath)).asNitricStack(); @@ -87,6 +92,7 @@ export default class DownCmd extends BaseCommand { wrapTaskForListr( new Down({ stackName: stack.name, + destroy: destroy, }), ), ], diff --git a/packages/plugins/gcp/src/resources/api.test.ts b/packages/plugins/gcp/src/resources/api.test.ts new file mode 100644 index 00000000..d6e31bdc --- /dev/null +++ b/packages/plugins/gcp/src/resources/api.test.ts @@ -0,0 +1,170 @@ +// Copyright 2021, Nitric Technologies Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as pulumi from '@pulumi/pulumi'; +import { NitricComputeCloudRun, NitricApiGcpApiGateway } from '.'; + +const outputToPromise = async (output: pulumi.Output | undefined): Promise => { + if (!output) { + return undefined; + } + + return await new Promise((res) => { + output.apply((t) => { + res(t); + }); + }); +}; + +describe('NitricApiGcpApiGateway', () => { + // Setup pulumi mocks + beforeAll(() => { + pulumi.runtime.setMocks({ + newResource: function ({ name, type, inputs }): { id: string; state: any } { + switch (type) { + case 'gcp:apigateway/api:Api': + return { + id: 'mock-api-id', + state: { + ...inputs, + // outputs... + name: inputs.name || name + '-sg', + apiId: 'mock-api-id', + }, + }; + case 'gcp:apigateway/apiConfig:ApiConfig': + return { + id: 'mock-config-id', + state: { + ...inputs, + // outputs ... + name: inputs.name || name + '-sg', + }, + }; + case 'gcp:apigateway/gateway:Gateway': + return { + id: 'mock-gateway-id', + state: { + ...inputs, + // outputs ... + name: inputs.name || name + '-sg', + defaultHostName: 'example.com', + }, + }; + case 'gcp:serviceAccount/account:Account': + return { + id: 'mock-account-id', + state: { + ...inputs, + // outputs ... + name: inputs.name || name + '-sg', + email: 'service.account@example.com', + }, + }; + default: + return { + id: inputs.name + '_id', + state: { + ...inputs, + }, + }; + } + }, + call: function ({ inputs }) { + return inputs; + }, + }); + }); + + describe('When creating a new GcpApiGateway resource', () => { + let api: NitricApiGcpApiGateway | null = null; + beforeAll(() => { + // Create the new Api Gateway resource + api = new NitricApiGcpApiGateway('my-gateway', { + api: { + swagger: '2.0', + info: { + title: 'test-api', + version: '2.0', + }, + paths: { + '/example/': { + get: { + operationId: 'getExample', + 'x-nitric-target': { + name: 'test-service', + type: 'function', + }, + description: 'Retrieve an existing example', + responses: { + '200': { + description: 'Successful response', + }, + }, + }, + }, + }, + }, + services: [ + { + name: 'test-service', + url: pulumi.output('https://example.com'), + cloudrun: { + name: 'test', + location: 'us-central-1', + } as unknown, + } as NitricComputeCloudRun, + ], + }); + }); + + // Assert its state + it('should create a new Api', async () => { + expect(api?.api).toBeDefined(); + await expect(outputToPromise(api?.api.name)).resolves.toEqual('my-gateway-sg'); + }); + + it('should create a new api config', async () => { + expect(api?.config).toBeDefined(); + await expect(outputToPromise(api?.config.name)).resolves.toEqual('my-gateway-config-sg'); + + // asset config has correct parent api + await expect(outputToPromise(api?.config.api)).resolves.toEqual('mock-api-id'); + }); + + it('should create a new gateway', async () => { + expect(api?.gateway).toBeDefined(); + await expect(outputToPromise(api?.gateway.name)).resolves.toEqual('my-gateway-gateway-sg'); + + // Assert gateway has correct config + const configId = await outputToPromise(api?.config.id); + await expect(outputToPromise(api?.gateway.apiConfig)).resolves.toEqual(configId); + }); + + it('should create a new Iam account', async () => { + expect(api?.invoker).toBeDefined(); + await expect(outputToPromise(api?.invoker.name)).resolves.toEqual('my-gateway-acct-sg'); + await expect(outputToPromise(api?.invoker.email)).resolves.toEqual('service.account@example.com'); + }); + + it('should create iam members for the invoker account for each provided service', async () => { + expect(api?.memberships).toHaveLength(1); + + // Assert service account wired up correctly + await expect(outputToPromise(api?.memberships[0].member)).resolves.toEqual( + 'serviceAccount:service.account@example.com', + ); + }); + }); +}); diff --git a/packages/plugins/gcp/src/resources/api.ts b/packages/plugins/gcp/src/resources/api.ts index 11d03aca..e92ea9b6 100644 --- a/packages/plugins/gcp/src/resources/api.ts +++ b/packages/plugins/gcp/src/resources/api.ts @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -import { NitricAPITarget, StackAPIDocument } from '@nitric/cli-common'; +import { NitricAPITarget, StackAPIDocument, constants } from '@nitric/cli-common'; import * as pulumi from '@pulumi/pulumi'; import { OpenAPIV2 } from 'openapi-types'; import * as gcp from '@pulumi/gcp'; @@ -43,6 +43,12 @@ export class NitricApiGcpApiGateway extends pulumi.ComponentResource { public readonly hostname: pulumi.Output; public readonly url: pulumi.Output; + public readonly api: gcp.apigateway.Api; + public readonly config: gcp.apigateway.ApiConfig; + public readonly gateway: gcp.apigateway.Gateway; + public readonly invoker: gcp.serviceaccount.Account; + public readonly memberships: gcp.cloudrun.IamMember[]; + constructor(name: string, args: NitricApiGcpApiGatewayArgs, opts?: pulumi.ComponentResourceOptions) { super('nitric:api:GcpApiGateway', name, {}, opts); const { api, services } = args; @@ -54,10 +60,10 @@ export class NitricApiGcpApiGateway extends pulumi.ComponentResource { const targetServices = Object.keys(api.paths).reduce((svcs, path) => { const p = api.paths[path] as OpenAPIV2.PathItemObject; - const services = Object.keys(path) + const s = Object.keys(p) .filter((k) => METHOD_KEYS.includes(k as method)) .reduce((acc, method) => { - const pathTarget = p[method as method]?.['x-nitric-target']; + const pathTarget = p[method as method]?.[constants.OAI_NITRIC_TARGET_EXT]; const svc = services.find(({ name }) => name === pathTarget?.name); if (svc && !acc.includes(svc)) { @@ -67,7 +73,7 @@ export class NitricApiGcpApiGateway extends pulumi.ComponentResource { return acc; }, svcs); - return svcs; + return s; }, [] as NitricComputeCloudRun[]); // Replace Nitric API Extensions with google api gateway extensions @@ -85,28 +91,35 @@ export class NitricApiGcpApiGateway extends pulumi.ComponentResource { const p = path[method as method]!; // The name of the function we want to target with this APIGateway - const targetName = p['x-nitric-target'].name; - - const invokeUrlPair = nameUrlPairs.find((f) => f.split('||')[0] === targetName); - - if (!invokeUrlPair) { - throw new Error(`Invalid nitric target ${targetName} defined in api: ${name}`); + if (p[constants.OAI_NITRIC_TARGET_EXT]) { + const targetName = p[constants.OAI_NITRIC_TARGET_EXT].name; + + const invokeUrlPair = nameUrlPairs.find((f) => f.split('||')[0] === targetName); + + if (!invokeUrlPair) { + throw new Error(`Invalid nitric target ${targetName} defined in api: ${name}`); + } + + const url = invokeUrlPair.split('||')[1]; + // Discard the old key on the transformed API + const { [constants.OAI_NITRIC_TARGET_EXT]: _, ...rest } = p; + + return { + ...acc, + // Inject the new method with translated nitric target + [method]: { + ...(rest as OpenAPIV2.OperationObject), + 'x-google-backend': { + address: url, + path_translation: 'APPEND_PATH_TO_ADDRESS', + }, + } as any, + }; } - const url = invokeUrlPair.split('||')[1]; - // Discard the old key on the transformed API - const { 'x-nitric-target': _, ...rest } = p; - return { ...acc, - // Inject the new method with translated nitric target - [method]: { - ...(rest as OpenAPIV2.OperationObject), - 'x-google-backend': { - address: url, - path_translation: 'APPEND_PATH_TO_ADDRESS', - }, - } as any, + [method]: p, }; }, {} as { [key: string]: OpenAPIV2.OperationObject }); @@ -124,7 +137,7 @@ export class NitricApiGcpApiGateway extends pulumi.ComponentResource { return Buffer.from(JSON.stringify(transformedApi)).toString('base64'); }); - const deployedApi = new gcp.apigateway.Api( + this.api = new gcp.apigateway.Api( name, { apiId: name, @@ -133,7 +146,7 @@ export class NitricApiGcpApiGateway extends pulumi.ComponentResource { ); // Create a new IAM account for invoking - const apiInvoker = new gcp.serviceaccount.Account( + this.invoker = new gcp.serviceaccount.Account( `${name}-acct`, { // Limit to 30 characters for service account name @@ -144,12 +157,13 @@ export class NitricApiGcpApiGateway extends pulumi.ComponentResource { ); // Bind that IAM account as a member of all available service targets - targetServices.map((svc) => { - new gcp.cloudrun.IamMember( - `${name}-acct-binding`, + this.memberships = targetServices.map((svc) => { + return new gcp.cloudrun.IamMember( + `${name}-${svc.name}-binding`, { service: svc.cloudrun.name, - member: pulumi.interpolate`serviceAccount:${apiInvoker.email}`, + location: svc.cloudrun.location, + member: pulumi.interpolate`serviceAccount:${this.invoker.email}`, role: 'roles/run.invoker', }, defaultResourceOptions, @@ -159,10 +173,10 @@ export class NitricApiGcpApiGateway extends pulumi.ComponentResource { // Now we need to create the document provided and interpolate the deployed service targets // i.e. their Urls... // Deploy the config - const deployedConfig = new gcp.apigateway.ApiConfig( + this.config = new gcp.apigateway.ApiConfig( `${name}-config`, { - api: deployedApi.apiId, + api: this.api.apiId, displayName: `${name}-config`, apiConfigId: `${name}-config`, openapiDocuments: [ @@ -176,7 +190,7 @@ export class NitricApiGcpApiGateway extends pulumi.ComponentResource { gatewayConfig: { backendConfig: { // Add the service account for the invoker here... - googleServiceAccount: apiInvoker.email, + googleServiceAccount: this.invoker.email, }, }, }, @@ -184,23 +198,28 @@ export class NitricApiGcpApiGateway extends pulumi.ComponentResource { ); // Deploy the gateway - const gateway = new gcp.apigateway.Gateway( + this.gateway = new gcp.apigateway.Gateway( `${name}-gateway`, { displayName: `${name}-gateway`, gatewayId: `${name}-gateway`, - apiConfig: deployedConfig.id, + apiConfig: this.config.id, }, defaultResourceOptions, ); - this.hostname = gateway.defaultHostname; - this.url = gateway.defaultHostname.apply((n) => `https://${n}`); + this.hostname = this.gateway.defaultHostname; + this.url = this.gateway.defaultHostname.apply((n) => `https://${n}`); this.registerOutputs({ name: this.name, hostname: this.hostname, url: this.url, + api: this.api, + invoker: this.invoker, + memberships: this.memberships, + config: this.config, + gateway: this.gateway, }); } diff --git a/packages/plugins/gcp/src/resources/compute.ts b/packages/plugins/gcp/src/resources/compute.ts index 1cb13fca..3ef96b6d 100644 --- a/packages/plugins/gcp/src/resources/compute.ts +++ b/packages/plugins/gcp/src/resources/compute.ts @@ -31,6 +31,7 @@ export class NitricComputeCloudRun extends pulumi.ComponentResource { public readonly name: string; public readonly cloudrun: gcp.cloudrun.Service; public readonly url: pulumi.Output; + public readonly account: gcp.serviceaccount.Account; constructor(name: string, args: NitricComputeCloudRunArgs, opts?: pulumi.ComponentResourceOptions) { super('nitric:func:CloudRun', name, {}, opts); @@ -42,6 +43,26 @@ export class NitricComputeCloudRun extends pulumi.ComponentResource { this.name = source.getName(); + // Create a service account for this cloud run instance + this.account = new gcp.serviceaccount.Account(`${name}-acct`, { + accountId: `${name}-acct`.substring(0, 30), + }); + + // Give project editor permissions + // FIXME: Trim this down + new gcp.projects.IAMMember(`${name}-editor`, { + role: 'roles/editor', + // Get the cloudrun service account email + member: pulumi.interpolate`serviceAccount:${this.account.email}`, + }); + + // Give secret accessor permissions + new gcp.projects.IAMMember(`${name}-secret-access`, { + role: 'roles/secretmanager.secretAccessor', + // Get the cloudrun service account email + member: pulumi.interpolate`serviceAccount:${this.account.email}`, + }); + // Deploy the func this.cloudrun = new gcp.cloudrun.Service( source.getName(), @@ -55,6 +76,7 @@ export class NitricComputeCloudRun extends pulumi.ComponentResource { }, }, spec: { + serviceAccountName: this.account.email, containers: [ { image: image.imageUri, @@ -88,6 +110,7 @@ export class NitricComputeCloudRun extends pulumi.ComponentResource { member: pulumi.interpolate`serviceAccount:${invokerAccount.email}`, role: 'roles/run.invoker', service: this.cloudrun.name, + location: this.cloudrun.location, }); triggers.topics.forEach((sub) => { diff --git a/packages/plugins/gcp/src/resources/entrypoint.ts b/packages/plugins/gcp/src/resources/entrypoint.ts index 029ec394..2ae8ca6c 100644 --- a/packages/plugins/gcp/src/resources/entrypoint.ts +++ b/packages/plugins/gcp/src/resources/entrypoint.ts @@ -59,7 +59,7 @@ export class NitricEntrypointGoogleCloudLB extends pulumi.ComponentResource { `${entrypointPath.target}-neg`, { networkEndpointType: 'INTERNET_FQDN_PORT', - //defaultPort: 443, + defaultPort: 443, }, defaultResourceOptions, ); @@ -78,9 +78,13 @@ export class NitricEntrypointGoogleCloudLB extends pulumi.ComponentResource { const backend = new gcp.compute.BackendService( `${entrypointPath.target}`, { + // Default options for timeout and connection draining + // without these, receiving 502 error + timeoutSec: 10, + connectionDrainingTimeoutSec: 10, // Link the NEG to the backend backends: [{ group: apiGatewayNEG.id }], - customRequestHeaders: [pulumi.interpolate`Host: ${deployedApi.hostname}`], + customRequestHeaders: [pulumi.interpolate`host: ${deployedApi.hostname}`], // TODO: Determine CDN requirements for API gateways enableCdn: false, protocol: 'HTTPS', @@ -90,6 +94,7 @@ export class NitricEntrypointGoogleCloudLB extends pulumi.ComponentResource { return { name: entrypointPath.target, + path: entrypointPath.path, backend, }; } @@ -103,7 +108,7 @@ export class NitricEntrypointGoogleCloudLB extends pulumi.ComponentResource { } const backend = new gcp.compute.BackendBucket( - `${entrypointPath.target}`, + `${entrypointPath.target}-bucket`, { bucketName: deployedSite.storage.name, // Enable CDN for sites @@ -114,6 +119,7 @@ export class NitricEntrypointGoogleCloudLB extends pulumi.ComponentResource { return { name: entrypointPath.target, + path: entrypointPath.path, backend, }; } @@ -140,7 +146,7 @@ export class NitricEntrypointGoogleCloudLB extends pulumi.ComponentResource { ); const backend = new gcp.compute.BackendService( - `${entrypointPath.target}`, + `${entrypointPath.target}-${entrypointPath.type}`, { // Link the NEG to the backend backends: [{ group: serverlessNEG.id }], @@ -153,6 +159,7 @@ export class NitricEntrypointGoogleCloudLB extends pulumi.ComponentResource { return { name: entrypointPath.target, + path: entrypointPath.path, backend, }; } @@ -178,7 +185,7 @@ export class NitricEntrypointGoogleCloudLB extends pulumi.ComponentResource { const pathRules = otherEntrypoints.length > 0 ? otherEntrypoints.map((ep) => { - const backend = backends.find((b) => b.name === ep.target)!.backend; + const backend = backends.find((b) => b.name === ep.target && b.path == ep.path)!.backend; pulumi.log.info(`other backend: ${ep.target}`, backend); return { paths: [`${ep.path}*`], @@ -268,7 +275,7 @@ export class NitricEntrypointGoogleCloudLB extends pulumi.ComponentResource { defaultResourceOptions, ); - this.url = pulumi.interpolate`https://${ipAddress}`; + this.url = pulumi.interpolate`https://${ipAddress.address}`; } pulumi.log.info('Connecting URL map to HTTP proxy', urlMap); @@ -296,10 +303,10 @@ export class NitricEntrypointGoogleCloudLB extends pulumi.ComponentResource { defaultResourceOptions, ); - (this.ipAddress = forwardingRule.ipAddress), - this.registerOutputs({ - url: this.url, - ipAddress: this.ipAddress, - }); + this.ipAddress = forwardingRule.ipAddress; + this.registerOutputs({ + url: this.url, + ipAddress: this.ipAddress, + }); } } diff --git a/packages/plugins/gcp/src/resources/index.ts b/packages/plugins/gcp/src/resources/index.ts index e880d2f2..0f9b7ed4 100644 --- a/packages/plugins/gcp/src/resources/index.ts +++ b/packages/plugins/gcp/src/resources/index.ts @@ -19,3 +19,4 @@ export * from './compute'; export * from './site'; export * from './topic'; export * from './project'; +export * from './queue'; diff --git a/packages/plugins/gcp/src/resources/project.ts b/packages/plugins/gcp/src/resources/project.ts index 0f8a9207..0a603bec 100644 --- a/packages/plugins/gcp/src/resources/project.ts +++ b/packages/plugins/gcp/src/resources/project.ts @@ -77,7 +77,7 @@ export class NitricGcpProject extends pulumi.ComponentResource { `pubsub-token-creator`, { role: 'roles/iam.serviceAccountTokenCreator', - member: pulumi.interpolate`serviceAccount:services-${project.number}@gcp-sa-pubsub.iam.gserviceaccount.com`, + member: pulumi.interpolate`serviceAccount:service-${project.number}@gcp-sa-pubsub.iam.gserviceaccount.com`, project: project.id, }, { diff --git a/packages/plugins/gcp/src/resources/queue.ts b/packages/plugins/gcp/src/resources/queue.ts new file mode 100644 index 00000000..56cf8ff6 --- /dev/null +++ b/packages/plugins/gcp/src/resources/queue.ts @@ -0,0 +1,56 @@ +// Copyright 2021, Nitric Technologies Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { NamedObject, NitricQueue } from '@nitric/cli-common'; +import * as pulumi from '@pulumi/pulumi'; +import * as gcp from '@pulumi/gcp'; + +interface NitricQueuePubsubArgs { + queue: NamedObject; +} + +/** + * Nitric Topic deployed to Google Cloud PubSub + */ +export class NitricQueuePubsub extends pulumi.ComponentResource { + public readonly name: string; + public readonly pubsub: gcp.pubsub.Topic; + + constructor(name: string, args: NitricQueuePubsubArgs, opts?: pulumi.ComponentResourceOptions) { + super('nitric:queue:PubSub', name, {}, opts); + const { queue } = args; + const defaultResourceOptions: pulumi.ResourceOptions = { parent: this }; + + this.name = queue.name; + + // Deploy the func + this.pubsub = new gcp.pubsub.Topic( + queue.name, + { + name: queue.name, + }, + defaultResourceOptions, + ); + + new gcp.pubsub.Subscription(`${queue.name}-sub`, { + // XXX: Currently required relationship with pubsub queue plugin + name: `${queue.name}-nitricqueue`, + topic: this.pubsub.name, + }); + + this.registerOutputs({ + name: this.name, + pubsub: this.pubsub, + }); + } +} diff --git a/packages/plugins/gcp/src/resources/schedule.ts b/packages/plugins/gcp/src/resources/schedule.ts index 0dcdf77d..d38db272 100644 --- a/packages/plugins/gcp/src/resources/schedule.ts +++ b/packages/plugins/gcp/src/resources/schedule.ts @@ -27,29 +27,35 @@ interface NitricScheduleCloudSchedulerArgs { export class NitricScheduleCloudScheduler extends pulumi.ComponentResource { public readonly job: gcp.cloudscheduler.Job; - constructor(name: string, args: NitricScheduleCloudSchedulerArgs, opts?: pulumi.ComponentResourceOptions) { + constructor( + projectId: string, + name: string, + args: NitricScheduleCloudSchedulerArgs, + opts?: pulumi.ComponentResourceOptions, + ) { super('nitric:schedule:CloudScheduler', name, {}, opts); const { schedule, topics } = args; const defaultResourceOptions: pulumi.ResourceOptions = { parent: this }; - const topic = topics.find((t) => t.name === schedule.target.id); + const topic = topics.find((t) => t.name === schedule.target.name); if (topic) { this.job = new gcp.cloudscheduler.Job( schedule.name, { timeZone: 'UTC', - description: `scheduled trigger for ${schedule.target.id}`, + description: `scheduled trigger for ${schedule.target.name}`, pubsubTarget: { - topicName: topic.pubsub.name, + // Interpolate required by pulumi to translate output into string + topicName: pulumi.interpolate`${projectId}/topics/${topic.pubsub.name}`, data: Buffer.from(JSON.stringify(schedule.event)).toString('base64'), }, - schedule: schedule.expression, + schedule: schedule.expression?.replace(/['"]+/g, ''), }, defaultResourceOptions, ); } else { - throw new Error(`topic ${schedule.target.id} defined as target for schedule, but does not exist in the stack!`); + throw new Error(`topic ${schedule.target.name} defined as target for schedule, but does not exist in the stack!`); } this.registerOutputs({ diff --git a/packages/plugins/gcp/src/resources/site.ts b/packages/plugins/gcp/src/resources/site.ts index f9436afe..5724b4c8 100644 --- a/packages/plugins/gcp/src/resources/site.ts +++ b/packages/plugins/gcp/src/resources/site.ts @@ -41,6 +41,9 @@ export class NitricSiteCloudStorage extends pulumi.ComponentResource { this.storage = new gcp.storage.Bucket( site.getName(), { + website: { + mainPageSuffix: 'index.html', + }, labels: { 'x-nitric-name': site.getName(), }, diff --git a/packages/plugins/gcp/src/tasks/deploy/index.ts b/packages/plugins/gcp/src/tasks/deploy/index.ts index e060ef48..e7efdedc 100644 --- a/packages/plugins/gcp/src/tasks/deploy/index.ts +++ b/packages/plugins/gcp/src/tasks/deploy/index.ts @@ -26,6 +26,7 @@ import { NitricScheduleCloudScheduler, NitricSiteCloudStorage, NitricTopicPubsub, + NitricQueuePubsub, } from '../../resources'; import fs from 'fs'; @@ -75,7 +76,7 @@ export class Deploy extends Task { async do(): Promise { const { stack, gcpProject, region } = this; - const { buckets = {}, topics = {}, schedules = {}, entrypoints } = stack.asNitricStack(); + const { buckets = {}, topics = {}, queues = {}, schedules = {}, entrypoints } = stack.asNitricStack(); const auth = new google.auth.GoogleAuth({ scopes: ['https://www.googleapis.com/auth/cloud-platform'], }); @@ -119,6 +120,9 @@ export class Deploy extends Task { (topic) => new NitricTopicPubsub(topic.name, { topic }, defaultResourceOptions), ); + // deploy the queues + mapObject(queues).map((queue) => new NitricQueuePubsub(queue.name, { queue }, defaultResourceOptions)); + // deploy the sites const deployedSites = stack .getSites() @@ -168,6 +172,7 @@ export class Deploy extends Task { mapObject(schedules).map( (s) => new NitricScheduleCloudScheduler( + project.id, s.name, { schedule: s, topics: deployedTopics }, defaultResourceOptions, diff --git a/packages/plugins/gcp/src/tasks/down/index.ts b/packages/plugins/gcp/src/tasks/down/index.ts index d5f502b6..300f0188 100644 --- a/packages/plugins/gcp/src/tasks/down/index.ts +++ b/packages/plugins/gcp/src/tasks/down/index.ts @@ -14,23 +14,48 @@ import { Task } from '@nitric/cli-common'; import { LocalWorkspace } from '@pulumi/pulumi/automation'; +import Deployment from '../../types/deployment'; /** * Options when tearing down a stack previously deployed to GCP */ interface DownOptions { stackName: string; + destroy: boolean; } +interface Target { + type: string; //e.g. bucket + pulumiTypes: string[]; +} + +//Map of the protected destroy targets +const protectedTargets: Target[] = [ + { + type: 'base', + pulumiTypes: ['pulumi:pulumi:Stack', 'pulumi:providers:gcp'], + }, + { + type: 'bucket', + pulumiTypes: ['gcp:projects/service:Service', 'gcp:storage/bucket:Bucket'], + }, + { + type: 'service_accounts', + pulumiTypes: ['gcp:projects/iAMMember:IAMMember', 'nitric:project:GcpProject', 'nitric:bucket:CloudStorage'], + }, +]; + /** * Tear down a deployed stack from GCP */ export class Down extends Task { private stackName: string; + private destroy: boolean; - constructor({ stackName }: DownOptions) { + constructor({ stackName, destroy }: DownOptions) { super(`Tearing Down Stack: ${stackName}`); this.stackName = stackName; + this.destroy = destroy; } async do(): Promise { @@ -45,10 +70,25 @@ export class Down extends Task { /*no op*/ }, }); - - // await pulumiStack.setConfig("gcp:region", { value: region }); - const res = await pulumiStack.destroy({ onOutput: this.update.bind(this) }); - console.log(res); + if (this.destroy) { + await pulumiStack.destroy({ onOutput: this.update.bind(this) }); + } else { + const deployment = (await pulumiStack.exportStack()).deployment as Deployment; + const nonTargets = protectedTargets //Possible to filter the protected targets in the future + .map((val) => val.pulumiTypes) + .reduce((acc, val) => acc.concat(val), []); + //List of targets that will be destroyed, filters out the ones that are protected + const targets = deployment.resources + .filter((resource) => !nonTargets.includes(resource.type)) + .map((resource) => resource.urn); + if (targets.length > 0) { + await pulumiStack.destroy({ + onOutput: this.update.bind(this), + target: targets, + targetDependents: true, + }); + } + } } catch (e) { console.log(e); throw e; diff --git a/packages/plugins/gcp/types/api-converter.d.ts b/packages/plugins/gcp/src/types/api-converter.d.ts similarity index 96% rename from packages/plugins/gcp/types/api-converter.d.ts rename to packages/plugins/gcp/src/types/api-converter.d.ts index 4c0bfd24..6f21f59f 100644 --- a/packages/plugins/gcp/types/api-converter.d.ts +++ b/packages/plugins/gcp/src/types/api-converter.d.ts @@ -15,7 +15,7 @@ interface ConvertOptions { from: string; to: string; - source: string | object; + source: string | any; } declare module 'api-spec-converter' { diff --git a/packages/plugins/gcp/src/types/deployment.ts b/packages/plugins/gcp/src/types/deployment.ts new file mode 100644 index 00000000..80716d94 --- /dev/null +++ b/packages/plugins/gcp/src/types/deployment.ts @@ -0,0 +1,28 @@ +// Copyright 2021, Nitric Technologies Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +interface Resource { + urn: string; + custom: boolean; + type: string; + outputs: string[]; + parent: string; + dependencies: string[]; +} + +interface Deployment { + resources: Resource[]; +} + +export default Deployment; diff --git a/yarn.lock b/yarn.lock index 4dfa25e8..4a67469d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -505,6 +505,13 @@ __metadata: languageName: node linkType: hard +"@iarna/toml@npm:^2.2.5": + version: 2.2.5 + resolution: "@iarna/toml@npm:2.2.5" + checksum: 929a8516a24996b75768f7e0591815e37004f2cdda12b245c9a5ae64f423b4bd2bdd6943fc80e9bb5360a7be0b6d05bac57c178578d9a73acfb2eab125c594ee + languageName: node + linkType: hard + "@istanbuljs/load-nyc-config@npm:^1.0.0": version: 1.1.0 resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" @@ -747,6 +754,7 @@ __metadata: version: 0.0.0-use.local resolution: "@nitric/cli-common@workspace:packages/common" dependencies: + "@iarna/toml": ^2.2.5 "@oclif/command": ^1 "@oclif/config": ^1 "@pulumi/docker": ^3.0.0 @@ -757,6 +765,7 @@ __metadata: "@types/stream-to-promise": ^2.2.1 "@types/universal-analytics": ^0.4.4 "@types/uuid": ^8.3.0 + "@types/which": ^2.0.1 ajv: ^8.6.2 common-tags: ^1.8.0 dotenv: ^10.0.0 @@ -779,6 +788,7 @@ __metadata: typescript: ^4.3 universal-analytics: ^0.4.23 uuid: ^8.3.2 + which: ^2.0.2 yaml: ^2.0.0-7 languageName: unknown linkType: soft @@ -887,6 +897,7 @@ __metadata: "@pulumi/docker": ^3.0.0 "@pulumi/pulumi": ^3.5.1 "@types/jest": ^26.0.15 + "@types/mime-types": ^2.1.1 "@types/node": ^10 api-spec-converter: ^2.11.4 cli-ux: ^5.5.1 @@ -901,6 +912,7 @@ __metadata: inquirer: ^8.0.0 jest: ^26.6.1 listr2: ^3.6.2 + mime-types: ^2.1.32 openapi-types: ^7.2.3 ts-jest: ^26.4.3 ts-node: ^8 @@ -1569,6 +1581,13 @@ __metadata: languageName: node linkType: hard +"@types/mime-types@npm:^2.1.1": + version: 2.1.1 + resolution: "@types/mime-types@npm:2.1.1" + checksum: d282f89f3a492cb04a0cf73a8dc933abca9c598ae8dc15ec77a23c36b658dbd4bddbc8b6afa10ca618839020ca890de0c1c11fca3704e2c47fb6a3d753cfc1ae + languageName: node + linkType: hard + "@types/mime@npm:^1": version: 1.3.2 resolution: "@types/mime@npm:1.3.2" @@ -1740,6 +1759,13 @@ __metadata: languageName: node linkType: hard +"@types/which@npm:^2.0.1": + version: 2.0.1 + resolution: "@types/which@npm:2.0.1" + checksum: 75d38ca462d462c60e8ff63be1551807403e20d4d8dc10a7cb41004625fa45ae0656af5f62907ae249e8551c0275d65618f7ec791c206bfd6c53ed12c0121401 + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 20.2.1 resolution: "@types/yargs-parser@npm:20.2.1" @@ -2522,6 +2548,15 @@ __metadata: languageName: node linkType: hard +"axios@npm:^0.21.4": + version: 0.21.4 + resolution: "axios@npm:0.21.4" + dependencies: + follow-redirects: ^1.14.0 + checksum: e6d42b269b599d36eb13be0671c237781f32e6ae72be824297c55a3e1ce63b22ba4f46bad5ab28da7d3bae50a72637d55c792cf803be1cf9de6a8bcd6d0dcc1a + languageName: node + linkType: hard + "axobject-query@npm:^2.2.0": version: 2.2.0 resolution: "axobject-query@npm:2.2.0" @@ -5357,6 +5392,16 @@ __metadata: languageName: node linkType: hard +"follow-redirects@npm:^1.14.0": + version: 1.14.4 + resolution: "follow-redirects@npm:1.14.4" + peerDependenciesMeta: + debug: + optional: true + checksum: 93def3c22eba16caa4ff6f6956ccfda5a50e889545b1ba0046194dd8e368ff79ba6ef681b06e6dc7032b50bc93ca59e9441aa8143cb26f43f40a20bbd6374639 + languageName: node + linkType: hard + "for-in@npm:^1.0.2": version: 1.0.2 resolution: "for-in@npm:1.0.2" @@ -8415,7 +8460,7 @@ fsevents@^2.1.2: languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:~2.1.19": +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.32, mime-types@npm:~2.1.19": version: 2.1.32 resolution: "mime-types@npm:2.1.32" dependencies: @@ -8722,6 +8767,7 @@ fsevents@^2.1.2: dependencies: "@typescript-eslint/eslint-plugin": ^4.29.0 "@typescript-eslint/parser": ^4.29.0 + axios: ^0.21.4 eslint: ^6.8.0 eslint-config-airbnb: ^18.1.0 eslint-config-prettier: ^6.11.0