Skip to content
This repository was archived by the owner on Feb 12, 2024. It is now read-only.

Commit 7314f0d

Browse files
dirkmcachingbrain
authored andcommitted
feat: Add config profile endpoint and CLI (#2165)
* feat: config profile * fix: increase command count * feat: ipfs init --profile option * chore: update dependencies License: MIT Signed-off-by: Alan Shaw <alan.shaw@protocol.ai> * fix: make sure default-config still works * chore: add http api tests * chore: address PR comments * chore: fix linting * test: let internals of config profiles be internal * chore: add test for listing profiles * chore: turn profile list into list outside of core * chore: expose profile list over http * fix: use chai exported from interface tests Workaround for chaijs/chai#1298 * chore: fix linting * chore: update deps and make keys agree with spec * chore: update interface tests * chore: udpate test skips/includes * chore: fix up interface tests
1 parent 8c01259 commit 7314f0d

File tree

20 files changed

+467
-48
lines changed

20 files changed

+467
-48
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
<p align="center">
1010
<a href="http://protocol.ai"><img src="https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat" /></a>
1111
<a href="http://ipfs.io/"><img src="https://img.shields.io/badge/project-IPFS-blue.svg?style=flat" /></a>
12-
</p>
12+
</p>
1313

14-
<p align="center">
14+
<p align="center">
1515
<a href="https://riot.im/app/#/room/#ipfs-dev:matrix.org"><img src="https://img.shields.io/badge/matrix-%23ipfs%3Amatrix.org-blue.svg?style=flat" /> </a>
1616
<a href="http://webchat.freenode.net/?channels=%23ipfs"><img src="https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat" /></a>
1717
<a href="https://discord.gg/24fmuwR"><img src="https://img.shields.io/discord/475789330380488707?color=blueviolet&label=discord&style=flat" /></a>

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
"ipfs-bitswap": "^0.26.0",
100100
"ipfs-block": "~0.8.1",
101101
"ipfs-block-service": "~0.16.0",
102-
"ipfs-http-client": "^38.0.1",
102+
"ipfs-http-client": "^38.1.0",
103103
"ipfs-http-response": "~0.3.1",
104104
"ipfs-mfs": "^0.13.0",
105105
"ipfs-multipart": "^0.2.0",
@@ -124,6 +124,7 @@
124124
"iso-url": "~0.4.6",
125125
"it-pipe": "^1.0.1",
126126
"it-to-stream": "^0.1.1",
127+
"jsondiffpatch": "~0.3.11",
127128
"just-safe-set": "^2.1.0",
128129
"kind-of": "^6.0.2",
129130
"ky": "^0.14.0",
@@ -202,7 +203,7 @@
202203
"execa": "^2.0.4",
203204
"form-data": "^2.5.1",
204205
"hat": "0.0.3",
205-
"interface-ipfs-core": "^0.115.3",
206+
"interface-ipfs-core": "^0.117.2",
206207
"ipfs-interop": "~0.1.0",
207208
"ipfsd-ctl": "^0.47.2",
208209
"libp2p-websocket-star": "~0.10.2",

src/cli/commands/config/profile.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict'
2+
3+
module.exports = {
4+
command: 'profile <command>',
5+
6+
description: 'Interact with config profiles.',
7+
8+
builder (yargs) {
9+
return yargs
10+
.commandDir('profile')
11+
},
12+
13+
handler (argv) {
14+
}
15+
}
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict'
2+
3+
const JSONDiff = require('jsondiffpatch')
4+
5+
module.exports = {
6+
command: 'apply <profile>',
7+
8+
describe: 'Apply profile to config',
9+
10+
builder: {
11+
'dry-run': {
12+
type: 'boolean',
13+
describe: 'print difference between the current config and the config that would be generated.'
14+
}
15+
},
16+
17+
handler (argv) {
18+
argv.resolve((async () => {
19+
const ipfs = await argv.getIpfs()
20+
const diff = await ipfs.config.profiles.apply(argv.profile, { dryRun: argv.dryRun })
21+
const delta = JSONDiff.diff(diff.original, diff.updated)
22+
const res = JSONDiff.formatters.console.format(delta, diff.original)
23+
24+
if (res) {
25+
argv.print(res)
26+
27+
if (ipfs.send) {
28+
argv.print('\nThe IPFS daemon is running in the background, you may need to restart it for changes to take effect.')
29+
}
30+
} else {
31+
argv.print(`IPFS config already contains the settings from the '${argv.profile}' profile`)
32+
}
33+
})())
34+
}
35+
}

src/cli/commands/config/profile/ls.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict'
2+
3+
module.exports = {
4+
command: 'ls',
5+
6+
describe: 'List available config profiles',
7+
8+
builder: {},
9+
10+
handler (argv) {
11+
argv.resolve(
12+
(async () => {
13+
const ipfs = await argv.getIpfs()
14+
15+
for (const profile of await ipfs.config.profiles.list()) {
16+
argv.print(`${profile.name}:\n ${profile.description}`)
17+
}
18+
})()
19+
)
20+
}
21+
}

src/cli/commands/init.js

+14-2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ const { ipfsPathHelp } = require('../utils')
66

77
module.exports = {
88
command: 'init [default-config] [options]',
9-
describe: 'Initialize a local IPFS node',
9+
describe: 'Initialize a local IPFS node\n\n' +
10+
'If you are going to run IPFS in a server environment, you may want to ' +
11+
'initialize it using the \'server\' profile.\n\n' +
12+
'For the list of available profiles run `jsipfs config profile ls`',
1013
builder (yargs) {
1114
return yargs
1215
.epilog(ipfsPathHelp)
1316
.positional('default-config', {
14-
describe: 'Initialize with the given configuration. Path to the config file. Check https://github.com/ipfs/js-ipfs#optionsconfig',
17+
describe: 'Node config, this should be a path to a file or JSON and will be merged with the default config. See https://github.com/ipfs/js-ipfs#optionsconfig',
1518
type: 'string'
1619
})
1720
.option('bits', {
@@ -30,6 +33,14 @@ module.exports = {
3033
type: 'string',
3134
describe: 'Pre-generated private key to use for the repo'
3235
})
36+
.option('profile', {
37+
alias: 'p',
38+
type: 'string',
39+
describe: 'Apply profile settings to config. Multiple profiles can be separated by \',\'',
40+
coerce: (value) => {
41+
return (value || '').split(',')
42+
}
43+
})
3344
},
3445

3546
handler (argv) {
@@ -66,6 +77,7 @@ module.exports = {
6677
bits: argv.bits,
6778
privateKey: argv.privateKey,
6879
emptyRepo: argv.emptyRepo,
80+
profiles: argv.profile,
6981
pass: argv.pass,
7082
log: argv.print
7183
})

src/core/components/config.js

+119-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,129 @@
11
'use strict'
22

33
const callbackify = require('callbackify')
4+
const getDefaultConfig = require('../runtime/config-nodejs.js')
5+
const log = require('debug')('ipfs:core:config')
46

57
module.exports = function config (self) {
68
return {
79
get: callbackify.variadic(self._repo.config.get),
810
set: callbackify(self._repo.config.set),
9-
replace: callbackify.variadic(self._repo.config.set)
11+
replace: callbackify.variadic(self._repo.config.set),
12+
profiles: {
13+
apply: callbackify.variadic(applyProfile),
14+
list: callbackify.variadic(listProfiles)
15+
}
16+
}
17+
18+
async function applyProfile (profileName, opts) {
19+
opts = opts || {}
20+
const { dryRun } = opts
21+
22+
const profile = profiles[profileName]
23+
24+
if (!profile) {
25+
throw new Error(`No profile with name '${profileName}' exists`)
26+
}
27+
28+
try {
29+
const oldCfg = await self.config.get()
30+
let newCfg = JSON.parse(JSON.stringify(oldCfg)) // clone
31+
newCfg = profile.transform(newCfg)
32+
33+
if (!dryRun) {
34+
await self.config.replace(newCfg)
35+
}
36+
37+
// Scrub private key from output
38+
delete oldCfg.Identity.PrivKey
39+
delete newCfg.Identity.PrivKey
40+
41+
return { original: oldCfg, updated: newCfg }
42+
} catch (err) {
43+
log(err)
44+
45+
throw new Error(`Could not apply profile '${profileName}' to config: ${err.message}`)
46+
}
47+
}
48+
}
49+
50+
async function listProfiles (options) { // eslint-disable-line require-await
51+
return Object.keys(profiles).map(name => ({
52+
name,
53+
description: profiles[name].description
54+
}))
55+
}
56+
57+
const profiles = {
58+
server: {
59+
description: 'Disables local host discovery - recommended when running IPFS on machines with public IPv4 addresses.',
60+
transform: (config) => {
61+
config.Discovery.MDNS.Enabled = false
62+
config.Discovery.webRTCStar.Enabled = false
63+
64+
return config
65+
}
66+
},
67+
'local-discovery': {
68+
description: 'Enables local host discovery - inverse of "server" profile.',
69+
transform: (config) => {
70+
config.Discovery.MDNS.Enabled = true
71+
config.Discovery.webRTCStar.Enabled = true
72+
73+
return config
74+
}
75+
},
76+
lowpower: {
77+
description: 'Reduces daemon overhead on the system - recommended for low power systems.',
78+
transform: (config) => {
79+
config.Swarm = config.Swarm || {}
80+
config.Swarm.ConnMgr = config.Swarm.ConnMgr || {}
81+
config.Swarm.ConnMgr.LowWater = 20
82+
config.Swarm.ConnMgr.HighWater = 40
83+
84+
return config
85+
}
86+
},
87+
'default-power': {
88+
description: 'Inverse of "lowpower" profile.',
89+
transform: (config) => {
90+
const defaultConfig = getDefaultConfig()
91+
92+
config.Swarm = defaultConfig.Swarm
93+
94+
return config
95+
}
96+
},
97+
test: {
98+
description: 'Reduces external interference of IPFS daemon - for running the daemon in test environments.',
99+
transform: (config) => {
100+
const defaultConfig = getDefaultConfig()
101+
102+
config.Addresses.API = defaultConfig.Addresses.API ? '/ip4/127.0.0.1/tcp/0' : ''
103+
config.Addresses.Gateway = defaultConfig.Addresses.Gateway ? '/ip4/127.0.0.1/tcp/0' : ''
104+
config.Addresses.Swarm = defaultConfig.Addresses.Swarm.length ? ['/ip4/127.0.0.1/tcp/0'] : []
105+
config.Bootstrap = []
106+
config.Discovery.MDNS.Enabled = false
107+
config.Discovery.webRTCStar.Enabled = false
108+
109+
return config
110+
}
111+
},
112+
'default-networking': {
113+
description: 'Restores default network settings - inverse of "test" profile.',
114+
transform: (config) => {
115+
const defaultConfig = getDefaultConfig()
116+
117+
config.Addresses.API = defaultConfig.Addresses.API
118+
config.Addresses.Gateway = defaultConfig.Addresses.Gateway
119+
config.Addresses.Swarm = defaultConfig.Addresses.Swarm
120+
config.Bootstrap = defaultConfig.Bootstrap
121+
config.Discovery.MDNS.Enabled = defaultConfig.Discovery.MDNS.Enabled
122+
config.Discovery.webRTCStar.Enabled = defaultConfig.Discovery.webRTCStar.Enabled
123+
124+
return config
125+
}
10126
}
11127
}
128+
129+
module.exports.profiles = profiles

src/core/components/files-mfs.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ module.exports = (/** @type { import("../index") } */ ipfs) => {
4646

4747
if (paths.length) {
4848
const options = args[args.length - 1]
49-
if (options.preload !== false) {
49+
if (options && options.preload !== false) {
5050
paths.forEach(path => ipfs._preload(path))
5151
}
5252
}

src/core/components/init.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const IPNS = require('../ipns')
1616
const OfflineDatastore = require('../ipns/routing/offline-datastore')
1717

1818
const addDefaultAssets = require('./init-assets')
19+
const { profiles } = require('./config')
1920

2021
function createPeerId (self, opts) {
2122
if (opts.privateKey) {
@@ -55,6 +56,8 @@ async function createRepo (self, opts) {
5556

5657
const config = mergeOptions(defaultConfig(), self._options.config)
5758

59+
applyProfile(self, config, opts)
60+
5861
// Verify repo does not exist yet
5962
const exists = await self._repo.exists()
6063
self.log('repo exists?', exists)
@@ -128,8 +131,24 @@ async function addRepoAssets (self, privateKey, opts) {
128131
}
129132
}
130133

134+
// Apply profiles (eg "server,lowpower") to config
135+
function applyProfile (self, config, opts) {
136+
if (opts.profiles) {
137+
for (const name of opts.profiles) {
138+
const profile = profiles[name]
139+
140+
if (!profile) {
141+
throw new Error(`Could not find profile with name '${name}'`)
142+
}
143+
144+
self.log(`applying profile ${name}`)
145+
profile.transform(config)
146+
}
147+
}
148+
}
149+
131150
module.exports = function init (self) {
132-
return callbackify(async (opts) => {
151+
return callbackify.variadic(async (opts) => {
133152
opts = opts || {}
134153

135154
await createRepo(self, opts)

src/http/api/resources/config.js

+52
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const log = debug('ipfs:http-api:config')
77
log.error = debug('ipfs:http-api:config:error')
88
const multipart = require('ipfs-multipart')
99
const Boom = require('@hapi/boom')
10+
const Joi = require('@hapi/joi')
11+
const { profiles } = require('../../../core/components/config')
1012
const all = require('async-iterator-all')
1113

1214
exports.getOrSet = {
@@ -163,3 +165,53 @@ exports.replace = {
163165
return h.response()
164166
}
165167
}
168+
169+
exports.profiles = {
170+
apply: {
171+
validate: {
172+
query: Joi.object().keys({
173+
'dry-run': Joi.boolean().default(false)
174+
}).unknown()
175+
},
176+
177+
// pre request handler that parses the args and returns `profile` which is assigned to `request.pre.args`
178+
parseArgs: function (request, h) {
179+
if (!request.query.arg) {
180+
throw Boom.badRequest("Argument 'profile' is required")
181+
}
182+
183+
if (!profiles[request.query.arg]) {
184+
throw Boom.badRequest("Argument 'profile' is not a valid profile name")
185+
}
186+
187+
return { profile: request.query.arg }
188+
},
189+
190+
handler: async function (request, h) {
191+
const { ipfs } = request.server.app
192+
const { profile } = request.pre.args
193+
const dryRun = request.query['dry-run']
194+
195+
try {
196+
const diff = await ipfs.config.profiles.apply(profile, { dryRun })
197+
198+
return h.response({ OldCfg: diff.original, NewCfg: diff.updated })
199+
} catch (err) {
200+
throw Boom.boomify(err, { message: 'Failed to apply profile' })
201+
}
202+
}
203+
},
204+
list: {
205+
handler: async function (request, h) {
206+
const { ipfs } = request.server.app
207+
const list = await ipfs.config.profiles.list()
208+
209+
return h.response(
210+
list.map(profile => ({
211+
Name: profile.name,
212+
Description: profile.description
213+
}))
214+
)
215+
}
216+
}
217+
}

0 commit comments

Comments
 (0)