keycloak-api-manager 3.2.1 → 4.0.1
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/.env.example +27 -0
- package/Handlers/clientsHandler.js +240 -30
- package/Handlers/groupsHandler.js +16 -1
- package/Handlers/httpApiHelper.js +87 -0
- package/Handlers/realmsHandler.js +26 -4
- package/Handlers/usersHandler.js +43 -10
- package/README.md +341 -10
- package/index.js +159 -29
- package/package.json +3 -14
- package/test/.mocharc.json +4 -0
- package/test/TESTING.md +327 -0
- package/test/config/CONFIGURATION.md +170 -0
- package/test/config/default.json +36 -0
- package/test/config/local.json.example +7 -0
- package/test/config/secrets.json.example +7 -0
- package/test/diagnostic-protocol-mappers.js +189 -0
- package/test/docker-keycloak/DEPLOYMENT_GUIDE.md +262 -0
- package/test/docker-keycloak/certs/.gitkeep +7 -0
- package/test/docker-keycloak/docker-compose-https.yml +50 -0
- package/test/docker-keycloak/docker-compose.yml +59 -0
- package/test/docker-keycloak/setup-keycloak.js +501 -0
- package/test/enableServerFeatures.js +315 -0
- package/test/helpers/config.js +218 -0
- package/test/helpers/docker-helpers.js +513 -0
- package/test/helpers/setup.js +186 -0
- package/test/package.json +18 -0
- package/test/setup.js +194 -0
- package/test/specs/authenticationManagement.test.js +224 -0
- package/test/specs/clientCredentials.test.js +76 -0
- package/test/specs/clientScopes.test.js +388 -0
- package/test/specs/clients.test.js +791 -0
- package/test/specs/components.test.js +151 -0
- package/test/specs/debugClientLibrary.test.js +88 -0
- package/test/specs/groups.test.js +362 -0
- package/test/specs/identityProviders.test.js +292 -0
- package/test/specs/realms.test.js +390 -0
- package/test/specs/roles.test.js +322 -0
- package/test/specs/users.test.js +445 -0
- package/test/testConfig.js +69 -0
- package/.mocharc.json +0 -7
- package/docker-compose.yml +0 -27
- package/test/authenticationManagement.test.js +0 -329
- package/test/clientScopes.test.js +0 -256
- package/test/clients.test.js +0 -284
- package/test/components.test.js +0 -122
- package/test/config.js +0 -137
- package/test/docker-helpers.js +0 -111
- package/test/groups.test.js +0 -284
- package/test/identityProviders.test.js +0 -197
- package/test/mocha.env.js +0 -55
- package/test/realms.test.js +0 -349
- package/test/roles.test.js +0 -215
- package/test/users.test.js +0 -405
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
|
|
3
|
+
# Keycloak HTTPS Configuration
|
|
4
|
+
# Use this file with docker-compose -f docker-compose-https.yml for HTTPS deployments
|
|
5
|
+
#
|
|
6
|
+
# Environment Variables Required:
|
|
7
|
+
# KEYCLOAK_CERT_PATH: Path to directory containing keycloak.crt and keycloak.key
|
|
8
|
+
# KEYCLOAK_HOSTNAME: Hostname for Keycloak (e.g., smart-dell-sml.crs4.it)
|
|
9
|
+
|
|
10
|
+
services:
|
|
11
|
+
keycloak:
|
|
12
|
+
image: keycloak/keycloak:latest
|
|
13
|
+
container_name: keycloak-test
|
|
14
|
+
ports:
|
|
15
|
+
# HTTP port (for backward compatibility)
|
|
16
|
+
- "8080:8080"
|
|
17
|
+
# HTTPS port
|
|
18
|
+
- "8443:8443"
|
|
19
|
+
environment:
|
|
20
|
+
KEYCLOAK_ADMIN: admin # Master realm admin username
|
|
21
|
+
KEYCLOAK_ADMIN_PASSWORD: admin # Master realm admin password
|
|
22
|
+
KC_DB: dev-mem # In-memory database (development only)
|
|
23
|
+
KC_METRICS_ENABLED: 'false' # Disable metrics in dev
|
|
24
|
+
KC_HEALTH_ENABLED: 'true' # Enable health checks
|
|
25
|
+
KC_HOSTNAME: ${KEYCLOAK_HOSTNAME:-keycloak.local}
|
|
26
|
+
KC_SCHEME: https # Use HTTPS
|
|
27
|
+
KC_HTTP_PORT: 8080
|
|
28
|
+
KC_HTTPS_PORT: 8443
|
|
29
|
+
# HTTPS configuration
|
|
30
|
+
KC_HTTP_ENABLED: 'true' # Admin console accessible via HTTP too
|
|
31
|
+
KC_PROXY: reencrypt # Trust reverse proxy headers
|
|
32
|
+
volumes:
|
|
33
|
+
# Mount certificates for HTTPS
|
|
34
|
+
- "${KEYCLOAK_CERT_PATH}/keycloak.crt:/etc/keycloak/certs/keycloak.crt:ro"
|
|
35
|
+
- "${KEYCLOAK_CERT_PATH}/keycloak.key:/etc/keycloak/certs/keycloak.key:ro"
|
|
36
|
+
command:
|
|
37
|
+
- start-dev
|
|
38
|
+
- "--https-certificate-file=/etc/keycloak/certs/keycloak.crt"
|
|
39
|
+
- "--https-certificate-key-file=/etc/keycloak/certs/keycloak.key"
|
|
40
|
+
healthcheck:
|
|
41
|
+
test: ["CMD", "curl", "-fk", "https://localhost:8443/health/ready"]
|
|
42
|
+
interval: 5s
|
|
43
|
+
timeout: 5s
|
|
44
|
+
retries: 12 # Wait up to 60 seconds for Keycloak to be ready
|
|
45
|
+
networks:
|
|
46
|
+
- keycloak-network
|
|
47
|
+
|
|
48
|
+
networks:
|
|
49
|
+
keycloak-network:
|
|
50
|
+
driver: bridge
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
|
|
3
|
+
# Keycloak Test Server Configuration
|
|
4
|
+
# Supports both HTTP (local dev) and HTTPS (production-like setup)
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# HTTP (local): docker-compose up -d
|
|
8
|
+
# HTTPS (remote): KEYCLOAK_HTTPS=true KEYCLOAK_CERT_PATH=/path/to/certs docker-compose up -d
|
|
9
|
+
#
|
|
10
|
+
# Environment Variables:
|
|
11
|
+
# KEYCLOAK_HTTPS: 'true' to enable HTTPS, 'false' for HTTP (default: false)
|
|
12
|
+
# KEYCLOAK_CERT_PATH: Path to directory containing certificate files (required if KEYCLOAK_HTTPS=true)
|
|
13
|
+
# KEYCLOAK_HOSTNAME: Hostname for Keycloak (default: localhost)
|
|
14
|
+
# KEYCLOAK_HTTPS_PORT: HTTPS port (default: 8443)
|
|
15
|
+
|
|
16
|
+
services:
|
|
17
|
+
keycloak:
|
|
18
|
+
image: keycloak/keycloak:latest
|
|
19
|
+
container_name: keycloak-test
|
|
20
|
+
ports:
|
|
21
|
+
# HTTP port (always enabled for admin console fallback)
|
|
22
|
+
- "8080:8080"
|
|
23
|
+
# HTTPS port (when enabled)
|
|
24
|
+
- "${KEYCLOAK_HTTPS_PORT:-8443}:8443"
|
|
25
|
+
environment:
|
|
26
|
+
KEYCLOAK_ADMIN: admin # Master realm admin username
|
|
27
|
+
KEYCLOAK_ADMIN_PASSWORD: admin # Master realm admin password
|
|
28
|
+
KC_DB: dev-mem # In-memory database (development only)
|
|
29
|
+
KC_METRICS_ENABLED: 'false' # Disable metrics in dev
|
|
30
|
+
KC_HEALTH_ENABLED: 'true' # Enable health checks
|
|
31
|
+
KC_HOSTNAME: ${KEYCLOAK_HOSTNAME:-localhost}
|
|
32
|
+
KC_SCHEME: ${KEYCLOAK_SCHEME:-http}
|
|
33
|
+
KC_HTTP_PORT: 8080
|
|
34
|
+
KC_HTTPS_PORT: 8443
|
|
35
|
+
# HTTPS configuration (only applied if KC_SCHEME=https)
|
|
36
|
+
KC_HTTP_ENABLED: 'true' # Admin console accessible via HTTP too
|
|
37
|
+
KC_PROXY: reencrypt # Trust reverse proxy headers
|
|
38
|
+
volumes:
|
|
39
|
+
# Mount certificates if HTTPS is enabled and path is provided
|
|
40
|
+
- "${KEYCLOAK_CERT_PATH:-./certs}:/etc/keycloak/certs:ro"
|
|
41
|
+
command:
|
|
42
|
+
- start-dev
|
|
43
|
+
# Note: To add HTTPS flags dynamically, use environment variable substitution
|
|
44
|
+
# For HTTPS mode, add these flags before start-dev:
|
|
45
|
+
# - --https-certificate-file=/etc/keycloak/certs/keycloak.crt
|
|
46
|
+
# - --https-certificate-key-file=/etc/keycloak/certs/keycloak.key
|
|
47
|
+
healthcheck:
|
|
48
|
+
test: ["CMD", "curl", "-f", "http://localhost:8080/health/ready"]
|
|
49
|
+
interval: 5s
|
|
50
|
+
timeout: 5s
|
|
51
|
+
retries: 12 # Wait up to 60 seconds for Keycloak to be ready
|
|
52
|
+
networks:
|
|
53
|
+
- keycloak-network
|
|
54
|
+
|
|
55
|
+
networks:
|
|
56
|
+
keycloak-network:
|
|
57
|
+
driver: bridge
|
|
58
|
+
|
|
59
|
+
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keycloak Container Setup Script
|
|
5
|
+
*
|
|
6
|
+
* Prompts user to choose local or remote deployment and configures Keycloak accordingly.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npm run setup-keycloak
|
|
10
|
+
* node test/setup-keycloak.js
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - Interactive prompts for deployment location
|
|
14
|
+
* - Local deployment with HTTP or HTTPS support
|
|
15
|
+
* - Remote SSH deployment with automatic docker-compose copying
|
|
16
|
+
* - Certificate configuration for HTTPS mode
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { spawn, exec } = require('child_process');
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const os = require('os');
|
|
23
|
+
const readline = require('readline');
|
|
24
|
+
|
|
25
|
+
// ANSI color codes for terminal output
|
|
26
|
+
const colors = {
|
|
27
|
+
reset: '\x1b[0m',
|
|
28
|
+
bright: '\x1b[1m',
|
|
29
|
+
green: '\x1b[32m',
|
|
30
|
+
yellow: '\x1b[33m',
|
|
31
|
+
blue: '\x1b[36m',
|
|
32
|
+
red: '\x1b[31m',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Detect docker compose command (docker-compose or docker compose)
|
|
36
|
+
function getDockerComposeCmdSync() {
|
|
37
|
+
try {
|
|
38
|
+
require('child_process').execSync('docker-compose --version', { stdio: 'ignore' });
|
|
39
|
+
return 'docker-compose';
|
|
40
|
+
} catch (err) {
|
|
41
|
+
try {
|
|
42
|
+
require('child_process').execSync('docker compose version', { stdio: 'ignore' });
|
|
43
|
+
return 'docker compose';
|
|
44
|
+
} catch (err2) {
|
|
45
|
+
return null; // Docker not available
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let DOCKER_COMPOSE_CMD = getDockerComposeCmdSync();
|
|
51
|
+
|
|
52
|
+
let rl;
|
|
53
|
+
|
|
54
|
+
function initReadline() {
|
|
55
|
+
if (!rl) {
|
|
56
|
+
rl = readline.createInterface({
|
|
57
|
+
input: process.stdin,
|
|
58
|
+
output: process.stdout,
|
|
59
|
+
terminal: process.stdin.isTTY
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return rl;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function log(message, color = 'reset') {
|
|
66
|
+
console.log(`${colors[color]}${message}${colors.reset}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function prompt(question) {
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
const interface = initReadline();
|
|
72
|
+
interface.question(question, (answer) => {
|
|
73
|
+
resolve(answer.trim());
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function askDeploymentLocation() {
|
|
79
|
+
log('\n=== Keycloak Container Deployment Setup ===\n', 'bright');
|
|
80
|
+
log('Choose deployment location:', 'blue');
|
|
81
|
+
|
|
82
|
+
const dockerAvailable = DOCKER_COMPOSE_CMD !== null;
|
|
83
|
+
|
|
84
|
+
if (dockerAvailable) {
|
|
85
|
+
log(' 1) Local machine (localhost:8080)', 'yellow');
|
|
86
|
+
log(' 2) Remote machine via SSH', 'yellow');
|
|
87
|
+
} else {
|
|
88
|
+
log(' ⚠ Docker not available locally', 'yellow');
|
|
89
|
+
log(' → Deploying to remote machine via SSH', 'yellow');
|
|
90
|
+
return 'remote';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let choice;
|
|
94
|
+
while (!['1', '2'].includes(choice)) {
|
|
95
|
+
choice = await prompt('\nEnter choice (1 or 2): ');
|
|
96
|
+
if (!['1', '2'].includes(choice)) {
|
|
97
|
+
log('Invalid choice. Please enter 1 or 2.', 'red');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return choice === '1' ? 'local' : 'remote';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function askHttpsSetup() {
|
|
105
|
+
log('\nEnable HTTPS?', 'blue');
|
|
106
|
+
log(' 1) No, use HTTP (development)', 'yellow');
|
|
107
|
+
log(' 2) Yes, use HTTPS (production-like)', 'yellow');
|
|
108
|
+
|
|
109
|
+
let choice;
|
|
110
|
+
while (!['1', '2'].includes(choice)) {
|
|
111
|
+
choice = await prompt('\nEnter choice (1 or 2): ');
|
|
112
|
+
if (!['1', '2'].includes(choice)) {
|
|
113
|
+
log('Invalid choice. Please enter 1 or 2.', 'red');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return choice === '2';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function askRemoteDetails() {
|
|
121
|
+
log('\nRemote Deployment Target', 'blue');
|
|
122
|
+
log(' Specify the user and machine where Keycloak will be deployed:', 'yellow');
|
|
123
|
+
log(' Format: username@hostname', 'yellow');
|
|
124
|
+
log(' Example: user@miodomino.it', 'yellow');
|
|
125
|
+
|
|
126
|
+
const host = await prompt('\nRemote host/IP (user@hostname): ');
|
|
127
|
+
if (!host) {
|
|
128
|
+
throw new Error('Host is required');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { host };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function askCertificatePath(isRemote = false) {
|
|
135
|
+
log('\nHTTPS requires certificate files.', 'blue');
|
|
136
|
+
|
|
137
|
+
// Check for default certificate files in test/docker-keycloak/certs
|
|
138
|
+
const defaultCertPath = path.join(__dirname, 'certs');
|
|
139
|
+
const defaultCertFile = path.join(defaultCertPath, 'keycloak.crt');
|
|
140
|
+
const defaultKeyFile = path.join(defaultCertPath, 'keycloak.key');
|
|
141
|
+
|
|
142
|
+
// For remote deployments, always use certificates from local test/docker-keycloak/certs
|
|
143
|
+
// and copy them to the remote server
|
|
144
|
+
if (isRemote) {
|
|
145
|
+
// Verify local certificates exist
|
|
146
|
+
if (!fs.existsSync(defaultCertFile) || !fs.existsSync(defaultKeyFile)) {
|
|
147
|
+
throw new Error(`Certificate files not found in ${defaultCertPath}\nExpected: keycloak.crt and keycloak.key`);
|
|
148
|
+
}
|
|
149
|
+
log(`✓ Using certificates from ${defaultCertPath}`, 'green');
|
|
150
|
+
log(' → Will be copied to remote server', 'yellow');
|
|
151
|
+
return { localPath: defaultCertPath, remotePath: null };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Local deployment - check for defaults first
|
|
155
|
+
if (fs.existsSync(defaultCertFile) && fs.existsSync(defaultKeyFile)) {
|
|
156
|
+
log(`✓ Found default certificates in ${defaultCertPath}`, 'green');
|
|
157
|
+
return { localPath: defaultCertPath, remotePath: null };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// If defaults not found, ask user for custom path
|
|
161
|
+
log(` Default location: ${defaultCertPath}`, 'yellow');
|
|
162
|
+
log(' Certificate files needed: keycloak.crt and keycloak.key', 'yellow');
|
|
163
|
+
|
|
164
|
+
const certPath = await prompt('Certificate directory path (or press Enter for default): ');
|
|
165
|
+
|
|
166
|
+
// Use default if user just pressed Enter
|
|
167
|
+
if (!certPath) {
|
|
168
|
+
if (fs.existsSync(defaultCertFile) && fs.existsSync(defaultKeyFile)) {
|
|
169
|
+
log(`✓ Using default certificates from ${defaultCertPath}`, 'green');
|
|
170
|
+
return { localPath: defaultCertPath, remotePath: null };
|
|
171
|
+
}
|
|
172
|
+
throw new Error('Certificate path is required for HTTPS');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Verify custom certificate files
|
|
176
|
+
const certFile = path.join(certPath, 'keycloak.crt');
|
|
177
|
+
const keyFile = path.join(certPath, 'keycloak.key');
|
|
178
|
+
|
|
179
|
+
if (!fs.existsSync(certFile) || !fs.existsSync(keyFile)) {
|
|
180
|
+
throw new Error(`Certificate files not found in ${certPath}\nExpected: keycloak.crt and keycloak.key`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { localPath: certPath, remotePath: null };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function executeCommand(command, cwd) {
|
|
187
|
+
return new Promise((resolve, reject) => {
|
|
188
|
+
const child = spawn('bash', ['-c', command], {
|
|
189
|
+
cwd: cwd || process.cwd(),
|
|
190
|
+
stdio: 'inherit',
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
child.on('close', (code) => {
|
|
194
|
+
if (code === 0) {
|
|
195
|
+
resolve();
|
|
196
|
+
} else {
|
|
197
|
+
reject(new Error(`Command failed with exit code ${code}: ${command}`));
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
child.on('error', (err) => {
|
|
202
|
+
reject(err);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function execSync(command, cwd) {
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
const options = {};
|
|
210
|
+
if (cwd) {
|
|
211
|
+
options.cwd = cwd;
|
|
212
|
+
}
|
|
213
|
+
exec(command, options, (error, stdout, stderr) => {
|
|
214
|
+
if (error) {
|
|
215
|
+
reject(error);
|
|
216
|
+
} else {
|
|
217
|
+
resolve(stdout.trim());
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function updateTestBaseUrl(baseUrl) {
|
|
224
|
+
const configPath = path.join(__dirname, '..', 'config', 'default.json');
|
|
225
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
226
|
+
const config = JSON.parse(raw);
|
|
227
|
+
if (!config.test || !config.test.keycloak) {
|
|
228
|
+
throw new Error('test.keycloak not found in test/config/default.json');
|
|
229
|
+
}
|
|
230
|
+
config.test.keycloak.baseUrl = baseUrl;
|
|
231
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
232
|
+
log(`✓ Updated test baseUrl: ${baseUrl}`, 'green');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function deployLocal(useHttps, certPath) {
|
|
236
|
+
log('\n=== Local Deployment ===\n', 'bright');
|
|
237
|
+
|
|
238
|
+
const dockerComposeDir = __dirname;
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
// Determine which compose file to use
|
|
242
|
+
const composeFile = useHttps ? 'docker-compose-https.yml' : 'docker-compose.yml';
|
|
243
|
+
const composeCmd = `docker-compose -f ${composeFile}`;
|
|
244
|
+
|
|
245
|
+
if (useHttps) {
|
|
246
|
+
log('Starting Keycloak with HTTPS...', 'blue');
|
|
247
|
+
|
|
248
|
+
// Write .env file for HTTPS
|
|
249
|
+
const finalCertPath = certPath && certPath.localPath ? certPath.localPath : certPath;
|
|
250
|
+
const envContent = `KEYCLOAK_CERT_PATH=${finalCertPath}
|
|
251
|
+
KEYCLOAK_HOSTNAME=localhost
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
fs.writeFileSync(path.join(dockerComposeDir, '.env'), envContent);
|
|
255
|
+
log('Created .env file with HTTPS configuration', 'green');
|
|
256
|
+
} else {
|
|
257
|
+
log('Starting Keycloak with HTTP...', 'blue');
|
|
258
|
+
|
|
259
|
+
// Remove .env file for HTTP (use defaults)
|
|
260
|
+
const envFilePath = path.join(dockerComposeDir, '.env');
|
|
261
|
+
if (fs.existsSync(envFilePath)) {
|
|
262
|
+
fs.unlinkSync(envFilePath);
|
|
263
|
+
}
|
|
264
|
+
log('Using default HTTP configuration', 'green');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Stop any existing containers
|
|
268
|
+
log('Stopping existing containers...', 'blue');
|
|
269
|
+
try {
|
|
270
|
+
await executeCommand(`${composeCmd} down 2>/dev/null || true`, dockerComposeDir);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
// Ignore errors
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Start containers
|
|
276
|
+
log(`Starting Keycloak with ${useHttps ? 'HTTPS' : 'HTTP'}...`, 'blue');
|
|
277
|
+
await executeCommand(`${composeCmd} up -d`, dockerComposeDir);
|
|
278
|
+
log(`\n✓ Keycloak is starting locally...`, 'green');
|
|
279
|
+
|
|
280
|
+
log('\nWaiting for Keycloak to be ready...', 'blue');
|
|
281
|
+
let ready = false;
|
|
282
|
+
let attempts = 0;
|
|
283
|
+
const maxAttempts = 60;
|
|
284
|
+
|
|
285
|
+
// For HTTPS health check, we need to use -k flag to skip certificate validation
|
|
286
|
+
const healthCheckProtocol = useHttps ? 'https' : 'http';
|
|
287
|
+
|
|
288
|
+
while (!ready && attempts < maxAttempts) {
|
|
289
|
+
try {
|
|
290
|
+
// Health check always on localhost:8080 HTTP for simplicity
|
|
291
|
+
const health = await execSync(`curl -s http://localhost:8080/health/ready 2>/dev/null`);
|
|
292
|
+
if (health.includes('UP')) {
|
|
293
|
+
ready = true;
|
|
294
|
+
}
|
|
295
|
+
} catch (err) {
|
|
296
|
+
// Still waiting
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!ready) {
|
|
300
|
+
attempts++;
|
|
301
|
+
process.stdout.write('.');
|
|
302
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (ready) {
|
|
307
|
+
log('\n\n✓ Keycloak is ready!', 'green');
|
|
308
|
+
log('\nAccess Keycloak:', 'bright');
|
|
309
|
+
if (useHttps) {
|
|
310
|
+
log(` Admin Console: https://localhost:8443`, 'yellow');
|
|
311
|
+
log(` (self-signed cert - use -k with curl or add to browser exceptions)`, 'yellow');
|
|
312
|
+
} else {
|
|
313
|
+
log(` Admin Console: http://localhost:8080`, 'yellow');
|
|
314
|
+
}
|
|
315
|
+
log(' Credentials: admin / admin', 'yellow');
|
|
316
|
+
} else {
|
|
317
|
+
log('\n\n⚠ Keycloak is taking longer than expected. Check Docker logs:', 'yellow');
|
|
318
|
+
log(` ${composeCmd} logs -f`, 'yellow');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
} catch (err) {
|
|
322
|
+
log(`\n✗ Error during local deployment: ${err.message}`, 'red');
|
|
323
|
+
throw err;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function deployRemote(host, deployPath, useHttps, certPath) {
|
|
328
|
+
log('\n=== Remote Deployment ===\n', 'bright');
|
|
329
|
+
|
|
330
|
+
const dockerComposeDir = __dirname;
|
|
331
|
+
const dockerComposePath = path.join(dockerComposeDir, 'docker-compose.yml');
|
|
332
|
+
const dockerComposeHttpsPath = path.join(dockerComposeDir, 'docker-compose-https.yml');
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
// Verify SSH connection
|
|
336
|
+
log(`Verifying SSH connection to ${host}...`, 'blue');
|
|
337
|
+
await execSync(`ssh -o ConnectTimeout=5 ${host} 'echo OK'`);
|
|
338
|
+
log('✓ SSH connection successful', 'green');
|
|
339
|
+
|
|
340
|
+
// Create deployment directory on remote
|
|
341
|
+
log(`Creating deployment directory on remote: ${deployPath}`, 'blue');
|
|
342
|
+
await execSync(`ssh ${host} 'mkdir -p "${deployPath}"'`);
|
|
343
|
+
log('✓ Directory created', 'green');
|
|
344
|
+
|
|
345
|
+
// Copy docker-compose files to remote
|
|
346
|
+
log('Copying docker-compose files to remote...', 'blue');
|
|
347
|
+
await execSync(`scp "${dockerComposePath}" "${host}:${deployPath}/docker-compose.yml"`);
|
|
348
|
+
await execSync(`scp "${dockerComposeHttpsPath}" "${host}:${deployPath}/docker-compose-https.yml"`);
|
|
349
|
+
log('✓ docker-compose files copied', 'green');
|
|
350
|
+
|
|
351
|
+
// Copy certificate files if HTTPS and certificates are on local machine
|
|
352
|
+
if (useHttps && certPath && certPath.localPath) {
|
|
353
|
+
log('Copying certificate files to remote...', 'blue');
|
|
354
|
+
const remoteDir = `${deployPath}/certs`;
|
|
355
|
+
await execSync(`ssh ${host} 'mkdir -p "${remoteDir}"'`);
|
|
356
|
+
|
|
357
|
+
const certFile = path.join(certPath.localPath, 'keycloak.crt');
|
|
358
|
+
const keyFile = path.join(certPath.localPath, 'keycloak.key');
|
|
359
|
+
|
|
360
|
+
await execSync(`scp "${certFile}" "${host}:${remoteDir}/"`);
|
|
361
|
+
await execSync(`scp "${keyFile}" "${host}:${remoteDir}/"`);
|
|
362
|
+
log('✓ Certificate files copied', 'green');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Determine which compose file to use
|
|
366
|
+
const composeFile = useHttps ? 'docker-compose-https.yml' : 'docker-compose.yml';
|
|
367
|
+
const composeCmd = DOCKER_COMPOSE_CMD ? `${DOCKER_COMPOSE_CMD} -f ${composeFile}` : `docker compose -f ${composeFile}`;
|
|
368
|
+
|
|
369
|
+
// Create .env file on remote
|
|
370
|
+
log('Creating configuration on remote...', 'blue');
|
|
371
|
+
|
|
372
|
+
let envContent = '';
|
|
373
|
+
const hostname = host.includes('@') ? host.split('@')[1] : host;
|
|
374
|
+
|
|
375
|
+
if (useHttps) {
|
|
376
|
+
// Use remotePath if certificates are already on server, otherwise use copied path
|
|
377
|
+
const certPathForEnv = certPath.remotePath || `${deployPath}/certs`;
|
|
378
|
+
envContent = `KEYCLOAK_CERT_PATH=${certPathForEnv}
|
|
379
|
+
KEYCLOAK_HOSTNAME=${hostname}
|
|
380
|
+
`;
|
|
381
|
+
} else {
|
|
382
|
+
envContent = `KEYCLOAK_HOSTNAME=${hostname}
|
|
383
|
+
`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const envBase64 = Buffer.from(envContent).toString('base64');
|
|
387
|
+
await execSync(`ssh ${host} 'echo "${envBase64}" | base64 -d > "${deployPath}/.env"'`);
|
|
388
|
+
log('✓ Configuration created', 'green');
|
|
389
|
+
|
|
390
|
+
// Stop any existing containers at the remote path
|
|
391
|
+
log('Stopping existing containers...', 'blue');
|
|
392
|
+
try {
|
|
393
|
+
await execSync(`ssh ${host} 'cd "${deployPath}" && ${DOCKER_COMPOSE_CMD} -f docker-compose.yml down 2>/dev/null || ${DOCKER_COMPOSE_CMD} -f docker-compose-https.yml down 2>/dev/null || true'`);
|
|
394
|
+
// Force remove the container in case down didn't work
|
|
395
|
+
await execSync(`ssh ${host} 'docker rm -f keycloak-test 2>/dev/null || true'`);
|
|
396
|
+
} catch (err) {
|
|
397
|
+
// Ignore errors if containers don't exist
|
|
398
|
+
}
|
|
399
|
+
log('✓ Old containers stopped', 'green');
|
|
400
|
+
|
|
401
|
+
// Start new containers
|
|
402
|
+
log(`Starting Keycloak container with ${useHttps ? 'HTTPS' : 'HTTP'}...`, 'blue');
|
|
403
|
+
await execSync(`ssh ${host} 'cd "${deployPath}" && ${composeCmd} up -d'`);
|
|
404
|
+
log('✓ Keycloak container started', 'green');
|
|
405
|
+
|
|
406
|
+
log('\nWaiting for Keycloak to be ready...', 'blue');
|
|
407
|
+
let ready = false;
|
|
408
|
+
let attempts = 0;
|
|
409
|
+
const maxAttempts = 60;
|
|
410
|
+
|
|
411
|
+
while (!ready && attempts < maxAttempts) {
|
|
412
|
+
try {
|
|
413
|
+
// Health check always uses HTTP on 8080 for simplicity
|
|
414
|
+
const checkCmd = `ssh ${host} 'curl -s http://localhost:8080/health/ready 2>/dev/null'`;
|
|
415
|
+
const health = await execSync(checkCmd);
|
|
416
|
+
if (health.includes('UP')) {
|
|
417
|
+
ready = true;
|
|
418
|
+
}
|
|
419
|
+
} catch (err) {
|
|
420
|
+
// Still waiting
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (!ready) {
|
|
424
|
+
attempts++;
|
|
425
|
+
process.stdout.write('.');
|
|
426
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (ready) {
|
|
431
|
+
log('\n\n✓ Keycloak is ready!', 'green');
|
|
432
|
+
log('\nAccess Keycloak:', 'bright');
|
|
433
|
+
if (useHttps) {
|
|
434
|
+
log(` Admin Console: https://${hostname}:8443`, 'yellow');
|
|
435
|
+
log(` (self-signed cert - use -k with curl or add to browser exceptions)`, 'yellow');
|
|
436
|
+
} else {
|
|
437
|
+
log(` Admin Console: http://${hostname}:8080`, 'yellow');
|
|
438
|
+
}
|
|
439
|
+
log(' Credentials: admin / admin', 'yellow');
|
|
440
|
+
log(`\nDeployed at: ${host}:${deployPath}`, 'yellow');
|
|
441
|
+
} else {
|
|
442
|
+
log('\n\n⚠ Keycloak is taking longer than expected. Logs:', 'yellow');
|
|
443
|
+
log(` ssh ${host} 'cd ${deployPath} && ${composeCmd} logs -f'`, 'yellow');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
} catch (err) {
|
|
447
|
+
log(`\n✗ Error during remote deployment: ${err.message}`, 'red');
|
|
448
|
+
throw err;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function main() {
|
|
453
|
+
try {
|
|
454
|
+
const deployLocation = await askDeploymentLocation();
|
|
455
|
+
const useHttps = await askHttpsSetup();
|
|
456
|
+
|
|
457
|
+
let certPath = null;
|
|
458
|
+
if (useHttps) {
|
|
459
|
+
// For remote deployments, certificate path doesn't need to exist locally yet
|
|
460
|
+
const isRemote = deployLocation === 'remote';
|
|
461
|
+
certPath = await askCertificatePath(isRemote);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (deployLocation === 'local') {
|
|
465
|
+
await deployLocal(useHttps, certPath);
|
|
466
|
+
const protocol = useHttps ? 'https' : 'http';
|
|
467
|
+
const port = useHttps ? 8443 : 8080;
|
|
468
|
+
updateTestBaseUrl(`${protocol}://localhost:${port}`);
|
|
469
|
+
} else {
|
|
470
|
+
const { host } = await askRemoteDetails();
|
|
471
|
+
|
|
472
|
+
// Extract username from host (format: user@host or just host)
|
|
473
|
+
let username = 'root';
|
|
474
|
+
if (host.includes('@')) {
|
|
475
|
+
username = host.split('@')[0];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Automatically create deployment path: /home/<username>/docker-keycloak-api-manager-test
|
|
479
|
+
const deployPath = `/home/${username}/docker-keycloak-api-manager-test`;
|
|
480
|
+
log(`\n✓ Deployment path: ${deployPath}`, 'green');
|
|
481
|
+
|
|
482
|
+
await deployRemote(host, deployPath, useHttps, certPath);
|
|
483
|
+
const protocol = useHttps ? 'https' : 'http';
|
|
484
|
+
const port = useHttps ? 8443 : 8080;
|
|
485
|
+
const hostname = host.includes('@') ? host.split('@')[1] : host;
|
|
486
|
+
updateTestBaseUrl(`${protocol}://${hostname}:${port}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
log('\n✓ Deployment complete!\n', 'green');
|
|
490
|
+
|
|
491
|
+
} catch (err) {
|
|
492
|
+
log(`\nSetup failed: ${err.message}\n`, 'red');
|
|
493
|
+
process.exit(1);
|
|
494
|
+
} finally {
|
|
495
|
+
if (rl) {
|
|
496
|
+
rl.close();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
main();
|