Skip to content

Commit 8c1d188

Browse files
CptSchnitzCopilot
andauthored
feat: bundle opa version and auth-cron logs [MAPCO-7868] (#66)
* feat(auth-core): added opaVersion column * feat(auth-manager): support the opa version bundle property * feat(auth-bundler): added opaVersion to bundle * fix(auth-bundler): added missing export to opa version command * feat(auth-cron): added opa version to bundle * refactor(auth-cron): added more logs * test(auth-cron): update packages/auth-cron/tests/job.spec.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 4d517d8 commit 8c1d188

20 files changed

Lines changed: 298 additions & 34 deletions

File tree

package-lock.json

Lines changed: 88 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/auth-bundler/src/db.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { BundleContent, BundleContentVersions } from './types';
44
import { extractNameAndVersion } from './util';
55
import { logger } from './logger';
66
import { ConnectionNotInitializedError, KeyNotFoundError } from './errors';
7+
import { getVersionCommand } from './opa';
78

89
/**
910
* This class handles all the database interactions required to creating a bundle.
@@ -60,6 +61,7 @@ export class BundleDatabase {
6061
connections: versions.connections,
6162
keyVersion: versions.keyVersion,
6263
hash,
64+
opaVersion: await getVersionCommand(),
6365
};
6466

6567
const res = await this.bundleRepository.save(bundle);

packages/auth-bundler/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export * from './errors';
1818
export { setLogger } from './logger';
1919
export { BundleDatabase } from './db';
2020
export * from './types';
21+
export { getVersionCommand } from './opa';
2122

2223
/**
2324
* This function creates an opa bundle tarball from the given content

packages/auth-bundler/src/opa.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,28 @@ export async function createBundleCommand(filesPath: string, bundlePath: string)
7979
}
8080
return [false, res.stdout];
8181
}
82+
83+
/**
84+
* Gets the OPA version from the CLI
85+
* @returns The OPA version string (e.g., "0.52.0")
86+
* @see {@link https://www.openpolicyagent.org/docs/latest/cli/#opa-version}
87+
* @ignore
88+
*/
89+
export async function getVersionCommand(): Promise<string> {
90+
const res = await execa('opa', ['version']);
91+
92+
// Extract version from first line: "Version: X.Y.Z"
93+
const versionLine = res.stdout.split('\n')[0];
94+
95+
if (!versionLine) {
96+
throw new Error('Unable to read OPA version output');
97+
}
98+
99+
const versionMatch = versionLine.match(/Version:\s*(\d+\.\d+\.\d+)/);
100+
101+
if (!versionMatch || !versionMatch[1]) {
102+
throw new Error('Unable to parse OPA version from output');
103+
}
104+
105+
return versionMatch[1];
106+
}

packages/auth-bundler/tests/db.spec.ts

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
/// <reference types="jest-extended" />
22

3-
import { Asset, Connection, Environment, Key, createConnectionOptions } from '@map-colonies/auth-core';
3+
import { Asset, Bundle, Connection, Environment, Key, createConnectionOptions } from '@map-colonies/auth-core';
44
import { DataSource } from 'typeorm';
55
import { ConnectionNotInitializedError } from '@src';
66
import { BundleDatabase } from '@src/db';
7+
import * as execa from '@src/wrappers/execa';
78
import { getMockKeys } from './utils/key';
89
import { getFakeAsset } from './utils/asset';
910
import { getFakeConnection } from './utils/connection';
1011
import { getConfig, initConfig } from './helpers/config';
1112

13+
jest.mock('../src/wrappers/execa', () => {
14+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
15+
return {
16+
// eslint-disable-next-line @typescript-eslint/naming-convention
17+
__esModule: true,
18+
...jest.requireActual('../src/wrappers/execa'),
19+
};
20+
});
21+
22+
type ExecaChildProcess = Awaited<ReturnType<(typeof execa)['execa']>>;
23+
1224
describe('db.ts', function () {
1325
let dataSource: DataSource;
1426
const asset = { ...getFakeAsset(), environment: [Environment.PRODUCTION], name: 'aviaviavi' };
@@ -57,45 +69,58 @@ describe('db.ts', function () {
5769

5870
describe('#saveBundle', function () {
5971
it('should save the bundle to the db', async function () {
72+
const VERSION_OUTPUT = 'Version: 0.52.0\nBuild Commit: 8d2c137662560cac83d9cf24cbdaecc934910333\nBuild Timestamp: 2023-04-27T17:57:23Z';
73+
jest.spyOn(execa, 'execa').mockResolvedValue({ stdout: VERSION_OUTPUT } as ExecaChildProcess);
74+
6075
const db = new BundleDatabase(dataSource);
6176

6277
const res = await db.saveBundle({ assets: [], connections: [], environment: Environment.PRODUCTION, keyVersion: 3 }, 'xdxd');
6378

6479
expect(res).toBeGreaterThan(0);
80+
const bundle = await dataSource.getRepository(Bundle).findOneByOrFail({ id: res });
81+
82+
expect(bundle).toMatchObject({
83+
environment: Environment.PRODUCTION,
84+
keyVersion: 3,
85+
opaVersion: '0.52.0',
86+
hash: 'xdxd',
87+
assets: [],
88+
connections: [],
89+
});
6590
});
66-
});
6791

68-
describe('#getLatestVersions', function () {
69-
it('should fetch the latest versions from the database', async function () {
70-
const db = new BundleDatabase(dataSource);
92+
describe('#getLatestVersions', function () {
93+
it('should fetch the latest versions from the database', async function () {
94+
const db = new BundleDatabase(dataSource);
7195

72-
const { assets, connections, keyVersion } = await db.getLatestVersions(Environment.PRODUCTION);
96+
const { assets, connections, keyVersion } = await db.getLatestVersions(Environment.PRODUCTION);
7397

74-
expect(keyVersion).toBe(1);
98+
expect(keyVersion).toBe(1);
7599

76-
const asset = assets.filter((a) => a.name === 'aviaviavi');
77-
expect(asset).toHaveLength(1);
78-
expect(asset[0]).toHaveProperty('version', 2);
100+
const asset = assets.filter((a) => a.name === 'aviaviavi');
101+
expect(asset).toHaveLength(1);
102+
expect(asset[0]).toHaveProperty('version', 2);
79103

80-
const connection = connections.filter((c) => c.name === 'xd');
81-
expect(connection).toHaveLength(1);
82-
expect(connection[0]).toHaveProperty('version', 2);
104+
const connection = connections.filter((c) => c.name === 'xd');
105+
expect(connection).toHaveLength(1);
106+
expect(connection[0]).toHaveProperty('version', 2);
107+
});
83108
});
84-
});
85109

86-
describe('#getBundleFromVersions', function () {
87-
it('should fetch the bundle content based on the versions from the database', async function () {
88-
const db = new BundleDatabase(dataSource);
110+
describe('#getBundleFromVersions', function () {
111+
it('should fetch the bundle content based on the versions from the database', async function () {
112+
const db = new BundleDatabase(dataSource);
89113

90-
const { assets } = await db.getBundleFromVersions({
91-
environment: Environment.PRODUCTION,
92-
assets: [{ name: 'aviaviavi', version: 1 }],
93-
connections: [],
94-
keyVersion: 1,
95-
});
114+
const { assets } = await db.getBundleFromVersions({
115+
environment: Environment.PRODUCTION,
116+
assets: [{ name: 'aviaviavi', version: 1 }],
117+
connections: [],
118+
keyVersion: 1,
119+
});
96120

97-
expect(assets).toSatisfyAll<Asset>((a) => a.environment.includes(Environment.PRODUCTION));
98-
expect(assets[0]).toHaveProperty('version', 1);
121+
expect(assets).toSatisfyAll<Asset>((a) => a.environment.includes(Environment.PRODUCTION));
122+
expect(assets[0]).toHaveProperty('version', 1);
123+
});
99124
});
100125
});
101126
});

packages/auth-bundler/tests/opa.spec.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { tmpdir } from 'node:os';
22
import path from 'node:path';
33
import { mkdir, writeFile } from 'node:fs/promises';
44
import * as execa from '@src/wrappers/execa';
5-
import { checkFilesCommand, createBundleCommand, testCommand, testCoverageCommand, validateBinaryExistCommand } from '@src/opa';
5+
import { checkFilesCommand, createBundleCommand, getVersionCommand, testCommand, testCoverageCommand, validateBinaryExistCommand } from '@src/opa';
66

77
jest.mock('../src/wrappers/execa', () => {
88
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
@@ -139,4 +139,33 @@ describe('opa.ts', function () {
139139
expect(err).toBe('oh no');
140140
});
141141
});
142+
143+
describe('#getVersionCommand', function () {
144+
it('should return the version string when command succeeds', async function () {
145+
const VERSION_OUTPUT = 'Version: 0.52.0\nBuild Commit: 8d2c137662560cac83d9cf24cbdaecc934910333\nBuild Timestamp: 2023-04-27T17:57:23Z';
146+
jest.spyOn(execa, 'execa').mockResolvedValue({ stdout: VERSION_OUTPUT } as ExecaChildProcess);
147+
148+
const version = await getVersionCommand();
149+
150+
expect(version).toBe('0.52.0');
151+
});
152+
153+
it('should throw error when output is empty', async function () {
154+
jest.spyOn(execa, 'execa').mockResolvedValue({ stdout: '' } as ExecaChildProcess);
155+
156+
await expect(getVersionCommand()).rejects.toThrow('Unable to read OPA version output');
157+
});
158+
159+
it('should throw error when output format is invalid', async function () {
160+
jest.spyOn(execa, 'execa').mockResolvedValue({ stdout: 'Invalid output format' } as ExecaChildProcess);
161+
162+
await expect(getVersionCommand()).rejects.toThrow('Unable to parse OPA version from output');
163+
});
164+
165+
it('should throw error when version line has no version', async function () {
166+
jest.spyOn(execa, 'execa').mockResolvedValue({ stdout: 'Version: \nOther info' } as ExecaChildProcess);
167+
168+
await expect(getVersionCommand()).rejects.toThrow('Unable to parse OPA version from output');
169+
});
170+
});
142171
});

packages/auth-bundler/tests/utils/bundle.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export function getFakeBundle(includeCreated?: boolean): IBundle {
3434
assets: [{ name: 'aaaa', version: 1 }],
3535
connections: [{ name: 'bbb', version: 2 }],
3636
metadata: { ccc: 123 },
37+
opaVersion: '0.52.0',
3738
};
3839
}
3940

packages/auth-core/src/db/entities/bundle.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,7 @@ export class Bundle implements IBundle {
2929

3030
@Column({ name: 'key_version', type: 'integer', nullable: true })
3131
public keyVersion?: number;
32+
33+
@Column({ name: 'opa_version', type: 'text', nullable: false })
34+
public opaVersion!: string;
3235
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm';
2+
3+
export class AddOpaVersionToBundle1749972920049 implements MigrationInterface {
4+
public name = 'AddOpaVersionToBundle1749972920049';
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
// Add the column as nullable first
8+
await queryRunner.query(`ALTER TABLE "auth_manager"."bundle" ADD "opa_version" text`);
9+
// Set default value for existing bundles
10+
await queryRunner.query(`UPDATE "auth_manager"."bundle" SET "opa_version" = '0.52.0' WHERE "opa_version" IS NULL`);
11+
// Make the column NOT NULL
12+
await queryRunner.query(`ALTER TABLE "auth_manager"."bundle" ALTER COLUMN "opa_version" SET NOT NULL`);
13+
}
14+
15+
public async down(queryRunner: QueryRunner): Promise<void> {
16+
await queryRunner.query(`ALTER TABLE "auth_manager"."bundle" DROP COLUMN "opa_version"`);
17+
}
18+
}

packages/auth-core/src/model/bundle.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,6 @@ export interface IBundle {
2020
createdAt?: Date;
2121
/** The version of the key that is part of the bundle. */
2222
keyVersion?: number;
23+
/** The version of the OPA cli that was used to create the bundle. */
24+
opaVersion: string;
2325
}

0 commit comments

Comments
 (0)