onesecret 0.0.0 → 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/README.md +561 -116
- package/bin/help.txt +9 -11
- package/bin/index.js +4 -4
- package/package.json +37 -6
- package/src/accessors.js +70 -0
- package/src/cli.js +105 -43
- package/src/index.js +2 -6
- package/src/secrets.js +76 -20
- package/src/store.js +6 -2
- package/src/write.js +18 -12
- package/src/deploy.js +0 -113
- package/src/fly.js +0 -109
package/src/write.js
CHANGED
|
@@ -172,20 +172,24 @@ const updateLayer = (options, layer, update) => {
|
|
|
172
172
|
return writeLayer(current, update({ ...current.tree }))
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
175
|
+
const set = (options, layer, name, readValue) => {
|
|
176
|
+
const current = readLayer(options, layer)
|
|
177
|
+
const { rules } = encryptionFor(current.file, current.existing)
|
|
178
|
+
assertWritable({ [name]: '' }, [name], rules)
|
|
179
|
+
const previous = current.tree[name]
|
|
180
|
+
const shown =
|
|
181
|
+
previous === undefined || previous === null ? '' : String(previous)
|
|
182
|
+
const value = readValue(shown)
|
|
183
|
+
if (previous !== undefined && previous !== null && shown === value) {
|
|
184
|
+
return { file: current.file, status: 'unchanged' }
|
|
185
|
+
}
|
|
186
|
+
if (value === null || value === '') {
|
|
187
|
+
return { file: current.file, status: 'cancelled' }
|
|
179
188
|
}
|
|
180
|
-
|
|
189
|
+
writeLayer(current, { ...current.tree, [name]: value })
|
|
190
|
+
return { file: current.file, status: 'encrypted' }
|
|
181
191
|
}
|
|
182
192
|
|
|
183
|
-
const set = (options, layer, assignments) =>
|
|
184
|
-
updateLayer(options, layer, tree => ({
|
|
185
|
-
...tree,
|
|
186
|
-
...Object.fromEntries(assignments.map(parseAssignment))
|
|
187
|
-
}))
|
|
188
|
-
|
|
189
193
|
const unset = (options, layer, secretNames) =>
|
|
190
194
|
updateLayer(options, layer, tree => {
|
|
191
195
|
for (const name of secretNames) {
|
|
@@ -234,7 +238,9 @@ const initialRecipients = defaultFile => {
|
|
|
234
238
|
const rule = store.creationRule(defaultFile)
|
|
235
239
|
if (rule) return { recipients: rule.recipients, source: store.SOPS_CONFIG }
|
|
236
240
|
const exported = exportedRecipients()
|
|
237
|
-
if (exported.length > 0) {
|
|
241
|
+
if (exported.length > 0) {
|
|
242
|
+
return { recipients: exported, source: 'SOPS_AGE_KEY' }
|
|
243
|
+
}
|
|
238
244
|
return null
|
|
239
245
|
}
|
|
240
246
|
|
package/src/deploy.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { execFileSync } = require('node:child_process')
|
|
4
|
-
const yaml = require('js-yaml')
|
|
5
|
-
const { loadSecrets, readLayers } = require('./secrets')
|
|
6
|
-
const { withoutAgeKey } = require('./keys')
|
|
7
|
-
const { checksum, toEnv } = require('./format')
|
|
8
|
-
const flyEncoding = require('./fly')
|
|
9
|
-
const { OnesecretError } = require('./error')
|
|
10
|
-
|
|
11
|
-
const MANAGED_BY_LABEL = 'app.kubernetes.io/managed-by'
|
|
12
|
-
|
|
13
|
-
const loadForDeploy = (target, options) => {
|
|
14
|
-
if (!options.env) {
|
|
15
|
-
throw new OnesecretError(
|
|
16
|
-
`${target} needs an explicit env, such as "production"`,
|
|
17
|
-
'EVALUE'
|
|
18
|
-
)
|
|
19
|
-
}
|
|
20
|
-
const layered = readLayers(options)
|
|
21
|
-
const envLayer = layered.layers.find(({ name }) => name === layered.env)
|
|
22
|
-
if (!envLayer.plainFile && !envLayer.encryptedFile) {
|
|
23
|
-
throw new OnesecretError(
|
|
24
|
-
`${target} found no "${layered.env}" layer in ${layered.dir}, pass --env default to deploy the default layer alone`,
|
|
25
|
-
'EVALUE'
|
|
26
|
-
)
|
|
27
|
-
}
|
|
28
|
-
return loadSecrets(layered)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const run = (binary, args, input) => {
|
|
32
|
-
try {
|
|
33
|
-
return execFileSync(binary, args, {
|
|
34
|
-
input,
|
|
35
|
-
encoding: 'utf8',
|
|
36
|
-
env: withoutAgeKey(process.env),
|
|
37
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
38
|
-
})
|
|
39
|
-
} catch (error) {
|
|
40
|
-
const output = [error.stdout, error.stderr].filter(Boolean).join('\n')
|
|
41
|
-
const redacted = output.replace(/=.*/g, '=***').trim()
|
|
42
|
-
const reason =
|
|
43
|
-
error.code === 'ENOENT'
|
|
44
|
-
? `${binary} is not installed`
|
|
45
|
-
: redacted || error.message
|
|
46
|
-
throw new OnesecretError(
|
|
47
|
-
`${binary} ${args.slice(0, 3).join(' ')} failed: ${reason}`,
|
|
48
|
-
'EDEPLOY'
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const kubernetesManifest = ({ name, namespace, secrets }) =>
|
|
54
|
-
yaml.dump(
|
|
55
|
-
{
|
|
56
|
-
apiVersion: 'v1',
|
|
57
|
-
kind: 'Secret',
|
|
58
|
-
metadata: {
|
|
59
|
-
name,
|
|
60
|
-
...(namespace && { namespace }),
|
|
61
|
-
labels: { [MANAGED_BY_LABEL]: 'onesecret' }
|
|
62
|
-
},
|
|
63
|
-
type: 'Opaque',
|
|
64
|
-
data: Object.fromEntries(
|
|
65
|
-
Object.entries(toEnv(secrets)).map(([key, value]) => [
|
|
66
|
-
key,
|
|
67
|
-
Buffer.from(value).toString('base64')
|
|
68
|
-
])
|
|
69
|
-
)
|
|
70
|
-
},
|
|
71
|
-
{ lineWidth: -1 }
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
const kubernetes = ({
|
|
75
|
-
name,
|
|
76
|
-
namespace,
|
|
77
|
-
context,
|
|
78
|
-
kubectl = 'kubectl',
|
|
79
|
-
args = [],
|
|
80
|
-
...options
|
|
81
|
-
}) => {
|
|
82
|
-
if (!name) {
|
|
83
|
-
throw new OnesecretError('kubernetes needs the Secret name', 'EVALUE')
|
|
84
|
-
}
|
|
85
|
-
const secrets = loadForDeploy('kubernetes', options)
|
|
86
|
-
const contextArgs = context ? ['--context', context] : []
|
|
87
|
-
const output = run(
|
|
88
|
-
kubectl,
|
|
89
|
-
[...contextArgs, ...args, 'apply', '-f', '-'],
|
|
90
|
-
kubernetesManifest({ name, namespace, secrets })
|
|
91
|
-
)
|
|
92
|
-
return { checksum: checksum(secrets), output: output.trim() }
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const flyImport = secrets => flyEncoding.encode(toEnv(secrets))
|
|
96
|
-
|
|
97
|
-
const fly = ({ app, stage = true, flyctl = 'fly', ...options }) => {
|
|
98
|
-
if (!app) throw new OnesecretError('fly needs the app name', 'EVALUE')
|
|
99
|
-
const secrets = loadForDeploy('fly', options)
|
|
100
|
-
const stageArgs = stage ? ['--stage'] : []
|
|
101
|
-
const output = run(
|
|
102
|
-
flyctl,
|
|
103
|
-
['secrets', 'import', ...stageArgs, '--app', app],
|
|
104
|
-
flyImport(secrets)
|
|
105
|
-
)
|
|
106
|
-
const summary = output
|
|
107
|
-
.split('\n')
|
|
108
|
-
.filter(line => line.trim() && !line.includes('='))
|
|
109
|
-
.join('\n')
|
|
110
|
-
return { checksum: checksum(secrets), output: summary }
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
module.exports = { fly, flyImport, kubernetes, kubernetesManifest }
|
package/src/fly.js
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { isDeepStrictEqual } = require('node:util')
|
|
4
|
-
const { OnesecretError } = require('./error')
|
|
5
|
-
|
|
6
|
-
const TRIPLE_QUOTE = '"""'
|
|
7
|
-
const SCANNER_MAX_LINE_BYTES = 64 * 1024 - 1
|
|
8
|
-
|
|
9
|
-
const scanLines = text => {
|
|
10
|
-
const lines = text.split('\n')
|
|
11
|
-
if (lines[lines.length - 1] === '') lines.pop()
|
|
12
|
-
return lines.map(line => line.replace(/\r$/, ''))
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const countQuotes = text => text.split('"').length - 1
|
|
16
|
-
|
|
17
|
-
const parseSinglelineValue = line => {
|
|
18
|
-
const separator = line.indexOf('=')
|
|
19
|
-
let value = line.slice(separator + 1).replace(/^ +/, '')
|
|
20
|
-
const hash = value.indexOf('#')
|
|
21
|
-
if (hash !== -1 && countQuotes(value.slice(0, hash)) % 2 === 0) { value = value.slice(0, hash).replace(/ +$/, '') }
|
|
22
|
-
return { key: line.slice(0, separator).trim(), value }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const parse = text => {
|
|
26
|
-
const secrets = {}
|
|
27
|
-
let multiline = null
|
|
28
|
-
for (const line of scanLines(text)) {
|
|
29
|
-
if (multiline) {
|
|
30
|
-
if (line.endsWith(TRIPLE_QUOTE)) {
|
|
31
|
-
secrets[multiline.key] =
|
|
32
|
-
multiline.value + line.slice(0, -TRIPLE_QUOTE.length)
|
|
33
|
-
multiline = null
|
|
34
|
-
} else {
|
|
35
|
-
multiline.value += `${line}\n`
|
|
36
|
-
}
|
|
37
|
-
continue
|
|
38
|
-
}
|
|
39
|
-
if (line.startsWith('#') || line.trim() === '') continue
|
|
40
|
-
if (!line.includes('=')) throw new Error(`"${line}" is not NAME=VALUE`)
|
|
41
|
-
const { key, value } = parseSinglelineValue(line)
|
|
42
|
-
if (
|
|
43
|
-
value.startsWith(TRIPLE_QUOTE) &&
|
|
44
|
-
value.endsWith(TRIPLE_QUOTE) &&
|
|
45
|
-
value.length >= 6
|
|
46
|
-
) {
|
|
47
|
-
secrets[key] = value.slice(3, -3)
|
|
48
|
-
} else if (value.startsWith(TRIPLE_QUOTE)) {
|
|
49
|
-
multiline = { key, value: `${value.slice(3)}\n` }
|
|
50
|
-
} else if (
|
|
51
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
52
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
53
|
-
) {
|
|
54
|
-
if (value.length < 2) throw new Error('flyctl panics on a lone quote')
|
|
55
|
-
secrets[key] = value.slice(1, -1)
|
|
56
|
-
} else {
|
|
57
|
-
secrets[key] = value
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return secrets
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const candidates = value => [
|
|
64
|
-
value,
|
|
65
|
-
`${TRIPLE_QUOTE}${value}${TRIPLE_QUOTE}`,
|
|
66
|
-
`"${value}"`,
|
|
67
|
-
`'${value}'`
|
|
68
|
-
]
|
|
69
|
-
|
|
70
|
-
const readsBackAs = (text, expected) => {
|
|
71
|
-
try {
|
|
72
|
-
return isDeepStrictEqual(parse(text), expected)
|
|
73
|
-
} catch {
|
|
74
|
-
return false
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const isWithinScanner = text =>
|
|
79
|
-
scanLines(text).every(
|
|
80
|
-
line => Buffer.byteLength(line) <= SCANNER_MAX_LINE_BYTES
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
const encodeLine = (name, value) => {
|
|
84
|
-
const line = candidates(value)
|
|
85
|
-
.map(encoded => `${name}=${encoded}\n`)
|
|
86
|
-
.find(line => isWithinScanner(line) && readsBackAs(line, { [name]: value }))
|
|
87
|
-
if (!line) {
|
|
88
|
-
throw new OnesecretError(
|
|
89
|
-
`"${name}" cannot be imported into fly without changing its value`,
|
|
90
|
-
'EVALUE'
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
return line
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const encode = secrets => {
|
|
97
|
-
const body = Object.entries(secrets)
|
|
98
|
-
.map(([name, value]) => encodeLine(name, value))
|
|
99
|
-
.join('')
|
|
100
|
-
if (!readsBackAs(body, secrets)) {
|
|
101
|
-
throw new OnesecretError(
|
|
102
|
-
'the fly import does not read back to the same secrets',
|
|
103
|
-
'EVALUE'
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
|
-
return body
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
module.exports = { encode, parse }
|