Compare commits
2 Commits
95fbc5167a
...
5f84009b61
Author | SHA1 | Date | |
---|---|---|---|
|
5f84009b61 | ||
|
4b9a7613fa |
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "all",
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": false,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"parser": "babel"
|
||||||
|
}
|
@ -7,8 +7,8 @@
|
|||||||
persist: ['data']
|
persist: ['data']
|
||||||
network:
|
network:
|
||||||
# valid values for interface: ['public', 'local']
|
# valid values for interface: ['public', 'local']
|
||||||
interface: ''
|
interface: 'public'
|
||||||
port: 5432
|
port: 1
|
||||||
domain: 'db.foo.org'
|
domain: 'db.foo.org'
|
||||||
ssl: false
|
ssl: false
|
||||||
pg_admin:
|
pg_admin:
|
||||||
@ -20,8 +20,8 @@
|
|||||||
allowed_ssh_public_keys: ['ssh-ed25519 <snip> my special ssh key']
|
allowed_ssh_public_keys: ['ssh-ed25519 <snip> my special ssh key']
|
||||||
persist: []
|
persist: []
|
||||||
network:
|
network:
|
||||||
interface: ''
|
interface: 'public'
|
||||||
port: 5432
|
port: 2
|
||||||
domain: 'api.foo.org'
|
domain: 'api.foo.org'
|
||||||
ssl: true
|
ssl: true
|
||||||
- ui:
|
- ui:
|
||||||
@ -30,7 +30,7 @@
|
|||||||
allowed_ssh_public_keys: ['ssh-ed25519 <snip> my special ssh key']
|
allowed_ssh_public_keys: ['ssh-ed25519 <snip> my special ssh key']
|
||||||
persist: []
|
persist: []
|
||||||
network:
|
network:
|
||||||
interface: ''
|
interface: 'public'
|
||||||
port: 5432
|
port: 3
|
||||||
domain: 'foo.org'
|
domain: 'foo.org'
|
||||||
ssl: true
|
ssl: true
|
||||||
|
@ -6,9 +6,16 @@
|
|||||||
"repository": "git@git.orionkindel.com:dnim/srv.git",
|
"repository": "git@git.orionkindel.com:dnim/srv.git",
|
||||||
"author": "Orion Kindel <cakekindel@gmail.com>",
|
"author": "Orion Kindel <cakekindel@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"fmt:check": "prettier --check 'src/**/*.js'",
|
||||||
|
"fmt": "prettier --write 'src/**/*.js'"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@matchbook/ts": "^1.0.0",
|
"@matchbook/ts": "^1.0.0",
|
||||||
"fp-ts": "^2.16.0",
|
"fp-ts": "^2.16.0",
|
||||||
"yaml": "^2.3.1"
|
"yaml": "^2.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^2.8.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
40
src/cmd.js
40
src/cmd.js
@ -1,34 +1,34 @@
|
|||||||
const Either = require("fp-ts/Either");
|
const Either = require('fp-ts/Either')
|
||||||
const { pipe, identity: id } = require("fp-ts/function");
|
const { pipe, identity: id } = require('fp-ts/function')
|
||||||
const cp = require("child_process");
|
const cp = require('child_process')
|
||||||
|
|
||||||
const run = (script) =>
|
const run = script =>
|
||||||
pipe(
|
pipe(
|
||||||
Either.tryCatch(
|
Either.tryCatch(
|
||||||
() => cp.spawnSync(script, { encoding: "utf8", shell: true }),
|
() => cp.spawnSync(script, { encoding: 'utf8', shell: true }),
|
||||||
id
|
id,
|
||||||
),
|
),
|
||||||
Either.flatMap((o) => (o.error ? Either.left(o.error) : Either.right(o))),
|
Either.flatMap(o => (o.error ? Either.left(o.error) : Either.right(o))),
|
||||||
Either.flatMap((o) =>
|
Either.flatMap(o =>
|
||||||
o.status > 0
|
o.status > 0
|
||||||
? Either.left(
|
? Either.left(
|
||||||
new Error(JSON.stringify({ reason: "status_gt_0", ...o }))
|
new Error(JSON.stringify({ reason: 'status_gt_0', ...o })),
|
||||||
)
|
)
|
||||||
: Either.right(o)
|
: Either.right(o),
|
||||||
),
|
),
|
||||||
Either.map((o) => o.stdout.trim())
|
Either.map(o => o.stdout.trim()),
|
||||||
);
|
)
|
||||||
|
|
||||||
const ignoringStatus = Either.orElse((e) =>
|
const ignoringStatus = Either.orElse(e =>
|
||||||
pipe(
|
pipe(
|
||||||
Either.tryCatch(() => JSON.parse(e.message), id),
|
Either.tryCatch(() => JSON.parse(e.message), id),
|
||||||
Either.flatMap((o) =>
|
Either.flatMap(o =>
|
||||||
o.reason && o.reason === "status_gt_0"
|
o.reason && o.reason === 'status_gt_0'
|
||||||
? Either.right(o.stdout.trim())
|
? Either.right(o.stdout.trim())
|
||||||
: Either.left(new Error())
|
: Either.left(new Error()),
|
||||||
),
|
),
|
||||||
Either.mapLeft(() => e)
|
Either.mapLeft(() => e),
|
||||||
)
|
),
|
||||||
);
|
)
|
||||||
|
|
||||||
module.exports = { run, ignoringStatus };
|
module.exports = { run, ignoringStatus }
|
||||||
|
256
src/config.js
256
src/config.js
@ -1,165 +1,231 @@
|
|||||||
const yaml = require("yaml");
|
const yaml = require('yaml')
|
||||||
const { strike, match, otherwise } = require("@matchbook/ts");
|
const { strike, match, otherwise } = require('@matchbook/ts')
|
||||||
const Array_ = require("fp-ts/Array");
|
const Array_ = require('fp-ts/Array')
|
||||||
const Either = require("fp-ts/Either");
|
const Either = require('fp-ts/Either')
|
||||||
const { identity: id, flow, pipe } = require("fp-ts/function");
|
const { identity: id, flow, pipe } = require('fp-ts/function')
|
||||||
|
|
||||||
const parseSegmentLinuxUser = (o) =>
|
const parseSegmentLinuxUser = o =>
|
||||||
"linux_user" in o
|
'linux_user' in o
|
||||||
? "username" in o.linux_user
|
? 'username' in o.linux_user
|
||||||
? Either.right({
|
? Either.right({
|
||||||
username: o.linux_user.username,
|
username: o.linux_user.username.trim(),
|
||||||
allowedSshPublicKeys: o.allowed_ssh_public_keys || [],
|
allowedSshPublicKeys: (o.allowed_ssh_public_keys || []).map(pubkey =>
|
||||||
|
pubkey.trim(),
|
||||||
|
),
|
||||||
})
|
})
|
||||||
: Either.left(new Error("linux_user.username required"))
|
: Either.left(new Error('linux_user.username required'))
|
||||||
: Either.left(new Error("linux_user required"));
|
: Either.left(new Error('linux_user required'))
|
||||||
|
|
||||||
const parseSegmentPgAdmin = flow(
|
const parseSegmentPgAdmin = flow(
|
||||||
(o) =>
|
o =>
|
||||||
"pg_admin" in o
|
'pg_admin' in o
|
||||||
? Either.right(o.pg_admin)
|
? Either.right(o.pg_admin)
|
||||||
: Either.left(
|
: Either.left(
|
||||||
new Error(
|
new Error(
|
||||||
["pg_admin required", `in: ${JSON.stringify(o)}`].join("\n")
|
['pg_admin required', `in: ${JSON.stringify(o)}`].join('\n'),
|
||||||
)
|
|
||||||
),
|
),
|
||||||
Either.flatMap((pg) =>
|
),
|
||||||
"username" in pg
|
Either.flatMap(pg =>
|
||||||
|
'username' in pg
|
||||||
? Either.right(pg)
|
? Either.right(pg)
|
||||||
: Either.left(
|
: Either.left(
|
||||||
new Error(
|
new Error(
|
||||||
["pg_admin.username required", `in: ${JSON.stringify(pg)}`].join(
|
['pg_admin.username required', `in: ${JSON.stringify(pg)}`].join(
|
||||||
"\n"
|
'\n',
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
Either.flatMap((pg) =>
|
),
|
||||||
"password" in pg
|
),
|
||||||
|
),
|
||||||
|
Either.flatMap(pg =>
|
||||||
|
'password' in pg
|
||||||
? Either.right(pg)
|
? Either.right(pg)
|
||||||
: Either.left(
|
: Either.left(
|
||||||
new Error(
|
new Error(
|
||||||
["pg_admin.password required", `in: ${JSON.stringify(pg)}`].join(
|
['pg_admin.password required', `in: ${JSON.stringify(pg)}`].join(
|
||||||
"\n"
|
'\n',
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
Either.map((pg) => ({ username: pg.username, password: pg.password }))
|
),
|
||||||
);
|
),
|
||||||
|
),
|
||||||
|
Either.map(pg => ({
|
||||||
|
username: pg.username.trim(),
|
||||||
|
password: pg.password.trim(),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
const parseSegmentNetwork = flow(
|
const parseSegmentNetwork = flow(
|
||||||
(o) =>
|
o =>
|
||||||
"network" in o
|
'network' in o
|
||||||
? Either.right(o.network)
|
? Either.right(o.network)
|
||||||
: Either.left(
|
: Either.left(
|
||||||
new Error(["network required", `in: ${JSON.stringify(o)}`].join("\n"))
|
new Error(
|
||||||
|
['network required', `in: ${JSON.stringify(o)}`].join('\n'),
|
||||||
),
|
),
|
||||||
Either.flatMap((n) =>
|
),
|
||||||
("interface" in n && n.interface === "public") || n.interface === "local"
|
Either.flatMap(n =>
|
||||||
|
('interface' in n && n.interface === 'public') || n.interface === 'local'
|
||||||
? Either.right(n)
|
? Either.right(n)
|
||||||
: Either.left(
|
: Either.left(
|
||||||
new Error(
|
new Error(
|
||||||
[
|
[
|
||||||
"network.interface required, and must be 'public' or 'local'",
|
"network.interface required, and must be 'public' or 'local'",
|
||||||
`in: ${JSON.stringify(n)}`,
|
`in: ${JSON.stringify(n)}`,
|
||||||
].join("\n")
|
].join('\n'),
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
Either.flatMap((n) =>
|
|
||||||
"port" in n
|
|
||||||
? Either.right(n)
|
|
||||||
: Either.left(
|
|
||||||
new Error(
|
|
||||||
["network.port required", `in: ${JSON.stringify(n)}`].join("\n")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
Either.flatMap((n) =>
|
),
|
||||||
"domain" in n
|
Either.flatMap(n =>
|
||||||
|
'port' in n && typeof n.port === 'number' && Number.isInteger(n.port)
|
||||||
? Either.right(n)
|
? Either.right(n)
|
||||||
: Either.left(
|
: Either.left(
|
||||||
new Error(
|
new Error(
|
||||||
[
|
[
|
||||||
"network.domain required",
|
'network.port required and must be an integer',
|
||||||
" (to not listen on a public domain, set this property to empty string '')",
|
|
||||||
`in: ${JSON.stringify(n)}`,
|
`in: ${JSON.stringify(n)}`,
|
||||||
].join("\n")
|
].join('\n'),
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
Either.flatMap((n) =>
|
),
|
||||||
"ssl" in n
|
),
|
||||||
|
Either.flatMap(n =>
|
||||||
|
n.interface === 'public' || (n.interface === 'local' && !'domain' in n)
|
||||||
? Either.right(n)
|
? Either.right(n)
|
||||||
: Either.left(
|
: Either.left(
|
||||||
new Error(
|
new Error(
|
||||||
["network.ssl required", `in: ${JSON.stringify(n)}`].join("\n")
|
[
|
||||||
)
|
"network.interface is 'local', network.domain must not be present.",
|
||||||
)
|
`in: ${JSON.stringify(n)}`,
|
||||||
|
].join('\n'),
|
||||||
),
|
),
|
||||||
Either.map((n) => ({
|
),
|
||||||
|
),
|
||||||
|
Either.flatMap(n =>
|
||||||
|
n.interface === 'public' &&
|
||||||
|
'domain' in n &&
|
||||||
|
typeof n.domain === 'string' &&
|
||||||
|
n.domain.length > 0
|
||||||
|
? Either.right(n)
|
||||||
|
: Either.left(
|
||||||
|
new Error(
|
||||||
|
[
|
||||||
|
"network.interface is 'public', network.domain be set to a value.",
|
||||||
|
`in: ${JSON.stringify(n)}`,
|
||||||
|
].join('\n'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Either.flatMap(n =>
|
||||||
|
'ssl' in n && typeof n.ssl === 'boolean'
|
||||||
|
? Either.right(n)
|
||||||
|
: Either.left(
|
||||||
|
new Error(
|
||||||
|
[
|
||||||
|
'network.ssl required and must be a bool',
|
||||||
|
`in: ${JSON.stringify(n)}`,
|
||||||
|
].join('\n'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Either.map(n => ({
|
||||||
ssl: n.ssl,
|
ssl: n.ssl,
|
||||||
domain: n.domain || undefined,
|
domain: (n.domain || '').trim(),
|
||||||
interface: n.interface,
|
interface: n.interface,
|
||||||
port: n.port,
|
port: n.port,
|
||||||
}))
|
})),
|
||||||
);
|
)
|
||||||
|
|
||||||
const parseServiceDb = (svc) =>
|
const parseServiceDb = svc =>
|
||||||
pipe(
|
pipe(
|
||||||
Either.Do,
|
Either.Do,
|
||||||
Either.bind("linuxUser", () => parseSegmentLinuxUser(svc)),
|
Either.bind('linuxUser', () => parseSegmentLinuxUser(svc)),
|
||||||
Either.let("persist", () =>
|
Either.let('persist', () =>
|
||||||
(svc.persist || []).filter((s) => typeof s === "string" && s.length > 0)
|
(svc.persist || []).filter(s => typeof s === 'string' && s.length > 0),
|
||||||
),
|
),
|
||||||
Either.bind("network", () => parseSegmentNetwork(svc)),
|
Either.bind('network', () => parseSegmentNetwork(svc)),
|
||||||
Either.bind("pgAdmin", () => parseSegmentPgAdmin(svc))
|
Either.bind('pgAdmin', () => parseSegmentPgAdmin(svc)),
|
||||||
);
|
)
|
||||||
|
|
||||||
const parseServiceApi = (svc) =>
|
const parseServiceApi = svc =>
|
||||||
pipe(
|
pipe(
|
||||||
Either.Do,
|
Either.Do,
|
||||||
Either.bind("linuxUser", () => parseSegmentLinuxUser(svc)),
|
Either.bind('linuxUser', () => parseSegmentLinuxUser(svc)),
|
||||||
Either.let("persist", () =>
|
Either.let('persist', () =>
|
||||||
(svc.persist || []).filter((s) => typeof s === "string" && s.length > 0)
|
(svc.persist || []).filter(s => typeof s === 'string' && s.length > 0),
|
||||||
),
|
),
|
||||||
Either.bind("network", () => parseSegmentNetwork(svc))
|
Either.bind('network', () => parseSegmentNetwork(svc)),
|
||||||
);
|
)
|
||||||
|
|
||||||
const parseServiceUi = (svc) =>
|
const parseServiceUi = svc =>
|
||||||
pipe(
|
pipe(
|
||||||
Either.Do,
|
Either.Do,
|
||||||
Either.bind("linuxUser", () => parseSegmentLinuxUser(svc)),
|
Either.bind('linuxUser', () => parseSegmentLinuxUser(svc)),
|
||||||
Either.let("persist", () =>
|
Either.let('persist', () =>
|
||||||
(svc.persist || []).filter((s) => typeof s === "string" && s.length > 0)
|
(svc.persist || []).filter(s => typeof s === 'string' && s.length > 0),
|
||||||
),
|
),
|
||||||
Either.bind("network", () => parseSegmentNetwork(svc))
|
Either.bind('network', () => parseSegmentNetwork(svc)),
|
||||||
);
|
)
|
||||||
|
|
||||||
const parseService = (svc) =>
|
const parseService = svc =>
|
||||||
"db" in svc
|
'db' in svc
|
||||||
? parseServiceDb(svc.db)
|
? parseServiceDb(svc.db)
|
||||||
: "api" in svc
|
: 'api' in svc
|
||||||
? parseServiceApi(svc.api)
|
? parseServiceApi(svc.api)
|
||||||
: "ui" in svc
|
: 'ui' in svc
|
||||||
? parseServiceUi(svc.ui)
|
? parseServiceUi(svc.ui)
|
||||||
: Either.left(
|
: Either.left(
|
||||||
new Error(
|
new Error(
|
||||||
[
|
[
|
||||||
`top-level array elements must be records with a key named "db", "ui", or "api".`,
|
`top-level array elements must be records with a key named "db", "ui", or "api".`,
|
||||||
`in: ${JSON.stringify(svc)}`,
|
`in: ${JSON.stringify(svc)}`,
|
||||||
].join("\n")
|
].join('\n'),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
);
|
|
||||||
|
const ensureUniq =
|
||||||
|
({ getWith, onConflict, isConflict }) =>
|
||||||
|
array =>
|
||||||
|
pipe(
|
||||||
|
array,
|
||||||
|
Array_.map(getWith),
|
||||||
|
Array_.reduce(Either.right([]), (res, cur) =>
|
||||||
|
Either.flatMap(seen =>
|
||||||
|
isConflict(seen, cur)
|
||||||
|
? Either.left(onConflict(cur))
|
||||||
|
: Either.right([...seen, cur]),
|
||||||
|
)(res),
|
||||||
|
),
|
||||||
|
Either.map(() => array),
|
||||||
|
)
|
||||||
|
const ensureServicesDoNotOverlap = flow(
|
||||||
|
ensureUniq({
|
||||||
|
getWith: s => s.linuxUser.username,
|
||||||
|
onConflict: usr => new Error(`linux_user.username must be unique: ${usr}`),
|
||||||
|
isConflict: (seen, cur) => seen.includes(cur),
|
||||||
|
}),
|
||||||
|
Either.flatMap(
|
||||||
|
ensureUniq({
|
||||||
|
getWith: s => s.network.port,
|
||||||
|
onConflict: port => new Error(`network.port must be unique: ${port}`),
|
||||||
|
isConflict: (seen, cur) => seen.includes(cur),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Either.flatMap(
|
||||||
|
ensureUniq({
|
||||||
|
getWith: s => s.network.domain,
|
||||||
|
onConflict: domain =>
|
||||||
|
new Error(`network.domain must be unique: ${domain}`),
|
||||||
|
isConflict: (seen, cur) => seen.includes(cur),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const parse = flow(
|
const parse = flow(
|
||||||
(cfg) => Either.tryCatch(() => yaml.parse(cfg), id),
|
cfg => Either.tryCatch(() => yaml.parse(cfg), id),
|
||||||
Either.filterOrElse(
|
Either.filterOrElse(
|
||||||
(p) => p instanceof Array,
|
p => p instanceof Array,
|
||||||
() => new Error("config must have top-level array elements")
|
() => new Error('config must have top-level array elements'),
|
||||||
),
|
),
|
||||||
Either.flatMap((svcs) => Either.sequenceArray(svcs.map(parseService)))
|
Either.flatMap(svcs => Either.sequenceArray(svcs.map(parseService))),
|
||||||
);
|
Either.tap(ensureServicesDoNotOverlap),
|
||||||
|
)
|
||||||
|
|
||||||
module.exports = { parse };
|
module.exports = { parse }
|
||||||
|
15
src/index.js
15
src/index.js
@ -1,10 +1,7 @@
|
|||||||
const Config = require("./config.js");
|
const Config = require('./config.js')
|
||||||
const Cmd = require("./cmd.js");
|
const Cmd = require('./cmd.js')
|
||||||
const fs = require("fs");
|
const fs = require('fs')
|
||||||
const {pipe} = require("fp-ts/function");
|
const { pipe } = require('fp-ts/function')
|
||||||
const Either = require("fp-ts/Either");
|
const Either = require('fp-ts/Either')
|
||||||
|
|
||||||
pipe(
|
pipe(Cmd.run('cat ./config.example.yml'), Either.flatMap(Config.parse))
|
||||||
Cmd.run('cat ./config.example.yml'),
|
|
||||||
Either.flatMap(Config.parse),
|
|
||||||
);
|
|
||||||
|
@ -12,6 +12,11 @@ fp-ts@^2.16.0:
|
|||||||
resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.16.0.tgz#64e03314dfc1c7ce5e975d3496ac14bc3eb7f92e"
|
resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.16.0.tgz#64e03314dfc1c7ce5e975d3496ac14bc3eb7f92e"
|
||||||
integrity sha512-bLq+KgbiXdTEoT1zcARrWEpa5z6A/8b7PcDW7Gef3NSisQ+VS7ll2Xbf1E+xsgik0rWub/8u0qP/iTTjj+PhxQ==
|
integrity sha512-bLq+KgbiXdTEoT1zcARrWEpa5z6A/8b7PcDW7Gef3NSisQ+VS7ll2Xbf1E+xsgik0rWub/8u0qP/iTTjj+PhxQ==
|
||||||
|
|
||||||
|
prettier@^2.8.8:
|
||||||
|
version "2.8.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
|
||||||
|
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
|
||||||
|
|
||||||
yaml@^2.3.1:
|
yaml@^2.3.1:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
|
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
|
||||||
|
Loading…
Reference in New Issue
Block a user