mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-01-28 07:03:53 +00:00
be986a62a5
Parses input from PATCH /v1/device/host-config into either type HostConfiguration, or if LegacyHostConfiguration if input is of an acceptable shape (for backwards compatibility). Once input has been determined to be of type HostConfiguration, we can easily extract ProxyConfig from the object for patching, stringifying, and writing to redsocks.conf. Change-type: minor Signed-off-by: Christina Ying Wang <christina@balena.io>
314 lines
9.6 KiB
TypeScript
314 lines
9.6 KiB
TypeScript
import { expect } from 'chai';
|
|
import type { TestFs } from 'mocha-pod';
|
|
import { testfs } from 'mocha-pod';
|
|
import * as path from 'path';
|
|
import type { SinonStub } from 'sinon';
|
|
import { stub } from 'sinon';
|
|
import * as fs from 'fs/promises';
|
|
|
|
import { get, patch } from '~/src/host-config';
|
|
import * as hostConfig from '~/src/host-config/index';
|
|
import * as config from '~/src/config';
|
|
import * as applicationManager from '~/src/compose/application-manager';
|
|
import type { InstancedAppState } from '~/src/compose/types';
|
|
import * as updateLock from '~/lib/update-lock';
|
|
import { UpdatesLockedError } from '~/lib/errors';
|
|
import * as dbus from '~/lib/dbus';
|
|
import { pathOnBoot, pathOnRoot } from '~/lib/host-utils';
|
|
import {
|
|
createApps,
|
|
createService,
|
|
DEFAULT_NETWORK,
|
|
} from '~/test-lib/state-helper';
|
|
|
|
describe('host-config', () => {
|
|
let tFs: TestFs.Disabled;
|
|
let currentApps: InstancedAppState;
|
|
const APP_ID = 1;
|
|
const SERVICE_NAME = 'one';
|
|
const proxyBase = pathOnBoot('system-proxy');
|
|
const redsocksConf = path.join(proxyBase, 'redsocks.conf');
|
|
const noProxy = path.join(proxyBase, 'no_proxy');
|
|
const hostname = pathOnRoot('/etc/hostname');
|
|
const appLockDir = pathOnRoot(updateLock.lockPath(APP_ID));
|
|
|
|
before(async () => {
|
|
await config.initialized();
|
|
|
|
// Create current state
|
|
currentApps = createApps(
|
|
{
|
|
services: [
|
|
await createService({
|
|
running: true,
|
|
appId: APP_ID,
|
|
serviceName: SERVICE_NAME,
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
},
|
|
false,
|
|
);
|
|
|
|
// Set up test fs
|
|
tFs = testfs({
|
|
[redsocksConf]: testfs.from(
|
|
'test/data/mnt/boot/system-proxy/redsocks.conf',
|
|
),
|
|
[noProxy]: testfs.from('test/data/mnt/boot/system-proxy/no_proxy'),
|
|
[hostname]: 'deadbeef',
|
|
// Create a lock. This won't prevent host config patch unless
|
|
// there are current apps present, in which case an updates locked
|
|
// error will be thrown.
|
|
[appLockDir]: {
|
|
[SERVICE_NAME]: {
|
|
'updates.lock': '',
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await tFs.enable();
|
|
// Stub external dependencies
|
|
stub(dbus, 'servicePartOf').resolves([]);
|
|
stub(dbus, 'restartService').resolves();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await tFs.restore();
|
|
(dbus.servicePartOf as SinonStub).restore();
|
|
(dbus.restartService as SinonStub).restore();
|
|
});
|
|
|
|
describe('hostname', () => {
|
|
it('reads hostname', async () => {
|
|
expect(await hostConfig.readHostname()).to.equal('deadbeef');
|
|
});
|
|
|
|
it('sets hostname', async () => {
|
|
await hostConfig.setHostname('test');
|
|
expect(await config.get('hostname')).to.equal('test');
|
|
});
|
|
|
|
it('sets hostname to first 7 characters of UUID if empty', async () => {
|
|
await config.set({ uuid: '1234567' });
|
|
await hostConfig.setHostname('');
|
|
expect(await config.get('hostname')).to.equal('1234567');
|
|
});
|
|
});
|
|
|
|
describe('noProxy', () => {
|
|
it('reads IPs to exclude from proxy', async () => {
|
|
expect(await hostConfig.readNoProxy()).to.deep.equal([
|
|
'152.10.30.4',
|
|
'253.1.1.0/16',
|
|
]);
|
|
});
|
|
|
|
it('sets IPs to exclude from proxy', async () => {
|
|
await hostConfig.setNoProxy(['balena.io', '1.1.1.1']);
|
|
expect(await fs.readFile(noProxy, 'utf-8')).to.equal(
|
|
'balena.io\n1.1.1.1',
|
|
);
|
|
});
|
|
|
|
it('removes no_proxy file if empty or invalid', async () => {
|
|
// Set initial no_proxy
|
|
await hostConfig.setNoProxy(['2.2.2.2']);
|
|
expect(await hostConfig.readNoProxy()).to.deep.equal(['2.2.2.2']);
|
|
|
|
// Set to empty array
|
|
await hostConfig.setNoProxy([]);
|
|
expect(await hostConfig.readNoProxy()).to.deep.equal([]);
|
|
expect(await fs.readdir(proxyBase)).to.not.have.members(['no_proxy']);
|
|
|
|
// Reset initial no_proxy
|
|
await hostConfig.setNoProxy(['2.2.2.2']);
|
|
expect(await hostConfig.readNoProxy()).to.deep.equal(['2.2.2.2']);
|
|
|
|
// Set to invalid value
|
|
await hostConfig.setNoProxy(null as any);
|
|
expect(await hostConfig.readNoProxy()).to.deep.equal([]);
|
|
expect(await fs.readdir(proxyBase)).to.not.have.members(['no_proxy']);
|
|
});
|
|
});
|
|
|
|
it('reads proxy configs and hostname', async () => {
|
|
const { network } = await get();
|
|
expect(network).to.have.property('hostname', 'deadbeef');
|
|
expect(network).to.have.property('proxy');
|
|
expect(network.proxy).to.have.property('ip', 'example.org');
|
|
expect(network.proxy).to.have.property('port', 1080);
|
|
expect(network.proxy).to.have.property('type', 'socks5');
|
|
expect(network.proxy).to.have.property('login', 'foo');
|
|
expect(network.proxy).to.have.property('password', 'bar');
|
|
expect(network.proxy).to.have.deep.property('noProxy', [
|
|
'152.10.30.4',
|
|
'253.1.1.0/16',
|
|
]);
|
|
});
|
|
|
|
it('prevents patch if update locks are present', async () => {
|
|
stub(applicationManager, 'getCurrentApps').resolves(currentApps);
|
|
|
|
try {
|
|
await patch({ network: { hostname: 'test' } });
|
|
expect.fail('Expected hostConfig.patch to throw UpdatesLockedError');
|
|
} catch (e: unknown) {
|
|
expect(e).to.be.instanceOf(UpdatesLockedError);
|
|
}
|
|
|
|
(applicationManager.getCurrentApps as SinonStub).restore();
|
|
});
|
|
|
|
it('patches if update locks are present but force is specified', async () => {
|
|
stub(applicationManager, 'getCurrentApps').resolves(currentApps);
|
|
|
|
try {
|
|
await patch({ network: { hostname: 'deadreef' } }, true);
|
|
expect(await config.get('hostname')).to.equal('deadreef');
|
|
} catch (e: unknown) {
|
|
expect.fail(`Expected hostConfig.patch to not throw, but got ${e}`);
|
|
}
|
|
|
|
(applicationManager.getCurrentApps as SinonStub).restore();
|
|
});
|
|
|
|
it('patches hostname', async () => {
|
|
await patch({ network: { hostname: 'test' } });
|
|
// /etc/hostname isn't changed until the balena-hostname service
|
|
// is restarted by the OS.
|
|
expect(await config.get('hostname')).to.equal('test');
|
|
});
|
|
|
|
it('patches proxy', async () => {
|
|
await patch({
|
|
network: {
|
|
proxy: {
|
|
ip: 'example2.org',
|
|
port: 1090,
|
|
type: 'http-relay',
|
|
login: 'bar',
|
|
password: 'foo',
|
|
noProxy: ['balena.io', '222.22.2.2'],
|
|
},
|
|
},
|
|
});
|
|
const { network } = await get();
|
|
expect(network).to.have.property('proxy');
|
|
expect(network.proxy).to.have.property('ip', 'example2.org');
|
|
expect(network.proxy).to.have.property('port', 1090);
|
|
expect(network.proxy).to.have.property('type', 'http-relay');
|
|
expect(network.proxy).to.have.property('login', 'bar');
|
|
expect(network.proxy).to.have.property('password', 'foo');
|
|
expect(network.proxy).to.have.deep.property('noProxy', [
|
|
'balena.io',
|
|
'222.22.2.2',
|
|
]);
|
|
});
|
|
|
|
it('patches proxy fields specified while leaving unspecified fields unchanged', async () => {
|
|
await patch({
|
|
network: {
|
|
proxy: {
|
|
ip: 'example2.org',
|
|
port: 1090,
|
|
},
|
|
},
|
|
});
|
|
const { network } = await get();
|
|
expect(network).to.have.property('proxy');
|
|
expect(network.proxy).to.have.property('ip', 'example2.org');
|
|
expect(network.proxy).to.have.property('port', 1090);
|
|
expect(network.proxy).to.have.property('type', 'socks5');
|
|
expect(network.proxy).to.have.property('login', 'foo');
|
|
expect(network.proxy).to.have.property('password', 'bar');
|
|
expect(network.proxy).to.have.deep.property('noProxy', [
|
|
'152.10.30.4',
|
|
'253.1.1.0/16',
|
|
]);
|
|
});
|
|
|
|
it('patches proxy to empty if input is empty', async () => {
|
|
await patch({ network: { proxy: {} } });
|
|
const { network } = await get();
|
|
expect(network).to.have.property('proxy', undefined);
|
|
});
|
|
|
|
it('keeps current proxy if input is invalid', async () => {
|
|
await patch({ network: { proxy: null as any } });
|
|
const { network } = await get();
|
|
expect(network).to.have.property('proxy');
|
|
expect(network.proxy).to.have.property('ip', 'example.org');
|
|
expect(network.proxy).to.have.property('port', 1080);
|
|
expect(network.proxy).to.have.property('type', 'socks5');
|
|
expect(network.proxy).to.have.property('login', 'foo');
|
|
expect(network.proxy).to.have.property('password', 'bar');
|
|
expect(network.proxy).to.have.deep.property('noProxy', [
|
|
'152.10.30.4',
|
|
'253.1.1.0/16',
|
|
]);
|
|
});
|
|
|
|
it('skips restarting proxy services when part of redsocks-conf.target', async () => {
|
|
(dbus.servicePartOf as SinonStub).resolves(['redsocks-conf.target']);
|
|
await patch({
|
|
network: {
|
|
proxy: {
|
|
ip: 'example2.org',
|
|
port: 1090,
|
|
type: 'http-relay',
|
|
login: 'bar',
|
|
password: 'foo',
|
|
noProxy: ['balena.io', '222.22.2.2'],
|
|
},
|
|
},
|
|
});
|
|
expect(dbus.restartService as SinonStub).to.not.have.been.called;
|
|
const { network } = await get();
|
|
expect(network).to.have.property('proxy');
|
|
expect(network.proxy).to.have.property('ip', 'example2.org');
|
|
expect(network.proxy).to.have.property('port', 1090);
|
|
expect(network.proxy).to.have.property('type', 'http-relay');
|
|
expect(network.proxy).to.have.property('login', 'bar');
|
|
expect(network.proxy).to.have.property('password', 'foo');
|
|
expect(network.proxy).to.have.deep.property('noProxy', [
|
|
'balena.io',
|
|
'222.22.2.2',
|
|
]);
|
|
});
|
|
|
|
it('patches redsocks.conf to be empty if prompted', async () => {
|
|
await patch({ network: { proxy: {} } });
|
|
const { network } = await get();
|
|
expect(network).to.have.property('proxy', undefined);
|
|
expect(await fs.readdir(proxyBase)).to.not.have.members([
|
|
'redsocks.conf',
|
|
'no_proxy',
|
|
]);
|
|
});
|
|
|
|
it('patches no_proxy to be empty if prompted', async () => {
|
|
await patch({
|
|
network: {
|
|
proxy: {
|
|
noProxy: [],
|
|
},
|
|
},
|
|
});
|
|
const { network } = await get();
|
|
expect(network).to.have.property('proxy');
|
|
expect(network.proxy).to.not.have.property('noProxy');
|
|
expect(await fs.readdir(proxyBase)).to.not.have.members(['no_proxy']);
|
|
});
|
|
|
|
it("doesn't update hostname or proxy when both are empty", async () => {
|
|
const { network } = await get();
|
|
await patch({ network: {} });
|
|
const { network: newNetwork } = await get();
|
|
expect(network.hostname).to.equal(newNetwork.hostname);
|
|
expect(network.proxy).to.deep.equal(newNetwork.proxy);
|
|
});
|
|
});
|