mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-04-13 06:02:58 +00:00
Add update lock check to PATCH /v1/device/host-config
This is necessary with the changes as of balenaOS 2.82.6, which watches config.json and will restart balena-hostname and some other services automatically on file change. Change-type: patch Relates-to: #1876 Signed-off-by: Christina Wang <christina@balena.io>
This commit is contained in:
parent
4f446103f4
commit
5f1a77da25
15
docs/API.md
15
docs/API.md
@ -548,16 +548,16 @@ $ curl -X POST --header "Content-Type:application/json" \
|
||||
|
||||
> **Note:** on devices with supervisor version lower than 7.22.0, replace all `BALENA_` variables with `RESIN_`, e.g. `RESIN_SUPERVISOR_ADDRESS` instead of `BALENA_SUPERVISOR_ADDRESS`.
|
||||
|
||||
This endpoint allows setting some configuration values for the host OS. Currently it supports
|
||||
proxy and hostname configuration.
|
||||
This endpoint allows setting some configuration values for the host OS. Currently it supports proxy and hostname configuration.
|
||||
|
||||
For proxy configuration, balenaOS 2.0.7 and higher provides a transparent proxy redirector (redsocks) that makes all connections be routed to a SOCKS or HTTP proxy. This endpoint allows services to modify these proxy settings at runtime.
|
||||
|
||||
|
||||
#### Request body
|
||||
|
||||
Is a JSON object with several optional fields. Proxy and hostname configuration go under a "network" key. If "proxy" or "hostname" are not present (undefined), those values will not be modified, so that a request can modify hostname
|
||||
without changing proxy settings and viceversa.
|
||||
A JSON object with several optional fields. Proxy and hostname configuration go under a `network` key. If `proxy` or `hostname` are not present (undefined), those values will not be modified, so that a request can modify hostname without changing proxy settings and vice versa.
|
||||
|
||||
By default, with [balenaOS 2.82.6](https://github.com/balena-os/meta-balena/blob/master/CHANGELOG.md#v2826) or newer, host config PATCH requests will cause a balenaEngine restart. Therefore, with Supervisor v12.11.34 and newer, the PATCH request will respect the presence of update locks. Specify the `force` boolean (`false` by default) property in the request body to ignore this.
|
||||
|
||||
```json
|
||||
{
|
||||
@ -570,7 +570,8 @@ without changing proxy settings and viceversa.
|
||||
"password": "password",
|
||||
"noProxy": [ "152.10.30.4", "253.1.1.0/16" ]
|
||||
},
|
||||
"hostname": "mynewhostname"
|
||||
"hostname": "mynewhostname",
|
||||
"force": true
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -581,10 +582,10 @@ guaranteed to work, especially if they block connections that the balena service
|
||||
|
||||
Keep in mind that, even if transparent proxy redirection will take effect immediately after the API call (i.e. all new connections will go through the proxy), open connections will not be closed. So, if for example, the device has managed to connect to the balenaCloud VPN without the proxy, it will stay connected directly without trying to reconnect through the proxy, unless the connection breaks - any reconnection attempts will then go through the proxy. To force *all* connections to go through the proxy, the best way is to reboot the device (see the /v1/reboot endpoint). In most networks were no connections to the Internet can be made if not through a proxy, this should not be necessary (as there will be no open connections before configuring the proxy settings).
|
||||
|
||||
The "noProxy" setting for the proxy is an optional array of IP addresses/subnets that should not be routed through the
|
||||
The `noProxy` setting for the proxy is an optional array of IP addresses/subnets that should not be routed through the
|
||||
proxy. Keep in mind that local/reserved subnets are already [excluded by balenaOS automatically](https://github.com/balena-os/meta-balena/blob/master/meta-balena-common/recipes-connectivity/balena-proxy-config/balena-proxy-config/balena-proxy-config).
|
||||
|
||||
If either "proxy" or "hostname" are null or empty values (i.e. `{}` for proxy or an empty string for hostname), they will be cleared to their default values (i.e. not using a proxy, and a hostname equal to the first 7 characters of the device's uuid, respectively).
|
||||
If either `proxy` or `hostname` are null or empty values (i.e. `{}` for proxy or an empty string for hostname), they will be cleared to their default values (i.e. not using a proxy, and a hostname equal to the first 7 characters of the device's uuid, respectively).
|
||||
|
||||
#### Examples:
|
||||
From an app container:
|
||||
|
@ -147,10 +147,19 @@ function createDeviceStateRouter() {
|
||||
const uuid = await config.get('uuid');
|
||||
req.body.network.hostname = uuid?.slice(0, 7);
|
||||
}
|
||||
|
||||
await hostConfig.patch(req.body);
|
||||
const lockOverride = await config.get('lockOverride');
|
||||
await hostConfig.patch(
|
||||
req.body,
|
||||
validation.checkTruthy(req.body.force) || lockOverride,
|
||||
);
|
||||
res.status(200).send('OK');
|
||||
} catch (err) {
|
||||
// TODO: We should be able to throw err if it's UpdatesLockedError
|
||||
// and the error middleware will handle it, but this doesn't work in
|
||||
// the test environment. Fix this when fixing API tests.
|
||||
if (err instanceof UpdatesLockedError) {
|
||||
return res.status(423).send(err?.message ?? err);
|
||||
}
|
||||
res.status(503).send(err?.message ?? err ?? 'Unknown error');
|
||||
}
|
||||
});
|
||||
|
@ -7,8 +7,9 @@ import * as path from 'path';
|
||||
import * as config from './config';
|
||||
import * as constants from './lib/constants';
|
||||
import * as dbus from './lib/dbus';
|
||||
import { ENOENT } from './lib/errors';
|
||||
import { ENOENT, InternalInconsistencyError } from './lib/errors';
|
||||
import { writeFileAtomic, mkdirp, unlinkAll } from './lib/fs-utils';
|
||||
import * as updateLock from './lib/update-lock';
|
||||
|
||||
const redsocksHeader = stripIndent`
|
||||
base {
|
||||
@ -214,15 +215,22 @@ export function get(): Bluebird<HostConfig> {
|
||||
});
|
||||
}
|
||||
|
||||
export function patch(conf: HostConfig): Bluebird<void> {
|
||||
const promises: Array<Promise<void>> = [];
|
||||
if (conf != null && conf.network != null) {
|
||||
if (conf.network.proxy != null) {
|
||||
promises.push(setProxy(conf.network.proxy));
|
||||
}
|
||||
if (conf.network.hostname != null) {
|
||||
promises.push(setHostname(conf.network.hostname));
|
||||
}
|
||||
export async function patch(conf: HostConfig, force: boolean): Promise<void> {
|
||||
const appId = await config.get('applicationId');
|
||||
if (!appId) {
|
||||
throw new InternalInconsistencyError('Could not find an appId');
|
||||
}
|
||||
return Bluebird.all(promises).return();
|
||||
|
||||
return updateLock.lock(appId, { force }, () => {
|
||||
const promises: Array<Promise<void>> = [];
|
||||
if (conf != null && conf.network != null) {
|
||||
if (conf.network.proxy != null) {
|
||||
promises.push(setProxy(conf.network.proxy));
|
||||
}
|
||||
if (conf.network.hostname != null) {
|
||||
promises.push(setHostname(conf.network.hostname));
|
||||
}
|
||||
}
|
||||
return Bluebird.all(promises).return();
|
||||
});
|
||||
}
|
||||
|
@ -900,6 +900,62 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
||||
restartServiceSpy.restore();
|
||||
});
|
||||
|
||||
it('prevents patch if update locks are present', async () => {
|
||||
stub(updateLock, 'lock').callsFake((__, opts, fn) => {
|
||||
if (opts.force) {
|
||||
return Bluebird.resolve(fn());
|
||||
}
|
||||
throw new UpdatesLockedError('Updates locked');
|
||||
});
|
||||
|
||||
await request
|
||||
.patch('/v1/device/host-config')
|
||||
.send({ network: { hostname: 'foobaz' } })
|
||||
.set('Accept', 'application/json')
|
||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
||||
.expect(423);
|
||||
|
||||
expect(updateLock.lock).to.be.calledOnce;
|
||||
(updateLock.lock as SinonStub).restore();
|
||||
|
||||
await request
|
||||
.get('/v1/device/host-config')
|
||||
.set('Accept', 'application/json')
|
||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
||||
.then((response) => {
|
||||
expect(response.body.network.hostname).to.deep.equal(
|
||||
'foobardevice',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('allows patch while update locks are present if force is in req.body', async () => {
|
||||
stub(updateLock, 'lock').callsFake((__, opts, fn) => {
|
||||
if (opts.force) {
|
||||
return Bluebird.resolve(fn());
|
||||
}
|
||||
throw new UpdatesLockedError('Updates locked');
|
||||
});
|
||||
|
||||
await request
|
||||
.patch('/v1/device/host-config')
|
||||
.send({ network: { hostname: 'foobaz' }, force: true })
|
||||
.set('Accept', 'application/json')
|
||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
||||
.expect(200);
|
||||
|
||||
expect(updateLock.lock).to.be.calledOnce;
|
||||
(updateLock.lock as SinonStub).restore();
|
||||
|
||||
await request
|
||||
.get('/v1/device/host-config')
|
||||
.set('Accept', 'application/json')
|
||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
||||
.then((response) => {
|
||||
expect(response.body.network.hostname).to.deep.equal('foobaz');
|
||||
});
|
||||
});
|
||||
|
||||
it('updates the hostname with provided string if string is not empty', async () => {
|
||||
// stub servicePartOf to throw exceptions for the new service names
|
||||
stub(dbus, 'servicePartOf').callsFake(
|
||||
|
Loading…
x
Reference in New Issue
Block a user