@ktmcp-cli/awsbatch 1.0.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/AGENT.md +96 -0
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/bin/awsbatch.js +9 -0
- package/package.json +27 -0
- package/src/api.js +230 -0
- package/src/config.js +21 -0
- package/src/index.js +576 -0
package/AGENT.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# AGENT.md — AWS Batch CLI for AI Agents
|
|
2
|
+
|
|
3
|
+
This document explains how to use the AWS Batch CLI as an AI agent.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The `awsbatch` CLI provides access to the AWS Batch Computing API. Requires AWS credentials with Batch permissions.
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
awsbatch config set accessKeyId YOUR_AWS_ACCESS_KEY_ID
|
|
13
|
+
awsbatch config set secretAccessKey YOUR_AWS_SECRET_ACCESS_KEY
|
|
14
|
+
awsbatch config set region us-east-1
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## All Commands
|
|
18
|
+
|
|
19
|
+
### Config
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
awsbatch config get <key>
|
|
23
|
+
awsbatch config set <key> <value>
|
|
24
|
+
awsbatch config list
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Jobs
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Submit
|
|
31
|
+
awsbatch jobs submit --name my-job --queue my-queue --definition my-def
|
|
32
|
+
awsbatch jobs submit --name my-job --queue my-queue --definition my-def --parameters '{"key":"val"}'
|
|
33
|
+
awsbatch jobs submit --name my-job --queue my-queue --definition my-def --container-overrides '{"command":["cmd"]}'
|
|
34
|
+
|
|
35
|
+
# Status
|
|
36
|
+
awsbatch jobs get <job-id>
|
|
37
|
+
|
|
38
|
+
# List
|
|
39
|
+
awsbatch jobs list --queue my-queue
|
|
40
|
+
awsbatch jobs list --queue my-queue --status RUNNING
|
|
41
|
+
awsbatch jobs list --queue my-queue --status SUCCEEDED
|
|
42
|
+
awsbatch jobs list --queue my-queue --status FAILED
|
|
43
|
+
|
|
44
|
+
# Describe multiple
|
|
45
|
+
awsbatch jobs describe <job-id-1> <job-id-2>
|
|
46
|
+
|
|
47
|
+
# Terminate
|
|
48
|
+
awsbatch jobs terminate <job-id>
|
|
49
|
+
awsbatch jobs terminate <job-id> --reason "No longer needed"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Queues
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
awsbatch queues list
|
|
56
|
+
awsbatch queues get <queue-name>
|
|
57
|
+
awsbatch queues create --name <name> --state ENABLED --priority 1
|
|
58
|
+
awsbatch queues update <queue-name> --state DISABLED
|
|
59
|
+
awsbatch queues update <queue-name> --priority 10
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Job Definitions
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
awsbatch definitions list
|
|
66
|
+
awsbatch definitions list --name <name>
|
|
67
|
+
awsbatch definitions list --status ACTIVE
|
|
68
|
+
awsbatch definitions describe <definition-name>
|
|
69
|
+
awsbatch definitions register --name <name> --type container --container '{"image":"img:tag","vcpus":1,"memory":512}'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## JSON Output
|
|
73
|
+
|
|
74
|
+
All commands support `--json`:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
awsbatch jobs get <id> --json
|
|
78
|
+
awsbatch queues list --json
|
|
79
|
+
awsbatch definitions list --json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Job Status Values
|
|
83
|
+
|
|
84
|
+
- SUBMITTED — Job submitted, awaiting scheduling
|
|
85
|
+
- PENDING — Job pending scheduling
|
|
86
|
+
- RUNNABLE — Job scheduled, waiting for resources
|
|
87
|
+
- STARTING — Job starting
|
|
88
|
+
- RUNNING — Job running
|
|
89
|
+
- SUCCEEDED — Job completed successfully
|
|
90
|
+
- FAILED — Job failed
|
|
91
|
+
|
|
92
|
+
## Error Handling
|
|
93
|
+
|
|
94
|
+
The CLI exits with code 1 on error and prints to stderr.
|
|
95
|
+
- `AWS authentication failed` — Check accessKeyId and secretAccessKey
|
|
96
|
+
- `Resource not found` — Check queue/definition names and job IDs
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 KTMCP
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
> "Six months ago, everyone was talking about MCPs. And I was like, screw MCPs. Every MCP would be better as a CLI."
|
|
2
|
+
>
|
|
3
|
+
> — [Peter Steinberger](https://twitter.com/steipete), Founder of OpenClaw
|
|
4
|
+
> [Watch on YouTube (~2:39:00)](https://www.youtube.com/@lexfridman) | [Lex Fridman Podcast #491](https://lexfridman.com/peter-steinberger/)
|
|
5
|
+
|
|
6
|
+
# AWS Batch CLI
|
|
7
|
+
|
|
8
|
+
Production-ready CLI for the AWS Batch Computing API. Submit and manage batch jobs, queues, and job definitions from your terminal.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g @ktmcp-cli/awsbatch
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
awsbatch config set accessKeyId YOUR_AWS_ACCESS_KEY_ID
|
|
20
|
+
awsbatch config set secretAccessKey YOUR_AWS_SECRET_ACCESS_KEY
|
|
21
|
+
awsbatch config set region us-east-1
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### Jobs
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Submit a job
|
|
30
|
+
awsbatch jobs submit --name my-job --queue my-queue --definition my-job-def
|
|
31
|
+
awsbatch jobs submit --name my-job --queue my-queue --definition my-job-def \
|
|
32
|
+
--parameters '{"key":"value"}' \
|
|
33
|
+
--container-overrides '{"command":["my-script.sh"]}'
|
|
34
|
+
|
|
35
|
+
# Get job status
|
|
36
|
+
awsbatch jobs get <job-id>
|
|
37
|
+
|
|
38
|
+
# List jobs in a queue
|
|
39
|
+
awsbatch jobs list --queue my-queue
|
|
40
|
+
awsbatch jobs list --queue my-queue --status RUNNING
|
|
41
|
+
awsbatch jobs list --queue my-queue --status SUCCEEDED --limit 20
|
|
42
|
+
|
|
43
|
+
# Describe multiple jobs
|
|
44
|
+
awsbatch jobs describe <job-id-1> <job-id-2>
|
|
45
|
+
|
|
46
|
+
# Terminate a job
|
|
47
|
+
awsbatch jobs terminate <job-id>
|
|
48
|
+
awsbatch jobs terminate <job-id> --reason "No longer needed"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Queues
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# List job queues
|
|
55
|
+
awsbatch queues list
|
|
56
|
+
|
|
57
|
+
# Get queue details
|
|
58
|
+
awsbatch queues get my-queue
|
|
59
|
+
|
|
60
|
+
# Create a queue
|
|
61
|
+
awsbatch queues create --name my-queue --priority 10
|
|
62
|
+
awsbatch queues create --name my-queue --state ENABLED --priority 5
|
|
63
|
+
|
|
64
|
+
# Update a queue
|
|
65
|
+
awsbatch queues update my-queue --state DISABLED
|
|
66
|
+
awsbatch queues update my-queue --priority 20
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Job Definitions
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# List job definitions
|
|
73
|
+
awsbatch definitions list
|
|
74
|
+
awsbatch definitions list --name my-definition
|
|
75
|
+
awsbatch definitions list --status ACTIVE
|
|
76
|
+
|
|
77
|
+
# Describe a job definition
|
|
78
|
+
awsbatch definitions describe my-definition
|
|
79
|
+
|
|
80
|
+
# Register a job definition
|
|
81
|
+
awsbatch definitions register --name my-job-def --type container \
|
|
82
|
+
--container '{"image":"my-image:latest","vcpus":1,"memory":512}'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### JSON Output
|
|
86
|
+
|
|
87
|
+
All commands support `--json`:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
awsbatch jobs get <id> --json
|
|
91
|
+
awsbatch queues list --json
|
|
92
|
+
awsbatch definitions list --json | jq '.[].jobDefinitionName'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
package/bin/awsbatch.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ktmcp-cli/awsbatch",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Production-ready CLI for AWS Batch Computing API - Kill The MCP",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"awsbatch": "bin/awsbatch.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": ["awsbatch", "aws", "batch", "jobs", "compute", "cli", "api", "ktmcp"],
|
|
11
|
+
"author": "KTMCP",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"commander": "^12.0.0",
|
|
15
|
+
"axios": "^1.6.7",
|
|
16
|
+
"chalk": "^5.3.0",
|
|
17
|
+
"ora": "^8.0.1",
|
|
18
|
+
"conf": "^12.0.0"
|
|
19
|
+
},
|
|
20
|
+
"engines": { "node": ">=18.0.0" },
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/ktmcp-cli/awsbatch.git"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://killthemcp.com/awsbatch-cli",
|
|
26
|
+
"bugs": { "url": "https://github.com/ktmcp-cli/awsbatch/issues" }
|
|
27
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import { getConfig } from './config.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AWS Signature Version 4 signing helper
|
|
7
|
+
*/
|
|
8
|
+
function sign(key, msg) {
|
|
9
|
+
return crypto.createHmac('sha256', key).update(msg).digest();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getSigningKey(secretKey, dateStamp, regionName, serviceName) {
|
|
13
|
+
const kDate = sign('AWS4' + secretKey, dateStamp);
|
|
14
|
+
const kRegion = sign(kDate, regionName);
|
|
15
|
+
const kService = sign(kRegion, serviceName);
|
|
16
|
+
return sign(kService, 'aws4_request');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildAuthHeader({ method, url, body, service, region, accessKeyId, secretAccessKey }) {
|
|
20
|
+
const parsedUrl = new URL(url);
|
|
21
|
+
const host = parsedUrl.host;
|
|
22
|
+
const path = parsedUrl.pathname;
|
|
23
|
+
const queryString = parsedUrl.search ? parsedUrl.search.slice(1) : '';
|
|
24
|
+
|
|
25
|
+
const now = new Date();
|
|
26
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '').slice(0, 15) + 'Z';
|
|
27
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
28
|
+
|
|
29
|
+
const payloadHash = crypto.createHash('sha256').update(body || '').digest('hex');
|
|
30
|
+
|
|
31
|
+
const canonicalHeaders = `host:${host}\nx-amz-date:${amzDate}\n`;
|
|
32
|
+
const signedHeaders = 'host;x-amz-date';
|
|
33
|
+
|
|
34
|
+
const canonicalRequest = [
|
|
35
|
+
method.toUpperCase(),
|
|
36
|
+
path,
|
|
37
|
+
queryString,
|
|
38
|
+
canonicalHeaders,
|
|
39
|
+
signedHeaders,
|
|
40
|
+
payloadHash
|
|
41
|
+
].join('\n');
|
|
42
|
+
|
|
43
|
+
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
|
|
44
|
+
const stringToSign = [
|
|
45
|
+
'AWS4-HMAC-SHA256',
|
|
46
|
+
amzDate,
|
|
47
|
+
credentialScope,
|
|
48
|
+
crypto.createHash('sha256').update(canonicalRequest).digest('hex')
|
|
49
|
+
].join('\n');
|
|
50
|
+
|
|
51
|
+
const signingKey = getSigningKey(secretAccessKey, dateStamp, region, service);
|
|
52
|
+
const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex');
|
|
53
|
+
|
|
54
|
+
const authorization = `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
55
|
+
|
|
56
|
+
return { authorization, amzDate };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getBatchClient() {
|
|
60
|
+
const accessKeyId = getConfig('accessKeyId');
|
|
61
|
+
const secretAccessKey = getConfig('secretAccessKey');
|
|
62
|
+
const region = getConfig('region') || 'us-east-1';
|
|
63
|
+
const baseURL = `https://batch.${region}.amazonaws.com`;
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
request: async (method, path, data = null) => {
|
|
67
|
+
const url = `${baseURL}${path}`;
|
|
68
|
+
const body = data ? JSON.stringify(data) : '';
|
|
69
|
+
const { authorization, amzDate } = buildAuthHeader({
|
|
70
|
+
method,
|
|
71
|
+
url,
|
|
72
|
+
body,
|
|
73
|
+
service: 'batch',
|
|
74
|
+
region,
|
|
75
|
+
accessKeyId,
|
|
76
|
+
secretAccessKey
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const response = await axios({
|
|
81
|
+
method,
|
|
82
|
+
url,
|
|
83
|
+
data: data || undefined,
|
|
84
|
+
headers: {
|
|
85
|
+
'Authorization': authorization,
|
|
86
|
+
'X-Amz-Date': amzDate,
|
|
87
|
+
'Content-Type': 'application/json',
|
|
88
|
+
'Accept': 'application/json'
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
return response.data;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
handleApiError(error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function handleApiError(error) {
|
|
100
|
+
if (error.response) {
|
|
101
|
+
const status = error.response.status;
|
|
102
|
+
const data = error.response.data;
|
|
103
|
+
if (status === 401 || status === 403) {
|
|
104
|
+
throw new Error('AWS authentication failed. Check your accessKeyId and secretAccessKey.');
|
|
105
|
+
} else if (status === 404) {
|
|
106
|
+
throw new Error('Resource not found.');
|
|
107
|
+
} else if (status === 429) {
|
|
108
|
+
throw new Error('Rate limit exceeded. Please wait before retrying.');
|
|
109
|
+
} else {
|
|
110
|
+
const message = data?.message || data?.Message || data?.error || JSON.stringify(data);
|
|
111
|
+
throw new Error(`AWS Batch Error (${status}): ${message}`);
|
|
112
|
+
}
|
|
113
|
+
} else if (error.request) {
|
|
114
|
+
throw new Error('No response from AWS Batch API. Check your internet connection and region.');
|
|
115
|
+
} else {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============================================================
|
|
121
|
+
// JOBS
|
|
122
|
+
// ============================================================
|
|
123
|
+
|
|
124
|
+
export async function submitJob({ jobName, jobQueue, jobDefinition, parameters, containerOverrides }) {
|
|
125
|
+
const client = getBatchClient();
|
|
126
|
+
const body = {
|
|
127
|
+
jobName,
|
|
128
|
+
jobQueue,
|
|
129
|
+
jobDefinition,
|
|
130
|
+
...(parameters && { parameters }),
|
|
131
|
+
...(containerOverrides && { containerOverrides })
|
|
132
|
+
};
|
|
133
|
+
const data = await client.request('POST', '/v1/submitjob', body);
|
|
134
|
+
return data;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function describeJobs(jobIds) {
|
|
138
|
+
const client = getBatchClient();
|
|
139
|
+
const data = await client.request('POST', '/v1/describejobs', { jobs: jobIds });
|
|
140
|
+
return data?.jobs || [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function listJobs({ jobQueue, jobStatus, maxResults = 50 } = {}) {
|
|
144
|
+
const client = getBatchClient();
|
|
145
|
+
const body = {};
|
|
146
|
+
if (jobQueue) body.jobQueue = jobQueue;
|
|
147
|
+
if (jobStatus) body.jobStatus = jobStatus;
|
|
148
|
+
if (maxResults) body.maxResults = maxResults;
|
|
149
|
+
const data = await client.request('POST', '/v1/listjobs', body);
|
|
150
|
+
return data?.jobSummaryList || [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function terminateJob(jobId, reason) {
|
|
154
|
+
const client = getBatchClient();
|
|
155
|
+
const data = await client.request('POST', '/v1/terminatejob', {
|
|
156
|
+
jobId,
|
|
157
|
+
reason: reason || 'Terminated via CLI'
|
|
158
|
+
});
|
|
159
|
+
return data;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================================
|
|
163
|
+
// JOB QUEUES
|
|
164
|
+
// ============================================================
|
|
165
|
+
|
|
166
|
+
export async function listQueues() {
|
|
167
|
+
const client = getBatchClient();
|
|
168
|
+
const data = await client.request('GET', '/v1/jobqueues');
|
|
169
|
+
return data?.jobQueues || [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function getQueue(queueName) {
|
|
173
|
+
const client = getBatchClient();
|
|
174
|
+
const data = await client.request('GET', `/v1/jobqueues?jobQueues=${encodeURIComponent(queueName)}`);
|
|
175
|
+
return (data?.jobQueues || [])[0] || null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function createQueue({ queueName, state, priority, computeEnvironmentOrder }) {
|
|
179
|
+
const client = getBatchClient();
|
|
180
|
+
const body = {
|
|
181
|
+
jobQueueName: queueName,
|
|
182
|
+
state: state || 'ENABLED',
|
|
183
|
+
priority: priority || 1,
|
|
184
|
+
computeEnvironmentOrder: computeEnvironmentOrder || []
|
|
185
|
+
};
|
|
186
|
+
const data = await client.request('POST', '/v1/createjobqueue', body);
|
|
187
|
+
return data;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function updateQueue({ queueName, state, priority }) {
|
|
191
|
+
const client = getBatchClient();
|
|
192
|
+
const body = { jobQueue: queueName };
|
|
193
|
+
if (state) body.state = state;
|
|
194
|
+
if (priority !== undefined) body.priority = priority;
|
|
195
|
+
const data = await client.request('POST', '/v1/updatejobqueue', body);
|
|
196
|
+
return data;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ============================================================
|
|
200
|
+
// JOB DEFINITIONS
|
|
201
|
+
// ============================================================
|
|
202
|
+
|
|
203
|
+
export async function listDefinitions({ definitionName, status } = {}) {
|
|
204
|
+
const client = getBatchClient();
|
|
205
|
+
let path = '/v1/jobdefinitions';
|
|
206
|
+
const params = [];
|
|
207
|
+
if (definitionName) params.push(`jobDefinitionName=${encodeURIComponent(definitionName)}`);
|
|
208
|
+
if (status) params.push(`status=${encodeURIComponent(status)}`);
|
|
209
|
+
if (params.length) path += '?' + params.join('&');
|
|
210
|
+
const data = await client.request('GET', path);
|
|
211
|
+
return data?.jobDefinitions || [];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function describeDefinitions(definitionNames) {
|
|
215
|
+
const client = getBatchClient();
|
|
216
|
+
const path = `/v1/jobdefinitions?jobDefinitionName=${encodeURIComponent(definitionNames[0])}`;
|
|
217
|
+
const data = await client.request('GET', path);
|
|
218
|
+
return data?.jobDefinitions || [];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function registerDefinition({ definitionName, type, containerProperties }) {
|
|
222
|
+
const client = getBatchClient();
|
|
223
|
+
const body = {
|
|
224
|
+
jobDefinitionName: definitionName,
|
|
225
|
+
type: type || 'container',
|
|
226
|
+
...(containerProperties && { containerProperties })
|
|
227
|
+
};
|
|
228
|
+
const data = await client.request('POST', '/v1/registerjobdefinition', body);
|
|
229
|
+
return data;
|
|
230
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
|
|
3
|
+
const config = new Conf({ projectName: '@ktmcp-cli/awsbatch' });
|
|
4
|
+
|
|
5
|
+
export function getConfig(key) {
|
|
6
|
+
return config.get(key);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function setConfig(key, value) {
|
|
10
|
+
config.set(key, value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isConfigured() {
|
|
14
|
+
return !!(config.get('accessKeyId') && config.get('secretAccessKey'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getAllConfig() {
|
|
18
|
+
return config.store;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default config;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { getConfig, setConfig, getAllConfig, isConfigured } from './config.js';
|
|
5
|
+
import {
|
|
6
|
+
submitJob,
|
|
7
|
+
describeJobs,
|
|
8
|
+
listJobs,
|
|
9
|
+
terminateJob,
|
|
10
|
+
listQueues,
|
|
11
|
+
getQueue,
|
|
12
|
+
createQueue,
|
|
13
|
+
updateQueue,
|
|
14
|
+
listDefinitions,
|
|
15
|
+
describeDefinitions,
|
|
16
|
+
registerDefinition
|
|
17
|
+
} from './api.js';
|
|
18
|
+
|
|
19
|
+
const program = new Command();
|
|
20
|
+
|
|
21
|
+
// ============================================================
|
|
22
|
+
// Helpers
|
|
23
|
+
// ============================================================
|
|
24
|
+
|
|
25
|
+
function printSuccess(message) {
|
|
26
|
+
console.log(chalk.green('✓') + ' ' + message);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function printError(message) {
|
|
30
|
+
console.error(chalk.red('✗') + ' ' + message);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function printTable(data, columns) {
|
|
34
|
+
if (!data || data.length === 0) {
|
|
35
|
+
console.log(chalk.yellow('No results found.'));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const widths = {};
|
|
40
|
+
columns.forEach(col => {
|
|
41
|
+
widths[col.key] = col.label.length;
|
|
42
|
+
data.forEach(row => {
|
|
43
|
+
const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
|
|
44
|
+
if (val.length > widths[col.key]) widths[col.key] = val.length;
|
|
45
|
+
});
|
|
46
|
+
widths[col.key] = Math.min(widths[col.key], 40);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const header = columns.map(col => col.label.padEnd(widths[col.key])).join(' ');
|
|
50
|
+
console.log(chalk.bold(chalk.cyan(header)));
|
|
51
|
+
console.log(chalk.dim('─'.repeat(header.length)));
|
|
52
|
+
|
|
53
|
+
data.forEach(row => {
|
|
54
|
+
const line = columns.map(col => {
|
|
55
|
+
const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
|
|
56
|
+
return val.substring(0, widths[col.key]).padEnd(widths[col.key]);
|
|
57
|
+
}).join(' ');
|
|
58
|
+
console.log(line);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
console.log(chalk.dim(`\n${data.length} result(s)`));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function printJson(data) {
|
|
65
|
+
console.log(JSON.stringify(data, null, 2));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function withSpinner(message, fn) {
|
|
69
|
+
const spinner = ora(message).start();
|
|
70
|
+
try {
|
|
71
|
+
const result = await fn();
|
|
72
|
+
spinner.stop();
|
|
73
|
+
return result;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
spinner.stop();
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function requireAuth() {
|
|
81
|
+
if (!isConfigured()) {
|
|
82
|
+
printError('AWS credentials not configured.');
|
|
83
|
+
console.log('\nRun the following to configure:');
|
|
84
|
+
console.log(chalk.cyan(' awsbatch config set accessKeyId YOUR_KEY'));
|
|
85
|
+
console.log(chalk.cyan(' awsbatch config set secretAccessKey YOUR_SECRET'));
|
|
86
|
+
console.log(chalk.cyan(' awsbatch config set region us-east-1'));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============================================================
|
|
92
|
+
// Program metadata
|
|
93
|
+
// ============================================================
|
|
94
|
+
|
|
95
|
+
program
|
|
96
|
+
.name('awsbatch')
|
|
97
|
+
.description(chalk.bold('AWS Batch CLI') + ' - Manage batch computing jobs from your terminal')
|
|
98
|
+
.version('1.0.0');
|
|
99
|
+
|
|
100
|
+
// ============================================================
|
|
101
|
+
// CONFIG
|
|
102
|
+
// ============================================================
|
|
103
|
+
|
|
104
|
+
const configCmd = program.command('config').description('Manage CLI configuration');
|
|
105
|
+
|
|
106
|
+
configCmd
|
|
107
|
+
.command('get <key>')
|
|
108
|
+
.description('Get a configuration value')
|
|
109
|
+
.action((key) => {
|
|
110
|
+
const value = getConfig(key);
|
|
111
|
+
if (value === undefined) {
|
|
112
|
+
printError(`Key '${key}' not found`);
|
|
113
|
+
} else {
|
|
114
|
+
console.log(value);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
configCmd
|
|
119
|
+
.command('set <key> <value>')
|
|
120
|
+
.description('Set a configuration value')
|
|
121
|
+
.action((key, value) => {
|
|
122
|
+
setConfig(key, value);
|
|
123
|
+
printSuccess(`Config '${key}' set`);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
configCmd
|
|
127
|
+
.command('list')
|
|
128
|
+
.description('List all configuration values')
|
|
129
|
+
.action(() => {
|
|
130
|
+
const all = getAllConfig();
|
|
131
|
+
console.log(chalk.bold('\nAWS Batch CLI Configuration\n'));
|
|
132
|
+
if (Object.keys(all).length === 0) {
|
|
133
|
+
console.log(chalk.yellow('No configuration set.'));
|
|
134
|
+
console.log('\nRun:');
|
|
135
|
+
console.log(chalk.cyan(' awsbatch config set accessKeyId YOUR_KEY'));
|
|
136
|
+
console.log(chalk.cyan(' awsbatch config set secretAccessKey YOUR_SECRET'));
|
|
137
|
+
console.log(chalk.cyan(' awsbatch config set region us-east-1'));
|
|
138
|
+
} else {
|
|
139
|
+
Object.entries(all).forEach(([k, v]) => {
|
|
140
|
+
const displayVal = k === 'secretAccessKey' ? chalk.green('*'.repeat(8)) : chalk.cyan(String(v));
|
|
141
|
+
console.log(`${k}: ${displayVal}`);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ============================================================
|
|
147
|
+
// JOBS
|
|
148
|
+
// ============================================================
|
|
149
|
+
|
|
150
|
+
const jobsCmd = program.command('jobs').description('Manage AWS Batch jobs');
|
|
151
|
+
|
|
152
|
+
jobsCmd
|
|
153
|
+
.command('submit')
|
|
154
|
+
.description('Submit a new batch job')
|
|
155
|
+
.requiredOption('--name <name>', 'Job name')
|
|
156
|
+
.requiredOption('--queue <queue>', 'Job queue name or ARN')
|
|
157
|
+
.requiredOption('--definition <def>', 'Job definition name or ARN')
|
|
158
|
+
.option('--parameters <json>', 'Job parameters as JSON')
|
|
159
|
+
.option('--container-overrides <json>', 'Container overrides as JSON')
|
|
160
|
+
.option('--json', 'Output as JSON')
|
|
161
|
+
.action(async (options) => {
|
|
162
|
+
requireAuth();
|
|
163
|
+
|
|
164
|
+
let parameters, containerOverrides;
|
|
165
|
+
if (options.parameters) {
|
|
166
|
+
try { parameters = JSON.parse(options.parameters); } catch { printError('Invalid JSON for --parameters'); process.exit(1); }
|
|
167
|
+
}
|
|
168
|
+
if (options.containerOverrides) {
|
|
169
|
+
try { containerOverrides = JSON.parse(options.containerOverrides); } catch { printError('Invalid JSON for --container-overrides'); process.exit(1); }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const result = await withSpinner('Submitting job...', () =>
|
|
174
|
+
submitJob({
|
|
175
|
+
jobName: options.name,
|
|
176
|
+
jobQueue: options.queue,
|
|
177
|
+
jobDefinition: options.definition,
|
|
178
|
+
parameters,
|
|
179
|
+
containerOverrides
|
|
180
|
+
})
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
if (options.json) {
|
|
184
|
+
printJson(result);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
printSuccess('Job submitted');
|
|
189
|
+
console.log('Job ID: ', chalk.cyan(result?.jobId || 'N/A'));
|
|
190
|
+
console.log('Job Name: ', result?.jobName || options.name);
|
|
191
|
+
console.log('Job ARN: ', result?.jobArn || 'N/A');
|
|
192
|
+
} catch (error) {
|
|
193
|
+
printError(error.message);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
jobsCmd
|
|
199
|
+
.command('get <job-id>')
|
|
200
|
+
.description('Get job details')
|
|
201
|
+
.option('--json', 'Output as JSON')
|
|
202
|
+
.action(async (jobId, options) => {
|
|
203
|
+
requireAuth();
|
|
204
|
+
try {
|
|
205
|
+
const jobs = await withSpinner(`Fetching job ${jobId}...`, () => describeJobs([jobId]));
|
|
206
|
+
const job = jobs[0];
|
|
207
|
+
|
|
208
|
+
if (!job) {
|
|
209
|
+
printError('Job not found');
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (options.json) {
|
|
214
|
+
printJson(job);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const statusColor = job.status === 'SUCCEEDED' ? chalk.green : job.status === 'FAILED' ? chalk.red : chalk.yellow;
|
|
219
|
+
|
|
220
|
+
console.log(chalk.bold('\nJob Details\n'));
|
|
221
|
+
console.log('Job ID: ', chalk.cyan(job.jobId));
|
|
222
|
+
console.log('Job Name: ', chalk.bold(job.jobName));
|
|
223
|
+
console.log('Status: ', statusColor(job.status || 'N/A'));
|
|
224
|
+
console.log('Queue: ', job.jobQueue || 'N/A');
|
|
225
|
+
console.log('Definition: ', job.jobDefinition || 'N/A');
|
|
226
|
+
console.log('Created: ', job.createdAt ? new Date(job.createdAt).toLocaleString() : 'N/A');
|
|
227
|
+
console.log('Started: ', job.startedAt ? new Date(job.startedAt).toLocaleString() : 'N/A');
|
|
228
|
+
console.log('Stopped: ', job.stoppedAt ? new Date(job.stoppedAt).toLocaleString() : 'N/A');
|
|
229
|
+
if (job.statusReason) console.log('Status Reason: ', chalk.dim(job.statusReason));
|
|
230
|
+
if (job.container?.exitCode !== undefined) console.log('Exit Code: ', job.container.exitCode);
|
|
231
|
+
console.log('');
|
|
232
|
+
} catch (error) {
|
|
233
|
+
printError(error.message);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
jobsCmd
|
|
239
|
+
.command('list')
|
|
240
|
+
.description('List jobs in a queue')
|
|
241
|
+
.requiredOption('--queue <queue>', 'Job queue name')
|
|
242
|
+
.option('--status <status>', 'Filter by status (SUBMITTED|PENDING|RUNNABLE|STARTING|RUNNING|SUCCEEDED|FAILED)')
|
|
243
|
+
.option('--limit <n>', 'Maximum number of results', '50')
|
|
244
|
+
.option('--json', 'Output as JSON')
|
|
245
|
+
.action(async (options) => {
|
|
246
|
+
requireAuth();
|
|
247
|
+
try {
|
|
248
|
+
const jobs = await withSpinner('Fetching jobs...', () =>
|
|
249
|
+
listJobs({
|
|
250
|
+
jobQueue: options.queue,
|
|
251
|
+
jobStatus: options.status,
|
|
252
|
+
maxResults: parseInt(options.limit)
|
|
253
|
+
})
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
if (options.json) {
|
|
257
|
+
printJson(jobs);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
printTable(jobs, [
|
|
262
|
+
{ key: 'jobId', label: 'Job ID', format: (v) => v ? String(v).substring(0, 16) + '...' : '' },
|
|
263
|
+
{ key: 'jobName', label: 'Name' },
|
|
264
|
+
{ key: 'status', label: 'Status' },
|
|
265
|
+
{ key: 'createdAt', label: 'Created', format: (v) => v ? new Date(v).toLocaleDateString() : '' }
|
|
266
|
+
]);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
printError(error.message);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
jobsCmd
|
|
274
|
+
.command('terminate <job-id>')
|
|
275
|
+
.description('Terminate a job')
|
|
276
|
+
.option('--reason <reason>', 'Termination reason', 'Terminated via CLI')
|
|
277
|
+
.action(async (jobId, options) => {
|
|
278
|
+
requireAuth();
|
|
279
|
+
try {
|
|
280
|
+
await withSpinner(`Terminating job ${jobId}...`, () =>
|
|
281
|
+
terminateJob(jobId, options.reason)
|
|
282
|
+
);
|
|
283
|
+
printSuccess(`Job ${jobId} terminated`);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
printError(error.message);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
jobsCmd
|
|
291
|
+
.command('describe <job-ids...>')
|
|
292
|
+
.description('Describe one or more jobs by ID')
|
|
293
|
+
.option('--json', 'Output as JSON')
|
|
294
|
+
.action(async (jobIds, options) => {
|
|
295
|
+
requireAuth();
|
|
296
|
+
try {
|
|
297
|
+
const jobs = await withSpinner('Fetching job details...', () => describeJobs(jobIds));
|
|
298
|
+
|
|
299
|
+
if (options.json) {
|
|
300
|
+
printJson(jobs);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
printTable(jobs, [
|
|
305
|
+
{ key: 'jobId', label: 'Job ID', format: (v) => v ? String(v).substring(0, 16) + '...' : '' },
|
|
306
|
+
{ key: 'jobName', label: 'Name' },
|
|
307
|
+
{ key: 'status', label: 'Status' },
|
|
308
|
+
{ key: 'jobQueue', label: 'Queue', format: (v) => (v || '').split('/').pop() },
|
|
309
|
+
{ key: 'createdAt', label: 'Created', format: (v) => v ? new Date(v).toLocaleDateString() : '' }
|
|
310
|
+
]);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
printError(error.message);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// ============================================================
|
|
318
|
+
// QUEUES
|
|
319
|
+
// ============================================================
|
|
320
|
+
|
|
321
|
+
const queuesCmd = program.command('queues').description('Manage AWS Batch job queues');
|
|
322
|
+
|
|
323
|
+
queuesCmd
|
|
324
|
+
.command('list')
|
|
325
|
+
.description('List job queues')
|
|
326
|
+
.option('--json', 'Output as JSON')
|
|
327
|
+
.action(async (options) => {
|
|
328
|
+
requireAuth();
|
|
329
|
+
try {
|
|
330
|
+
const queues = await withSpinner('Fetching job queues...', () => listQueues());
|
|
331
|
+
|
|
332
|
+
if (options.json) {
|
|
333
|
+
printJson(queues);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
printTable(queues, [
|
|
338
|
+
{ key: 'jobQueueName', label: 'Name' },
|
|
339
|
+
{ key: 'state', label: 'State' },
|
|
340
|
+
{ key: 'status', label: 'Status' },
|
|
341
|
+
{ key: 'priority', label: 'Priority', format: (v) => String(v || 0) }
|
|
342
|
+
]);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
printError(error.message);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
queuesCmd
|
|
350
|
+
.command('get <queue-name>')
|
|
351
|
+
.description('Get job queue details')
|
|
352
|
+
.option('--json', 'Output as JSON')
|
|
353
|
+
.action(async (queueName, options) => {
|
|
354
|
+
requireAuth();
|
|
355
|
+
try {
|
|
356
|
+
const queue = await withSpinner(`Fetching queue ${queueName}...`, () => getQueue(queueName));
|
|
357
|
+
|
|
358
|
+
if (!queue) {
|
|
359
|
+
printError('Queue not found');
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (options.json) {
|
|
364
|
+
printJson(queue);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
console.log(chalk.bold('\nJob Queue Details\n'));
|
|
369
|
+
console.log('Name: ', chalk.cyan(queue.jobQueueName));
|
|
370
|
+
console.log('ARN: ', queue.jobQueueArn || 'N/A');
|
|
371
|
+
console.log('State: ', queue.state || 'N/A');
|
|
372
|
+
console.log('Status: ', queue.status || 'N/A');
|
|
373
|
+
console.log('Priority: ', queue.priority !== undefined ? String(queue.priority) : 'N/A');
|
|
374
|
+
if (queue.statusReason) console.log('Status Reason: ', queue.statusReason);
|
|
375
|
+
console.log('');
|
|
376
|
+
} catch (error) {
|
|
377
|
+
printError(error.message);
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
queuesCmd
|
|
383
|
+
.command('create')
|
|
384
|
+
.description('Create a new job queue')
|
|
385
|
+
.requiredOption('--name <name>', 'Queue name')
|
|
386
|
+
.option('--state <state>', 'Queue state (ENABLED|DISABLED)', 'ENABLED')
|
|
387
|
+
.option('--priority <n>', 'Queue priority (1-1000)', '1')
|
|
388
|
+
.option('--compute-envs <json>', 'Compute environment order as JSON array')
|
|
389
|
+
.option('--json', 'Output as JSON')
|
|
390
|
+
.action(async (options) => {
|
|
391
|
+
requireAuth();
|
|
392
|
+
|
|
393
|
+
let computeEnvironmentOrder = [];
|
|
394
|
+
if (options.computeEnvs) {
|
|
395
|
+
try { computeEnvironmentOrder = JSON.parse(options.computeEnvs); } catch { printError('Invalid JSON for --compute-envs'); process.exit(1); }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const result = await withSpinner('Creating job queue...', () =>
|
|
400
|
+
createQueue({
|
|
401
|
+
queueName: options.name,
|
|
402
|
+
state: options.state,
|
|
403
|
+
priority: parseInt(options.priority),
|
|
404
|
+
computeEnvironmentOrder
|
|
405
|
+
})
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
if (options.json) {
|
|
409
|
+
printJson(result);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
printSuccess(`Queue '${options.name}' created`);
|
|
414
|
+
if (result) {
|
|
415
|
+
console.log('Queue ARN: ', result.jobQueueArn || 'N/A');
|
|
416
|
+
}
|
|
417
|
+
} catch (error) {
|
|
418
|
+
printError(error.message);
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
queuesCmd
|
|
424
|
+
.command('update <queue-name>')
|
|
425
|
+
.description('Update a job queue')
|
|
426
|
+
.option('--state <state>', 'Queue state (ENABLED|DISABLED)')
|
|
427
|
+
.option('--priority <n>', 'Queue priority')
|
|
428
|
+
.option('--json', 'Output as JSON')
|
|
429
|
+
.action(async (queueName, options) => {
|
|
430
|
+
requireAuth();
|
|
431
|
+
try {
|
|
432
|
+
const result = await withSpinner(`Updating queue ${queueName}...`, () =>
|
|
433
|
+
updateQueue({
|
|
434
|
+
queueName,
|
|
435
|
+
state: options.state,
|
|
436
|
+
priority: options.priority ? parseInt(options.priority) : undefined
|
|
437
|
+
})
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
if (options.json) {
|
|
441
|
+
printJson(result);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
printSuccess(`Queue '${queueName}' updated`);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
printError(error.message);
|
|
448
|
+
process.exit(1);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// ============================================================
|
|
453
|
+
// DEFINITIONS
|
|
454
|
+
// ============================================================
|
|
455
|
+
|
|
456
|
+
const definitionsCmd = program.command('definitions').description('Manage AWS Batch job definitions');
|
|
457
|
+
|
|
458
|
+
definitionsCmd
|
|
459
|
+
.command('list')
|
|
460
|
+
.description('List job definitions')
|
|
461
|
+
.option('--name <name>', 'Filter by definition name')
|
|
462
|
+
.option('--status <status>', 'Filter by status (ACTIVE|INACTIVE)', 'ACTIVE')
|
|
463
|
+
.option('--json', 'Output as JSON')
|
|
464
|
+
.action(async (options) => {
|
|
465
|
+
requireAuth();
|
|
466
|
+
try {
|
|
467
|
+
const definitions = await withSpinner('Fetching job definitions...', () =>
|
|
468
|
+
listDefinitions({ definitionName: options.name, status: options.status })
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
if (options.json) {
|
|
472
|
+
printJson(definitions);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
printTable(definitions, [
|
|
477
|
+
{ key: 'jobDefinitionName', label: 'Name' },
|
|
478
|
+
{ key: 'revision', label: 'Rev', format: (v) => String(v || '') },
|
|
479
|
+
{ key: 'type', label: 'Type' },
|
|
480
|
+
{ key: 'status', label: 'Status' }
|
|
481
|
+
]);
|
|
482
|
+
} catch (error) {
|
|
483
|
+
printError(error.message);
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
definitionsCmd
|
|
489
|
+
.command('register')
|
|
490
|
+
.description('Register a new job definition')
|
|
491
|
+
.requiredOption('--name <name>', 'Job definition name')
|
|
492
|
+
.option('--type <type>', 'Job type (container|multinode)', 'container')
|
|
493
|
+
.option('--container <json>', 'Container properties as JSON')
|
|
494
|
+
.option('--json', 'Output as JSON')
|
|
495
|
+
.action(async (options) => {
|
|
496
|
+
requireAuth();
|
|
497
|
+
|
|
498
|
+
let containerProperties;
|
|
499
|
+
if (options.container) {
|
|
500
|
+
try { containerProperties = JSON.parse(options.container); } catch { printError('Invalid JSON for --container'); process.exit(1); }
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
const result = await withSpinner('Registering job definition...', () =>
|
|
505
|
+
registerDefinition({
|
|
506
|
+
definitionName: options.name,
|
|
507
|
+
type: options.type,
|
|
508
|
+
containerProperties
|
|
509
|
+
})
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
if (options.json) {
|
|
513
|
+
printJson(result);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
printSuccess(`Job definition '${options.name}' registered`);
|
|
518
|
+
if (result) {
|
|
519
|
+
console.log('ARN: ', result.jobDefinitionArn || 'N/A');
|
|
520
|
+
console.log('Revision: ', result.revision !== undefined ? String(result.revision) : 'N/A');
|
|
521
|
+
}
|
|
522
|
+
} catch (error) {
|
|
523
|
+
printError(error.message);
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
definitionsCmd
|
|
529
|
+
.command('describe <definition-name>')
|
|
530
|
+
.description('Describe a job definition')
|
|
531
|
+
.option('--json', 'Output as JSON')
|
|
532
|
+
.action(async (definitionName, options) => {
|
|
533
|
+
requireAuth();
|
|
534
|
+
try {
|
|
535
|
+
const definitions = await withSpinner(`Fetching definition ${definitionName}...`, () =>
|
|
536
|
+
describeDefinitions([definitionName])
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
if (options.json) {
|
|
540
|
+
printJson(definitions);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (!definitions || definitions.length === 0) {
|
|
545
|
+
printError('Job definition not found');
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const def = definitions[0];
|
|
550
|
+
console.log(chalk.bold('\nJob Definition Details\n'));
|
|
551
|
+
console.log('Name: ', chalk.cyan(def.jobDefinitionName));
|
|
552
|
+
console.log('ARN: ', def.jobDefinitionArn || 'N/A');
|
|
553
|
+
console.log('Revision: ', def.revision !== undefined ? String(def.revision) : 'N/A');
|
|
554
|
+
console.log('Type: ', def.type || 'N/A');
|
|
555
|
+
console.log('Status: ', def.status || 'N/A');
|
|
556
|
+
if (def.containerProperties) {
|
|
557
|
+
console.log('Image: ', def.containerProperties.image || 'N/A');
|
|
558
|
+
console.log('vCPUs: ', def.containerProperties.vcpus || 'N/A');
|
|
559
|
+
console.log('Memory: ', def.containerProperties.memory ? `${def.containerProperties.memory} MB` : 'N/A');
|
|
560
|
+
}
|
|
561
|
+
console.log('');
|
|
562
|
+
} catch (error) {
|
|
563
|
+
printError(error.message);
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// ============================================================
|
|
569
|
+
// Parse
|
|
570
|
+
// ============================================================
|
|
571
|
+
|
|
572
|
+
program.parse(process.argv);
|
|
573
|
+
|
|
574
|
+
if (process.argv.length <= 2) {
|
|
575
|
+
program.help();
|
|
576
|
+
}
|