@uknows/setup 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +142 -2
- package/bin/uknows-setup.mjs +14 -0
- package/lib/check.mjs +211 -0
- package/lib/cli.mjs +166 -0
- package/lib/contract-docs.mjs +1154 -0
- package/lib/contract.mjs +66 -0
- package/lib/detect.mjs +575 -0
- package/lib/diff.mjs +147 -0
- package/lib/doc.mjs +98 -0
- package/lib/dockerfile.mjs +372 -0
- package/lib/endpoint.mjs +50 -0
- package/lib/engine.mjs +607 -0
- package/lib/existing.mjs +168 -0
- package/lib/git.mjs +47 -0
- package/lib/hosts.mjs +145 -0
- package/lib/manifest.mjs +112 -0
- package/lib/settings.mjs +280 -0
- package/lib/stacks/dotnet.mjs +165 -0
- package/lib/stacks/guided.mjs +104 -0
- package/lib/stacks/index.mjs +10 -0
- package/lib/stacks/java.mjs +56 -0
- package/lib/stacks/k8s.mjs +31 -0
- package/lib/stacks/node.mjs +235 -0
- package/lib/stacks/otel.mjs +11 -0
- package/lib/stacks/python-bootstrap.mjs +51 -0
- package/lib/stacks/python.mjs +434 -0
- package/lib/types.mjs +161 -0
- package/lib/ui.mjs +106 -0
- package/lib/workspace.mjs +198 -0
- package/package.json +42 -4
package/lib/cli.mjs
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Command line: parse the arguments, then run setup, remove or check.
|
|
3
|
+
* A hand-written parser, because util.parseArgs is experimental on Node 18.
|
|
4
|
+
*/
|
|
5
|
+
import { statSync } from 'node:fs'
|
|
6
|
+
import { resolve } from 'node:path'
|
|
7
|
+
import { runCheck } from './check.mjs'
|
|
8
|
+
import { CLI, DOCS_URL, INGEST_ENDPOINT, SECRET, SITE_URL } from './contract.mjs'
|
|
9
|
+
import { toServiceName } from './detect.mjs'
|
|
10
|
+
import { ingestEndpoint, siteUrl } from './endpoint.mjs'
|
|
11
|
+
import { runRemove, runSetup } from './engine.mjs'
|
|
12
|
+
import { TOOL, VERSION } from './manifest.mjs'
|
|
13
|
+
import { UsageError, createUI } from './ui.mjs'
|
|
14
|
+
|
|
15
|
+
const FLAGS = { '--dry-run': 'dryRun', '--yes': 'yes', '-y': 'yes', '--all': 'all', '--json': 'json', '--force': 'force',
|
|
16
|
+
'--check': 'check', '--help': 'help', '-h': 'help', '--version': 'version' }
|
|
17
|
+
const VALUES = { '--path': 'path', '--service-name': 'serviceName' }
|
|
18
|
+
const COMMANDS = { setup: ['dryRun', 'yes', 'path', 'all', 'json', 'serviceName', 'force'], remove: ['yes', 'path', 'json', 'force'], check: ['json'] }
|
|
19
|
+
|
|
20
|
+
export const HELP = `uknows-setup adds OpenTelemetry to your app, so it sends traces to UKNOWS.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
npx @uknows/setup [setup] [options] Find your app, show the plan, then apply it.
|
|
24
|
+
npx @uknows/setup remove [options] Undo what setup changed.
|
|
25
|
+
npx @uknows/setup check Send 1 test span with the key in ${SECRET.name}.
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--dry-run Show the plan. Change nothing.
|
|
29
|
+
--yes, -y Apply without asking. Never asks any question.
|
|
30
|
+
--path <folder> The folder to set up. The default is this folder.
|
|
31
|
+
--all Set up every service folder. Each one gets its own OTEL_SERVICE_NAME.
|
|
32
|
+
--service-name <name> The name of the service in UKNOWS.
|
|
33
|
+
--json Print a JSON report. Without --yes, setup only plans.
|
|
34
|
+
--force Run even when the repo has changes that are not committed.
|
|
35
|
+
--check The same as the check command.
|
|
36
|
+
--help, -h Show this help.
|
|
37
|
+
--version Show the version.
|
|
38
|
+
|
|
39
|
+
Exit codes:
|
|
40
|
+
0 Done, or nothing to do.
|
|
41
|
+
1 Stopped or failed. Nothing changed.
|
|
42
|
+
2 Usage error, or you need to pick a service.
|
|
43
|
+
3 You need to do 1 or more steps by hand.
|
|
44
|
+
|
|
45
|
+
Environment:
|
|
46
|
+
${SECRET.name}
|
|
47
|
+
Your key, for check, as ${SECRET.value}. Set it only in your terminal.
|
|
48
|
+
${CLI.siteEnv}
|
|
49
|
+
A test UKNOWS site, for check and the docs links. The default is ${SITE_URL}.
|
|
50
|
+
${CLI.endpointEnv}
|
|
51
|
+
A test ingest address, for setup and check. The default is ${INGEST_ENDPOINT}.
|
|
52
|
+
|
|
53
|
+
After setup, the tool asks for your key to check it. The key does not show on the screen.
|
|
54
|
+
This tool never writes your key to a file.
|
|
55
|
+
Docs: ${DOCS_URL}
|
|
56
|
+
`
|
|
57
|
+
|
|
58
|
+
/** @returns {{command: 'setup'|'remove'|'check', dryRun: boolean, yes: boolean, all: boolean, json: boolean, force: boolean, help: boolean, version: boolean, path: string|null, serviceName: string|null}} */
|
|
59
|
+
export function parseArgs(argv) {
|
|
60
|
+
const opts = { command: null, dryRun: false, yes: false, all: false, json: false, force: false, help: false, version: false, path: null, serviceName: null }
|
|
61
|
+
const given = []
|
|
62
|
+
let check = false
|
|
63
|
+
for (let i = 0; i < argv.length; i++) {
|
|
64
|
+
const arg = argv[i]
|
|
65
|
+
if (!arg.startsWith('-') || arg === '-') {
|
|
66
|
+
if (opts.command) throw new UsageError(`Unexpected argument: ${arg}.`)
|
|
67
|
+
if (!(arg in COMMANDS)) throw new UsageError(`Unknown command: ${arg}.`)
|
|
68
|
+
opts.command = arg
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
const eq = arg.startsWith('--') ? arg.indexOf('=') : -1
|
|
72
|
+
const flag = eq > 0 ? arg.slice(0, eq) : arg
|
|
73
|
+
if (flag in VALUES) {
|
|
74
|
+
let value = eq > 0 ? arg.slice(eq + 1) : argv[i + 1]
|
|
75
|
+
if (eq < 0) {
|
|
76
|
+
if (value === undefined || value.startsWith('-')) throw new UsageError(`${flag} needs a value.`)
|
|
77
|
+
i++
|
|
78
|
+
}
|
|
79
|
+
if (!value) throw new UsageError(`${flag} needs a value.`)
|
|
80
|
+
opts[VALUES[flag]] = value
|
|
81
|
+
given.push([VALUES[flag], flag])
|
|
82
|
+
} else if (flag in FLAGS) {
|
|
83
|
+
if (eq > 0) throw new UsageError(`${flag} does not take a value.`)
|
|
84
|
+
if (FLAGS[flag] === 'check') check = true
|
|
85
|
+
else opts[FLAGS[flag]] = true
|
|
86
|
+
if (!['help', 'version', 'check'].includes(FLAGS[flag])) given.push([FLAGS[flag], flag])
|
|
87
|
+
} else throw new UsageError(`Unknown option: ${flag}.`)
|
|
88
|
+
}
|
|
89
|
+
if (check && opts.command && opts.command !== 'check') throw new UsageError(`--check does not work with ${opts.command}.`)
|
|
90
|
+
opts.command = check ? 'check' : opts.command ?? 'setup'
|
|
91
|
+
for (const [key, flag] of given) {
|
|
92
|
+
if (!COMMANDS[opts.command].includes(key)) throw new UsageError(`${flag} does not work with ${opts.command}.`)
|
|
93
|
+
}
|
|
94
|
+
return opts
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Run the CLI. Returns the exit code.
|
|
99
|
+
* @param {string[]} argv
|
|
100
|
+
* @param {{stdin?: any, stdout?: any, stderr?: any, cwd?: string, env?: Record<string, string|undefined>, interactive?: boolean, deps?: object}} [io]
|
|
101
|
+
*/
|
|
102
|
+
export async function main(argv, io = {}) {
|
|
103
|
+
const { stdin = process.stdin, stdout = process.stdout, stderr = process.stderr, cwd = process.cwd(), env = process.env, deps = {} } = io
|
|
104
|
+
const json = argv.includes('--json')
|
|
105
|
+
const usage = (message) => {
|
|
106
|
+
stderr.write(`${message}\nRun uknows-setup --help.\n`)
|
|
107
|
+
if (json) stdout.write(`${JSON.stringify({ schema: 1, tool: TOOL, version: VERSION, command: null, status: 'usage-error', exitCode: 2, problems: [{ code: 'usage', message }] }, null, 2)}\n`)
|
|
108
|
+
return 2
|
|
109
|
+
}
|
|
110
|
+
let args
|
|
111
|
+
try {
|
|
112
|
+
args = parseArgs(argv)
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error instanceof UsageError) return usage(error.message)
|
|
115
|
+
throw error
|
|
116
|
+
}
|
|
117
|
+
if (args.help) { stdout.write(HELP); return 0 }
|
|
118
|
+
if (args.version) { stdout.write(`${VERSION}\n`); return 0 }
|
|
119
|
+
if (args.command === 'check') return check(args, { stdout, stderr, env }, deps.runCheck ?? runCheck)
|
|
120
|
+
|
|
121
|
+
// setup writes the same ingest address that check sends to.
|
|
122
|
+
const endpoint = ingestEndpoint(env)
|
|
123
|
+
if (!endpoint) return usage(`${CLI.endpointEnv} must be an https address.`)
|
|
124
|
+
if (!siteUrl(env)) return usage(`${CLI.siteEnv} must be an https address with no path, for example https://uknows.example.com.`)
|
|
125
|
+
const root = resolve(cwd, args.path ?? '.')
|
|
126
|
+
let folder = false
|
|
127
|
+
try { folder = statSync(root).isDirectory() } catch { /* missing */ }
|
|
128
|
+
if (!folder) return usage(`${args.path ?? root} is not a folder.`)
|
|
129
|
+
if (args.serviceName !== null && (deps.toServiceName ?? toServiceName)(args.serviceName) !== args.serviceName) {
|
|
130
|
+
return usage(`${args.serviceName} is not a valid service name. Use lowercase letters, digits and -, up to 63 characters.`)
|
|
131
|
+
}
|
|
132
|
+
const ui = createUI({ stdin, stdout, stderr, json: args.json, interactive: io.interactive })
|
|
133
|
+
try {
|
|
134
|
+
const { code, report } = await (args.command === 'remove' ? runRemove : runSetup)({ ...args, root, endpoint, env }, { ...deps, ui })
|
|
135
|
+
if (args.json) stdout.write(`${JSON.stringify(report, null, 2)}\n`)
|
|
136
|
+
return code
|
|
137
|
+
} finally {
|
|
138
|
+
ui.close()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function check(args, { stdout, stderr, env }, run) {
|
|
143
|
+
const result = await run({ env })
|
|
144
|
+
const status = result.exit === 2 ? 'no-key' : result.ok ? 'reached' : result.status === null ? 'network-error' : 'rejected'
|
|
145
|
+
const lines = redact(result.lines, env[SECRET.name])
|
|
146
|
+
if (args.json) {
|
|
147
|
+
stdout.write(`${JSON.stringify({ schema: 1, tool: TOOL, version: VERSION, command: 'check', status, exitCode: result.exit,
|
|
148
|
+
httpStatus: result.status, workspace: result.workspace, environment: result.environment, lines }, null, 2)}\n`)
|
|
149
|
+
} else {
|
|
150
|
+
(result.exit === 1 ? stderr : stdout).write(lines.map((line) => `${line}\n`).join(''))
|
|
151
|
+
}
|
|
152
|
+
return result.exit
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Belt and braces: take every header value out of the lines, even if the server echoed it. */
|
|
156
|
+
function redact(lines, headers) {
|
|
157
|
+
const secrets = []
|
|
158
|
+
for (const pair of String(headers ?? '').split(',')) {
|
|
159
|
+
const value = pair.slice(pair.indexOf('=') + 1).trim()
|
|
160
|
+
if (pair.includes('=') && value.length >= 4) {
|
|
161
|
+
secrets.push(value)
|
|
162
|
+
try { secrets.push(decodeURIComponent(value)) } catch { /* not encoded */ }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return lines.map((line) => secrets.reduce((out, secret) => out.split(secret).join('***'), String(line)))
|
|
166
|
+
}
|