Merge pull request #1338 from balena-io/detect-unique-uuid-error

Check for 409 status code, rather than string matching uuid conflicts
This commit is contained in:
M. Casqueira 2020-05-21 18:16:28 -04:00 committed by GitHub
commit 81f6a77855
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 80 additions and 17 deletions

27
package-lock.json generated
View File

@ -1606,13 +1606,14 @@
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"balena-register-device": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/balena-register-device/-/balena-register-device-6.1.1.tgz",
"integrity": "sha512-ztwzs4/x8Pkf1DhZ/vFSteTR1jGJJmGjgnk7p9Fmj3RTa4Q3ucu6LGFm/Ugcc9y95epqAtssP7HStJ9Lsy2/eQ==",
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/balena-register-device/-/balena-register-device-6.1.5.tgz",
"integrity": "sha512-fQ2KCCSW+igWb79y1UiJgLjtiZhaKHVj1iR4RajPFE4EuMEa03LP0bMLVuqVBe1ggcYoyfAaKmIFLoGokYOhGw==",
"dev": true,
"requires": {
"bluebird": "^3.7.2",
"randomstring": "^1.1.5"
"randomstring": "^1.1.5",
"typed-error": "^2.0.0"
}
},
"balena-semver": {
@ -4352,6 +4353,13 @@
"object-assign": "^4.1.0"
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"dev": true,
"optional": true
},
"fill-range": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
@ -11321,6 +11329,16 @@
"integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
"dev": true
},
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dev": true,
"optional": true,
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"chokidar": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz",
@ -11348,6 +11366,7 @@
"dev": true,
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
},

View File

@ -62,7 +62,7 @@
"@types/supertest": "^2.0.9",
"@types/tmp": "^0.1.0",
"@types/yargs": "^15.0.4",
"balena-register-device": "^6.1.1",
"balena-register-device": "^6.1.5",
"blinking": "~0.0.3",
"bluebird": "^3.7.2",
"body-parser": "^1.19.0",

View File

@ -17,7 +17,7 @@ import constants = require('./lib/constants');
import {
ContractValidationError,
ContractViolationError,
DuplicateUuidError,
isHttpConflictError,
ExchangeKeyError,
InternalInconsistencyError,
} from './lib/errors';
@ -868,7 +868,7 @@ export class APIBinder {
try {
device = await deviceRegister.register(opts).timeout(opts.apiTimeout);
} catch (err) {
if (DuplicateUuidError(err)) {
if (isHttpConflictError(err.response)) {
log.debug('UUID already registered, trying a key exchange');
device = await this.exchangeKeyAndGetDeviceOrRegenerate(opts);
} else {

View File

@ -1,4 +1,4 @@
import { endsWith, map, startsWith } from 'lodash';
import { endsWith, map } from 'lodash';
import TypedError = require('typed-error');
import { checkInt } from './validation';
@ -45,8 +45,8 @@ export class InvalidAppIdError extends TypedError {
export class UpdatesLockedError extends TypedError {}
export function DuplicateUuidError(err: Error) {
return startsWith(err.message, '"uuid" must be unique');
export function isHttpConflictError(err: StatusCodeError): boolean {
return checkInt(err.statusCode) === 409;
}
export class ExchangeKeyError extends TypedError {}

View File

@ -1,18 +1,18 @@
import { fs } from 'mz';
import { Server } from 'net';
import { spy, stub } from 'sinon';
import { SinonSpy, spy, stub, SinonStub } from 'sinon';
import chai = require('./lib/chai-config');
import balenaAPI = require('./lib/mocked-balena-api');
import prepare = require('./lib/prepare');
const { expect } = chai;
import ApiBinder from '../src/api-binder';
import Config from '../src/config';
import Log from '../src/lib/supervisor-console';
import DB from '../src/db';
import DeviceState from '../src/device-state';
const { expect } = chai;
const initModels = async (obj: Dictionary<any>, filename: string) => {
prepare();
@ -99,6 +99,45 @@ describe('ApiBinder', () => {
});
});
it('exchanges keys if resource conflict when provisioning', async () => {
// Get current config to extend
const currentConfig = await components.apiBinder.config.get(
'provisioningOptions',
);
// Stub config values so we have correct conditions
const configStub = stub(Config.prototype, 'get').resolves({
...currentConfig,
registered_at: null,
provisioningApiKey: '123', // Previous test case deleted the provisioningApiKey so add one
uuid: 'not-unique', // This UUID is used in mocked-balena-api as an existing registered UUID
});
// If api-binder reaches this function then tests pass
const functionToReach = stub(
components.apiBinder,
'exchangeKeyAndGetDeviceOrRegenerate',
).rejects('expected-rejection'); // We throw an error so we don't have to keep stubbing
spy(Log, 'debug');
try {
await components.apiBinder.provision();
} catch (e) {
// Check that the error thrown is from this test
if (e.name !== 'expected-rejection') {
throw e;
}
}
expect(functionToReach).to.be.calledOnce;
expect((Log.debug as SinonSpy).lastCall.lastArg).to.equal(
'UUID already registered, trying a key exchange',
);
// Restore stubs
functionToReach.restore();
configStub.restore();
(Log.debug as SinonStub).restore();
});
it('deletes the provisioning key', async () => {
expect(await components.config.get('apiKey')).to.be.undefined;
});

View File

@ -29,9 +29,14 @@ api.balenaBackend = {
registerHandler: (req, res) => {
console.log('/device/register called with ', req.body);
const device = req.body;
device.id = api.balenaBackend!.currentId++;
api.balenaBackend!.devices[device.id] = device;
return res.status(201).json(device);
switch (req.body.uuid) {
case 'not-unique':
return res.status(409).json(device);
default:
device.id = api.balenaBackend!.currentId++;
api.balenaBackend!.devices[device.id] = device;
return res.status(201).json(device);
}
},
getDeviceHandler: (req, res) => {
const uuid =