mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-04-18 00:06:01 +00:00
Add HostConfig.parse method
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>
This commit is contained in:
parent
f17f7efe60
commit
be986a62a5
@ -7,6 +7,7 @@ import * as deviceState from '../device-state';
|
||||
import * as logger from '../logger';
|
||||
import * as config from '../config';
|
||||
import * as hostConfig from '../host-config';
|
||||
import { parse } from '../host-config/index';
|
||||
import * as applicationManager from '../compose/application-manager';
|
||||
import type { CompositionStepAction } from '../compose/composition-steps';
|
||||
import { generateStep } from '../compose/composition-steps';
|
||||
@ -445,9 +446,9 @@ export const getHostConfig = async () => {
|
||||
* Used by:
|
||||
* - PATCH /v1/device/host-config
|
||||
*/
|
||||
export const patchHostConfig = async (
|
||||
conf: Parameters<typeof hostConfig.patch>[0],
|
||||
force: boolean,
|
||||
) => {
|
||||
await hostConfig.patch(conf, force);
|
||||
export const patchHostConfig = async (conf: unknown, force: boolean) => {
|
||||
const parsedConf = parse(conf);
|
||||
if (parsedConf) {
|
||||
await hostConfig.patch(parsedConf, force);
|
||||
}
|
||||
};
|
||||
|
@ -1,8 +1,12 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { isRight } from 'fp-ts/lib/Either';
|
||||
import Reporter from 'io-ts-reporters';
|
||||
|
||||
import type { RedsocksConfig, ProxyConfig } from './types';
|
||||
import { HostConfiguration, LegacyHostConfiguration } from './types';
|
||||
import * as config from '../config';
|
||||
import { pathOnRoot } from '../lib/host-utils';
|
||||
import log from '../lib/supervisor-console';
|
||||
|
||||
export * from './proxy';
|
||||
export * from './types';
|
||||
@ -27,6 +31,30 @@ export async function setHostname(val: string) {
|
||||
await config.set({ hostname });
|
||||
}
|
||||
|
||||
export function parse(
|
||||
conf: unknown,
|
||||
): HostConfiguration | LegacyHostConfiguration | null {
|
||||
const decoded = HostConfiguration.decode(conf);
|
||||
|
||||
if (isRight(decoded)) {
|
||||
return decoded.right;
|
||||
} else {
|
||||
log.warn(
|
||||
['Malformed host config detected, things may not behave as expected:']
|
||||
.concat(Reporter.report(decoded))
|
||||
.join('\n'),
|
||||
);
|
||||
// We haven't strictly validated user input since introducing the API,
|
||||
// endpoint, so accept configs where values may not be of the right type
|
||||
// but have the right shape for backwards compatibility.
|
||||
const legacyDecoded = LegacyHostConfiguration.decode(conf);
|
||||
if (isRight(legacyDecoded)) {
|
||||
return legacyDecoded.right;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function patchProxy(
|
||||
currentConf: RedsocksConfig,
|
||||
inputConf: Partial<{
|
||||
|
@ -28,3 +28,42 @@ export const RedsocksConfig = t.partial({
|
||||
redsocks: ProxyConfig,
|
||||
});
|
||||
export type RedsocksConfig = t.TypeOf<typeof RedsocksConfig>;
|
||||
|
||||
/**
|
||||
* An intersection of writeable redsocks.conf configurations, and
|
||||
* additional noProxy field (which is a config relating to proxy configuration)
|
||||
*/
|
||||
export const HostProxyConfig = t.intersection([
|
||||
ProxyConfig,
|
||||
t.partial({
|
||||
noProxy: t.array(t.string),
|
||||
}),
|
||||
]);
|
||||
export type HostProxyConfig = t.TypeOf<typeof HostProxyConfig>;
|
||||
|
||||
/**
|
||||
* A host configuration object which includes redsocks proxy configuration
|
||||
* and hostname configuration. This is the input type provided by the user
|
||||
* with host-config PATCH and provided to the user with host-config GET.
|
||||
*/
|
||||
export const HostConfiguration = t.type({
|
||||
network: t.partial({
|
||||
proxy: HostProxyConfig,
|
||||
hostname: t.string,
|
||||
}),
|
||||
});
|
||||
export type HostConfiguration = t.TypeOf<typeof HostConfiguration>;
|
||||
|
||||
/**
|
||||
* A user may provide an input which is not a valid HostConfiguration object,
|
||||
* but we've historically accepted these malformed configurations. This type
|
||||
* covers the case of a user providing a configuration which is not strictly
|
||||
* valid but has the correct shape.
|
||||
*/
|
||||
export const LegacyHostConfiguration = t.type({
|
||||
network: t.partial({
|
||||
proxy: t.record(t.string, t.any),
|
||||
hostname: t.string,
|
||||
}),
|
||||
});
|
||||
export type LegacyHostConfiguration = t.TypeOf<typeof LegacyHostConfiguration>;
|
||||
|
@ -75,7 +75,6 @@ const constants = {
|
||||
// (this number is used as an upper bound when generating
|
||||
// a random jitter)
|
||||
maxApiJitterDelay: 60 * 1000,
|
||||
validRedsocksProxyTypes: ['socks4', 'socks5', 'http-connect', 'http-relay'],
|
||||
};
|
||||
|
||||
if (process.env.DOCKER_HOST == null) {
|
||||
|
@ -7,11 +7,11 @@ base {
|
||||
}
|
||||
|
||||
redsocks {
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
ip = example.org;
|
||||
port = 1080;
|
||||
type = socks5;
|
||||
login = "foo";
|
||||
password = "bar";
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
ip = example.org;
|
||||
port = 1080;
|
||||
type = socks5;
|
||||
login = "foo";
|
||||
password = "bar";
|
||||
}
|
||||
|
@ -208,6 +208,49 @@ describe('host-config', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
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({
|
||||
|
@ -3,8 +3,8 @@ import { stripIndent } from 'common-tags';
|
||||
import type { SinonStub } from 'sinon';
|
||||
|
||||
import * as hostConfig from '~/src/host-config/index';
|
||||
import type { RedsocksConfig, ProxyConfig } from '~/src/host-config/types';
|
||||
import { RedsocksConf } from '~/src/host-config/index';
|
||||
import type { RedsocksConfig, ProxyConfig } from '~/src/host-config/types';
|
||||
import log from '~/lib/supervisor-console';
|
||||
|
||||
describe('RedsocksConf', () => {
|
||||
@ -429,4 +429,214 @@ describe('src/host-config', () => {
|
||||
expect(hostConfig.patchProxy(current, {})).to.deep.equal({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse', () => {
|
||||
it('parses valid HostConfiguration', () => {
|
||||
const conf = {
|
||||
network: {
|
||||
proxy: {
|
||||
type: 'socks4',
|
||||
ip: 'balena.io',
|
||||
port: 1079,
|
||||
login: '"baz"',
|
||||
password: '"foo"',
|
||||
noProxy: ['8.8.8.8'],
|
||||
},
|
||||
hostname: 'balena',
|
||||
},
|
||||
};
|
||||
expect(hostConfig.parse(conf)).to.deep.equal({
|
||||
network: {
|
||||
proxy: {
|
||||
type: 'socks4',
|
||||
ip: 'balena.io',
|
||||
port: 1079,
|
||||
login: '"baz"',
|
||||
password: '"foo"',
|
||||
noProxy: ['8.8.8.8'],
|
||||
},
|
||||
hostname: 'balena',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('parses valid HostConfiguration with only hostname', () => {
|
||||
const conf = {
|
||||
network: {
|
||||
hostname: 'balena2',
|
||||
},
|
||||
};
|
||||
expect(hostConfig.parse(conf)).to.deep.equal({
|
||||
network: {
|
||||
hostname: 'balena2',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('parses valid HostConfiguration with only proxy', () => {
|
||||
const conf = {
|
||||
network: {
|
||||
proxy: {
|
||||
type: 'http-connect',
|
||||
ip: 'test.balena.io',
|
||||
port: 1081,
|
||||
login: '"foo"',
|
||||
password: '"bar"',
|
||||
noProxy: ['3.3.3.3'],
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(hostConfig.parse(conf)).to.deep.equal({
|
||||
network: {
|
||||
proxy: {
|
||||
type: 'http-connect',
|
||||
ip: 'test.balena.io',
|
||||
port: 1081,
|
||||
login: '"foo"',
|
||||
password: '"bar"',
|
||||
noProxy: ['3.3.3.3'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('parses valid HostConfiguration with only noProxy', () => {
|
||||
const conf = {
|
||||
network: {
|
||||
proxy: {
|
||||
noProxy: ['1.1.1.1', '2.2.2.2'],
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(hostConfig.parse(conf)).to.deep.equal({
|
||||
network: {
|
||||
proxy: {
|
||||
noProxy: ['1.1.1.1', '2.2.2.2'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('parses HostConfiguration where auth fields are missing double quotes', () => {
|
||||
const conf = {
|
||||
network: {
|
||||
proxy: {
|
||||
type: 'http-connect',
|
||||
ip: 'test.balena.io',
|
||||
port: 1081,
|
||||
login: 'foo',
|
||||
password: 'bar',
|
||||
noProxy: ['3.3.3.3'],
|
||||
},
|
||||
},
|
||||
};
|
||||
(log.warn as SinonStub).resetHistory();
|
||||
expect(hostConfig.parse(conf)).to.deep.equal({
|
||||
network: {
|
||||
proxy: {
|
||||
type: 'http-connect',
|
||||
ip: 'test.balena.io',
|
||||
port: 1081,
|
||||
login: 'foo',
|
||||
password: 'bar',
|
||||
noProxy: ['3.3.3.3'],
|
||||
},
|
||||
},
|
||||
});
|
||||
// Should not warn about missing double quotes
|
||||
expect(log.warn as SinonStub).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('parses HostConfiguration where port is a string', () => {
|
||||
const conf = {
|
||||
network: {
|
||||
proxy: {
|
||||
type: 'http-connect',
|
||||
ip: 'test.balena.io',
|
||||
port: '1081',
|
||||
login: '"foo"',
|
||||
password: '"bar"',
|
||||
noProxy: ['3.3.3.3'],
|
||||
},
|
||||
},
|
||||
};
|
||||
(log.warn as SinonStub).resetHistory();
|
||||
expect(hostConfig.parse(conf)).to.deep.equal({
|
||||
network: {
|
||||
proxy: {
|
||||
type: 'http-connect',
|
||||
ip: 'test.balena.io',
|
||||
port: 1081,
|
||||
login: '"foo"',
|
||||
password: '"bar"',
|
||||
noProxy: ['3.3.3.3'],
|
||||
},
|
||||
},
|
||||
});
|
||||
// Should not warn about port being a string
|
||||
expect(log.warn as SinonStub).to.not.have.been.called;
|
||||
});
|
||||
|
||||
// Allow invalid fields through for backwards compatibility
|
||||
it('parses input with invalid proxy as LegacyHostConfiguration with console warnings', () => {
|
||||
const conf = {
|
||||
network: {
|
||||
proxy: {
|
||||
type: 'socks6',
|
||||
ip: 123,
|
||||
port: 'abc',
|
||||
login: 'user',
|
||||
password: 'pass',
|
||||
noProxy: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
(log.warn as SinonStub).resetHistory();
|
||||
expect(hostConfig.parse(conf)).to.deep.equal({
|
||||
network: {
|
||||
proxy: {
|
||||
type: 'socks6',
|
||||
ip: 123,
|
||||
port: 'abc',
|
||||
login: 'user',
|
||||
password: 'pass',
|
||||
noProxy: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect((log.warn as SinonStub).lastCall.args[0]).to.equal(
|
||||
'Malformed host config detected, things may not behave as expected:\n' +
|
||||
'Expecting string at network.proxy.0.0.ip but instead got: 123\n' +
|
||||
'Expecting NumericIdentifier at network.proxy.0.0.port but instead got: "abc" (must be be an positive integer)\n' +
|
||||
'Expecting one of:\n' +
|
||||
' "socks4"\n' +
|
||||
' "socks5"\n' +
|
||||
' "http-connect"\n' +
|
||||
' "http-relay"\n' +
|
||||
'at network.proxy.0.0.type but instead got: "socks6"\n' +
|
||||
'Expecting Array<string> at network.proxy.1.noProxy but instead got: true',
|
||||
);
|
||||
(log.warn as SinonStub).resetHistory();
|
||||
});
|
||||
|
||||
it('parses to null for HostConfiguration without network key', () => {
|
||||
expect(hostConfig.parse({})).to.be.null;
|
||||
});
|
||||
|
||||
it('parses to null for HostConfiguration with invalid network key', () => {
|
||||
const conf = {
|
||||
network: 123,
|
||||
};
|
||||
expect(hostConfig.parse(conf)).to.be.null;
|
||||
});
|
||||
|
||||
it('parses to null for HostConfiguration with invalid hostname', () => {
|
||||
const conf = {
|
||||
network: {
|
||||
hostname: 123,
|
||||
},
|
||||
};
|
||||
expect(hostConfig.parse(conf)).to.be.null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user