Skip to content

Commit 53bfe12

Browse files
Refactor and add tests
1 parent 0bd3f62 commit 53bfe12

14 files changed

+9796
-482
lines changed

README.md

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,61 @@
11
# TeamCityCloudAgentUpdater
22

3-
When a new TeamCity agent image is built (e.g., via [packer](packer.io)), you need to tell TeamCity to start using it.
3+
When a new TeamCity agent image is built (e.g., via [packer](packer.io)), you need to tell TeamCity to start using it.
44

55
This is a simple NodeJS app that can:
6-
1. update TeamCity Cloud Agents images and disable any agents that are running that are based on the old image
7-
2. remove any agents that were disabled during update that are no longer running a build
6+
1. Update TeamCity Cloud Agents images and disable any agents that are running that are based on the old image
7+
2. Remove any agents that were disabled during update that are no longer running a build
88

99
## Usage
1010

1111
When you have a new agent, update the cloud profile:
12-
```
13-
node index.js [update-cloud-image] --token XXXXX --server https://teamcity.example.com --image ami-XXXXXXX --cloudprofile "AWS Agents" --agentprefix "Ubuntu" [--dryrun]
12+
```bash
13+
node index.js update-cloud-profile --token XXXXX --server https://teamcity.example.com --image ami-XXXXXXX --cloudprofile "AWS Agents" --agentprefix "Ubuntu" [--dryrun]
1414
```
1515

1616
To remove any agents that were disabled as part of the update, and are no longer running a build (run on a schedule):
17-
```
17+
```bash
1818
node index.js remove-disabled-agents --token XXXXX --server https://teamcity.example.com [--dryrun]
1919
```
2020

21-
The `--dryrun` flag allows you to check what actions the script would have taken with out any real modifications.
21+
The `--dryrun` flag allows you to check what actions the script would have taken without any real modifications.
22+
23+
## Requirements
24+
25+
- Node.js >= 22.0.0
26+
- npm >= 11.0.0
27+
- TeamCity `2019.1` or newer (for user access tokens)
28+
29+
## Development
30+
31+
### Installation
32+
```bash
33+
npm install
34+
```
35+
36+
### Testing
37+
```bash
38+
# Run all tests
39+
npm test
40+
41+
# Run tests in watch mode
42+
npm run test:watch
2243

23-
## Requiremens
44+
# Run tests with coverage
45+
npm run test:coverage
46+
```
2447

25-
This app uses features (user access tokens) that require TeamCity `2019.1` or newer.
48+
### Project Structure
49+
```
50+
.
51+
├── index.js # Entry point
52+
├── cli.js # CLI command definitions
53+
├── lib/
54+
│ ├── agents.js # Agent management operations
55+
│ ├── cloud-profiles.js # Cloud profile management
56+
│ └── utils.js # Utility functions
57+
└── *.test.js # Test files co-located with source
58+
```
2659

2760
## License
2861

cli.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use strict';
2+
3+
const { Command } = require('commander');
4+
const lib = require('./lib/index');
5+
6+
function createProgram() {
7+
const program = new Command();
8+
9+
program
10+
.name('TeamCity Cloud Agent Updater')
11+
.description('Simple NodeJS app to update images for TeamCity Cloud Agents, via the 2017.1 API.')
12+
.version('1.0.0');
13+
14+
program
15+
.command('update-cloud-profile', { isDefault: true })
16+
.requiredOption('--token <string>', 'A valid TeamCity user access token (requires TC 2019.1)')
17+
.requiredOption('--server <string>', 'The url of the TeamCity server, eg "http://teamcity.example.com"')
18+
.requiredOption('--image <string>', 'The AMI id (for AWS), or full url to the VHD / resource id of the managed image (for Azure)')
19+
.requiredOption('--cloudprofile <string>', 'The name of the TeamCity Cloud Profile to modify')
20+
.requiredOption('--agentprefix <string>', 'The agent prefix used in the Cloud Profile image that should be updated')
21+
.option('--dryrun', 'Output what changes the app would make, but dont actually make the changes')
22+
.action((options) => lib.updateCloudImage(options.server, "Bearer " + options.token, options.cloudprofile, options.agentprefix, options.image, options.dryrun));
23+
24+
program
25+
.command('remove-disabled-agents')
26+
.requiredOption('--token <string>', 'A valid TeamCity user access token (requires TC 2019.1)')
27+
.requiredOption('--server <string>', 'The url of the TeamCity server, eg "http://teamcity.example.com"')
28+
.option('--dryrun', 'Output what changes the app would make, but dont actually make the changes')
29+
.action((options) => lib.removeDisabledAgents(options.server, "Bearer " + options.token, options.dryrun));
30+
31+
return program;
32+
}
33+
34+
function run(argv) {
35+
const program = createProgram();
36+
return program.parse(argv);
37+
}
38+
39+
module.exports = {
40+
createProgram,
41+
run
42+
};

cli.test.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
const { spawn } = require('child_process');
2+
const path = require('path');
3+
const cli = require('./cli');
4+
5+
describe('CLI Commands', () => {
6+
const indexPath = path.join(__dirname, 'index.js');
7+
8+
const runCommand = (args) => {
9+
return new Promise((resolve, reject) => {
10+
const process = spawn('node', [indexPath, ...args]);
11+
let stdout = '';
12+
let stderr = '';
13+
14+
process.stdout.on('data', (data) => {
15+
stdout += data.toString();
16+
});
17+
18+
process.stderr.on('data', (data) => {
19+
stderr += data.toString();
20+
});
21+
22+
process.on('close', (code) => {
23+
resolve({ code, stdout, stderr });
24+
});
25+
26+
process.on('error', (error) => {
27+
reject(error);
28+
});
29+
});
30+
};
31+
32+
describe('update-cloud-profile command', () => {
33+
it('should show error when required options are missing', async () => {
34+
const result = await runCommand(['update-cloud-profile']);
35+
expect(result.code).not.toBe(0);
36+
expect(result.stderr).toContain('required option');
37+
});
38+
39+
it('should show help with --help flag', async () => {
40+
const result = await runCommand(['update-cloud-profile', '--help']);
41+
expect(result.code).toBe(0);
42+
expect(result.stdout).toContain('--token');
43+
expect(result.stdout).toContain('--server');
44+
expect(result.stdout).toContain('--image');
45+
expect(result.stdout).toContain('--cloudprofile');
46+
expect(result.stdout).toContain('--agentprefix');
47+
expect(result.stdout).toContain('--dryrun');
48+
});
49+
50+
it('should validate required parameters', async () => {
51+
const result = await runCommand([
52+
'update-cloud-profile',
53+
'--token', 'test-token'
54+
]);
55+
expect(result.code).not.toBe(0);
56+
expect(result.stderr).toContain('required option');
57+
});
58+
});
59+
60+
describe('remove-disabled-agents command', () => {
61+
it('should show error when required options are missing', async () => {
62+
const result = await runCommand(['remove-disabled-agents']);
63+
expect(result.code).not.toBe(0);
64+
expect(result.stderr).toContain('required option');
65+
});
66+
67+
it('should show help with --help flag', async () => {
68+
const result = await runCommand(['remove-disabled-agents', '--help']);
69+
expect(result.code).toBe(0);
70+
expect(result.stdout).toContain('--token');
71+
expect(result.stdout).toContain('--server');
72+
expect(result.stdout).toContain('--dryrun');
73+
});
74+
75+
it('should validate required parameters', async () => {
76+
const result = await runCommand([
77+
'remove-disabled-agents',
78+
'--token', 'test-token'
79+
]);
80+
expect(result.code).not.toBe(0);
81+
expect(result.stderr).toContain('required option');
82+
});
83+
});
84+
85+
describe('General CLI', () => {
86+
it('should show version with --version flag', async () => {
87+
const result = await runCommand(['--version']);
88+
expect(result.code).toBe(0);
89+
expect(result.stdout).toContain('1.0.0');
90+
});
91+
92+
it('should show help with --help flag', async () => {
93+
const result = await runCommand(['--help']);
94+
expect(result.code).toBe(0);
95+
expect(result.stdout).toContain('TeamCity Cloud Agent Updater');
96+
expect(result.stdout).toContain('update-cloud-profile');
97+
expect(result.stdout).toContain('remove-disabled-agents');
98+
});
99+
100+
it('should use update-cloud-profile as default command', async () => {
101+
const result = await runCommand([]);
102+
expect(result.code).not.toBe(0);
103+
// Should show error for missing required options from update-cloud-profile
104+
expect(result.stderr).toContain('required option');
105+
});
106+
});
107+
});

0 commit comments

Comments
 (0)