Skip to content

Commit bec49fe

Browse files
chore: new release with minimal changes
1 parent badb16f commit bec49fe

File tree

4 files changed

+122
-6
lines changed

4 files changed

+122
-6
lines changed

docs/commands.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,8 @@ Commands:
280280
adc Setup Application Default Credentials
281281
project <projectId> Set active project
282282
set-service-account [options] Configure service account credentials
283+
list-keys List available service account keys
284+
select-key [options] Interactively select a service account key
283285
help [command] display help for command
284286
```
285287

@@ -325,3 +327,4 @@ Options:
325327
--json Output status in JSON format
326328
-h, --help display help for command
327329
```
330+

src/commands/auth.ts

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { SYSTEM_PATHS, USER_PATHS, ENV_VARS } from '../system/paths.js';
66
import { runPs } from '../system/powershell.js';
77
import fs from 'fs-extra';
88
import path from 'path';
9+
import inquirer from 'inquirer';
910

1011
export const authCommand = new Command('auth')
1112
.description('Manage authentication and credentials');
@@ -68,8 +69,9 @@ authCommand.command('set-service-account')
6869
.description('Configure service account credentials')
6970
.requiredOption('-f, --file <path>', 'Path to service account JSON key')
7071
.option('-s, --scope <scope>', 'Scope (Machine or User)', 'User')
72+
.option('--no-rename', 'Do not rename the file to service account name')
7173
.action(async (options) => {
72-
const { file, scope } = options;
74+
const { file, scope, rename } = options;
7375
if (!['Machine', 'User'].includes(scope)) {
7476
logger.error('Scope must be either Machine or User');
7577
process.exit(1);
@@ -81,10 +83,21 @@ authCommand.command('set-service-account')
8183
process.exit(1);
8284
}
8385

86+
const keyData = await fs.readJson(file);
87+
if (keyData.type !== 'service_account') {
88+
logger.error('Invalid service account key file.');
89+
process.exit(1);
90+
}
91+
8492
const paths = scope === 'Machine' ? SYSTEM_PATHS : USER_PATHS;
8593
await fs.ensureDir(paths.SECRETS);
8694

87-
const fileName = path.basename(file);
95+
let fileName = path.basename(file);
96+
if (rename && keyData.client_email) {
97+
const saName = keyData.client_email.split('@')[0];
98+
fileName = `${saName}.json`;
99+
}
100+
88101
const dest = path.join(paths.SECRETS, fileName);
89102

90103
await fs.copy(file, dest, { overwrite: true });
@@ -105,3 +118,69 @@ authCommand.command('set-service-account')
105118
process.exit(1);
106119
}
107120
});
121+
122+
authCommand.command('list-keys')
123+
.description('List available service account keys')
124+
.action(async () => {
125+
try {
126+
const userKeys = (await fs.pathExists(USER_PATHS.SECRETS)) ? await fs.readdir(USER_PATHS.SECRETS) : [];
127+
const systemKeys = (await fs.pathExists(SYSTEM_PATHS.SECRETS)) ? await fs.readdir(SYSTEM_PATHS.SECRETS) : [];
128+
129+
logger.info('Available Service Account Keys:');
130+
131+
if (userKeys.length > 0) {
132+
logger.info('\n User Scope:');
133+
userKeys.filter(f => f.endsWith('.json')).forEach(f => logger.info(` - ${f}`));
134+
}
135+
136+
if (systemKeys.length > 0) {
137+
logger.info('\n Machine Scope:');
138+
systemKeys.filter(f => f.endsWith('.json')).forEach(f => logger.info(` - ${f}`));
139+
}
140+
141+
if (userKeys.length === 0 && systemKeys.length === 0) {
142+
logger.info(' No keys found.');
143+
}
144+
} catch (error) {
145+
logger.error('Failed to list keys', error);
146+
}
147+
});
148+
149+
authCommand.command('select-key')
150+
.description('Interactively select a service account key')
151+
.option('-s, --scope <scope>', 'Scope (Machine or User)', 'User')
152+
.action(async (options) => {
153+
const { scope } = options;
154+
const paths = scope === 'Machine' ? SYSTEM_PATHS : USER_PATHS;
155+
156+
try {
157+
if (!await fs.pathExists(paths.SECRETS)) {
158+
logger.error(`No keys found in ${scope} scope.`);
159+
return;
160+
}
161+
162+
const keys = (await fs.readdir(paths.SECRETS)).filter(f => f.endsWith('.json'));
163+
if (keys.length === 0) {
164+
logger.error(`No keys found in ${scope} scope.`);
165+
return;
166+
}
167+
168+
const { selectedKey } = await inquirer.prompt([
169+
{
170+
type: 'list',
171+
name: 'selectedKey',
172+
message: `Select a service account key (${scope} scope):`,
173+
choices: keys
174+
}
175+
]);
176+
177+
const fullPath = path.join(paths.SECRETS, selectedKey);
178+
await setEnv(ENV_VARS.GOOGLE_CREDS, fullPath, scope);
179+
logger.info(`Successfully selected ${selectedKey} and updated ${ENV_VARS.GOOGLE_CREDS}`);
180+
181+
} catch (error) {
182+
logger.error('Failed to select key', error);
183+
process.exit(1);
184+
}
185+
});
186+

src/commands/service.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@ serviceCommand.command('install')
99
.description('Install the Windows Service')
1010
.requiredOption('-i, --instance <connectionName>', 'Instance connection name')
1111
.option('-p, --port <port>', 'Port number', '5432')
12+
.option('-c, --credentials <path>', 'Path to service account JSON key')
1213
.action(async (options) => {
1314
try {
14-
await installService(options.instance, parseInt(options.port), []);
15+
const extraArgs = [];
16+
if (options.credentials) {
17+
extraArgs.push(`--credentials-file "${options.credentials}"`);
18+
}
19+
await installService(options.instance, parseInt(options.port), extraArgs);
1520
logger.info('Service installed successfully.');
1621
} catch (error) {
1722
logger.error('Failed to install service', error);
@@ -23,9 +28,14 @@ serviceCommand.command('configure')
2328
.description('Update Service Configuration')
2429
.requiredOption('-i, --instance <connectionName>', 'Instance connection name')
2530
.option('-p, --port <port>', 'Port number', '5432')
31+
.option('-c, --credentials <path>', 'Path to service account JSON key')
2632
.action(async (options) => {
2733
try {
28-
await updateServiceBinPath(options.instance, parseInt(options.port), []);
34+
const extraArgs = [];
35+
if (options.credentials) {
36+
extraArgs.push(`--credentials-file "${options.credentials}"`);
37+
}
38+
await updateServiceBinPath(options.instance, parseInt(options.port), extraArgs);
2939
logger.info('Service configured successfully.');
3040
} catch (error) {
3141
logger.error('Failed to configure service', error);

src/system/service.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { runPs, isAdmin } from './powershell.js';
2-
import { SERVICE_NAME, SYSTEM_PATHS } from './paths.js';
1+
import { runPs, isAdmin, getEnvVar } from './powershell.js';
2+
import { SERVICE_NAME, SYSTEM_PATHS, ENV_VARS } from './paths.js';
33
import { logger } from '../core/logger.js';
44

55
export async function isServiceInstalled(): Promise<boolean> {
@@ -36,6 +36,22 @@ export async function installService(instance: string, port: number = 5432, extr
3636

3737
logger.info(`Installing service ${SERVICE_NAME}...`);
3838

39+
// Auto-detect credentials if not provided in extraArgs
40+
if (!extraArgs.some(arg => arg.includes('--credentials-file'))) {
41+
const machineCreds = await getEnvVar(ENV_VARS.GOOGLE_CREDS, 'Machine');
42+
if (machineCreds) {
43+
logger.info(`Auto-detected machine-level credentials: ${machineCreds}`);
44+
extraArgs.push(`--credentials-file "${machineCreds}"`);
45+
} else {
46+
const userCreds = await getEnvVar(ENV_VARS.GOOGLE_CREDS, 'User');
47+
if (userCreds) {
48+
logger.warn('Detected user-level credentials. Services running as LocalSystem may not be able to access them.');
49+
logger.warn('Recommendation: Use "cloudsqlctl auth set-service-account --file <path> --scope Machine"');
50+
extraArgs.push(`--credentials-file "${userCreds}"`);
51+
}
52+
}
53+
}
54+
3955
const binPath = buildServiceBinPath(SYSTEM_PATHS.PROXY_EXE, instance, port, extraArgs);
4056

4157
// Use New-Service with single-quoted BinaryPathName
@@ -47,6 +63,14 @@ export async function updateServiceBinPath(instance: string, port: number = 5432
4763
throw new Error('Admin privileges required to update service configuration.');
4864
}
4965

66+
// Auto-detect credentials if not provided in extraArgs
67+
if (!extraArgs.some(arg => arg.includes('--credentials-file'))) {
68+
const machineCreds = await getEnvVar(ENV_VARS.GOOGLE_CREDS, 'Machine');
69+
if (machineCreds) {
70+
extraArgs.push(`--credentials-file "${machineCreds}"`);
71+
}
72+
}
73+
5074
const binPath = buildServiceBinPath(SYSTEM_PATHS.PROXY_EXE, instance, port, extraArgs);
5175

5276
// Use CIM/WMI to update service path

0 commit comments

Comments
 (0)