testdriverai 6.1.4 → 6.1.5-canary.b2eb46e.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/self-hosted.yml +98 -0
- package/agent/index.js +17 -5
- package/agent/interface.js +8 -0
- package/docs/docs.json +1 -0
- package/docs/getting-started/self-hosting.mdx +303 -0
- package/docs/images/content/self-hosted/launchtemplateid.png +0 -0
- package/package.json +1 -1
- package/setup/aws/cloudformation.yaml +463 -0
- package/setup/aws/spawn-runner.sh +189 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
name: AWS
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
gather:
|
|
9
|
+
name: Gather Test Files
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
outputs:
|
|
12
|
+
test_files: ${{ steps.test_list.outputs.files }}
|
|
13
|
+
steps:
|
|
14
|
+
- name: Check out repository
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Find all test files
|
|
18
|
+
id: test_list
|
|
19
|
+
run: |
|
|
20
|
+
FILES=$(ls ./testdriver/acceptance/*.yaml)
|
|
21
|
+
FILENAMES=$(basename -a $FILES)
|
|
22
|
+
FILES_JSON=$(echo "$FILENAMES" | jq -R -s -c 'split("\n")[:-1]')
|
|
23
|
+
echo "files=$FILES_JSON" >> $GITHUB_OUTPUT
|
|
24
|
+
|
|
25
|
+
test:
|
|
26
|
+
needs: gather
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
strategy:
|
|
29
|
+
matrix:
|
|
30
|
+
test: ${{ fromJson(needs.gather.outputs.test_files) }}
|
|
31
|
+
fail-fast: false
|
|
32
|
+
steps:
|
|
33
|
+
- name: Checkout repository
|
|
34
|
+
uses: actions/checkout@v4
|
|
35
|
+
with:
|
|
36
|
+
fetch-depth: 0
|
|
37
|
+
# only needed for `act`
|
|
38
|
+
# - name: Install AWS CLI
|
|
39
|
+
# run: |
|
|
40
|
+
# apt-get update
|
|
41
|
+
# apt-get install curl unzip -y
|
|
42
|
+
# curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
|
|
43
|
+
# unzip awscliv2.zip
|
|
44
|
+
# ./aws/install
|
|
45
|
+
- name: Set up Node.js
|
|
46
|
+
uses: actions/setup-node@v4
|
|
47
|
+
with:
|
|
48
|
+
node-version: "20"
|
|
49
|
+
cache: "npm"
|
|
50
|
+
- name: Install dependencies
|
|
51
|
+
run: NODE_ENV=production npm ci
|
|
52
|
+
- name: Setup AWS Instance
|
|
53
|
+
id: aws-setup
|
|
54
|
+
run: |
|
|
55
|
+
OUTPUT=$(./setup/aws/spawn-runner.sh | tee /dev/stderr) # Capture and display output
|
|
56
|
+
echo "$OUTPUT"
|
|
57
|
+
PUBLIC_IP=$(echo "$OUTPUT" | grep "PUBLIC_IP=" | cut -d'=' -f2)
|
|
58
|
+
INSTANCE_ID=$(echo "$OUTPUT" | grep "INSTANCE_ID=" | cut -d'=' -f2)
|
|
59
|
+
AWS_REGION=$(echo "$OUTPUT" | grep "AWS_REGION=" | cut -d'=' -f2)
|
|
60
|
+
echo "public-ip=$PUBLIC_IP" >> $GITHUB_OUTPUT
|
|
61
|
+
echo "instance-id=$INSTANCE_ID" >> $GITHUB_OUTPUT
|
|
62
|
+
echo "aws-region=$AWS_REGION" >> $GITHUB_OUTPUT
|
|
63
|
+
env:
|
|
64
|
+
FORCE_COLOR: 3
|
|
65
|
+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
66
|
+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
67
|
+
AWS_REGION: us-east-2
|
|
68
|
+
AWS_LAUNCH_TEMPLATE_ID: lt-00d02f31cfc602f27
|
|
69
|
+
AMI_ID: ami-085f872ca0cd80fed
|
|
70
|
+
- name: Run TestDriver
|
|
71
|
+
run: node bin/testdriverai.js run testdriver/acceptance/${{ matrix.test }} --ip="${{ steps.aws-setup.outputs.public-ip }}" --junit=out.xml
|
|
72
|
+
env:
|
|
73
|
+
TD_API_KEY: ${{ secrets.TD_API_KEY }}
|
|
74
|
+
TD_WEBSITE: https://testdriver-sandbox.vercel.app
|
|
75
|
+
TD_THIS_FILE: ${{ matrix.test }}
|
|
76
|
+
- name: Upload TestDriver AI CLI logs
|
|
77
|
+
if: always()
|
|
78
|
+
uses: actions/upload-artifact@v4
|
|
79
|
+
with:
|
|
80
|
+
name: testdriverai-cli-logs-${{ matrix.test }}
|
|
81
|
+
path: /tmp/testdriverai-cli-*.log
|
|
82
|
+
if-no-files-found: warn
|
|
83
|
+
retention-days: 30
|
|
84
|
+
- name: Upload test results as artifact
|
|
85
|
+
if: always()
|
|
86
|
+
uses: actions/upload-artifact@v4
|
|
87
|
+
with:
|
|
88
|
+
name: test-results-${{ matrix.test }}
|
|
89
|
+
path: out.xml
|
|
90
|
+
retention-days: 30
|
|
91
|
+
- name: Shutdown AWS Instance
|
|
92
|
+
if: always()
|
|
93
|
+
run: aws ec2 terminate-instances --region "$AWS_REGION" --instance-ids "$INSTANCE_ID"
|
|
94
|
+
env:
|
|
95
|
+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
96
|
+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
97
|
+
AWS_REGION: ${{ steps.aws-setup.outputs.aws-region }}
|
|
98
|
+
INSTANCE_ID: ${{ steps.aws-setup.outputs.instance-id }}
|
package/agent/index.js
CHANGED
|
@@ -74,6 +74,7 @@ class TestDriverAgent extends EventEmitter2 {
|
|
|
74
74
|
this.sandboxId = flags["sandbox-id"] || null;
|
|
75
75
|
this.sandboxAmi = flags["sandbox-ami"] || null;
|
|
76
76
|
this.sandboxInstance = flags["sandbox-instance"] || null;
|
|
77
|
+
this.ip = flags.ip || null;
|
|
77
78
|
this.workingDir = flags.workingDir || process.cwd();
|
|
78
79
|
|
|
79
80
|
// Resolve thisFile to absolute path with proper extension
|
|
@@ -1709,7 +1710,20 @@ ${regression}
|
|
|
1709
1710
|
const recentId = createNew ? null : this.getRecentSandboxId();
|
|
1710
1711
|
|
|
1711
1712
|
// Set sandbox ID for reconnection (only if not creating new and recent ID exists)
|
|
1712
|
-
if (
|
|
1713
|
+
if (this.ip) {
|
|
1714
|
+
let instance = await this.sandbox.send({
|
|
1715
|
+
type: "direct",
|
|
1716
|
+
resolution: this.config.TD_RESOLUTION,
|
|
1717
|
+
ci: this.config.CI,
|
|
1718
|
+
ip: this.ip,
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
await this.renderSandbox(instance.instance, headless);
|
|
1722
|
+
await this.newSession();
|
|
1723
|
+
await this.runLifecycle("provision");
|
|
1724
|
+
|
|
1725
|
+
return;
|
|
1726
|
+
} else if (!createNew && recentId) {
|
|
1713
1727
|
this.emitter.emit(
|
|
1714
1728
|
events.log.narration,
|
|
1715
1729
|
theme.dim(`using recent sandbox: ${recentId}`),
|
|
@@ -1720,10 +1734,8 @@ ${regression}
|
|
|
1720
1734
|
events.log.narration,
|
|
1721
1735
|
theme.dim(`no recent sandbox found, creating a new one.`),
|
|
1722
1736
|
);
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
// Only attempt to connect to existing sandbox if not in CI mode and not creating new
|
|
1726
|
-
if (this.sandboxId && !this.config.CI && !createNew) {
|
|
1737
|
+
} else if (this.sandboxId && !this.config.CI) {
|
|
1738
|
+
// Only attempt to connect to existing sandbox if not in CI mode and not creating new
|
|
1727
1739
|
// Attempt to connect to known instance
|
|
1728
1740
|
this.emitter.emit(
|
|
1729
1741
|
events.log.narration,
|
package/agent/interface.js
CHANGED
|
@@ -55,6 +55,10 @@ function createCommandDefinitions(agent) {
|
|
|
55
55
|
"sandbox-instance": Flags.string({
|
|
56
56
|
description: "Specify EC2 instance type for sandbox (e.g., i3.metal)",
|
|
57
57
|
}),
|
|
58
|
+
ip: Flags.string({
|
|
59
|
+
description:
|
|
60
|
+
"Connect directly to a sandbox at the specified IP address",
|
|
61
|
+
}),
|
|
58
62
|
summary: Flags.string({
|
|
59
63
|
description: "Specify output file for summarize results",
|
|
60
64
|
}),
|
|
@@ -129,6 +133,10 @@ function createCommandDefinitions(agent) {
|
|
|
129
133
|
"sandbox-instance": Flags.string({
|
|
130
134
|
description: "Specify EC2 instance type for sandbox (e.g., i3.metal)",
|
|
131
135
|
}),
|
|
136
|
+
ip: Flags.string({
|
|
137
|
+
description:
|
|
138
|
+
"Connect directly to a sandbox at the specified IP address",
|
|
139
|
+
}),
|
|
132
140
|
summary: Flags.string({
|
|
133
141
|
description: "Specify output file for summarize results",
|
|
134
142
|
}),
|
package/docs/docs.json
CHANGED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Self-Hosting TestDriver"
|
|
3
|
+
sidebarTitle: "Self-Hosting"
|
|
4
|
+
description: "Complete guide to self-hosting TestDriver instances on AWS"
|
|
5
|
+
icon: "server"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
```mermaid
|
|
9
|
+
graph LR
|
|
10
|
+
A[CLI] <--> B[api.testdriver.ai]
|
|
11
|
+
B <--> C[Your AWS EC2 Instance]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Self-hosting TestDriver allows you to run tests on your own infrastructure, giving you full control over the environment, security, and configurations. This guide walks you through setting up and managing self-hosted TestDriver instances using AWS.
|
|
15
|
+
|
|
16
|
+
## Why self host?
|
|
17
|
+
|
|
18
|
+
- **Enhanced security**: Get complete control over ingress and egress rules.
|
|
19
|
+
- **Complete customization**: Modify the TestDriver Golden Image to include custom dependencies, software, and configurations at launch time.
|
|
20
|
+
- **Powerful Infrastructure**: Run tests on bare metal infrastructure that support emulators and simulators.
|
|
21
|
+
|
|
22
|
+
## Quick Start (TL;DR)
|
|
23
|
+
|
|
24
|
+
1. **Copy the workflow file**: Use [`.github/workflows/self-hosted.yml`](https://github.com/testdriverai/cli/tree/main/.github/workflows/self-hosted.yml) as your template
|
|
25
|
+
2. **Run CloudFormation**: Deploy our [`setup/aws/cloudformation.yaml`](https://github.com/testdriverai/cli/tree/main/setup/aws/cloudformation.yaml) to provision infrastructure
|
|
26
|
+
3. **Setup instances**: Use [`setup/aws/spawn-runner.sh`](https://github.com/testdriverai/cli/tree/main/setup/aws/spawn-runner.sh) with your launch template ID
|
|
27
|
+
4. **Configure GitHub Actions**: Add AWS credentials to your repository secrets
|
|
28
|
+
|
|
29
|
+
## Overview
|
|
30
|
+
|
|
31
|
+
Self-hosting TestDriver gives you complete control over your test execution environment. You'll provision EC2 instances on AWS using our pre-configured AMI and infrastructure templates.
|
|
32
|
+
|
|
33
|
+
## Prerequisites
|
|
34
|
+
|
|
35
|
+
- AWS account with appropriate permissions
|
|
36
|
+
- AWS CLI installed locally
|
|
37
|
+
- Access to TestDriver's shared AMI. [Contact us for access](https://form.typeform.com/to/UECf9rDx?typeform-source=testdriver.ai).
|
|
38
|
+
- GitHub repository for your tests
|
|
39
|
+
|
|
40
|
+
## Step 1: Set Up AWS Infrastructure
|
|
41
|
+
|
|
42
|
+
### Deploy CloudFormation Stack
|
|
43
|
+
|
|
44
|
+
Our [`setup/aws/cloudformation.yaml`](https://github.com/testdriverai/cli/tree/main/setup/aws/cloudformation.yaml) template creates:
|
|
45
|
+
|
|
46
|
+
- Dedicated VPC with public subnet
|
|
47
|
+
- Security group with proper port access
|
|
48
|
+
- IAM roles and instance profiles
|
|
49
|
+
- EC2 launch template for programmatic instance creation
|
|
50
|
+
|
|
51
|
+
This is a one-time setup used to generate a template ID for launching instances.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Deploy the CloudFormation stack
|
|
55
|
+
aws cloudformation deploy \
|
|
56
|
+
--template-file setup/aws/cloudformation.yaml \
|
|
57
|
+
--stack-name testdriver-infrastructure-11 \
|
|
58
|
+
--parameter-overrides \
|
|
59
|
+
ProjectTag=testdriver \
|
|
60
|
+
AllowedIngressCidr=0.0.0.0/0 \
|
|
61
|
+
InstanceType=c5.xlarge \
|
|
62
|
+
CreateKeyPair=true \
|
|
63
|
+
--capabilities CAPABILITY_IAM
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
<Danger>
|
|
67
|
+
**Security**: Replace `AllowedIngressCidr=0.0.0.0/0` with your specific IP ranges to lock down access to your VPC.
|
|
68
|
+
</Danger>
|
|
69
|
+
|
|
70
|
+
### Get Launch Template ID
|
|
71
|
+
|
|
72
|
+
After CloudFormation completes, find the launch template ID in the stack outputs:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
aws cloudformation describe-stacks \
|
|
76
|
+
--stack-name testdriver-infrastructure-11 \
|
|
77
|
+
--query 'Stacks[0].Outputs[?OutputKey==`LaunchTemplateId`].OutputValue' \
|
|
78
|
+
--output text
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Save this ID - you'll need it for the next step.
|
|
82
|
+
|
|
83
|
+
## Step 2: Spawn a New TestDriver Runner
|
|
84
|
+
|
|
85
|
+
### Using aws-setup.sh
|
|
86
|
+
|
|
87
|
+
Our [`setup/aws/spawn-runner.sh`](https://github.com/testdriverai/cli/tree/main/setup/aws/spawn-runner.sh) spawns and initializes instances:
|
|
88
|
+
|
|
89
|
+
- Launches instances using your launch template
|
|
90
|
+
- Completes TestDriver handshake
|
|
91
|
+
- Returns instance details for CLI usage
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Launch an instance
|
|
95
|
+
export AWS_REGION=us-east-2
|
|
96
|
+
export AMI_ID=ami-085f872ca0cd80fed # Your TestDriver AMI (contact us to get one)
|
|
97
|
+
export AWS_LAUNCH_TEMPLATE_ID=lt-00d02f31cfc602f27 # From CloudFormation output from step 1
|
|
98
|
+
|
|
99
|
+
./aws-setup.sh
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The script outputs:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
PUBLIC_IP=1.2.3.4
|
|
106
|
+
INSTANCE_ID=i-1234567890abcdef0
|
|
107
|
+
AWS_REGION=us-east-2
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### CLI Usage
|
|
111
|
+
|
|
112
|
+
Once you have an instance IP, run tests directly:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Basic test execution
|
|
116
|
+
npx testdriverai run test.yaml --ip=1.2.3.4
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
You can use the `PUBLIC_IP` to target the instance you just spawned via `./setup/aws/spawn-runner.sh`:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
npx testdriverai@latest run testdriver/your-test.yaml \
|
|
123
|
+
--ip="$PUBLIC_IP" \
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Note that the instance will remain running until you terminate it. You can do this manually via the AWS console, or programmatically in your CI workflow:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
aws ec2 terminate-instances \
|
|
130
|
+
--region us-east-2 \
|
|
131
|
+
--instance-ids $INSTANCE_ID
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Step 3: GitHub Actions Integration
|
|
135
|
+
|
|
136
|
+
### Example Workflow
|
|
137
|
+
|
|
138
|
+
Our [`.github/workflows/self-hosted.yml`](https://github.com/testdriverai/cli/tree/main/.github/workflows/self-hosted.yml) demonstrates the complete workflow:
|
|
139
|
+
|
|
140
|
+
```yaml
|
|
141
|
+
name: TestDriver Self-Hosted
|
|
142
|
+
|
|
143
|
+
on:
|
|
144
|
+
workflow_dispatch:
|
|
145
|
+
push:
|
|
146
|
+
|
|
147
|
+
jobs:
|
|
148
|
+
test:
|
|
149
|
+
runs-on: ubuntu-latest
|
|
150
|
+
steps:
|
|
151
|
+
- name: Checkout repository
|
|
152
|
+
uses: actions/checkout@v4
|
|
153
|
+
|
|
154
|
+
- name: Setup AWS Instance
|
|
155
|
+
id: aws-setup
|
|
156
|
+
run: |
|
|
157
|
+
OUTPUT=$(./setup/aws/spawn-runner.sh | tee /dev/stderr)
|
|
158
|
+
PUBLIC_IP=$(echo "$OUTPUT" | grep "PUBLIC_IP=" | cut -d'=' -f2)
|
|
159
|
+
INSTANCE_ID=$(echo "$OUTPUT" | grep "INSTANCE_ID=" | cut -d'=' -f2)
|
|
160
|
+
echo "public-ip=$PUBLIC_IP" >> $GITHUB_OUTPUT
|
|
161
|
+
echo "instance-id=$INSTANCE_ID" >> $GITHUB_OUTPUT
|
|
162
|
+
env:
|
|
163
|
+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
164
|
+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
165
|
+
AWS_REGION: us-east-2
|
|
166
|
+
AWS_LAUNCH_TEMPLATE_ID: ${{ secrets.AWS_LAUNCH_TEMPLATE_ID }}
|
|
167
|
+
AMI_ID: ${{ secrets.AMI_ID }}
|
|
168
|
+
|
|
169
|
+
- name: Run TestDriver
|
|
170
|
+
run: |
|
|
171
|
+
npx testdriverai run your-test.yaml \
|
|
172
|
+
--ip="${{ steps.aws-setup.outputs.public-ip }}"
|
|
173
|
+
env:
|
|
174
|
+
TD_API_KEY: ${{ secrets.TD_API_KEY }}
|
|
175
|
+
|
|
176
|
+
- name: Shutdown AWS Instance
|
|
177
|
+
if: always()
|
|
178
|
+
run: |
|
|
179
|
+
aws ec2 terminate-instances \
|
|
180
|
+
--region us-east-2 \
|
|
181
|
+
--instance-ids ${{ steps.aws-setup.outputs.instance-id }}
|
|
182
|
+
env:
|
|
183
|
+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
184
|
+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Required Secrets
|
|
188
|
+
|
|
189
|
+
Configure these secrets in your GitHub repository:
|
|
190
|
+
|
|
191
|
+
| Secret | Description | Example |
|
|
192
|
+
| ------------------------ | ----------------------------------- | ------------------------------------------------------------ |
|
|
193
|
+
| `AWS_ACCESS_KEY_ID` | AWS access key | `AKIAIOSFODNN7EXAMPLE` |
|
|
194
|
+
| `AWS_SECRET_ACCESS_KEY` | AWS secret key | `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY` |
|
|
195
|
+
| `AWS_LAUNCH_TEMPLATE_ID` | Launch template from CloudFormation | `lt-07c53ce8349b958d1` |
|
|
196
|
+
| `AMI_ID` | TestDriver AMI ID | `ami-085f872ca0cd80fed` |
|
|
197
|
+
| `TD_API_KEY` | TestDriver API key | Your API key from [the dashboard](https://app.testdriver.ai) |
|
|
198
|
+
|
|
199
|
+
## AMI Customization
|
|
200
|
+
|
|
201
|
+
### Using the Base AMI
|
|
202
|
+
|
|
203
|
+
Our AMI comes pre-configured with:
|
|
204
|
+
|
|
205
|
+
- Windows Server with desktop environment
|
|
206
|
+
- Required TestDriver dependencies
|
|
207
|
+
- Optimized settings for test execution
|
|
208
|
+
|
|
209
|
+
### Modifying the AMI
|
|
210
|
+
|
|
211
|
+
You can customize the AMI for your specific needs:
|
|
212
|
+
|
|
213
|
+
1. **Launch an instance** from our base AMI
|
|
214
|
+
2. **Make your changes** (install software, configure settings)
|
|
215
|
+
3. **Create a new AMI** from your modified instance
|
|
216
|
+
4. **Update your workflow** to use the new AMI ID
|
|
217
|
+
|
|
218
|
+
### Amazon Image Builder
|
|
219
|
+
|
|
220
|
+
For automated AMI builds, use [Amazon EC2 Image Builder](https://aws.amazon.com/image-builder/):
|
|
221
|
+
|
|
222
|
+
```yaml
|
|
223
|
+
# Example Image Builder pipeline
|
|
224
|
+
Components:
|
|
225
|
+
- Name: testdriver-base
|
|
226
|
+
Version: 1.0.0
|
|
227
|
+
Platform: Windows
|
|
228
|
+
Type: BUILD
|
|
229
|
+
Data: |
|
|
230
|
+
name: TestDriver Custom Setup
|
|
231
|
+
description: Custom TestDriver AMI with additional software
|
|
232
|
+
schemaVersion: 1.0
|
|
233
|
+
phases:
|
|
234
|
+
- name: build
|
|
235
|
+
steps:
|
|
236
|
+
- name: InstallSoftware
|
|
237
|
+
action: ExecutePowerShell
|
|
238
|
+
inputs:
|
|
239
|
+
commands:
|
|
240
|
+
- "# Your custom installation commands here"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Security Considerations
|
|
244
|
+
|
|
245
|
+
### Network Security
|
|
246
|
+
|
|
247
|
+
1. **Restrict CIDR blocks**: Only allow access from your known IP ranges
|
|
248
|
+
2. **Use VPC endpoints**: For private communication with AWS services
|
|
249
|
+
3. **Enable VPC Flow Logs**: For network monitoring and debugging
|
|
250
|
+
|
|
251
|
+
### AWS Authentication
|
|
252
|
+
|
|
253
|
+
Use [OIDC for GitHub Actions](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) instead of long-term credentials:
|
|
254
|
+
|
|
255
|
+
```yaml
|
|
256
|
+
permissions:
|
|
257
|
+
id-token: write
|
|
258
|
+
contents: read
|
|
259
|
+
|
|
260
|
+
steps:
|
|
261
|
+
- name: Configure AWS credentials
|
|
262
|
+
uses: aws-actions/configure-aws-credentials@v4
|
|
263
|
+
with:
|
|
264
|
+
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
|
|
265
|
+
aws-region: us-east-2
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Instance Security
|
|
269
|
+
|
|
270
|
+
- **Terminate instances** immediately after use
|
|
271
|
+
- **Monitor costs** with AWS billing alerts
|
|
272
|
+
- **Use least-privilege IAM roles** for instance profiles
|
|
273
|
+
- **Enable CloudTrail** for audit logging
|
|
274
|
+
|
|
275
|
+
## Troubleshooting
|
|
276
|
+
|
|
277
|
+
### Common Issues
|
|
278
|
+
|
|
279
|
+
**Instance not responding:**
|
|
280
|
+
|
|
281
|
+
- Check security group rules allow necessary ports
|
|
282
|
+
- Verify instance has passed all status checks
|
|
283
|
+
- Ensure AMI is compatible with selected instance type
|
|
284
|
+
|
|
285
|
+
**Connection timeouts:**
|
|
286
|
+
|
|
287
|
+
- Verify network connectivity from runner to instance
|
|
288
|
+
- Check VPC routing and internet gateway configuration
|
|
289
|
+
- Confirm instance is in correct subnet
|
|
290
|
+
|
|
291
|
+
**AWS CLI errors:**
|
|
292
|
+
|
|
293
|
+
- Validate AWS credentials and permissions
|
|
294
|
+
- Check AWS service quotas and limits
|
|
295
|
+
- Verify region consistency across all resources
|
|
296
|
+
|
|
297
|
+
### Getting Help
|
|
298
|
+
|
|
299
|
+
For enterprise customers:
|
|
300
|
+
|
|
301
|
+
- Contact your account manager for AMI access issues
|
|
302
|
+
- Use support channels for infrastructure questions
|
|
303
|
+
- Check the TestDriver documentation for CLI usage
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
AWSTemplateFormatVersion: "2010-09-09"
|
|
2
|
+
Description: >-
|
|
3
|
+
Baseline artifacts (NO EC2 instance): Creates a dedicated VPC with public subnet, Security Group,
|
|
4
|
+
IAM Role/Profile, optional KeyPair, and an EC2 Launch Template so you can spawn many instances
|
|
5
|
+
programmatically. Writes handy IDs into SSM Parameter Store for easy lookup in scripts or automation.
|
|
6
|
+
|
|
7
|
+
Metadata:
|
|
8
|
+
AWS::CloudFormation::Interface:
|
|
9
|
+
ParameterGroups:
|
|
10
|
+
- Label:
|
|
11
|
+
default: "Notification Configuration"
|
|
12
|
+
Parameters:
|
|
13
|
+
- NotificationEmail
|
|
14
|
+
- Label:
|
|
15
|
+
default: "Project Configuration"
|
|
16
|
+
Parameters:
|
|
17
|
+
- ProjectTag
|
|
18
|
+
- Label:
|
|
19
|
+
default: "Network Configuration"
|
|
20
|
+
Parameters:
|
|
21
|
+
- AllowedIngressCidr
|
|
22
|
+
- Label:
|
|
23
|
+
default: "Instance Configuration"
|
|
24
|
+
Parameters:
|
|
25
|
+
- InstanceType
|
|
26
|
+
- Label:
|
|
27
|
+
default: "Key Pair Configuration"
|
|
28
|
+
Parameters:
|
|
29
|
+
- CreateKeyPair
|
|
30
|
+
- ExistingKeyName
|
|
31
|
+
ParameterLabels:
|
|
32
|
+
ProjectTag:
|
|
33
|
+
default: "Project Tag"
|
|
34
|
+
InstanceType:
|
|
35
|
+
default: "Instance Type"
|
|
36
|
+
AllowedIngressCidr:
|
|
37
|
+
default: "Allowed Ingress CIDR"
|
|
38
|
+
CreateKeyPair:
|
|
39
|
+
default: "Create New Key Pair"
|
|
40
|
+
ExistingKeyName:
|
|
41
|
+
default: "Existing Key Name (only required if 'Create New Key Pair' is 'no')"
|
|
42
|
+
NotificationEmail:
|
|
43
|
+
default: "Notification Email (optional)"
|
|
44
|
+
|
|
45
|
+
Rules:
|
|
46
|
+
ValidateKeyPairConfiguration:
|
|
47
|
+
RuleCondition: !Equals [!Ref CreateKeyPair, "no"]
|
|
48
|
+
Assertions:
|
|
49
|
+
- Assert: !Not [!Equals [!Ref ExistingKeyName, ""]]
|
|
50
|
+
AssertDescription: "ExistingKeyName must be provided when CreateKeyPair is 'no'"
|
|
51
|
+
|
|
52
|
+
Parameters:
|
|
53
|
+
NotificationEmail:
|
|
54
|
+
Type: String
|
|
55
|
+
Default: ""
|
|
56
|
+
Description: Email address to receive deployment completion notifications (optional)
|
|
57
|
+
|
|
58
|
+
ProjectTag:
|
|
59
|
+
Type: String
|
|
60
|
+
Default: testdriver
|
|
61
|
+
|
|
62
|
+
InstanceType:
|
|
63
|
+
Type: String
|
|
64
|
+
Default: c5.xlarge
|
|
65
|
+
AllowedValues:
|
|
66
|
+
- c5.xlarge
|
|
67
|
+
- c5.2xlarge
|
|
68
|
+
- c5.4xlarge
|
|
69
|
+
- c5.9xlarge
|
|
70
|
+
- c5.12xlarge
|
|
71
|
+
- c5.18xlarge
|
|
72
|
+
- c5.24xlarge
|
|
73
|
+
- c5.metal
|
|
74
|
+
- i3.metal
|
|
75
|
+
Description: Instance type - only c5.xlarge or larger, plus c5.metal and i3.metal allowed
|
|
76
|
+
|
|
77
|
+
AllowedIngressCidr:
|
|
78
|
+
Type: String
|
|
79
|
+
Default: 0.0.0.0/0
|
|
80
|
+
Description: CIDR allowed to access inbound ports (0.0.0.0/0 means "anyone", we recommend tightening this in production).
|
|
81
|
+
|
|
82
|
+
CreateKeyPair:
|
|
83
|
+
Type: String
|
|
84
|
+
AllowedValues: [yes, no]
|
|
85
|
+
Default: yes
|
|
86
|
+
Description: Create a new key pair for instance access? (If 'no', you must provide an existing key name)
|
|
87
|
+
ExistingKeyName:
|
|
88
|
+
Type: String
|
|
89
|
+
Default: ""
|
|
90
|
+
Description: Name of existing EC2 Key Pair (only required when CreateKeyPair is 'no')
|
|
91
|
+
|
|
92
|
+
Conditions:
|
|
93
|
+
UseExistingKeyProvided: !Not [!Equals [!Ref ExistingKeyName, ""]]
|
|
94
|
+
CreateKey: !Equals [!Ref CreateKeyPair, "yes"]
|
|
95
|
+
SendNotification: !Not [!Equals [!Ref NotificationEmail, ""]]
|
|
96
|
+
|
|
97
|
+
Resources:
|
|
98
|
+
# VPC for TestDriver
|
|
99
|
+
TestDriverVpc:
|
|
100
|
+
Type: AWS::EC2::VPC
|
|
101
|
+
Properties:
|
|
102
|
+
CidrBlock: 10.0.0.0/16
|
|
103
|
+
EnableDnsHostnames: true
|
|
104
|
+
EnableDnsSupport: true
|
|
105
|
+
Tags:
|
|
106
|
+
- { Key: Name, Value: !Sub "${AWS::StackName}-vpc" }
|
|
107
|
+
- { Key: Project, Value: !Ref ProjectTag }
|
|
108
|
+
|
|
109
|
+
# Public subnet for EC2 instances
|
|
110
|
+
PublicSubnet:
|
|
111
|
+
Type: AWS::EC2::Subnet
|
|
112
|
+
Properties:
|
|
113
|
+
VpcId: !Ref TestDriverVpc
|
|
114
|
+
CidrBlock: 10.0.1.0/24
|
|
115
|
+
AvailabilityZone: !Select [0, !GetAZs ""]
|
|
116
|
+
MapPublicIpOnLaunch: true
|
|
117
|
+
Tags:
|
|
118
|
+
- { Key: Name, Value: !Sub "${AWS::StackName}-public-subnet" }
|
|
119
|
+
- { Key: Project, Value: !Ref ProjectTag }
|
|
120
|
+
|
|
121
|
+
# Internet Gateway
|
|
122
|
+
InternetGateway:
|
|
123
|
+
Type: AWS::EC2::InternetGateway
|
|
124
|
+
Properties:
|
|
125
|
+
Tags:
|
|
126
|
+
- { Key: Name, Value: !Sub "${AWS::StackName}-igw" }
|
|
127
|
+
- { Key: Project, Value: !Ref ProjectTag }
|
|
128
|
+
|
|
129
|
+
# Attach Internet Gateway to VPC
|
|
130
|
+
AttachGateway:
|
|
131
|
+
Type: AWS::EC2::VPCGatewayAttachment
|
|
132
|
+
Properties:
|
|
133
|
+
VpcId: !Ref TestDriverVpc
|
|
134
|
+
InternetGatewayId: !Ref InternetGateway
|
|
135
|
+
|
|
136
|
+
# Route table for public subnet
|
|
137
|
+
PublicRouteTable:
|
|
138
|
+
Type: AWS::EC2::RouteTable
|
|
139
|
+
Properties:
|
|
140
|
+
VpcId: !Ref TestDriverVpc
|
|
141
|
+
Tags:
|
|
142
|
+
- { Key: Name, Value: !Sub "${AWS::StackName}-public-rt" }
|
|
143
|
+
- { Key: Project, Value: !Ref ProjectTag }
|
|
144
|
+
|
|
145
|
+
# Route to Internet Gateway
|
|
146
|
+
PublicRoute:
|
|
147
|
+
Type: AWS::EC2::Route
|
|
148
|
+
DependsOn: AttachGateway
|
|
149
|
+
Properties:
|
|
150
|
+
RouteTableId: !Ref PublicRouteTable
|
|
151
|
+
DestinationCidrBlock: 0.0.0.0/0
|
|
152
|
+
GatewayId: !Ref InternetGateway
|
|
153
|
+
|
|
154
|
+
# Associate route table with public subnet
|
|
155
|
+
SubnetRouteTableAssociation:
|
|
156
|
+
Type: AWS::EC2::SubnetRouteTableAssociation
|
|
157
|
+
Properties:
|
|
158
|
+
SubnetId: !Ref PublicSubnet
|
|
159
|
+
RouteTableId: !Ref PublicRouteTable
|
|
160
|
+
|
|
161
|
+
SecurityGroup:
|
|
162
|
+
Type: AWS::EC2::SecurityGroup
|
|
163
|
+
Properties:
|
|
164
|
+
GroupDescription: SG for QA desktop testing (RDP/HTTPS/NGINX/pyautogui + VNC)
|
|
165
|
+
VpcId: !Ref TestDriverVpc
|
|
166
|
+
SecurityGroupIngress:
|
|
167
|
+
- {
|
|
168
|
+
IpProtocol: tcp,
|
|
169
|
+
FromPort: 8765,
|
|
170
|
+
ToPort: 8765,
|
|
171
|
+
CidrIp: !Ref AllowedIngressCidr,
|
|
172
|
+
Description: "pyautogui-cli WebSockets",
|
|
173
|
+
}
|
|
174
|
+
- {
|
|
175
|
+
IpProtocol: tcp,
|
|
176
|
+
FromPort: 8443,
|
|
177
|
+
ToPort: 8443,
|
|
178
|
+
CidrIp: !Ref AllowedIngressCidr,
|
|
179
|
+
Description: "Custom 8443",
|
|
180
|
+
}
|
|
181
|
+
- {
|
|
182
|
+
IpProtocol: tcp,
|
|
183
|
+
FromPort: 8080,
|
|
184
|
+
ToPort: 8080,
|
|
185
|
+
CidrIp: !Ref AllowedIngressCidr,
|
|
186
|
+
Description: "NGINX 8080",
|
|
187
|
+
}
|
|
188
|
+
- {
|
|
189
|
+
IpProtocol: tcp,
|
|
190
|
+
FromPort: 80,
|
|
191
|
+
ToPort: 80,
|
|
192
|
+
CidrIp: !Ref AllowedIngressCidr,
|
|
193
|
+
Description: "HTTP 80",
|
|
194
|
+
}
|
|
195
|
+
- {
|
|
196
|
+
IpProtocol: tcp,
|
|
197
|
+
FromPort: 443,
|
|
198
|
+
ToPort: 443,
|
|
199
|
+
CidrIp: !Ref AllowedIngressCidr,
|
|
200
|
+
Description: "HTTPS 443",
|
|
201
|
+
}
|
|
202
|
+
- {
|
|
203
|
+
IpProtocol: tcp,
|
|
204
|
+
FromPort: 3389,
|
|
205
|
+
ToPort: 3389,
|
|
206
|
+
CidrIp: !Ref AllowedIngressCidr,
|
|
207
|
+
Description: "RDP 3389",
|
|
208
|
+
}
|
|
209
|
+
- {
|
|
210
|
+
IpProtocol: tcp,
|
|
211
|
+
FromPort: 5900,
|
|
212
|
+
ToPort: 5900,
|
|
213
|
+
CidrIp: !Ref AllowedIngressCidr,
|
|
214
|
+
Description: "TightVNC 5900",
|
|
215
|
+
}
|
|
216
|
+
- {
|
|
217
|
+
IpProtocol: tcp,
|
|
218
|
+
FromPort: 5901,
|
|
219
|
+
ToPort: 5901,
|
|
220
|
+
CidrIp: !Ref AllowedIngressCidr,
|
|
221
|
+
Description: "noVNC Websockify 5901",
|
|
222
|
+
}
|
|
223
|
+
- {
|
|
224
|
+
IpProtocol: tcp,
|
|
225
|
+
FromPort: 6080,
|
|
226
|
+
ToPort: 6080,
|
|
227
|
+
CidrIp: !Ref AllowedIngressCidr,
|
|
228
|
+
Description: "noVNC HTTP 6080",
|
|
229
|
+
}
|
|
230
|
+
SecurityGroupEgress:
|
|
231
|
+
- IpProtocol: -1
|
|
232
|
+
CidrIp: 0.0.0.0/0
|
|
233
|
+
Description: Allow all outbound
|
|
234
|
+
Tags:
|
|
235
|
+
- { Key: Project, Value: !Ref ProjectTag }
|
|
236
|
+
|
|
237
|
+
InstanceRole:
|
|
238
|
+
Type: AWS::IAM::Role
|
|
239
|
+
Properties:
|
|
240
|
+
AssumeRolePolicyDocument:
|
|
241
|
+
Version: "2012-10-17"
|
|
242
|
+
Statement:
|
|
243
|
+
- Effect: Allow
|
|
244
|
+
Principal: { Service: ec2.amazonaws.com }
|
|
245
|
+
Action: sts:AssumeRole
|
|
246
|
+
ManagedPolicyArns:
|
|
247
|
+
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
|
|
248
|
+
Tags:
|
|
249
|
+
- { Key: Project, Value: !Ref ProjectTag }
|
|
250
|
+
|
|
251
|
+
InstanceProfile:
|
|
252
|
+
Type: AWS::IAM::InstanceProfile
|
|
253
|
+
Properties:
|
|
254
|
+
Roles: [!Ref InstanceRole]
|
|
255
|
+
|
|
256
|
+
KeyPair:
|
|
257
|
+
Type: AWS::EC2::KeyPair
|
|
258
|
+
Condition: CreateKey
|
|
259
|
+
Properties:
|
|
260
|
+
KeyName: !Sub "${AWS::StackName}-key"
|
|
261
|
+
KeyType: rsa
|
|
262
|
+
|
|
263
|
+
LaunchTemplate:
|
|
264
|
+
Type: AWS::EC2::LaunchTemplate
|
|
265
|
+
Properties:
|
|
266
|
+
LaunchTemplateName: !Sub "${AWS::StackName}-lt"
|
|
267
|
+
LaunchTemplateData:
|
|
268
|
+
InstanceType: !Ref InstanceType
|
|
269
|
+
IamInstanceProfile:
|
|
270
|
+
Name: !Ref InstanceProfile
|
|
271
|
+
# Lock SG + Subnet to the same VPC
|
|
272
|
+
NetworkInterfaces:
|
|
273
|
+
- DeviceIndex: 0
|
|
274
|
+
SubnetId: !Ref PublicSubnet
|
|
275
|
+
Groups: [!Ref SecurityGroup]
|
|
276
|
+
AssociatePublicIpAddress: true
|
|
277
|
+
KeyName: !If
|
|
278
|
+
- CreateKey
|
|
279
|
+
- !Ref KeyPair
|
|
280
|
+
- !If
|
|
281
|
+
- UseExistingKeyProvided
|
|
282
|
+
- !Ref ExistingKeyName
|
|
283
|
+
- !Ref AWS::NoValue
|
|
284
|
+
TagSpecifications:
|
|
285
|
+
- ResourceType: instance
|
|
286
|
+
Tags: [{ Key: Project, Value: !Ref ProjectTag }]
|
|
287
|
+
- ResourceType: volume
|
|
288
|
+
Tags: [{ Key: Project, Value: !Ref ProjectTag }]
|
|
289
|
+
|
|
290
|
+
# SNS Topic for deployment notifications
|
|
291
|
+
DeploymentNotificationTopic:
|
|
292
|
+
Type: AWS::SNS::Topic
|
|
293
|
+
Condition: SendNotification
|
|
294
|
+
Properties:
|
|
295
|
+
TopicName: !Sub "${AWS::StackName}-deployment-notifications"
|
|
296
|
+
DisplayName: !Sub "${AWS::StackName} Deployment Notifications"
|
|
297
|
+
Tags:
|
|
298
|
+
- { Key: Project, Value: !Ref ProjectTag }
|
|
299
|
+
|
|
300
|
+
# SNS Subscription for email notifications
|
|
301
|
+
EmailSubscription:
|
|
302
|
+
Type: AWS::SNS::Subscription
|
|
303
|
+
Condition: SendNotification
|
|
304
|
+
Properties:
|
|
305
|
+
Protocol: email
|
|
306
|
+
TopicArn: !Ref DeploymentNotificationTopic
|
|
307
|
+
Endpoint: !Ref NotificationEmail
|
|
308
|
+
|
|
309
|
+
# Custom resource to send completion notification
|
|
310
|
+
DeploymentCompleteNotification:
|
|
311
|
+
Type: AWS::CloudFormation::CustomResource
|
|
312
|
+
Condition: SendNotification
|
|
313
|
+
Properties:
|
|
314
|
+
ServiceToken: !GetAtt NotificationLambda.Arn
|
|
315
|
+
StackName: !Ref AWS::StackName
|
|
316
|
+
TopicArn: !Ref DeploymentNotificationTopic
|
|
317
|
+
DependsOn:
|
|
318
|
+
- SsmParamSg
|
|
319
|
+
- SsmParamIp
|
|
320
|
+
- SsmParamLt
|
|
321
|
+
- SsmParamLtLatest
|
|
322
|
+
- SsmParamVpc
|
|
323
|
+
- SsmParamSubnet
|
|
324
|
+
|
|
325
|
+
# Lambda function to send the notification
|
|
326
|
+
NotificationLambda:
|
|
327
|
+
Type: AWS::Lambda::Function
|
|
328
|
+
Condition: SendNotification
|
|
329
|
+
Properties:
|
|
330
|
+
FunctionName: !Sub "${AWS::StackName}-deployment-notifier"
|
|
331
|
+
Runtime: python3.11
|
|
332
|
+
Handler: index.lambda_handler
|
|
333
|
+
Role: !GetAtt NotificationLambdaRole.Arn
|
|
334
|
+
Code:
|
|
335
|
+
ZipFile: |
|
|
336
|
+
import boto3
|
|
337
|
+
import json
|
|
338
|
+
import cfnresponse
|
|
339
|
+
|
|
340
|
+
def lambda_handler(event, context):
|
|
341
|
+
try:
|
|
342
|
+
if event['RequestType'] == 'Create':
|
|
343
|
+
sns = boto3.client('sns')
|
|
344
|
+
stack_name = event['ResourceProperties']['StackName']
|
|
345
|
+
topic_arn = event['ResourceProperties']['TopicArn']
|
|
346
|
+
|
|
347
|
+
message = f"""
|
|
348
|
+
TestDriver Infrastructure Deployment Complete!
|
|
349
|
+
|
|
350
|
+
Stack Name: {stack_name}
|
|
351
|
+
Status: CREATE_COMPLETE
|
|
352
|
+
|
|
353
|
+
Your TestDriver infrastructure is now ready to use.
|
|
354
|
+
Check the CloudFormation outputs for resource IDs and configuration details.
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
sns.publish(
|
|
358
|
+
TopicArn=topic_arn,
|
|
359
|
+
Subject=f'TestDriver Stack {stack_name} - Deployment Complete',
|
|
360
|
+
Message=message
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
|
|
364
|
+
except Exception as e:
|
|
365
|
+
print(f"Error: {e}")
|
|
366
|
+
cfnresponse.send(event, context, cfnresponse.FAILED, {})
|
|
367
|
+
Tags:
|
|
368
|
+
- { Key: Project, Value: !Ref ProjectTag }
|
|
369
|
+
|
|
370
|
+
# IAM Role for the notification Lambda
|
|
371
|
+
NotificationLambdaRole:
|
|
372
|
+
Type: AWS::IAM::Role
|
|
373
|
+
Condition: SendNotification
|
|
374
|
+
Properties:
|
|
375
|
+
AssumeRolePolicyDocument:
|
|
376
|
+
Version: "2012-10-17"
|
|
377
|
+
Statement:
|
|
378
|
+
- Effect: Allow
|
|
379
|
+
Principal:
|
|
380
|
+
Service: lambda.amazonaws.com
|
|
381
|
+
Action: sts:AssumeRole
|
|
382
|
+
ManagedPolicyArns:
|
|
383
|
+
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
|
|
384
|
+
Policies:
|
|
385
|
+
- PolicyName: SNSPublishPolicy
|
|
386
|
+
PolicyDocument:
|
|
387
|
+
Version: "2012-10-17"
|
|
388
|
+
Statement:
|
|
389
|
+
- Effect: Allow
|
|
390
|
+
Action:
|
|
391
|
+
- sns:Publish
|
|
392
|
+
Resource: !Ref DeploymentNotificationTopic
|
|
393
|
+
Tags:
|
|
394
|
+
- { Key: Project, Value: !Ref ProjectTag }
|
|
395
|
+
|
|
396
|
+
SsmParamSg:
|
|
397
|
+
Type: AWS::SSM::Parameter
|
|
398
|
+
Properties:
|
|
399
|
+
Name: !Sub "/testdriver/infra/${AWS::StackName}/security-group-id"
|
|
400
|
+
Type: String
|
|
401
|
+
Value: !Ref SecurityGroup
|
|
402
|
+
|
|
403
|
+
SsmParamIp:
|
|
404
|
+
Type: AWS::SSM::Parameter
|
|
405
|
+
Properties:
|
|
406
|
+
Name: !Sub "/testdriver/infra/${AWS::StackName}/instance-profile-name"
|
|
407
|
+
Type: String
|
|
408
|
+
Value: !Ref InstanceProfile
|
|
409
|
+
|
|
410
|
+
SsmParamLt:
|
|
411
|
+
Type: AWS::SSM::Parameter
|
|
412
|
+
Properties:
|
|
413
|
+
Name: !Sub "/testdriver/infra/${AWS::StackName}/launch-template-id"
|
|
414
|
+
Type: String
|
|
415
|
+
Value: !Ref LaunchTemplate
|
|
416
|
+
|
|
417
|
+
SsmParamLtLatest:
|
|
418
|
+
Type: AWS::SSM::Parameter
|
|
419
|
+
Properties:
|
|
420
|
+
Name: !Sub "/testdriver/infra/${AWS::StackName}/launch-template-latest-version"
|
|
421
|
+
Type: String
|
|
422
|
+
Value: !GetAtt LaunchTemplate.LatestVersionNumber
|
|
423
|
+
|
|
424
|
+
SsmParamVpc:
|
|
425
|
+
Type: AWS::SSM::Parameter
|
|
426
|
+
Properties:
|
|
427
|
+
Name: !Sub "/testdriver/infra/${AWS::StackName}/testdriver-vpc-id"
|
|
428
|
+
Type: String
|
|
429
|
+
Value: !Ref TestDriverVpc
|
|
430
|
+
|
|
431
|
+
SsmParamSubnet:
|
|
432
|
+
Type: AWS::SSM::Parameter
|
|
433
|
+
Properties:
|
|
434
|
+
Name: !Sub "/testdriver/infra/${AWS::StackName}/testdriver-public-subnet-id"
|
|
435
|
+
Type: String
|
|
436
|
+
Value: !Ref PublicSubnet
|
|
437
|
+
|
|
438
|
+
Outputs:
|
|
439
|
+
VpcId:
|
|
440
|
+
Value: !Ref TestDriverVpc
|
|
441
|
+
Description: VPC ID created for TestDriver
|
|
442
|
+
SubnetId:
|
|
443
|
+
Value: !Ref PublicSubnet
|
|
444
|
+
Description: Public subnet ID for TestDriver instances
|
|
445
|
+
SecurityGroupId:
|
|
446
|
+
Value: !Ref SecurityGroup
|
|
447
|
+
Description: Security Group for QA desktop testing
|
|
448
|
+
InstanceProfileName:
|
|
449
|
+
Value: !Ref InstanceProfile
|
|
450
|
+
Description: Instance Profile to attach to instances
|
|
451
|
+
LaunchTemplateId:
|
|
452
|
+
Value: !Ref LaunchTemplate
|
|
453
|
+
Description: EC2 Launch Template ID
|
|
454
|
+
LaunchTemplateLatestVersion:
|
|
455
|
+
Value: !GetAtt LaunchTemplate.LatestVersionNumber
|
|
456
|
+
Description: Latest Launch Template version
|
|
457
|
+
KeyPairSsmParam:
|
|
458
|
+
Condition: CreateKey
|
|
459
|
+
Value: !Sub "/ec2/keypair/${KeyPair.KeyPairId}"
|
|
460
|
+
Description: SSM parameter that stores the generated private key
|
|
461
|
+
SsmNamespaceUsed:
|
|
462
|
+
Value: !Sub "/testdriver/infra/${AWS::StackName}"
|
|
463
|
+
Description: Prefix where IDs are stored for discovery
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# --- Config (reads from env) ---
|
|
5
|
+
: "${AWS_REGION:?Set AWS_REGION}"
|
|
6
|
+
: "${AMI_ID:?Set AMI_ID (TestDriver Ami)}"
|
|
7
|
+
: "${AWS_LAUNCH_TEMPLATE_ID:?Set AWS_LAUNCH_TEMPLATE_ID}"
|
|
8
|
+
: "${AWS_LAUNCH_TEMPLATE_VERSION:=\$Latest}"
|
|
9
|
+
: "${AWS_TAG_PREFIX:=td}"
|
|
10
|
+
: "${RUNNER_CLASS_ID:=default}"
|
|
11
|
+
|
|
12
|
+
TAG_NAME="${AWS_TAG_PREFIX}-"$(date +%s)
|
|
13
|
+
WS_CONFIG_PATH='C:\Windows\Temp\pyautogui-ws.json'
|
|
14
|
+
|
|
15
|
+
echo "Launching AWS Instance..."
|
|
16
|
+
|
|
17
|
+
# --- 1) Launch instance ---
|
|
18
|
+
RUN_JSON=$(aws ec2 run-instances \
|
|
19
|
+
--region "$AWS_REGION" \
|
|
20
|
+
--image-id "$AMI_ID" \
|
|
21
|
+
--launch-template "LaunchTemplateId=$AWS_LAUNCH_TEMPLATE_ID,Version=$AWS_LAUNCH_TEMPLATE_VERSION" \
|
|
22
|
+
--tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=${TAG_NAME}},{Key=Class,Value=${RUNNER_CLASS_ID}}]" \
|
|
23
|
+
--output json)
|
|
24
|
+
|
|
25
|
+
INSTANCE_ID=$(jq -r '.Instances[0].InstanceId' <<<"$RUN_JSON")
|
|
26
|
+
|
|
27
|
+
echo "Launched: $INSTANCE_ID"
|
|
28
|
+
echo "Instance details:"
|
|
29
|
+
echo " Region: $AWS_REGION"
|
|
30
|
+
echo " AMI ID: $AMI_ID"
|
|
31
|
+
echo " Launch Template ID: $AWS_LAUNCH_TEMPLATE_ID"
|
|
32
|
+
echo " Launch Template Version: $AWS_LAUNCH_TEMPLATE_VERSION"
|
|
33
|
+
|
|
34
|
+
echo "Waiting for instance to be running..."
|
|
35
|
+
|
|
36
|
+
# --- 2) Wait for running + status checks ---
|
|
37
|
+
aws ec2 wait instance-running --region "$AWS_REGION" --instance-ids "$INSTANCE_ID"
|
|
38
|
+
echo "✓ Instance is now running"
|
|
39
|
+
|
|
40
|
+
echo "Waiting for instance to pass status checks..."
|
|
41
|
+
|
|
42
|
+
aws ec2 wait instance-status-ok --region "$AWS_REGION" --instance-ids "$INSTANCE_ID"
|
|
43
|
+
echo "✓ Instance passed all status checks"
|
|
44
|
+
|
|
45
|
+
# Additional validation - check instance state details
|
|
46
|
+
echo "Validating instance readiness..."
|
|
47
|
+
INSTANCE_STATE=$(aws ec2 describe-instances --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" \
|
|
48
|
+
--query 'Reservations[0].Instances[0].{State:State.Name,StatusChecks:StateTransitionReason}' \
|
|
49
|
+
--output json)
|
|
50
|
+
echo "Instance state details: $INSTANCE_STATE"
|
|
51
|
+
|
|
52
|
+
# --- 3) Ensure SSM connectivity ---
|
|
53
|
+
echo "Waiting for SSM connectivity..."
|
|
54
|
+
echo "This can take several minutes for the SSM agent to be fully ready..."
|
|
55
|
+
|
|
56
|
+
# First, check if the instance is registered with SSM
|
|
57
|
+
echo "Checking SSM instance registration..."
|
|
58
|
+
TRIES=0; MAX_TRIES=60
|
|
59
|
+
while :; do
|
|
60
|
+
echo "Attempt $((TRIES+1))/$MAX_TRIES: Checking if instance is registered with SSM..."
|
|
61
|
+
|
|
62
|
+
# Check if instance appears in SSM managed instances
|
|
63
|
+
if aws ssm describe-instance-information \
|
|
64
|
+
--region "$AWS_REGION" \
|
|
65
|
+
--filters "Key=InstanceIds,Values=$INSTANCE_ID" \
|
|
66
|
+
--query 'InstanceInformationList[0].InstanceId' \
|
|
67
|
+
--output text 2>/dev/null | grep -q "$INSTANCE_ID"; then
|
|
68
|
+
echo "✓ Instance is registered with SSM"
|
|
69
|
+
break
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
TRIES=$((TRIES+1))
|
|
73
|
+
if [ $TRIES -ge $MAX_TRIES ]; then
|
|
74
|
+
echo "❌ SSM registration timeout - instance may not have proper IAM role or SSM agent"
|
|
75
|
+
echo "Checking instance details for debugging..."
|
|
76
|
+
aws ec2 describe-instances --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" \
|
|
77
|
+
--query 'Reservations[0].Instances[0].{State:State.Name,IAMProfile:IamInstanceProfile.Arn,SecurityGroups:SecurityGroups[].GroupId}' \
|
|
78
|
+
--output table
|
|
79
|
+
exit 2
|
|
80
|
+
fi
|
|
81
|
+
echo "Instance not yet registered with SSM, waiting..."
|
|
82
|
+
sleep 10
|
|
83
|
+
done
|
|
84
|
+
|
|
85
|
+
# Now test SSM command execution
|
|
86
|
+
echo "Testing SSM command execution..."
|
|
87
|
+
TRIES=0; MAX_TRIES=30
|
|
88
|
+
while :; do
|
|
89
|
+
echo "Attempt $((TRIES+1))/$MAX_TRIES: Sending test SSM command..."
|
|
90
|
+
|
|
91
|
+
if CMD_JSON=$(aws ssm send-command \
|
|
92
|
+
--region "$AWS_REGION" \
|
|
93
|
+
--targets "Key=instanceIds,Values=$INSTANCE_ID" \
|
|
94
|
+
--document-name "AWS-RunPowerShellScript" \
|
|
95
|
+
--parameters 'commands=["echo SSM connectivity test successful"]' \
|
|
96
|
+
--output json 2>/dev/null); then
|
|
97
|
+
|
|
98
|
+
COMMAND_ID=$(jq -r '.Command.CommandId' <<<"$CMD_JSON")
|
|
99
|
+
echo "✓ SSM command sent successfully (Command ID: $COMMAND_ID)"
|
|
100
|
+
|
|
101
|
+
# Wait for command to complete and check status
|
|
102
|
+
echo "Waiting for command execution..."
|
|
103
|
+
if aws ssm wait command-executed --region "$AWS_REGION" --command-id "$COMMAND_ID" --instance-id "$INSTANCE_ID" 2>/dev/null; then
|
|
104
|
+
echo "✓ SSM connectivity confirmed"
|
|
105
|
+
break
|
|
106
|
+
else
|
|
107
|
+
echo "⚠ Command execution may have failed, checking status..."
|
|
108
|
+
CMD_STATUS=$(aws ssm get-command-invocation \
|
|
109
|
+
--region "$AWS_REGION" \
|
|
110
|
+
--command-id "$COMMAND_ID" \
|
|
111
|
+
--instance-id "$INSTANCE_ID" \
|
|
112
|
+
--query 'Status' \
|
|
113
|
+
--output text 2>/dev/null || echo "Unknown")
|
|
114
|
+
echo "Command status: $CMD_STATUS"
|
|
115
|
+
|
|
116
|
+
if [ "$CMD_STATUS" = "Success" ]; then
|
|
117
|
+
echo "✓ Command actually succeeded"
|
|
118
|
+
break
|
|
119
|
+
fi
|
|
120
|
+
fi
|
|
121
|
+
else
|
|
122
|
+
echo "⚠ Failed to send SSM command"
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
TRIES=$((TRIES+1))
|
|
126
|
+
if [ $TRIES -ge $MAX_TRIES ]; then
|
|
127
|
+
echo "❌ SSM command execution timeout"
|
|
128
|
+
echo "Final debugging information:"
|
|
129
|
+
|
|
130
|
+
# Get SSM agent status
|
|
131
|
+
echo "SSM Agent status on instance:"
|
|
132
|
+
aws ssm describe-instance-information \
|
|
133
|
+
--region "$AWS_REGION" \
|
|
134
|
+
--filters "Key=InstanceIds,Values=$INSTANCE_ID" \
|
|
135
|
+
--query 'InstanceInformationList[0].{PingStatus:PingStatus,LastPingDateTime:LastPingDateTime,AgentVersion:AgentVersion}' \
|
|
136
|
+
--output table 2>/dev/null || echo "Could not retrieve SSM status"
|
|
137
|
+
|
|
138
|
+
exit 2
|
|
139
|
+
fi
|
|
140
|
+
echo "Retrying in 20 seconds..."
|
|
141
|
+
sleep 20
|
|
142
|
+
done
|
|
143
|
+
|
|
144
|
+
echo "Getting Public IP..."
|
|
145
|
+
|
|
146
|
+
# # --- 4) Get instance Public IP ---
|
|
147
|
+
DESC_JSON=$(aws ec2 describe-instances --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" --output json)
|
|
148
|
+
PUBLIC_IP=$(jq -r '.Reservations[0].Instances[0].PublicIpAddress // empty' <<<"$DESC_JSON")
|
|
149
|
+
[ -n "$PUBLIC_IP" ] || PUBLIC_IP="No public IP assigned"
|
|
150
|
+
|
|
151
|
+
# echo "Getting Websocket Port..."
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# --- 5) Read WebSocket config JSON ---
|
|
155
|
+
echo "Reading WebSocket configuration from: $WS_CONFIG_PATH"
|
|
156
|
+
READ_JSON=$(aws ssm send-command \
|
|
157
|
+
--region "$AWS_REGION" \
|
|
158
|
+
--instance-ids "$INSTANCE_ID" \
|
|
159
|
+
--document-name "AWS-RunPowerShellScript" \
|
|
160
|
+
--parameters "commands=[\"if (Test-Path '${WS_CONFIG_PATH}') { Get-Content -Raw '${WS_CONFIG_PATH}' } else { Write-Output 'Config file not found at ${WS_CONFIG_PATH}' }\"]" \
|
|
161
|
+
--output json)
|
|
162
|
+
|
|
163
|
+
READ_CMD_ID=$(jq -r '.Command.CommandId' <<<"$READ_JSON")
|
|
164
|
+
echo "WebSocket config read command ID: $READ_CMD_ID"
|
|
165
|
+
|
|
166
|
+
echo "Waiting for WebSocket config command to complete..."
|
|
167
|
+
aws ssm wait command-executed --region "$AWS_REGION" --command-id "$READ_CMD_ID" --instance-id "$INSTANCE_ID"
|
|
168
|
+
|
|
169
|
+
INVOC=$(aws ssm get-command-invocation \
|
|
170
|
+
--region "$AWS_REGION" \
|
|
171
|
+
--command-id "$READ_CMD_ID" \
|
|
172
|
+
--instance-id "$INSTANCE_ID" \
|
|
173
|
+
--output json)
|
|
174
|
+
|
|
175
|
+
STDOUT=$(jq -r '.StandardOutputContent // ""' <<<"$INVOC")
|
|
176
|
+
STDERR=$(jq -r '.StandardErrorContent // ""' <<<"$INVOC")
|
|
177
|
+
CMD_STATUS=$(jq -r '.Status // ""' <<<"$INVOC")
|
|
178
|
+
|
|
179
|
+
echo "WebSocket config command status: $CMD_STATUS"
|
|
180
|
+
if [ -n "$STDERR" ] && [ "$STDERR" != "null" ]; then
|
|
181
|
+
echo "WebSocket config stderr: $STDERR"
|
|
182
|
+
fi
|
|
183
|
+
echo "WebSocket config raw output: $STDOUT"
|
|
184
|
+
|
|
185
|
+
# --- 6) Output results ---
|
|
186
|
+
echo "Setup complete!"
|
|
187
|
+
echo "PUBLIC_IP=$PUBLIC_IP"
|
|
188
|
+
echo "INSTANCE_ID=$INSTANCE_ID"
|
|
189
|
+
echo "AWS_REGION=$AWS_REGION"
|