Skip to content

device-types: Sync only device-types with DB device_type records #1837

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 34 additions & 8 deletions src/features/contracts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ const upsertEntries = async (
existingData: Map<string | number | boolean, AnyObject>,
newData: Array<Promise<AnyObject>>,
) => {
const addedContractSlugs: unknown[] = [];
await Bluebird.map(
newData,
async (fullEntry) => {
Expand Down Expand Up @@ -197,6 +198,7 @@ const upsertEntries = async (
body: entryFieldData,
options: { returnResource: reversePropMapEntries.length > 0 },
});
addedContractSlugs.push(uniqueFieldValue);
}
// upsert reverse navigation resources defined inline in the contract
for (const [propKey, { isReferencedBy }] of reversePropMapEntries) {
Expand Down Expand Up @@ -262,6 +264,7 @@ const upsertEntries = async (
},
{ concurrency: 10 },
);
return addedContractSlugs;
};

const syncContractsToDb = async (
Expand Down Expand Up @@ -307,7 +310,7 @@ const syncContractsToDb = async (
]),
);

await upsertEntries(
return await upsertEntries(
rootApi,
typeMap,
reversePropMapEntries,
Expand All @@ -316,6 +319,25 @@ const syncContractsToDb = async (
);
};

const contractTypes = [
'arch.sw',
'hw.device-manufacturer',
'hw.device-family',
'hw.device-type',
] as const;

let onContractsSynced:
| ((
stats: Partial<
Record<(typeof contractTypes)[number], { added: unknown[] }>
>,
) => void)
| undefined;

export const setOnContractsSynced = (callback: typeof onContractsSynced) => {
onContractsSynced = callback;
};

export const synchronizeContracts = async (contractRepos: RepositoryInfo[]) => {
// We want to separate fetching from syncing, since in air-gapped environments the user can
// preload the contracts folder inside the container and the sync should still work
Expand All @@ -325,23 +347,27 @@ export const synchronizeContracts = async (contractRepos: RepositoryInfo[]) => {
console.error(`Failed to fetch contracts, skipping...`, err.message);
}

const stats = {} as Parameters<NonNullable<typeof onContractsSynced>>[0];

// We don't have automatic dependency resolution, so the order matters here.
for (const contractType of [
'arch.sw',
'hw.device-manufacturer',
'hw.device-family',
'hw.device-type',
]) {
for (const contractType of contractTypes) {
try {
const contracts = await getContracts(contractType);
await syncContractsToDb(contractType, contracts, globalSyncSettings);
stats[contractType] = {
added: await syncContractsToDb(
contractType,
contracts,
globalSyncSettings,
),
};
} catch (err) {
captureException(
err,
`Failed to synchronize contract type: ${contractType}, skipping...`,
);
}
}
onContractsSynced?.(stats);
};

export const startContractSynchronization = _.once(() => {
Expand Down
17 changes: 16 additions & 1 deletion src/features/device-types/device-types-list.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import _ from 'lodash';

import type { DeviceTypeJson } from './device-type-json.js';
import { errors } from '@balena/pinejs';
import { errors, permissions, sbvrUtils } from '@balena/pinejs';
import * as semver from 'balena-semver';
const { InternalRequestError } = errors;

Expand All @@ -21,6 +21,8 @@ import {
IMAGE_STORAGE_DEBUG_REQUEST_ERRORS,
} from '../../lib/config.js';

const { api } = sbvrUtils;

export interface DeviceTypeInfo {
latest: DeviceTypeJson;
versions: string[];
Expand Down Expand Up @@ -68,6 +70,19 @@ export const getDeviceTypes = multiCacheMemoizee(
const result: Dictionary<DeviceTypeInfo> = {};
let slugs = await listFolders(IMAGE_STORAGE_PREFIX);

const rootApi = api.resin.clone({ passthrough: { req: permissions.root } });
const knownDeviceTypeSlugs = new Set(
(
await rootApi.get({
resource: 'device_type',
options: {
$select: 'slug',
},
})
).map((dt) => dt.slug),
);
slugs = slugs.filter((slug) => knownDeviceTypeSlugs.has(slug));

// If there are explicit includes, then everything else is excluded so we need to
// filter the slugs list to include only contracts that are in the CONTRACT_ALLOWLIST map
if (CONTRACT_ALLOWLIST.size > 0) {
Expand Down
11 changes: 10 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,10 @@ import * as baseAuth from './lib/auth.js';
import { varListInsert } from './features/device-state/state-get-utils.js';
import type { GetUrlFunction } from './features/request-logging/index.js';
import { setupRequestLogging } from './features/request-logging/index.js';
import { startContractSynchronization } from './features/contracts/index.js';
import {
setOnContractsSynced,
startContractSynchronization,
} from './features/contracts/index.js';

import { addToModel as addUserHasDirectAccessToApplicationToModel } from './features/applications/models/user__has_direct_access_to__application.js';
import { getApplicationSlug } from './features/applications/index.js';
Expand Down Expand Up @@ -270,6 +273,7 @@ export const envVarsConfig = {
// Needed so that the augmented `@balena/sbvr-types` typings
// automatically become available to consumer projects.
import './translations/v6/numeric-big-integer-hack.js';
import { getDeviceTypes } from './features/device-types/device-types-list.js';

export const translations = {
v7: {
Expand Down Expand Up @@ -427,6 +431,11 @@ export async function setup(app: Application, options: SetupOptions) {
getDeviceOnlineStateManager().start();

startContractSynchronization();
setOnContractsSynced((stats) => {
if (stats['hw.device-type']?.added.length ?? 0 > 0) {
void getDeviceTypes.delete();
}
});

return {
app,
Expand Down
Loading