diff --git a/resource-types/VSCodeRemoteDevelopment/.devcontainer/Dockerfile b/resource-types/VSCodeRemoteDevelopment/.devcontainer/Dockerfile new file mode 100644 index 0000000..f84f5ac --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/.devcontainer/Dockerfile @@ -0,0 +1,67 @@ +FROM mcr.microsoft.com/vscode/devcontainers/base:ubuntu-18.04 + +# get back to root because we like danger +USER 0 +ENV HOME=/root + +# Get to latest versions of all packages +RUN apt-get update && apt-get -y upgrade --no-install-recommends + +# Install common dependencies +RUN apt-get -y install --no-install-recommends \ + build-essential \ + git \ + openssh-client \ + less \ + iproute2 \ + procps \ + curl \ + wget \ + unzip \ + nano \ + jq \ + lsb-release \ + ca-certificates \ + apt-transport-https \ + dialog \ + gnupg2 \ + libc6 \ + libgcc1 \ + libgssapi-krb5-2 \ + libicu[0-9][0-9] \ + liblttng-ust0 \ + libstdc++6 \ + zlib1g \ + locales + +# Node.js +RUN curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - \ + && sudo apt-get install -y nodejs + +# Python +RUN apt install -y software-properties-common \ + && add-apt-repository -y ppa:deadsnakes/ppa \ + && apt-get -y install --no-install-recommends python3.7 python3-pip + +# Docker +RUN apt-get install -y apt-transport-https ca-certificates curl lsb-release \ + && curl -fsSL https://download.docker.com/linux/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/gpg | apt-key add - 2>/dev/null \ + && echo "deb [arch=amd64] https://download.docker.com/linux/$(lsb_release -is | tr '[:upper:]' '[:lower:]') $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list \ + && apt-get update \ + && apt-get install -y docker-ce-cli \ + && LATEST_COMPOSE_VERSION=1.25.5 curl -sSL "https://github.com/docker/compose/releases/download/${LATEST_COMPOSE_VERSION}/docker-compose-Linux-x86_64" -o /usr/local/bin/docker-compose \ + && chmod +x /usr/local/bin/docker-compose + +# AWS CLI v2 +RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" \ + && unzip awscliv2.zip \ + && ./aws/install \ + && rm -rf awscliv2.zip aws + +# CDK +RUN npm install -g aws-cdk && npm install -g typescript + +# Cleanup + RUN apt-get autoremove -y \ + && apt-get clean -y \ + && rm -rf /var/lib/apt/lists/* \ No newline at end of file diff --git a/resource-types/VSCodeRemoteDevelopment/.devcontainer/devcontainer.json b/resource-types/VSCodeRemoteDevelopment/.devcontainer/devcontainer.json new file mode 100644 index 0000000..6d50dea --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/.devcontainer/devcontainer.json @@ -0,0 +1,34 @@ +{ + "name": "AWS Cloud Dev Container", + "dockerFile": "Dockerfile", + // "image": "mcr.microsoft.com/vscode/devcontainers/base:ubuntu-18.04", + + // Use 'settings' to set *default* container specific settings.json values on container create. + "settings": { + "terminal.integrated.shell.linux": "/bin/bash" + }, + + // Add the IDs of extensions you want installed when the container is created in the array below. + "extensions": [ + "ms-python.python", + "vscode-snippet.snippet", + "4tron.stack-overflow-view", + "kenhowardpdx.vscode-gist", + "aws-amplify.aws-amplify-vscode", + "amazonwebservices.aws-toolkit-vscode", + "aws-scripting-guy.cform", + "ms-azuretools.vscode-docker" + ], + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [80], + + "workspaceFolder": "/mnt/ebs/fs1/workspace", + "workspaceMount": "source=/mnt/ebs/fs1/workspace,target=/mnt/ebs/fs1/workspace,type=bind,consistency=cached", + + // Mounts Docker socket into the container so that you can use Docker. + "mounts": [ + "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind", + "source=/mnt/ebs/fs1/home,target=/root,type=bind,consistency=cached" + ] +} diff --git a/resource-types/VSCodeRemoteDevelopment/.gitignore b/resource-types/VSCodeRemoteDevelopment/.gitignore new file mode 100644 index 0000000..445512f --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/.gitignore @@ -0,0 +1,34 @@ +# Distribution / packaging +build/ +dist/ + +# Unit test / coverage reports +.cache +.hypothesis/ +.pytest_cache/ + +# RPDK logs +rpdk.log + +# Node.js +node_modules/ +coverage/ + +# CDK +cdk.out/ + +# cfn cli build output +awssamples-devtools-devinstance.zip + +# personal VS Code config +.vscode/settings.json + +# env file for local testing +env.json + +# envrc +.envrc + +# sam +sam.log +.aws-sam \ No newline at end of file diff --git a/resource-types/VSCodeRemoteDevelopment/.npmrc b/resource-types/VSCodeRemoteDevelopment/.npmrc new file mode 100644 index 0000000..bed0a38 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/.npmrc @@ -0,0 +1 @@ +optional = true diff --git a/resource-types/VSCodeRemoteDevelopment/.rpdk-config b/resource-types/VSCodeRemoteDevelopment/.rpdk-config new file mode 100644 index 0000000..536d99f --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/.rpdk-config @@ -0,0 +1,11 @@ +{ + "typeName": "AWSSamples::Devtools::Devinstance", + "language": "typescript", + "runtime": "nodejs12.x", + "entrypoint": "dist/handlers.entrypoint", + "testEntrypoint": "dist/handlers.testEntrypoint", + "settings": { + "useDocker": true, + "protocolVersion": "2.0.0" + } +} diff --git a/resource-types/VSCodeRemoteDevelopment/.vscode/settings.template.json b/resource-types/VSCodeRemoteDevelopment/.vscode/settings.template.json new file mode 100644 index 0000000..693d3ea --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/.vscode/settings.template.json @@ -0,0 +1,4 @@ +// make a copy of this file as .vscode/settings.json and set your SSH URI +{ + "docker.host": "ssh://ec2-user@" +} diff --git a/resource-types/VSCodeRemoteDevelopment/Dockerfile b/resource-types/VSCodeRemoteDevelopment/Dockerfile new file mode 100644 index 0000000..3f4648f --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/Dockerfile @@ -0,0 +1,82 @@ +FROM mcr.microsoft.com/vscode/devcontainers/base:ubuntu-18.04 + +# get back to root because we like danger +USER 0 +ENV HOME=/root + +# Get to latest versions of all packages +RUN apt-get update && apt-get -y upgrade --no-install-recommends + +# Install common dependencies +RUN apt-get -y install --no-install-recommends \ + build-essential \ + git \ + openssh-client \ + less \ + iproute2 \ + procps \ + curl \ + wget \ + unzip \ + nano \ + jq \ + lsb-release \ + ca-certificates \ + apt-transport-https \ + dialog \ + gnupg2 \ + libc6 \ + libgcc1 \ + libgssapi-krb5-2 \ + libicu[0-9][0-9] \ + liblttng-ust0 \ + libstdc++6 \ + zlib1g \ + locales + +# Node.js +RUN curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - \ + && sudo apt-get install -y nodejs + +# Python +RUN apt install -y software-properties-common \ + && add-apt-repository -y ppa:deadsnakes/ppa \ + && apt-get -y install --no-install-recommends python3.7 python3-pip + +# Docker +RUN apt-get install -y apt-transport-https ca-certificates curl lsb-release \ + && curl -fsSL https://download.docker.com/linux/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/gpg | apt-key add - 2>/dev/null \ + && echo "deb [arch=amd64] https://download.docker.com/linux/$(lsb_release -is | tr '[:upper:]' '[:lower:]') $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list \ + && apt-get update \ + && apt-get install -y docker-ce-cli \ + && LATEST_COMPOSE_VERSION=1.25.5 curl -sSL "https://github.com/docker/compose/releases/download/${LATEST_COMPOSE_VERSION}/docker-compose-Linux-x86_64" -o /usr/local/bin/docker-compose \ + && chmod +x /usr/local/bin/docker-compose + +# AWS CLI v2 +RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" \ + && unzip awscliv2.zip \ + && ./aws/install \ + && rm -rf awscliv2.zip aws + +# CDK +RUN npm install -g aws-cdk && npm install -g typescript + +# SAM CLI & AWS CloudFormation Resource Provider TypeScript Plugin +RUN curl https://raw.githubusercontent.com/Homebrew/install/master/install.sh -o brew.sh \ + && sed -i "s/abort \"Don't run this as root\!\"/echo \"Don't run this as root\!\"/" brew.sh \ + && chmod +x brew.sh \ + && ./brew.sh \ + && rm -rf brew.sh \ + && eval $(/home/linuxbrew/.linuxbrew/bin/brew shellenv) \ + && echo "eval \$($(brew --prefix)/bin/brew shellenv)" >>~/.profile \ + && brew tap aws/tap \ + && brew install aws-sam-cli \ + && pip3 install setuptools \ + && pip3 install wheel \ + && pip3 install git+https://github.com/eduardomourar/cloudformation-cli-typescript-plugin.git@v0.5.0#egg=cloudformation-cli-typescript-plugin \ + && pip3 install cloudformation-cli==0.1.* + +# Cleanup + RUN apt-get autoremove -y \ + && apt-get clean -y \ + && rm -rf /var/lib/apt/lists/* \ No newline at end of file diff --git a/resource-types/VSCodeRemoteDevelopment/README.md b/resource-types/VSCodeRemoteDevelopment/README.md new file mode 100644 index 0000000..dcde481 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/README.md @@ -0,0 +1,165 @@ +# Managed VS Code Remote Dev Environments + +The `AWSSamples::Devtools::Devinstance` resource manages an EC2 instance and additional services that make a great dev environment for the [Visual Studio Code Remote - Containers](https://code.visualstudio.com/docs/remote/containers) extension. VS Code and other IDEs like Cloud9 are popularizing remote dev environments, which in general offer the following benefits: + +- Ability to acquire computing resources (CPU, RAM, GPU...) as needed and tear them down when idle +- Don't be restricted by the hardware limits of the local machine +- Try different CPU architectures and hardware variants with minimal effort (e.g. ARM) +- Enjoy a consistent developer experience from any device +- Easily recreate the dev environment with automation if something went terribly wrong + +The `Devinstance` resource supports this in the following ways: + +- **The developer can choose from a wide variety of instance types**, depending on the requirements (currently, only instances with hibernate support are allowed, although technically anything should work) + +- **Dev environment as code:** With [VS Code Remote Containers]((https://code.visualstudio.com/docs/remote/containers)), the developer connects to the instance via VS Code, which builds and starts a container defined by a `Dockerfile` in the developer's local project directory. The instance is preconfigured with Docker to allow VS Code to use it as a remote environment. + +- **The instance is cattle, but your data is persistent:** The developer can update the `Devinstance` resource to change the instance type, while not losing any data. This is possible because a persistent EBS volume is used to store the developer's workspace that is mounted into the Docker container. This volume is automatically moved between instances and is protected from deletion (will remain even after the resource is deleted). + +- **The size of the EBS volume can be increased at any time** by updating the resource (currently triggers the recreation of the instance, which might feel like overkill but is a nice way to stop all processes before changing volume properties). The filesystem size is extendend automatically. + +- **The instance has a stable hostname, even throughout recreation**, so the configuration in VS Code (the SSH URI) always stays the same. + +## Installing the resource type + +This resource type is built with the [Typescript plugin for CloudFormation](https://github.com/aws-cloudformation/cloudformation-cli-typescript-plugin): + +``` +pip install cloudformation-cli-typescript-plugin +``` + +After checking out this repository, + +``` +git clone https://github.com/aws-cloudformation/aws-cloudformation-samples.git +``` + + a complete build and submission can be started with: + +``` +cd aws-cloudformation-samples/resource-types/VSCodeRemoteDevelopment +# equals "npm install && npm run build && cfn submit --set-default" +npm run all + +``` + +### Installing - the Docker way + +The included Dockerfile has everything to build and submit the resource type. Provided that you are running on MacOS or Linux and have your AWS CLI config in `~/.aws`: + +``` +git clone https://github.com/aws-cloudformation/aws-cloudformation-samples.git +cd aws-cloudformation-samples/resource-types/VSCodeRemoteDevelopment +docker build . -t dev-container +SRC=$(pwd) && docker run --rm -it -e SRC=$SRC -v /var/run/docker.sock:/var/run/docker.sock -v $SRC:$SRC -v ~/.aws:/root/.aws dev-container /bin/bash -c "cd \$SRC && eval \$(/home/linuxbrew/.linuxbrew/bin/brew shellenv) && npm run all" +``` + +If you want to use a specific AWS profile, you can add `-e AWS_PROFILE=your_profile_name` to the docker command. + +## Installing and preparing VS Code + +1) You need to have `ssh` installed. +2) [Install VS Code](https://code.visualstudio.com/docs/setup/setup-overview). +3) [Install the Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension. + +## Usage + +You need to have a [key pair](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html) set up. Put the name of the key pair into the `Keypair` property in `cloudformation/template.json`. The key must be known by your ssh client. + +``` +aws cloudformation create-stack --stack-name VSCodeRemoteDevelopment --template-body file://cloudformation/template.json +``` + +Get the output of the stack to find the SSH URI of your instance: + +``` +aws cloudformation describe-stacks --stack-name VSCodeRemoteDevelopment --query 'Stacks[].Outputs[?OutputKey==`ssh`].OutputValue' --output text + +ssh://ec2-user@ec2-example-do-not-use.compute-1.amazonaws.com +``` + +Connect to the instance once via your ssh client so that you can accept the fingerprint: + +``` +ec2-user@ec2-example-do-not-use.compute-1.amazonaws.com +``` + +In this code repository, make a copy of `.vscode/settings.template.json`, name it `settings.json` and set your SSH URI (e.g. `ssh://ec2-user@ec2-example-do-not-use.compute-1.amazonaws.com`) for `docker.host`. + +Open this repository in VS Code, or, if you already did that, restart VS Code so that the config changes are applied. + +You should now be able to open your remote environment. Click the green icon in the bottom left of the IDE and choose *Remote-Containers: Reopen in Container*. VS Code will connect with the instance, build and run a container as specified by the `Dockerfile` and `devcontainer.json` in the `.devcontainer` directory. You can customize this Dockerfile to your liking. + +Once done, you will be greeted with an empty workspace - that's expected, there is no way to sync your local files with a remote environment. You'd now check out the source code for whatever project you are working on. + +To open a shell, hit F1 and start typing "terminal", then select *Terminal: Create New Integrated Terminal*. + +Your workspace and home directory are backed by the mounted EBS volume, so as long as you put your data there, it will persist even if the container stops or is rebuilt. + +## Hibernate your instance + +You can hibernate your instance to save costs via the AWS Console or other well-known means. + +## Reconfiguring your `Devinstance` resource + +You can + +- change the instance type, e.g. from `m4.xlarge` to `m4.2xlarge` (others should work, too) +- increase the EBS volume size +- change your key pair name + +in `template.json` and then update your stack: + +``` +aws cloudformation update-stack --stack-name VSCodeRemoteDevelopment --template-body file://cloudformation/template.json +``` + +Any update will result in the recreation of the instance. After that, you will need to clean your `known_hosts` file from the old fingerprint and connect once via your SSH client to accept the new one. + +After the update is finished, you will find that the container starts quicker than the first time (because the Docker images are preserved on the EBS volume, too) and all your files in the workspace are still there. + +## Under the hood: Using CDK to simplify resource management + +The resource handler uses CDK programmatically by defining a CDK App and then calling `app.synth()` to produce dynamic CloudFormation templates to manage the resources comprising a dev environment. Typically, resource handlers use the AWS SDK to create resources in an imperative way; an alternative is using a declarative approach via CDK and CloudFormation. The benefits of this approach are: + +- reuse existing CDK code or CloudFormation templates +- benefit from simpler resource configuration via CDK (a single `Devinstance` resource is already made up of 15 AWS resources) +- delegate the long-running task of standing up all resources in the correct order to CloudFormation +- easily map CloudFormation states and errors to your Resource Type +- debug your deployment by testing / sharing the CloudFormation template instead of your whole handler code and benefit from a large community of CloudFormation experts + +As CDK is invoked at runtime in the resource handler code, the CDK App code can react to arbitrary input values before synthesizing the CloudFormation template, thus enabling any reconfiguration of the template, e.g. depending on the current state of a resource. + +For example, in order to force a clean recreation of the EC2 instance when the instance type is changed (which is required because hibernate-enabled instances cannot change their type in-place), the CDK App code, when called, always adds a new UUID to the logical identifier of the instance, which results in CloudFormation seeing a new instance, while the old one is no longer present in the template. Such a behavior is hard to implement with plain CloudFormation templates, but the dynamic "pre-configuration" via CDK code enables almost any rearrangement of the template. + +It should be possible to derive a more generalized handler framework that could take any CDK App and expose it as a Resource Type. + +## Passing `cfn test` + +The run the tests, you need to pass a set of AWS credentials with full permissions. The tester is injecting credentials that aren't authorized to make IAM calls, which is required for the CloudFormation template to be deployed. + +1. Make a copy of `env.template.json` as `env.json` and add an access key ID and secret access key (with full permissions) to it. +2. Start the funtion locally via `sam local start-lambda -n env.json` +3. Run the tests with `cfn test` + +**Note: Make sure to delete the superfluous EBS volumes afterwards, as they are not deleted with the resource.** + +## Using the CDK stack directly + +You can apply the stack directly for testing purposes (or because you do not want to consume it as a Resource Type). + +``` +npm install +npm run build + +# After that, you can use cdk cli commands like: + +# Create the CloudFormation template +cdk --app dist/devinstance-app.js synth DevinstanceStack + +# Show diff to currently deployed stack +cdk --app dist/devinstance-app.js diff DevinstanceStack + +# Deploy the stack +cdk --app dist/devinstance-app.js deploy DevinstanceStack +``` \ No newline at end of file diff --git a/resource-types/VSCodeRemoteDevelopment/awssamples-devtools-devinstance.json b/resource-types/VSCodeRemoteDevelopment/awssamples-devtools-devinstance.json new file mode 100644 index 0000000..7ba7c85 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/awssamples-devtools-devinstance.json @@ -0,0 +1,73 @@ +{ + "typeName": "AWSSamples::Devtools::Devinstance", + "description": "An example resource schema demonstrating some basic constructs and validation rules.", + "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-rpdk.git", + "properties": { + "UID": { + "description": "The ID of the developer instance", + "type": "string" + }, + "InstanceType": { + "description": "The EC2 Instance Type of developer instance. You can only select instance types that support hibernation", + "type": "string", + "default": "m4.xlarge", + "enum": ["c4.large", "c4.xlarge", "c4.2xlarge", "c4.4xlarge", "c4.8xlarge", "c5.large", "c5.xlarge", "c5.2xlarge", "c5.4xlarge", + "c5.9xlarge", "c5.12xlarge", "c5.18xlarge", "m4.large", "m4.xlarge", "m4.2xlarge", "m4.4xlarge", "m5.large", "m5.xlarge", + "m5.2xlarge", "m5.4xlarge", "m5.8xlarge", "r4.large", "r4.xlarge", "r4.2xlarge", "r4.4xlarge", "r5.large", "r5.xlarge", + "r5.2xlarge", "r5.4xlarge", "t2.nano", "t2.micro", "t2.small", "t2.medium", "t2.large", "t2.xlarge", "t2.2xlarge"] + }, + "DiskSize": { + "description": "The persistent disk size in Gibibytes", + "type": "integer", + "default": 100, + "minimum" : 1 + }, + "Keypair": { + "description": "The name of the SSH keypair to connect to your dev environment", + "type": "string" + }, + "SSH": { + "description": "The SSH URI for your dev environment", + "type": "string" + } + + }, + "additionalProperties": false, + "required": [ + "Keypair" + ], + "readOnlyProperties": [ + "/properties/UID", + "/properties/SSH" + ], + "primaryIdentifier": [ + "/properties/UID" + ], + "handlers": { + "create": { + "permissions": [ + "*" + ] + }, + "read": { + "permissions": [ + "*" + ] + }, + "update": { + "permissions": [ + "*" + ] + }, + "delete": { + "permissions": [ + "*" + ] + }, + "list": { + "permissions": [ + "*" + ] + } + } +} diff --git a/resource-types/VSCodeRemoteDevelopment/cloudformation/template.json b/resource-types/VSCodeRemoteDevelopment/cloudformation/template.json new file mode 100644 index 0000000..e62a09e --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/cloudformation/template.json @@ -0,0 +1,24 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "The template creates a new DevInstance", + "Resources": { + "MyInstance": { + "Type": "AWSSamples::Devtools::Devinstance", + "Properties": { + "InstanceType":"m4.xlarge", + "DiskSize": 100, + "Keypair" : "dev" + } + } + }, + "Outputs": { + "ssh": { + "Value": { + "Fn::GetAtt": [ + "MyInstance", + "SSH" + ] + } + } + } +} \ No newline at end of file diff --git a/resource-types/VSCodeRemoteDevelopment/docs/README.md b/resource-types/VSCodeRemoteDevelopment/docs/README.md new file mode 100644 index 0000000..6d25563 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/docs/README.md @@ -0,0 +1,85 @@ +# AWSSamples::Devtools::Devinstance + +An example resource schema demonstrating some basic constructs and validation rules. + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Type" : "AWSSamples::Devtools::Devinstance",
+    "Properties" : {
+        "InstanceType" : String,
+        "DiskSize" : Integer,
+        "Keypair" : String,
+    }
+}
+
+ +### YAML + +
+Type: AWSSamples::Devtools::Devinstance
+Properties:
+    InstanceType: String
+    DiskSize: Integer
+    Keypair: String
+
+ +## Properties + +#### InstanceType + +The EC2 Instance Type of developer instance. You can only select instance types that support hibernation + +_Required_: No + +_Type_: String + +_Allowed Values_: c4.large | c4.xlarge | c4.2xlarge | c4.4xlarge | c4.8xlarge | c5.large | c5.xlarge | c5.2xlarge | c5.4xlarge | c5.9xlarge | c5.12xlarge | c5.18xlarge | m4.large | m4.xlarge | m4.2xlarge | m4.4xlarge | m5.large | m5.xlarge | m5.2xlarge | m5.4xlarge | m5.8xlarge | r4.large | r4.xlarge | r4.2xlarge | r4.4xlarge | r5.large | r5.xlarge | r5.2xlarge | r5.4xlarge | t2.nano | t2.micro | t2.small | t2.medium | t2.large | t2.xlarge | t2.2xlarge + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### DiskSize + +The persistent disk size in Gibibytes + +_Required_: No + +_Type_: Integer + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Keypair + +The name of the SSH keypair to connect to your dev environment + +_Required_: Yes + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +## Return Values + +### Ref + +When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the UID. + +### Fn::GetAtt + +The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values. + +For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html). + +#### UID + +The ID of the developer instance + +#### SSH + +The SSH URI for your dev environment + diff --git a/resource-types/VSCodeRemoteDevelopment/env.template.json b/resource-types/VSCodeRemoteDevelopment/env.template.json new file mode 100644 index 0000000..75aec32 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/env.template.json @@ -0,0 +1,13 @@ +{ + "TestEntrypoint" : { + "HY_OVERRIDE_CREDENTIALS" : "yes", + "HY_ACCESS_KEY_ID" : "-", + "HY_SECRET_ACCESS_KEY" : "-" + }, + "TypeFunction" : { + "HY_OVERRIDE_CREDENTIALS" : "yes", + "HY_ACCESS_KEY_ID" : "-", + "HY_SECRET_ACCESS_KEY" : "-" + } + +} \ No newline at end of file diff --git a/resource-types/VSCodeRemoteDevelopment/inputs/inputs_1_create.json b/resource-types/VSCodeRemoteDevelopment/inputs/inputs_1_create.json new file mode 100644 index 0000000..b0a014f --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/inputs/inputs_1_create.json @@ -0,0 +1,5 @@ +{ + "InstanceType":"m4.xlarge", + "DiskSize": 100, + "Keypair" : "dev" +} \ No newline at end of file diff --git a/resource-types/VSCodeRemoteDevelopment/inputs/inputs_1_invalid.json b/resource-types/VSCodeRemoteDevelopment/inputs/inputs_1_invalid.json new file mode 100644 index 0000000..f93ba0d --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/inputs/inputs_1_invalid.json @@ -0,0 +1,5 @@ +{ + "InstanceType":"notAnInstance", + "DiskSize": 100, + "Keypair" : "dev" +} \ No newline at end of file diff --git a/resource-types/VSCodeRemoteDevelopment/inputs/inputs_1_update.json b/resource-types/VSCodeRemoteDevelopment/inputs/inputs_1_update.json new file mode 100644 index 0000000..cfb9055 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/inputs/inputs_1_update.json @@ -0,0 +1,5 @@ +{ + "InstanceType":"m4.2xlarge", + "DiskSize": 200, + "Keypair" : "dev" +} \ No newline at end of file diff --git a/resource-types/VSCodeRemoteDevelopment/npm-shrinkwrap.json b/resource-types/VSCodeRemoteDevelopment/npm-shrinkwrap.json new file mode 100644 index 0000000..979583c --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/npm-shrinkwrap.json @@ -0,0 +1,597 @@ +{ + "name": "awssamples-devtools-devinstance", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@aws-cdk/aws-cloudwatch": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-cloudwatch/-/aws-cloudwatch-1.58.0.tgz", + "integrity": "sha512-tbK+nc5F+QvBHzetL6KoaWVVIfmTEPeV6pLJlyCnT1FV3C91hU8RIOdKLfPMh9yxpbxjy4gmZFEfzqgXjb++aA==", + "requires": { + "@aws-cdk/aws-iam": "1.58.0", + "@aws-cdk/core": "1.58.0", + "constructs": "^3.0.2" + } + }, + "@aws-cdk/aws-ec2": { + "version": "npm:@rhaws/aws-ec2@1.58.0", + "resolved": "https://registry.npmjs.org/@rhaws/aws-ec2/-/aws-ec2-1.58.0.tgz", + "integrity": "sha512-S/YTk8FdyVK7e6EQmDz1feMV4qhOHWf907+mJhf7hoZSgqhgXF9QYFHsYX9usCIuF5hlBizGv2GeYsgDl3j6+g==", + "requires": { + "@aws-cdk/aws-cloudwatch": "1.58.0", + "@aws-cdk/aws-iam": "1.58.0", + "@aws-cdk/aws-kms": "1.58.0", + "@aws-cdk/aws-logs": "1.58.0", + "@aws-cdk/aws-s3": "1.58.0", + "@aws-cdk/aws-ssm": "1.58.0", + "@aws-cdk/cloud-assembly-schema": "1.58.0", + "@aws-cdk/core": "1.58.0", + "@aws-cdk/cx-api": "1.58.0", + "@aws-cdk/region-info": "1.58.0", + "constructs": "^3.0.2" + } + }, + "@aws-cdk/aws-events": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-events/-/aws-events-1.58.0.tgz", + "integrity": "sha512-U+Km4D/BuQuF/SCzDJiA6+92rbiEoTR5cWbkn03cL4dy26db1kTugLwhfTvr66JKIAc8RW09w30rS5qL6arivA==", + "requires": { + "@aws-cdk/aws-iam": "1.58.0", + "@aws-cdk/core": "1.58.0", + "constructs": "^3.0.2" + } + }, + "@aws-cdk/aws-iam": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-iam/-/aws-iam-1.58.0.tgz", + "integrity": "sha512-hY/HtvWjkt6N1y5ZmcnzGKgcUjPqwJ2oO3l5di11HHVRmZhkbhM1tuI4LMNMVs1bUyZGjQApHt0+Qwyjzx2JuQ==", + "requires": { + "@aws-cdk/core": "1.58.0", + "@aws-cdk/region-info": "1.58.0", + "constructs": "^3.0.2" + } + }, + "@aws-cdk/aws-kms": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-kms/-/aws-kms-1.58.0.tgz", + "integrity": "sha512-2scgHAqI5KX6JWmvlTf2oJZ1WQQlIKBkCJmxJT0y525o0hkVLMB2xUCMqA/CsTFt8j/RJ1jp8LOHedux4vyJXw==", + "requires": { + "@aws-cdk/aws-iam": "1.58.0", + "@aws-cdk/core": "1.58.0", + "constructs": "^3.0.2" + } + }, + "@aws-cdk/aws-logs": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-logs/-/aws-logs-1.58.0.tgz", + "integrity": "sha512-el18uIhK5t4bOtETvWvCfJU/yh3aTJscsN2nhHe0EiQ0L3TViwDUnKxwLHK+Eh+VHGjAQFUbidR0ygf+3q9I8Q==", + "requires": { + "@aws-cdk/aws-cloudwatch": "1.58.0", + "@aws-cdk/aws-iam": "1.58.0", + "@aws-cdk/core": "1.58.0", + "constructs": "^3.0.2" + } + }, + "@aws-cdk/aws-s3": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-s3/-/aws-s3-1.58.0.tgz", + "integrity": "sha512-SrLctJLMUysik2olk6uoj7s5ND5DKiFYitSRpqLHheQMgfHp1KrLHz/hpCEn4V8stqnDjTkyi9W1T36YO8z0sw==", + "requires": { + "@aws-cdk/aws-events": "1.58.0", + "@aws-cdk/aws-iam": "1.58.0", + "@aws-cdk/aws-kms": "1.58.0", + "@aws-cdk/core": "1.58.0", + "constructs": "^3.0.2" + } + }, + "@aws-cdk/aws-ssm": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-ssm/-/aws-ssm-1.58.0.tgz", + "integrity": "sha512-z3pXuofqTevMl2r7AuSO83j2CP2Fvsqw4dw7yspk9qQMbVm9jA6XkyfA9n0rgqalr+5MDtLJE1VXDhMiHLQ0dQ==", + "requires": { + "@aws-cdk/aws-iam": "1.58.0", + "@aws-cdk/aws-kms": "1.58.0", + "@aws-cdk/cloud-assembly-schema": "1.58.0", + "@aws-cdk/core": "1.58.0", + "constructs": "^3.0.2" + } + }, + "@aws-cdk/cloud-assembly-schema": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-1.58.0.tgz", + "integrity": "sha512-uihmnZL76Yq/PrOpGRV4CX0Y0OLnVQbXnNpR0WSHfLn1gbBxzJIDik0HIGOzOhA6JckhKivouJswbephuug0Ug==", + "requires": { + "jsonschema": "^1.2.5", + "semver": "^7.2.2" + }, + "dependencies": { + "jsonschema": { + "version": "1.2.6", + "bundled": true + }, + "semver": { + "version": "7.3.2", + "bundled": true + } + } + }, + "@aws-cdk/core": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/core/-/core-1.58.0.tgz", + "integrity": "sha512-3omLo3fZABhumuPXpHj9Vq35MSbuZuWmtazLdnWZp7VAACR9wVxRYcdBu3hpetWRm4eXiwECx6cIfT5jtPMkIQ==", + "requires": { + "@aws-cdk/cloud-assembly-schema": "1.58.0", + "@aws-cdk/cx-api": "1.58.0", + "constructs": "^3.0.2", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4" + }, + "dependencies": { + "at-least-node": { + "version": "1.0.0", + "bundled": true + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "fs-extra": { + "version": "9.0.1", + "bundled": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.3", + "bundled": true + }, + "jsonfile": { + "version": "6.0.1", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "universalify": { + "version": "1.0.0", + "bundled": true + } + } + }, + "@aws-cdk/cx-api": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cx-api/-/cx-api-1.58.0.tgz", + "integrity": "sha512-t1Q7fXtk8Ux/psz0zGXY9YZkJtPY7fGurMkrAwoLGpM3GCmBR+jRBYqSHG1Rtvle6H+JdVtd1Ocs/Odfaz3bYg==", + "requires": { + "@aws-cdk/cloud-assembly-schema": "1.58.0", + "semver": "^7.2.2" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "bundled": true + } + } + }, + "@aws-cdk/region-info": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/region-info/-/region-info-1.58.0.tgz", + "integrity": "sha512-PAkoyPu4uxGEVTKYb1Tea6A0GoAfvC09iLfoIujHFNx38IdfrHm3X9Pn1995HvFJBuzLx1hv5AcGXOFVmwb8hg==" + }, + "@org-formation/tombok": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@org-formation/tombok/-/tombok-0.0.1.tgz", + "integrity": "sha512-6F0zitevY+H3VT3MVsAo4JFlDl5kfqnhGLUwXNc652/HYEBzMru5iLkTIF6+cp/lgvTWxQJQJzH4yoYja2f9Pg==" + }, + "@types/node": { + "version": "12.19.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.16.tgz", + "integrity": "sha512-7xHmXm/QJ7cbK2laF+YYD7gb5MggHIIQwqyjin3bpEGiSuvScMQ5JZZXPvRipi1MwckTQbJZROMns/JxdnIL1Q==", + "dev": true + }, + "@types/uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==" + }, + "autobind-decorator": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/autobind-decorator/-/autobind-decorator-2.4.0.tgz", + "integrity": "sha512-OGYhWUO72V6DafbF8PM8rm3EPbfuyMZcJhtm5/n26IDwO18pohE4eNazLoCGhPiXOCD0gEGmrbU3849QvM8bbw==" + }, + "aws-sdk": { + "version": "2.732.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.732.0.tgz", + "integrity": "sha512-dCTw/7sCtV8KkBe5BlfngBWH3PsPYafWmWm+tLY7LyN81RHvMs3VbvnLbOOO2m/c9eMk10njXjXIPsFebdGMQw==", + "optional": true, + "requires": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + }, + "dependencies": { + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "optional": true + } + } + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "optional": true + }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "optional": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "cfn-rpdk": { + "version": "npm:@amazon-web-services-cloudformation/cloudformation-cli-typescript-lib@1.0.1", + "resolved": "https://registry.npmjs.org/@amazon-web-services-cloudformation/cloudformation-cli-typescript-lib/-/cloudformation-cli-typescript-lib-1.0.1.tgz", + "integrity": "sha512-degYITXnwEJbkUyWBTRr6dSoly9GKetpvhJ+PB04IX7vKe9+kJcCC0C+fiKSag5e3dLWamf1x6O2jWdgHkRu2Q==", + "requires": { + "@org-formation/tombok": "^0.0.1", + "autobind-decorator": "^2.4.0", + "class-transformer": "^0.3.1", + "reflect-metadata": "^0.1.13", + "string.prototype.replaceall": "^1.0.3", + "uuid": "^7.0.2" + }, + "dependencies": { + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" + } + } + }, + "class-transformer": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.3.1.tgz", + "integrity": "sha512-cKFwohpJbuMovS8xVLmn8N2AUbAuc8pVo4zEfsUVo8qgECOogns1WVk/FkOZoxhOPTyTYFckuoH+13FO+MQ8GA==" + }, + "constructs": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-3.0.4.tgz", + "integrity": "sha512-CDvg7gMjphE3DFX4pzkF6j73NREbR8npPFW8Mx/CLRnMR035+Y1o1HrXIsNSss/dq3ZUnNTU9jKyd3fL9EOlfw==" + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "es-abstract": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz", + "integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==", + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.2", + "is-string": "^1.0.5", + "object-inspect": "^1.9.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.0" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "optional": true + }, + "is-bigint": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.1.tgz", + "integrity": "sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg==" + }, + "is-boolean-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz", + "integrity": "sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==", + "requires": { + "call-bind": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==" + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" + }, + "is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==" + }, + "is-number-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", + "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==" + }, + "is-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz", + "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.1" + } + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==" + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "optional": true + }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=", + "optional": true + }, + "object-inspect": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.2.tgz", + "integrity": "sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "optional": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "optional": true + }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=", + "optional": true + }, + "string.prototype.replaceall": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.replaceall/-/string.prototype.replaceall-1.0.5.tgz", + "integrity": "sha512-YUjdWElI9pgKo7mrPOMKHFZxcAa0v1uqoJkMHtlJW63rMkPLkQH71ao2XNkKY2ksHKHC8ZUFwNjN9Vry+QyCvg==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.2", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.1", + "is-regex": "^1.1.2" + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "typescript": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "requires": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + } + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "optional": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "optional": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "optional": true + } + } +} diff --git a/resource-types/VSCodeRemoteDevelopment/package.json b/resource-types/VSCodeRemoteDevelopment/package.json new file mode 100644 index 0000000..fd77490 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/package.json @@ -0,0 +1,31 @@ +{ + "name": "awssamples-devtools-devinstance", + "version": "1.0.0", + "description": "AWS custom resource provider named AWSSamples::Devtools::Devinstance.", + "main": "dist/handlers.js", + "files": [ + "dist" + ], + "scripts": { + "all": "npm install && npm run build && cfn submit --set-default", + "e2e": "npm run build && sam build --use-container && sam local invoke --event sam-tests/create.json TestEntrypoint", + "build": "npx tsc", + "prepack": "npm run build", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@aws-cdk/aws-ec2": "npm:@rhaws/aws-ec2@^1.58.0", + "@aws-cdk/aws-iam": "1.58.0", + "@aws-cdk/core": "1.58.0", + "@types/uuid": "^8.3.0", + "cfn-rpdk": "npm:@amazon-web-services-cloudformation/cloudformation-cli-typescript-lib@^1.0.1", + "uuid": "^8.3.2" + }, + "devDependencies": { + "@types/node": "^12.19.16", + "typescript": "^4.2.4" + }, + "optionalDependencies": { + "aws-sdk": "^2.656.0" + } +} diff --git a/resource-types/VSCodeRemoteDevelopment/resource-role.yaml b/resource-types/VSCodeRemoteDevelopment/resource-role.yaml new file mode 100644 index 0000000..1f26106 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/resource-role.yaml @@ -0,0 +1,31 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: > + This CloudFormation template creates a role assumed by CloudFormation + during CRUDL operations to mutate resources on behalf of the customer. + +Resources: + ExecutionRole: + Type: AWS::IAM::Role + Properties: + MaxSessionDuration: 8400 + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: resources.cloudformation.amazonaws.com + Action: sts:AssumeRole + Path: "/" + Policies: + - PolicyName: ResourceTypePolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - "*" + Resource: "*" +Outputs: + ExecutionRoleArn: + Value: + Fn::GetAtt: ExecutionRole.Arn diff --git a/resource-types/VSCodeRemoteDevelopment/sam-tests/create.json b/resource-types/VSCodeRemoteDevelopment/sam-tests/create.json new file mode 100644 index 0000000..1810fc5 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/sam-tests/create.json @@ -0,0 +1,19 @@ +{ + "credentials": { + "accessKeyId": "", + "secretAccessKey": "", + "sessionToken": "" + }, + "action": "CREATE", + "request": { + "clientRequestToken": "4b90a7e4-b790-456b-a937-0cfdfa211dfe", + "desiredResourceState": { + "InstanceType":"m4.xlarge", + "DiskSize":200, + "Keypair": "dev" + }, + "previousResourceState": null, + "logicalResourceIdentifier": null + }, + "callbackContext": null +} diff --git a/resource-types/VSCodeRemoteDevelopment/sam-tests/create2.json b/resource-types/VSCodeRemoteDevelopment/sam-tests/create2.json new file mode 100644 index 0000000..6f2f4d1 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/sam-tests/create2.json @@ -0,0 +1,20 @@ +{ + "credentials": { + "accessKeyId": "", + "secretAccessKey": "", + "sessionToken": "" + }, + "action": "CREATE", + "request": { + "clientRequestToken": "4b90a7e4-b790-456b-a937-0cfdfa211dfe", + "desiredResourceState": { + "InstanceType":"m4.xlarge", + "DiskSize":200, + "Keypair": "dev", + "UID": "DevInstance-c97e3580-f33e-4cf6-a30d-a52c212e8d25" + }, + "previousResourceState": null, + "logicalResourceIdentifier": null + }, + "callbackContext": {"state":"creating"} +} diff --git a/resource-types/VSCodeRemoteDevelopment/sam-tests/delete.json b/resource-types/VSCodeRemoteDevelopment/sam-tests/delete.json new file mode 100644 index 0000000..a3e82ea --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/sam-tests/delete.json @@ -0,0 +1,19 @@ +{ + "credentials": { + "accessKeyId": "", + "secretAccessKey": "", + "sessionToken": "" + }, + "action": "DELETE", + "request": { + "clientRequestToken": "4b90a7e4-b790-456b-a937-0cfdfa211dfe", + "desiredResourceState": { + "InstanceType":"m4.2xlarge", + "DiskSize":200, + "UID": "DevInstance-cdd083c5-f2c9-4107-9691-f2a173ae7b16" + }, + "previousResourceState": null, + "logicalResourceIdentifier": null + }, + "callbackContext": null +} diff --git a/resource-types/VSCodeRemoteDevelopment/sam-tests/delete2.json b/resource-types/VSCodeRemoteDevelopment/sam-tests/delete2.json new file mode 100644 index 0000000..d467366 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/sam-tests/delete2.json @@ -0,0 +1,16 @@ +{ + "credentials": { + "accessKeyId": "", + "secretAccessKey": "", + "sessionToken": "" + }, + "action": "DELETE", + "request": { + "clientRequestToken": "4b90a7e4-b790-456b-a937-0cfdfa211dfe", + "desiredResourceState": { + }, + "previousResourceState": null, + "logicalResourceIdentifier": null + }, + "callbackContext": {"state": "deleting", "uid": "DevInstance-b47b8408-1ea4-4207-a4e3-94233f897451"} +} diff --git a/resource-types/VSCodeRemoteDevelopment/sam-tests/list.json b/resource-types/VSCodeRemoteDevelopment/sam-tests/list.json new file mode 100644 index 0000000..bdd6965 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/sam-tests/list.json @@ -0,0 +1,16 @@ +{ + "credentials": { + "accessKeyId": "", + "secretAccessKey": "", + "sessionToken": "" + }, + "action": "LIST", + "request": { + "clientRequestToken": "4b90a7e4-b790-456b-a937-0cfdfa211dfe", + "desiredResourceState": { + }, + "previousResourceState": null, + "logicalResourceIdentifier": null + }, + "callbackContext": null +} diff --git a/resource-types/VSCodeRemoteDevelopment/sam-tests/read.json b/resource-types/VSCodeRemoteDevelopment/sam-tests/read.json new file mode 100644 index 0000000..7a3656b --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/sam-tests/read.json @@ -0,0 +1,17 @@ +{ + "credentials": { + "accessKeyId": "", + "secretAccessKey": "", + "sessionToken": "" + }, + "action": "READ", + "request": { + "clientRequestToken": "4b90a7e4-b790-456b-a937-0cfdfa211dfe", + "desiredResourceState": { + "UID": "DevInstance-c97e3580-f33e-4cf6-a30d-a52c212e8d25" + }, + "previousResourceState": null, + "logicalResourceIdentifier": null + }, + "callbackContext": null +} diff --git a/resource-types/VSCodeRemoteDevelopment/sam-tests/update.json b/resource-types/VSCodeRemoteDevelopment/sam-tests/update.json new file mode 100644 index 0000000..a494b18 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/sam-tests/update.json @@ -0,0 +1,20 @@ +{ + "credentials": { + "accessKeyId": "", + "secretAccessKey": "", + "sessionToken": "" + }, + "action": "UPDATE", + "request": { + "clientRequestToken": "4b90a7e4-b790-456b-a937-0cfdfa211dfe", + "desiredResourceState": { + "InstanceType":"m4.xlarge", + "DiskSize":200, + "UID": "DevInstance-c97e3580-f33e-4cf6-a30d-a52c212e8d25", + "Keypair" : "dev" + }, + "previousResourceState": null, + "logicalResourceIdentifier": null + }, + "callbackContext": null +} diff --git a/resource-types/VSCodeRemoteDevelopment/sam-tests/update2.json b/resource-types/VSCodeRemoteDevelopment/sam-tests/update2.json new file mode 100644 index 0000000..c54d570 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/sam-tests/update2.json @@ -0,0 +1,20 @@ +{ + "credentials": { + "accessKeyId": "", + "secretAccessKey": "", + "sessionToken": "" + }, + "action": "UPDATE", + "request": { + "clientRequestToken": "4b90a7e4-b790-456b-a937-0cfdfa211dfe", + "desiredResourceState": { + "InstanceType":"m4.2xlarge", + "DiskSize":200, + "UID": "DevInstance-8fadeaab-6608-48a2-b0a7-d650411f8d65", + "Keypair" : "dev" + }, + "previousResourceState": null, + "logicalResourceIdentifier": null + }, + "callbackContext": {"state":"updating"} +} diff --git a/resource-types/VSCodeRemoteDevelopment/src/devinstance-app.ts b/resource-types/VSCodeRemoteDevelopment/src/devinstance-app.ts new file mode 100755 index 0000000..6bcd062 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/src/devinstance-app.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node +import * as cdk from '@aws-cdk/core'; +import { DevinstanceStack } from './devinstance-stack'; + +const app = new cdk.App(); +new DevinstanceStack(app, 'DevinstanceStack'); +new DevinstanceStack(app, 'CanaryStack', {}, true); \ No newline at end of file diff --git a/resource-types/VSCodeRemoteDevelopment/src/devinstance-stack.ts b/resource-types/VSCodeRemoteDevelopment/src/devinstance-stack.ts new file mode 100644 index 0000000..58de178 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/src/devinstance-stack.ts @@ -0,0 +1,148 @@ +import * as cdk from '@aws-cdk/core' +import * as ec2 from '@aws-cdk/aws-ec2' +import * as iam from '@aws-cdk/aws-iam' +import { v4 as uuidv4 } from 'uuid' + +export class DevinstanceStack extends cdk.Stack { + constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps, canary: boolean = false) { + super(scope, id, props); + + const keypairParameter = new cdk.CfnParameter(this, "keypair", { + type: "String", + default: 'dev', + description: "The name of the SSH keypair to connect to your dev environment." + }) + + const instanceTypeParameter = new cdk.CfnParameter(this, "instanceType", { + type: "String", + default: 'm4.xlarge', + description: "The instance type for your dev environment." + }) + + const diskSizeParameter = new cdk.CfnParameter(this, "diskSize", { + type: "Number", + default: 100, + description: "The size of the persistent volume for your dev environment in Gibibytes." + }) + + const vpc = new ec2.Vpc(this, 'DevVpc', { + maxAzs: 1, + subnetConfiguration: [{ + cidrMask: 26, + name: 'publicSubnet', + subnetType: ec2.SubnetType.PUBLIC, + }], + natGateways: 0 + }) + + const volume = new ec2.Volume(this, 'Volume', { + availabilityZone: vpc.availabilityZones[0], + size: cdk.Size.gibibytes(diskSizeParameter.valueAsNumber), + }); + + const cfnVolume = volume.node.findChild('Resource') as cdk.CfnResource + cfnVolume.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN); + + const publicSubnet = vpc.selectSubnets({ + subnetType: ec2.SubnetType.PUBLIC + }) + + const ec2Sg = new ec2.SecurityGroup(this, "ec2Sg", { + vpc: vpc + }) + + ec2Sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22)) + + const instance = new ec2.Instance(this, "Devinstance-" + uuidv4(), { + vpc: vpc, + vpcSubnets: publicSubnet, + instanceType: new ec2.InstanceType(instanceTypeParameter.valueAsString), + machineImage: ec2.MachineImage.latestAmazonLinux({ + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2 + }), + keyName: keypairParameter.valueAsString, + securityGroup: ec2Sg, + blockDevices: [ + { + deviceName: '/dev/xvda', + volume: ec2.BlockDeviceVolume.ebs(100, { encrypted: true }), + } + ] + }) + + const volumesPolicy = new iam.PolicyStatement({ + resources: ['*'], + actions: [ + "ec2:DescribeAvailabilityZones", + "ec2:DescribeInstances", + "ec2:DescribeVolumes", + "ec2:DescribeVolumeAttribute", + "ec2:DescribeVolumeStatus", + "ec2:DescribeSnapshots", + "ec2:DescribeSnapshotAttribute", + "ec2:DescribeTags" + ] + }) + + instance.addToRolePolicy(volumesPolicy) + + const targetDevice = '/dev/xvdz' + const mointPoint = '/mnt/ebs/fs1' + + const attachGrant = volume.grantAttachVolumeByResourceTag(instance.grantPrincipal, [instance]); + const detachGrant = volume.grantDetachVolumeByResourceTag(instance.grantPrincipal, [instance]); + + const eIp = new ec2.CfnEIP(this, "DevIp"); + const ec2Assoc = new ec2.CfnEIPAssociation(this, "Ec2Association", { + eip: eIp.ref, + instanceId: instance.instanceId + }); + + const cfnInstance = instance.node.defaultChild as ec2.CfnInstance + cfnInstance.hibernationOptions = { configured: true } + + instance.userData.addCommands( + "yum check-update -y", + "yum upgrade -y", + ) + + instance.userData.addCommands(` +aws configure set region ${this.region} +INST_ID=$(curl http://169.254.169.254/latest/meta-data/instance-id) +VOL_ID=${volume.volumeId} +# aws ec2 detach-volume --force --volume-id $VOL_ID +VOLUME_STATUS='' +until [ "$VOLUME_STATUS" == \\\"available\\\" ]; do + echo "Waiting for volume $VOL_ID to be available for attaching..." + VOLUME_STATUS=$(aws ec2 describe-volumes --volume-ids $VOL_ID --query 'Volumes[0].State') + echo "Volume status is $VOLUME_STATUS" + sleep 5 +done +echo "Attaching volume..." +aws ec2 attach-volume --volume-id $VOL_ID --instance-id $INST_ID --device ${targetDevice} +while ! test -e ${targetDevice}; do sleep 1; done +` + ) + + instance.userData.addCommands( + `mkfs.xfs ${targetDevice}`, + `mkdir -p ${mointPoint}`, + `mount ${targetDevice} ${mointPoint}`, + `xfs_growfs -d ${mointPoint}`, + `echo "${targetDevice} ${mointPoint} xfs defaults,nofail 0 2" >> /etc/fstab`, + `mkdir -p ${mointPoint}/home ${mointPoint}/workspace ${mointPoint}/docker`) + + instance.userData.addCommands( + `ln -s ${mointPoint}/docker /var/lib/docker`, + "amazon-linux-extras install docker", + "service docker start", + "usermod -a -G docker ec2-user", + "chkconfig docker on") + + new cdk.CfnOutput(this, 'ssh', { + value: `ssh://ec2-user@${instance.instancePublicDnsName}`, + }); + } + +} + diff --git a/resource-types/VSCodeRemoteDevelopment/src/devinstance.ts b/resource-types/VSCodeRemoteDevelopment/src/devinstance.ts new file mode 100644 index 0000000..2233a47 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/src/devinstance.ts @@ -0,0 +1,12 @@ +#!/usr/bin/env node +import * as cdk from '@aws-cdk/core'; +import { DevinstanceStack } from './devinstance-stack'; + + + +export function getTemplate(): any { + const app = new cdk.App(); + new DevinstanceStack(app, 'DevinstanceStack'); + const assembly = app.synth() + return assembly.getStackByName('DevinstanceStack').template +} \ No newline at end of file diff --git a/resource-types/VSCodeRemoteDevelopment/src/handlers.ts b/resource-types/VSCodeRemoteDevelopment/src/handlers.ts new file mode 100644 index 0000000..9d6e81f --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/src/handlers.ts @@ -0,0 +1,402 @@ +import { + Action, + BaseResource, + exceptions, + handlerEvent, + HandlerErrorCode, + OperationStatus, + Optional, + ProgressEvent, + ResourceHandlerRequest, + SessionProxy, +} from 'cfn-rpdk'; +import { ResourceModel } from './models'; +import AWS from 'aws-sdk' +import { v4 as uuidv4 } from 'uuid' +import { getTemplate } from './devinstance' +import { env } from 'process' + +interface CallbackContext extends Record { } + +function getCfClient(session: Optional) { + if (env.HY_OVERRIDE_CREDENTIALS === "yes") { + console.log('use override credentials') + AWS.config.accessKeyId = env.HY_ACCESS_KEY_ID + AWS.config.secretAccessKey = env.HY_SECRET_ACCESS_KEY + return new AWS.CloudFormation() + } + + const plainSession = session as any + if (plainSession.options.credentials.secretAccessKey) { + var cf = session.client("CloudFormation") as AWS.CloudFormation + console.log("use provided credentials") + } else { + var cf = new AWS.CloudFormation() + console.log("use local credentials") + } + + return cf +} + +function createErrorProgressEvent(err: any, code?: HandlerErrorCode): ProgressEvent { + code = code || HandlerErrorCode.GeneralServiceException + const progress = ProgressEvent.failed(code, `${err.code}: ${err.message}`) + console.log(progress) + return progress +} + +function cfFailed(stackStatus: string): boolean { + return ["UPDATE_ROLLBACK_COMPLETE", "ROLLBACK_IN_PROGRESS", "ROLLBACK_COMPLETE", "CREATE_FAILED", "ROLLBACK_FAILED", "DELETE_FAILED", "UPDATE_ROLLBACK_FAILED", "IMPORT_ROLLBACK_FAILED"].includes(stackStatus) +} + +function emptyContext(progress: ProgressEvent) { + const ctx = {} + progress.callbackContext = ctx + return ctx +} + +function setContext(progress: ProgressEvent, ctx: CallbackContext) { + progress.callbackContext = ctx + return ctx +} + +async function getResourceModelFromStack(session: Optional, stackName: string): Promise { + if (stackName === undefined || stackName === "") { + const err = new Error("Empty UID does not exist") as any + err.code = "ValidationError" + throw err + } + const response: AWS.CloudFormation.DescribeStacksOutput = await getCfClient(session).describeStacks({ StackName: stackName }).promise() + + const outputs = response.Stacks[0].Outputs.reduce((map: any, item) => { + map[item.OutputKey] = item.OutputValue + return map + }, {}) + + const props = response.Stacks[0].Parameters.reduce((map: any, item) => { + map[item.ParameterKey] = item.ParameterValue + return map + }, {}) + + return new ResourceModel( + { + uID: stackName, + instanceType: props.instanceType, + diskSize: parseInt(props.diskSize) as number, + keypair: props.keypair, + sSH: outputs.ssh + } + ) +} + +function getStackParametersFromModel(model: ResourceModel): AWS.CloudFormation.Parameters { + return [ + { ParameterKey: "keypair", ParameterValue: model.keypair }, + { ParameterKey: "instanceType", ParameterValue: model.instanceType }, + { ParameterKey: "diskSize", ParameterValue: model.diskSize.toString() } + ] +} + +function debugLog(action: Action, request: ResourceHandlerRequest, callbackContext: CallbackContext) { + console.log(action + " -----------------") + console.log("Request: " + JSON.stringify(request)) + console.log("Context: " + JSON.stringify(callbackContext)) +} + +class Resource extends BaseResource { + + /** + * CloudFormation invokes this handler when the resource is initially created + * during stack create operations. + * + * @param session Current AWS session passed through from caller + * @param request The request object for the provisioning request passed to the implementor + * @param callbackContext Custom context object to enable handlers to process re-invocation + */ + @handlerEvent(Action.Create) + public async create( + session: Optional, + request: ResourceHandlerRequest, + callbackContext: CallbackContext, + ): Promise { + const model = new ResourceModel(request.desiredResourceState); + const progress: ProgressEvent = ProgressEvent.builder() + .status(OperationStatus.InProgress) + .resourceModel(model) + .callbackContext(callbackContext) + .build() as ProgressEvent; + + debugLog(Action.Create, request, callbackContext) + + const state = callbackContext.state + + if (state === "creating") { + const uuid: string = model.uID + const response = await getCfClient(session).describeStacks({ StackName: uuid }).promise() + const stack = response.Stacks[0] + if (stack.StackStatus === "CREATE_COMPLETE") { + const outputs = stack.Outputs.reduce((map: any, item) => { + map[item.OutputKey] = item.OutputValue + return map + }, {}) + model.sSH = outputs.ssh + progress.status = OperationStatus.Success + emptyContext(progress) + } else if (cfFailed(stack.StackStatus)) { + emptyContext(progress) + return createErrorProgressEvent(new Error(stack.StackStatus + ": " + stack.StackStatusReason), HandlerErrorCode.InvalidRequest) + } + + } else { + + try { + if (model.sSH !== undefined || model.uID !== undefined) { + return createErrorProgressEvent(new Error("The SSH and UID properties are read-only."), HandlerErrorCode.InvalidRequest) + } + const uuid: string = "DevInstance-" + uuidv4() + const parameters = getStackParametersFromModel(model) + const template = JSON.stringify(getTemplate()) + await getCfClient(session).createStack({ StackName: uuid, Parameters: parameters, Capabilities: ["CAPABILITY_IAM"], TemplateBody: template }).promise() + setContext(progress, { + state: "creating" + }) + model.uID = uuid + } catch (err) { + emptyContext(progress) + return createErrorProgressEvent(err, HandlerErrorCode.InvalidRequest) + } + + } + console.log(progress) + return progress + + + } + + /** + * CloudFormation invokes this handler when the resource is updated + * as part of a stack update operation. + * + * @param session Current AWS session passed through from caller + * @param request The request object for the provisioning request passed to the implementor + * @param callbackContext Custom context object to enable handlers to process re-invocation + */ + @handlerEvent(Action.Update) + public async update( + session: Optional, + request: ResourceHandlerRequest, + callbackContext: CallbackContext, + ): Promise { + const model: ResourceModel = request.desiredResourceState; + const progress: ProgressEvent = ProgressEvent.builder() + .status(OperationStatus.InProgress) + .resourceModel(model) + .callbackContext(callbackContext) + .build() as ProgressEvent + + debugLog(Action.Update, request, callbackContext) + + const state = callbackContext.state + + if (state === "updating") { + const response = await getCfClient(session).describeStacks({ StackName: model.uID }).promise() + const stack = response.Stacks[0] + if (stack.StackStatus === "UPDATE_COMPLETE") { + progress.status = OperationStatus.Success + emptyContext(progress) + } else if (cfFailed(stack.StackStatus)) { + emptyContext(progress) + return createErrorProgressEvent(new Error(stack.StackStatus + ": " + stack.StackStatusReason)) + } + + } else { + + try { + if (model.uID === undefined || model.uID === "") { + const err = new Error("Empty UID does not exist") as any + err.code = "ValidationError" + throw err + } + const parameters = getStackParametersFromModel(model) + const template = JSON.stringify(getTemplate()) + await getCfClient(session).updateStack({ StackName: model.uID, Parameters: parameters, Capabilities: ["CAPABILITY_IAM"], TemplateBody: template }).promise() + setContext(progress, { + state: "updating" + }) + } catch (err) { + emptyContext(progress) + if (err.code === "ValidationError" && err.message.includes("does not exist")) { + return createErrorProgressEvent(err, HandlerErrorCode.NotFound) + } else { + return createErrorProgressEvent(err) + } + } + } + console.log(progress) + return progress + + + } + + /** + * CloudFormation invokes this handler when the resource is deleted, either when + * the resource is deleted from the stack as part of a stack update operation, + * or the stack itself is deleted. + * + * @param session Current AWS session passed through from caller + * @param request The request object for the provisioning request passed to the implementor + * @param callbackContext Custom context object to enable handlers to process re-invocation + */ + @handlerEvent(Action.Delete) + public async delete( + session: Optional, + request: ResourceHandlerRequest, + callbackContext: CallbackContext, + ): Promise { + const model: ResourceModel = request.desiredResourceState; + const progress: ProgressEvent = ProgressEvent.builder() + .status(OperationStatus.InProgress) + .callbackContext(callbackContext) + .build() as ProgressEvent + + debugLog(Action.Delete, request, callbackContext) + + const state = callbackContext.state + var uuid: string + + if (state === "deleting") { + uuid = callbackContext.uid + try { + const response = await getCfClient(session).describeStacks({ StackName: uuid }).promise() + const stack = response.Stacks[0] + if (stack.StackStatus === "DELETE_COMPLETE") { + progress.status = OperationStatus.Success + emptyContext(progress) + } else if (cfFailed(stack.StackStatus)) { + emptyContext(progress) + return createErrorProgressEvent(new Error(stack.StackStatus + ": " + stack.StackStatusReason)) + } + } catch (err) { + emptyContext(progress) + if (err.code === "ValidationError" && err.message.includes("does not exist")) { + progress.status = OperationStatus.Success + } else { + return createErrorProgressEvent(err) + } + } + + } else { + uuid = model.uID + try { + const response = await getCfClient(session).describeStacks({ StackName: uuid }).promise() + const stack = response.Stacks[0] + if (stack.StackStatus === "DELETE_COMPLETE") { + emptyContext(progress) + return createErrorProgressEvent(new Error("Already deleted."), HandlerErrorCode.NotFound) + } + await getCfClient(session).deleteStack({ StackName: uuid }).promise() + setContext(progress, { + state: "deleting", + uid: uuid + }) + } catch (err) { + emptyContext(progress) + return createErrorProgressEvent(err, HandlerErrorCode.NotFound) + } + + } + if (progress.status === OperationStatus.InProgress) { + const minimalModel = new ResourceModel() + minimalModel.uID = uuid + progress.resourceModel = minimalModel + } + + console.log(progress) + return progress + + } + + /** + * CloudFormation invokes this handler as part of a stack update operation when + * detailed information about the resource's current state is required. + * + * @param session Current AWS session passed through from caller + * @param request The request object for the provisioning request passed to the implementor + * @param callbackContext Custom context object to enable handlers to process re-invocation + */ + @handlerEvent(Action.Read) + public async read( + session: Optional, + request: ResourceHandlerRequest, + callbackContext: CallbackContext, + ): Promise { + + debugLog(Action.Read, request, callbackContext) + try { + const model: ResourceModel = await getResourceModelFromStack(session, request.desiredResourceState.uID) + const progress: ProgressEvent = ProgressEvent.builder() + .status(OperationStatus.Success) + .resourceModel(model) + .callbackContext(callbackContext) + .build() as ProgressEvent + + console.log(progress) + return progress + + } catch (err) { + if (err.code === "ValidationError" && err.message.includes("does not exist")) { + return createErrorProgressEvent(err, HandlerErrorCode.NotFound) + } else { + return createErrorProgressEvent(err) + } + } + + + } + + /** + * CloudFormation invokes this handler when summary information about multiple + * resources of this resource provider is required. + * + * @param session Current AWS session passed through from caller + * @param request The request object for the provisioning request passed to the implementor + * @param callbackContext Custom context object to enable handlers to process re-invocation + */ + @handlerEvent(Action.List) + public async list( + session: Optional, + request: ResourceHandlerRequest, + callbackContext: CallbackContext, + ): Promise { + + debugLog(Action.List, request, callbackContext) + + const stacks = await getCfClient(session).listStacks({}).promise() + + const results = stacks.StackSummaries + .filter((stack: AWS.CloudFormation.StackSummary) => { + return stack.StackName.includes("DevInstance-") && !["DELETE_COMPLETE"].includes(stack.StackStatus) + }) + .map(async (summary: AWS.CloudFormation.StackSummary) => { + return await getResourceModelFromStack(session, summary.StackName) + }) + + const models: Array = await Promise.all(results) + + const progress: ProgressEvent = ProgressEvent.builder() + .status(OperationStatus.Success) + .resourceModels(models) + .callbackContext(callbackContext) + .build() as ProgressEvent + + console.log(progress) + return progress; + } +} + + +const resource = new Resource(ResourceModel.TYPE_NAME, ResourceModel); + +export const entrypoint = resource.entrypoint; + +export const testEntrypoint = resource.testEntrypoint; diff --git a/resource-types/VSCodeRemoteDevelopment/src/models.ts b/resource-types/VSCodeRemoteDevelopment/src/models.ts new file mode 100644 index 0000000..0574a69 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/src/models.ts @@ -0,0 +1,78 @@ +// This is a generated file. Modifications will be overwritten. +import { BaseModel, Dict, integer, Integer, Optional, transformValue } from 'cfn-rpdk'; +import { Exclude, Expose, Type, Transform } from 'class-transformer'; + +export class ResourceModel extends BaseModel { + ['constructor']: typeof ResourceModel; + + @Exclude() + public static readonly TYPE_NAME: string = 'AWSSamples::Devtools::Devinstance'; + + @Exclude() + protected readonly IDENTIFIER_KEY_UID: string = '/properties/UID'; + + @Expose({ name: 'UID' }) + @Transform( + (value: any, obj: any) => + transformValue(String, 'uID', value, obj, []), + { + toClassOnly: true, + } + ) + uID?: Optional; + @Expose({ name: 'InstanceType' }) + @Transform( + (value: any, obj: any) => + transformValue(String, 'instanceType', value, obj, []), + { + toClassOnly: true, + } + ) + instanceType?: Optional; + @Expose({ name: 'DiskSize' }) + @Transform( + (value: any, obj: any) => + transformValue(Integer, 'diskSize', value, obj, []), + { + toClassOnly: true, + } + ) + diskSize?: Optional; + @Expose({ name: 'Keypair' }) + @Transform( + (value: any, obj: any) => + transformValue(String, 'keypair', value, obj, []), + { + toClassOnly: true, + } + ) + keypair?: Optional; + @Expose({ name: 'SSH' }) + @Transform( + (value: any, obj: any) => + transformValue(String, 'sSH', value, obj, []), + { + toClassOnly: true, + } + ) + sSH?: Optional; + + @Exclude() + public getPrimaryIdentifier(): Dict { + const identifier: Dict = {}; + if (this.uID != null) { + identifier[this.IDENTIFIER_KEY_UID] = this.uID; + } + + // only return the identifier if it can be used, i.e. if all components are present + return Object.keys(identifier).length === 1 ? identifier : null; + } + + @Exclude() + public getAdditionalIdentifiers(): Array { + const identifiers: Array = new Array(); + // only return the identifiers if any can be used + return identifiers.length === 0 ? null : identifiers; + } +} + diff --git a/resource-types/VSCodeRemoteDevelopment/template.yml b/resource-types/VSCodeRemoteDevelopment/template.yml new file mode 100644 index 0000000..d36e7a6 --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/template.yml @@ -0,0 +1,33 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the AWSSamples::Devtools::Devinstance resource type + +Globals: + Function: + Timeout: 180 # docker start-up times can be long for SAM CLI + MemorySize: 256 + +Resources: + TestEntrypoint: + Type: AWS::Serverless::Function + Properties: + Handler: dist/handlers.testEntrypoint + Runtime: nodejs12.x + CodeUri: ./ + Environment: + Variables: + HY_OVERRIDE_CREDENTIALS: + HY_ACCESS_KEY_ID: + HY_SECRET_ACCESS_KEY: + + TypeFunction: + Type: AWS::Serverless::Function + Properties: + Handler: dist/handlers.entrypoint + Runtime: nodejs12.x + CodeUri: ./ + Environment: + Variables: + HY_OVERRIDE_CREDENTIALS: + HY_ACCESS_KEY_ID: + HY_SECRET_ACCESS_KEY: diff --git a/resource-types/VSCodeRemoteDevelopment/tsconfig.json b/resource-types/VSCodeRemoteDevelopment/tsconfig.json new file mode 100644 index 0000000..ffbcc7d --- /dev/null +++ b/resource-types/VSCodeRemoteDevelopment/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2017", + "module": "commonjs", + "noImplicitAny": true, + "alwaysStrict": true, + "esModuleInterop": true, + "moduleResolution": "node", + "allowJs": true, + "experimentalDecorators": true, + "outDir": "dist" + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +}