Address second round of PR review feedback
- Replace getConfig<T> with getMetadata/setMetadata on CliAuth, removing the unsafe type parameter in favor of returning unknown - Move updateInstanceConfig from cli-module-auth public API to CliAuth.setMetadata, removing the cross-package dependency - Rename 'config' to 'metadata' in StoredInstance and storage schemas - Add zod validation at consumer sites (cli-module-actions) for type-safe metadata access - Fix zod imports to use zod/v3 for compatibility with zod v4 - Add proper-lockfile to cli-node for metadata write locking - Refactor cli-node storage from fs-extra to node:fs - Remove @backstage/cli-module-auth dependency from cli-module-actions Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
'@backstage/cli-module-auth': patch
|
||||
---
|
||||
|
||||
Export auth helper utilities for use by other CLI modules. Added per-instance config storage with `getInstanceConfig` and `updateInstanceConfig`.
|
||||
Export auth helper utilities for use by other CLI modules.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
'@backstage/cli-node': minor
|
||||
'@backstage/cli-node': patch
|
||||
---
|
||||
|
||||
Added `CliAuth` class for managing CLI authentication state. This provides a class-based API with a static `create` method that resolves the currently selected (or explicitly named) auth instance, transparently refreshes expired access tokens, and exposes helpers for other CLI modules to authenticate with a Backstage backend.
|
||||
|
||||
@@ -33,10 +33,10 @@
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/cli-module-auth": "workspace:^",
|
||||
"@backstage/cli-node": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"cleye": "^2.3.0"
|
||||
"cleye": "^2.3.0",
|
||||
"zod": "^3.25.76 || ^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import { CliAuth, type CliCommandContext } from '@backstage/cli-node';
|
||||
import { updateInstanceConfig } from '@backstage/cli-module-auth';
|
||||
import { z } from 'zod/v3';
|
||||
|
||||
const pluginSourcesSchema = z.array(z.string()).default([]);
|
||||
|
||||
export default async ({ args, info }: CliCommandContext) => {
|
||||
const parsed = cli(
|
||||
@@ -31,7 +33,9 @@ export default async ({ args, info }: CliCommandContext) => {
|
||||
const pluginId = parsed._[0];
|
||||
|
||||
const auth = await CliAuth.create();
|
||||
const existing = (await auth.getConfig<string[]>('pluginSources')) ?? [];
|
||||
const existing = pluginSourcesSchema.parse(
|
||||
await auth.getMetadata('pluginSources'),
|
||||
);
|
||||
|
||||
if (existing.includes(pluginId)) {
|
||||
process.stderr.write(
|
||||
@@ -40,10 +44,7 @@ export default async ({ args, info }: CliCommandContext) => {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateInstanceConfig(auth.getInstanceName(), 'pluginSources', [
|
||||
...existing,
|
||||
pluginId,
|
||||
]);
|
||||
await auth.setMetadata('pluginSources', [...existing, pluginId]);
|
||||
|
||||
process.stdout.write(`Added plugin source "${pluginId}".\n`);
|
||||
};
|
||||
|
||||
@@ -16,12 +16,17 @@
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import { CliAuth, type CliCommandContext } from '@backstage/cli-node';
|
||||
import { z } from 'zod/v3';
|
||||
|
||||
const pluginSourcesSchema = z.array(z.string()).default([]);
|
||||
|
||||
export default async ({ args, info }: CliCommandContext) => {
|
||||
cli({ help: info }, undefined, args);
|
||||
|
||||
const auth = await CliAuth.create();
|
||||
const sources = (await auth.getConfig<string[]>('pluginSources')) ?? [];
|
||||
const sources = pluginSourcesSchema.parse(
|
||||
await auth.getMetadata('pluginSources'),
|
||||
);
|
||||
|
||||
if (!sources.length) {
|
||||
process.stderr.write('No plugin sources configured.\n');
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import { CliAuth, type CliCommandContext } from '@backstage/cli-node';
|
||||
import { updateInstanceConfig } from '@backstage/cli-module-auth';
|
||||
import { z } from 'zod/v3';
|
||||
|
||||
const pluginSourcesSchema = z.array(z.string()).default([]);
|
||||
|
||||
export default async ({ args, info }: CliCommandContext) => {
|
||||
const parsed = cli(
|
||||
@@ -31,15 +33,16 @@ export default async ({ args, info }: CliCommandContext) => {
|
||||
const pluginId = parsed._[0];
|
||||
|
||||
const auth = await CliAuth.create();
|
||||
const existing = (await auth.getConfig<string[]>('pluginSources')) ?? [];
|
||||
const existing = pluginSourcesSchema.parse(
|
||||
await auth.getMetadata('pluginSources'),
|
||||
);
|
||||
|
||||
if (!existing.includes(pluginId)) {
|
||||
process.stderr.write(`Plugin source "${pluginId}" is not configured.\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
await updateInstanceConfig(
|
||||
auth.getInstanceName(),
|
||||
await auth.setMetadata(
|
||||
'pluginSources',
|
||||
existing.filter(s => s !== pluginId),
|
||||
);
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('resolveAuth', () => {
|
||||
getInstanceName: jest.fn().mockReturnValue('production'),
|
||||
getBaseUrl: jest.fn().mockReturnValue('https://backstage.example.com'),
|
||||
getAccessToken: jest.fn().mockResolvedValue('test-access-token'),
|
||||
getConfig: jest.fn().mockResolvedValue(['catalog', 'scaffolder']),
|
||||
getMetadata: jest.fn().mockResolvedValue(['catalog', 'scaffolder']),
|
||||
} as unknown as CliAuth);
|
||||
|
||||
const result = await resolveAuth();
|
||||
@@ -56,7 +56,7 @@ describe('resolveAuth', () => {
|
||||
getInstanceName: jest.fn().mockReturnValue('staging'),
|
||||
getBaseUrl: jest.fn().mockReturnValue('https://staging.example.com'),
|
||||
getAccessToken: jest.fn().mockResolvedValue('test-access-token'),
|
||||
getConfig: jest.fn().mockResolvedValue([]),
|
||||
getMetadata: jest.fn().mockResolvedValue([]),
|
||||
} as unknown as CliAuth);
|
||||
|
||||
await resolveAuth('staging');
|
||||
@@ -73,7 +73,7 @@ describe('resolveAuth', () => {
|
||||
.mockRejectedValue(
|
||||
new Error('No access token found. Run "auth login" to authenticate.'),
|
||||
),
|
||||
getConfig: jest.fn().mockResolvedValue([]),
|
||||
getMetadata: jest.fn().mockResolvedValue([]),
|
||||
} as unknown as CliAuth);
|
||||
|
||||
await expect(resolveAuth()).rejects.toThrow(
|
||||
@@ -86,7 +86,7 @@ describe('resolveAuth', () => {
|
||||
getInstanceName: jest.fn().mockReturnValue('production'),
|
||||
getBaseUrl: jest.fn().mockReturnValue('https://backstage.example.com'),
|
||||
getAccessToken: jest.fn().mockResolvedValue('test-access-token'),
|
||||
getConfig: jest.fn().mockResolvedValue(undefined),
|
||||
getMetadata: jest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as CliAuth);
|
||||
|
||||
const result = await resolveAuth();
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
|
||||
import { CliAuth } from '@backstage/cli-node';
|
||||
import { z } from 'zod/v3';
|
||||
|
||||
const pluginSourcesSchema = z.array(z.string()).default([]);
|
||||
|
||||
export async function resolveAuth(instanceFlag?: string): Promise<{
|
||||
baseUrl: string;
|
||||
@@ -24,7 +27,9 @@ export async function resolveAuth(instanceFlag?: string): Promise<{
|
||||
}> {
|
||||
const auth = await CliAuth.create({ instanceName: instanceFlag });
|
||||
const accessToken = await auth.getAccessToken();
|
||||
const pluginSources = (await auth.getConfig<string[]>('pluginSources')) ?? [];
|
||||
const pluginSources = pluginSourcesSchema.parse(
|
||||
await auth.getMetadata('pluginSources'),
|
||||
);
|
||||
|
||||
return {
|
||||
baseUrl: auth.getBaseUrl(),
|
||||
|
||||
@@ -9,12 +9,5 @@ import { CliModule } from '@backstage/cli-node';
|
||||
const _default: CliModule;
|
||||
export default _default;
|
||||
|
||||
// @public (undocumented)
|
||||
export function updateInstanceConfig(
|
||||
instanceName: string,
|
||||
key: string,
|
||||
value: unknown,
|
||||
): Promise<void>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
@@ -52,5 +52,3 @@ export default createCliModule({
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export { updateInstanceConfig } from './lib/storage';
|
||||
|
||||
@@ -22,8 +22,8 @@ import {
|
||||
getAllInstances,
|
||||
getSelectedInstance,
|
||||
getInstanceByName,
|
||||
getInstanceConfig,
|
||||
updateInstanceConfig,
|
||||
getInstanceMetadata,
|
||||
updateInstanceMetadata,
|
||||
upsertInstance,
|
||||
removeInstance,
|
||||
setSelectedInstance,
|
||||
@@ -359,65 +359,65 @@ describe('storage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInstanceConfig', () => {
|
||||
it('should return undefined when no config set', async () => {
|
||||
describe('getInstanceMetadata', () => {
|
||||
it('should return undefined when no metadata set', async () => {
|
||||
await upsertInstance(mockInstance1);
|
||||
|
||||
const result = await getInstanceConfig('production', 'someKey');
|
||||
const result = await getInstanceMetadata('production', 'someKey');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return config value for a key', async () => {
|
||||
it('should return metadata value for a key', async () => {
|
||||
await upsertInstance(mockInstance1);
|
||||
await updateInstanceConfig('production', 'myKey', 'myValue');
|
||||
await updateInstanceMetadata('production', 'myKey', 'myValue');
|
||||
|
||||
const result = await getInstanceConfig('production', 'myKey');
|
||||
const result = await getInstanceMetadata('production', 'myKey');
|
||||
expect(result).toBe('myValue');
|
||||
});
|
||||
|
||||
it('should throw NotFoundError for unknown instance', async () => {
|
||||
await expect(getInstanceConfig('nonexistent', 'key')).rejects.toThrow(
|
||||
await expect(getInstanceMetadata('nonexistent', 'key')).rejects.toThrow(
|
||||
NotFoundError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateInstanceConfig', () => {
|
||||
it('should set a config value', async () => {
|
||||
describe('updateInstanceMetadata', () => {
|
||||
it('should set a metadata value', async () => {
|
||||
await upsertInstance(mockInstance1);
|
||||
await updateInstanceConfig('production', 'key1', 'value1');
|
||||
await updateInstanceMetadata('production', 'key1', 'value1');
|
||||
|
||||
const result = await getInstanceConfig('production', 'key1');
|
||||
const result = await getInstanceMetadata('production', 'key1');
|
||||
expect(result).toBe('value1');
|
||||
});
|
||||
|
||||
it('should preserve existing config keys', async () => {
|
||||
it('should preserve existing metadata keys', async () => {
|
||||
await upsertInstance(mockInstance1);
|
||||
await updateInstanceConfig('production', 'key1', 'value1');
|
||||
await updateInstanceConfig('production', 'key2', 'value2');
|
||||
await updateInstanceMetadata('production', 'key1', 'value1');
|
||||
await updateInstanceMetadata('production', 'key2', 'value2');
|
||||
|
||||
const result1 = await getInstanceConfig('production', 'key1');
|
||||
const result2 = await getInstanceConfig('production', 'key2');
|
||||
const result1 = await getInstanceMetadata('production', 'key1');
|
||||
const result2 = await getInstanceMetadata('production', 'key2');
|
||||
expect(result1).toBe('value1');
|
||||
expect(result2).toBe('value2');
|
||||
});
|
||||
|
||||
it('should throw NotFoundError for unknown instance', async () => {
|
||||
await expect(
|
||||
updateInstanceConfig('nonexistent', 'key', 'value'),
|
||||
updateInstanceMetadata('nonexistent', 'key', 'value'),
|
||||
).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
it('should remove instance along with its config', async () => {
|
||||
it('should remove instance along with its metadata', async () => {
|
||||
await upsertInstance(mockInstance1);
|
||||
await updateInstanceConfig('production', 'key1', 'value1');
|
||||
await updateInstanceMetadata('production', 'key1', 'value1');
|
||||
await removeInstance('production');
|
||||
|
||||
const { instances } = await getAllInstances();
|
||||
expect(instances.find(i => i.name === 'production')).toBeUndefined();
|
||||
|
||||
await upsertInstance(mockInstance1);
|
||||
const result = await getInstanceConfig('production', 'key1');
|
||||
const result = await getInstanceMetadata('production', 'key1');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ export type StoredInstance = {
|
||||
issuedAt: number;
|
||||
accessTokenExpiresAt: number;
|
||||
selected?: boolean;
|
||||
config?: Record<string, unknown>;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const METADATA_FILE = 'auth-instances.yaml';
|
||||
@@ -46,7 +46,7 @@ const storedInstanceSchema = z.object({
|
||||
issuedAt: z.number().int().nonnegative(),
|
||||
accessTokenExpiresAt: z.number().int().nonnegative(),
|
||||
selected: z.boolean().optional(),
|
||||
config: z.record(z.string(), z.unknown()).optional(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
});
|
||||
|
||||
const authYamlSchema = z.object({
|
||||
@@ -168,16 +168,15 @@ export async function setSelectedInstance(name: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function getInstanceConfig<T = unknown>(
|
||||
export async function getInstanceMetadata(
|
||||
instanceName: string,
|
||||
key: string,
|
||||
): Promise<T | undefined> {
|
||||
): Promise<unknown> {
|
||||
const instance = await getInstanceByName(instanceName);
|
||||
return instance.config?.[key] as T | undefined;
|
||||
return instance.metadata?.[key];
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export async function updateInstanceConfig(
|
||||
export async function updateInstanceMetadata(
|
||||
instanceName: string,
|
||||
key: string,
|
||||
value: unknown,
|
||||
@@ -190,7 +189,7 @@ export async function updateInstanceConfig(
|
||||
}
|
||||
data.instances[idx] = {
|
||||
...data.instances[idx],
|
||||
config: { ...data.instances[idx].config, [key]: value },
|
||||
metadata: { ...data.instances[idx].metadata, [key]: value },
|
||||
};
|
||||
await writeAll(data);
|
||||
});
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"commander": "^12.0.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"pirates": "^4.0.6",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"semver": "^7.5.3",
|
||||
"yaml": "^2.0.0",
|
||||
"zod": "^3.25.76 || ^4.0.0"
|
||||
@@ -50,6 +51,7 @@
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@types/proper-lockfile": "^4",
|
||||
"@types/yarnpkg__lockfile": "^1.1.4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
@@ -91,8 +91,9 @@ export class CliAuth {
|
||||
static create(options?: CliAuthCreateOptions): Promise<CliAuth>;
|
||||
getAccessToken(): Promise<string>;
|
||||
getBaseUrl(): string;
|
||||
getConfig<T = unknown>(key: string): Promise<T | undefined>;
|
||||
getInstanceName(): string;
|
||||
getMetadata(key: string): Promise<unknown>;
|
||||
setMetadata(key: string, value: unknown): Promise<void>;
|
||||
}
|
||||
|
||||
// @public
|
||||
|
||||
@@ -180,30 +180,43 @@ describe('CliAuth', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfig', () => {
|
||||
it('returns a config value from the instance', async () => {
|
||||
mockStorage.getInstanceConfig.mockResolvedValue([
|
||||
describe('getMetadata / setMetadata', () => {
|
||||
it('returns a metadata value from the instance', async () => {
|
||||
mockStorage.getInstanceMetadata.mockResolvedValue([
|
||||
'catalog',
|
||||
'scaffolder',
|
||||
]);
|
||||
|
||||
const auth = await CliAuth.create();
|
||||
const sources = await auth.getConfig<string[]>('pluginSources');
|
||||
const sources = await auth.getMetadata('pluginSources');
|
||||
|
||||
expect(sources).toEqual(['catalog', 'scaffolder']);
|
||||
expect(mockStorage.getInstanceConfig).toHaveBeenCalledWith(
|
||||
expect(mockStorage.getInstanceMetadata).toHaveBeenCalledWith(
|
||||
'production',
|
||||
'pluginSources',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns undefined for missing config keys', async () => {
|
||||
mockStorage.getInstanceConfig.mockResolvedValue(undefined);
|
||||
it('returns undefined for missing metadata keys', async () => {
|
||||
mockStorage.getInstanceMetadata.mockResolvedValue(undefined);
|
||||
|
||||
const auth = await CliAuth.create();
|
||||
const value = await auth.getConfig('nonexistent');
|
||||
const value = await auth.getMetadata('nonexistent');
|
||||
|
||||
expect(value).toBeUndefined();
|
||||
});
|
||||
|
||||
it('writes a metadata value to the instance store', async () => {
|
||||
mockStorage.updateInstanceMetadata.mockResolvedValue(undefined);
|
||||
|
||||
const auth = await CliAuth.create();
|
||||
await auth.setMetadata('pluginSources', ['catalog']);
|
||||
|
||||
expect(mockStorage.updateInstanceMetadata).toHaveBeenCalledWith(
|
||||
'production',
|
||||
'pluginSources',
|
||||
['catalog'],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,13 +17,14 @@
|
||||
import {
|
||||
type StoredInstance,
|
||||
getSelectedInstance,
|
||||
getInstanceConfig,
|
||||
getInstanceMetadata,
|
||||
updateInstanceMetadata,
|
||||
accessTokenNeedsRefresh,
|
||||
} from './storage';
|
||||
import { getSecretStore, type SecretStore } from './secretStore';
|
||||
import { getAuthInstanceService } from './authIdentifiers';
|
||||
import { httpJson } from './httpJson';
|
||||
import { z } from 'zod';
|
||||
import { z } from 'zod/v3';
|
||||
|
||||
const TokenResponseSchema = z.object({
|
||||
access_token: z.string().min(1),
|
||||
@@ -104,11 +105,18 @@ export class CliAuth {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a per-instance configuration value previously stored by the
|
||||
* Reads a per-instance metadata value previously stored by the
|
||||
* auth module (e.g. `pluginSources`).
|
||||
*/
|
||||
async getConfig<T = unknown>(key: string): Promise<T | undefined> {
|
||||
return getInstanceConfig<T>(this.#instance.name, key);
|
||||
async getMetadata(key: string): Promise<unknown> {
|
||||
return getInstanceMetadata(this.#instance.name, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a per-instance metadata value to the on-disk instance store.
|
||||
*/
|
||||
async setMetadata(key: string, value: unknown): Promise<void> {
|
||||
return updateInstanceMetadata(this.#instance.name, key, value);
|
||||
}
|
||||
|
||||
async #refreshAccessToken(): Promise<void> {
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
*/
|
||||
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import fs from 'fs-extra';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import lockfile from 'proper-lockfile';
|
||||
import YAML from 'yaml';
|
||||
import { z } from 'zod';
|
||||
import { z } from 'zod/v3';
|
||||
|
||||
const METADATA_FILE = 'auth-instances.yaml';
|
||||
|
||||
@@ -35,10 +36,13 @@ const storedInstanceSchema = z.object({
|
||||
issuedAt: z.number().int().nonnegative(),
|
||||
accessTokenExpiresAt: z.number().int().nonnegative(),
|
||||
selected: z.boolean().optional(),
|
||||
config: z.record(z.string(), z.unknown()).optional(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
});
|
||||
|
||||
const authYamlSchema = z.object({
|
||||
instances: z.array(storedInstanceSchema).default([]),
|
||||
});
|
||||
|
||||
/** @public */
|
||||
export type StoredInstance = {
|
||||
name: string;
|
||||
baseUrl: string;
|
||||
@@ -46,12 +50,17 @@ export type StoredInstance = {
|
||||
issuedAt: number;
|
||||
accessTokenExpiresAt: number;
|
||||
selected?: boolean;
|
||||
config?: Record<string, unknown>;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const authYamlSchema = z.object({
|
||||
instances: z.array(storedInstanceSchema).default([]),
|
||||
});
|
||||
async function pathExists(p: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.stat(p);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function getMetadataFilePath(): string {
|
||||
@@ -67,7 +76,7 @@ export function getMetadataFilePath(): string {
|
||||
/** @internal */
|
||||
export async function readAll(): Promise<{ instances: StoredInstance[] }> {
|
||||
const file = getMetadataFilePath();
|
||||
if (!(await fs.pathExists(file))) {
|
||||
if (!(await pathExists(file))) {
|
||||
return { instances: [] };
|
||||
}
|
||||
const text = await fs.readFile(file, 'utf8');
|
||||
@@ -86,6 +95,29 @@ export async function readAll(): Promise<{ instances: StoredInstance[] }> {
|
||||
}
|
||||
}
|
||||
|
||||
async function writeAll(data: { instances: StoredInstance[] }): Promise<void> {
|
||||
const file = getMetadataFilePath();
|
||||
await fs.mkdir(path.dirname(file), { recursive: true });
|
||||
const yaml = YAML.stringify(authYamlSchema.parse(data), { indentSeq: false });
|
||||
await fs.writeFile(file, yaml, { encoding: 'utf8', mode: 0o600 });
|
||||
}
|
||||
|
||||
async function withMetadataLock<T>(fn: () => Promise<T>): Promise<T> {
|
||||
const file = getMetadataFilePath();
|
||||
await fs.mkdir(path.dirname(file), { recursive: true });
|
||||
if (!(await pathExists(file))) {
|
||||
await fs.writeFile(file, '', { encoding: 'utf8', mode: 0o600 });
|
||||
}
|
||||
const release = await lockfile.lock(file, {
|
||||
retries: { retries: 5, factor: 1.5, minTimeout: 100, maxTimeout: 1000 },
|
||||
});
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function getAllInstances(): Promise<{
|
||||
instances: StoredInstance[];
|
||||
@@ -129,12 +161,32 @@ export async function getInstanceByName(name: string): Promise<StoredInstance> {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function getInstanceConfig<T = unknown>(
|
||||
export async function getInstanceMetadata(
|
||||
instanceName: string,
|
||||
key: string,
|
||||
): Promise<T | undefined> {
|
||||
): Promise<unknown> {
|
||||
const instance = await getInstanceByName(instanceName);
|
||||
return instance.config?.[key] as T | undefined;
|
||||
return instance.metadata?.[key];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function updateInstanceMetadata(
|
||||
instanceName: string,
|
||||
key: string,
|
||||
value: unknown,
|
||||
): Promise<void> {
|
||||
return withMetadataLock(async () => {
|
||||
const data = await readAll();
|
||||
const idx = data.instances.findIndex(i => i.name === instanceName);
|
||||
if (idx === -1) {
|
||||
throw new NotFoundError(`Instance '${instanceName}' not found`);
|
||||
}
|
||||
data.instances[idx] = {
|
||||
...data.instances[idx],
|
||||
metadata: { ...data.instances[idx].metadata, [key]: value },
|
||||
};
|
||||
await writeAll(data);
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
||||
@@ -2828,10 +2828,10 @@ __metadata:
|
||||
dependencies:
|
||||
"@backstage/backend-test-utils": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/cli-module-auth": "workspace:^"
|
||||
"@backstage/cli-node": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
cleye: "npm:^2.3.0"
|
||||
zod: "npm:^3.25.76 || ^4.0.0"
|
||||
bin:
|
||||
cli-module-actions: bin/backstage-cli-module-actions
|
||||
languageName: unknown
|
||||
@@ -3154,6 +3154,7 @@ __metadata:
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
"@manypkg/get-packages": "npm:^1.1.3"
|
||||
"@types/proper-lockfile": "npm:^4"
|
||||
"@types/yarnpkg__lockfile": "npm:^1.1.4"
|
||||
"@yarnpkg/lockfile": "npm:^1.1.0"
|
||||
"@yarnpkg/parsers": "npm:^3.0.0"
|
||||
@@ -3162,6 +3163,7 @@ __metadata:
|
||||
fs-extra: "npm:^11.2.0"
|
||||
keytar: "npm:^7.9.0"
|
||||
pirates: "npm:^4.0.6"
|
||||
proper-lockfile: "npm:^4.1.2"
|
||||
semver: "npm:^7.5.3"
|
||||
yaml: "npm:^2.0.0"
|
||||
zod: "npm:^3.25.76 || ^4.0.0"
|
||||
|
||||
Reference in New Issue
Block a user