diff --git a/src/balena-init.sql b/src/balena-init.sql index 89a40d507..6d7015d27 100644 --- a/src/balena-init.sql +++ b/src/balena-init.sql @@ -63,9 +63,13 @@ CREATE INDEX IF NOT EXISTS "device_device_type_idx" ON "device" ("is of-device type"); CREATE INDEX IF NOT EXISTS "device_is_running_release_idx" ON "device" ("is running-release"); --- Also optimizes should be running succesful release rule +-- Also optimizes should be running successful release rule +-- TODO: remove device_should_be_running_release_application_idx as soon as the is_pinned_on_release migration is ended CREATE INDEX IF NOT EXISTS "device_should_be_running_release_application_idx" ON "device" ("should be running-release", "belongs to-application"); +-- Also optimizes is pinned on successful release rule +CREATE INDEX IF NOT EXISTS "device_is_pinned_on_release_application_idx" +ON "device" ("is pinned on-release", "belongs to-application"); CREATE INDEX IF NOT EXISTS "device_should_be_operated_by_release_device_type_idx" ON "device" ("should be operated by-release", "is of-device type"); -- Also optimizes the supervisor cpu arch should match device cpu arch rule diff --git a/src/balena-model.ts b/src/balena-model.ts index 5f81b31de..c21fdd8ab 100644 --- a/src/balena-model.ts +++ b/src/balena-model.ts @@ -398,6 +398,7 @@ export interface Device { | [ServiceInstance?] | null; should_be_running__release: { __id: Release['id'] } | [Release?] | null; + is_pinned_on__release: { __id: Release['id'] } | [Release?] | null; should_be_operated_by__release: { __id: Release['id'] } | [Release?] | null; should_be_managed_by__release: { __id: Release['id'] } | [Release?] | null; is_web_accessible: boolean | null; @@ -519,6 +520,7 @@ export interface Release { should_be_running_on__application?: Application[]; should_be_running_on__device?: Device[]; is_running_on__device?: Device[]; + is_pinned_to__device?: Device[]; should_operate__device?: Device[]; should_manage__device?: Device[]; provides__device__installs__image?: ImageInstall[]; diff --git a/src/balena.sbvr b/src/balena.sbvr index 9508c5271..c0f6e00bc 100644 --- a/src/balena.sbvr +++ b/src/balena.sbvr @@ -579,6 +579,9 @@ Fact type: device is managed by service instance Fact type: device should be running release Synonymous Form: release should be running on device Necessity: each device should be running at most one release +Fact type: device is pinned on release + Synonymous Form: release is pinned to device + Necessity: each device is pinned on at most one release Fact type: device should be operated by release Synonymous Form: release should operate device Necessity: each device should be operated by at most one release diff --git a/src/features/devices/hooks/defaults-validation.ts b/src/features/devices/hooks/defaults-validation.ts index 4ac90b708..c79f7f505 100644 --- a/src/features/devices/hooks/defaults-validation.ts +++ b/src/features/devices/hooks/defaults-validation.ts @@ -29,11 +29,20 @@ hooks.addPureHook('POST', 'resin', 'device', { 'Device UUID must be a 32 or 62 character long lower case hex string.', ); } + + // TODO[device management next step]: Drop this after re-migrating all data on step 2: + if (request.values.should_be_running__release !== undefined) { + // Add an async boundary so that value updates, + // and doesn't remove the properties that we add. + await null; + request.values.is_pinned_on__release = + request.values.should_be_running__release; + } }, }); hooks.addPureHook('PATCH', 'resin', 'device', { - POSTPARSE: ({ request }) => { + POSTPARSE: async ({ request }) => { // Check for extra whitespace characters if ( request.values.device_name != null && @@ -73,5 +82,14 @@ hooks.addPureHook('PATCH', 'resin', 'device', { if (request.values.is_online != null) { request.values.last_connectivity_event = new Date(); } + + // TODO[device management next step]: Drop this after re-migrating all data on step 2: + if (request.values.should_be_running__release !== undefined) { + // Add an async boundary so that value updates, + // and doesn't remove the properties that we add. + await null; + request.values.is_pinned_on__release = + request.values.should_be_running__release; + } }, }); diff --git a/src/migrations/00087-add-device-pinned-on-release.sql b/src/migrations/00087-add-device-pinned-on-release.sql new file mode 100644 index 000000000..6ce05913b --- /dev/null +++ b/src/migrations/00087-add-device-pinned-on-release.sql @@ -0,0 +1,25 @@ +-- Add the column if it does not exist +ALTER TABLE "device" +ADD COLUMN IF NOT EXISTS "is pinned on-release" INTEGER NULL; + +-- Add an index for optimization if it does not exist +CREATE INDEX IF NOT EXISTS "device_is_pinned_on_release_application_idx" +ON "device" ("is pinned on-release", "belongs to-application"); + +-- Check and add the foreign key constraint conditionally +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name + WHERE tc.constraint_type = 'FOREIGN KEY' + AND tc.table_schema = CURRENT_SCHEMA() + AND tc.table_name = 'device' + AND kcu.column_name = 'is pinned on-release' + ) THEN + ALTER TABLE "device" + ADD CONSTRAINT "device_is pinned on-release_fkey" FOREIGN KEY ("is pinned on-release") REFERENCES "release" ("id"); + END IF; +END; +$$;