Compare commits

..

2 Commits

Author SHA1 Message Date
AJ ONeal f913f8d193 wip: update docs 2019-11-30 16:52:29 -07:00
AJ ONeal 08743cc6ba wip: update docs (and helpers) 2019-11-30 16:52:16 -07:00
4 changed files with 152 additions and 222 deletions

View File

@ -1,4 +1,36 @@
# Migrating from Greenlock v2 to v3 # Migrating Guide
Greenlock v4 is the current version.
# v3 to v4
v4 is a very minor, but breaking, change from v3
### `configFile` is replaced with `configDir`
The default config file `./greenlock.json` is now `./greenlock.d/config.json`.
This was change was mode to eliminate unnecessary configuration that was inadvertantly introduced in v3.
### `.greenlockrc` is auto-generated
`.greenlockrc` exists for the sake of tooling - so that the CLI, Web API, and your code naturally stay in sync.
It looks like this:
```json
{
"manager": {
"module": "@greenlock/manager"
},
"configDir": "./greenlock.d"
}
```
If you deploy to a read-only filesystem, it is best that you create the `.greenlockrc` file as part
of your image and use that rather than including any configuration in your code.
# v2 to v4
**Greenlock Express** uses Greenlock directly, the same as before. **Greenlock Express** uses Greenlock directly, the same as before.
@ -195,11 +227,11 @@ as well as a set of callbacks for easy configurability.
### Default Manager ### Default Manager
The default manager is `greenlock-manager-fs` and the default `configFile` is `~/.config/greenlock/manager.json`. The default manager is `@greenlock/manager` and the default `configDir` is `./.greenlock.d`.
The config file should look something like this: The config file should look something like this:
`~/.config/greenlock/manager.json`: `./greenlock.d/config.json`:
```json ```json
{ {
@ -256,29 +288,20 @@ The same is true with `greenlock-store-*` plugins:
### Customer Manager, the lazy way ### Customer Manager, the lazy way
At the very least you have to implement `find({ servername })`. At the very least you have to implement `get({ servername, wildname })`.
Since this is a very common use case, it's supported out of the box as part of the default manager plugin:
```js ```js
var greenlock = Greenlock.create({ var greenlock = Greenlock.create({
packageAgent: pkg.name + '/' + pkg.version, packageAgent: pkg.name + '/' + pkg.version,
maintainerEmail: 'jon@example.com', maintainerEmail: 'jon@example.com',
notify: notify, notify: notify,
find: find
packageRoot: __dirname,
manager: {
module: './manager.js'
}
}); });
// In the simplest case you can ignore all incoming options
// and return a single site config in the same format as the config file
function find(options) {
var servername = options.servername; // www.example.com
var wildname = options.wildname; // *.example.com
return Promise.resolve([
{ subject: 'example.com', altnames: ['example.com', 'www.example.com'] }
]);
}
function notify(ev, args) { function notify(ev, args) {
if ('error' === ev || 'warning' === ev) { if ('error' === ev || 'warning' === ev) {
console.error(ev, args); console.error(ev, args);
@ -288,102 +311,61 @@ function notify(ev, args) {
} }
``` ```
If you want to use wildcards or local domains, you must specify the `dns-01` challenge plugin to use: In the simplest case you can ignore all incoming options
and return a single site config in the same format as the config file
```js `./manager.js`:
function find(options) {
var subject = options.subject;
// may include wildcard
var altnames = options.altnames;
var wildname = options.wildname; // *.example.com
return Promise.resolve([
{
subject: 'example.com',
altnames: ['example.com', 'www.example.com'],
challenges: {
'dns-01': { module: 'acme-dns-01-namedotcom', apikey: 'xxxx' }
}
}
]);
}
```
### Customer Manager, complete
To use a fully custom manager, you give the npm package name, or absolute path to the file to load
```js
Greenlock.create({
// Greenlock Options
maintainerEmail: 'jon@example.com',
packageAgent: 'my-package/v2.1.1',
notify: notify,
// file path or npm package name
manager: '/path/to/manager.js',
// options that get passed to the manager
myFooOption: 'whatever'
});
```
The manager itself is, again relatively simple:
- find(options)
- set(siteConfig)
- remove(options)
- defaults(globalOptions) (as setter)
- defaults() => globalOptions (as getter)
`/path/to/manager.js`:
```js ```js
'use strict'; 'use strict';
module.exports.create = function() { module.exports.create = function() {
var manager = {}; return {
get: async function({ servername }) {
// do something to fetch the site
var site = {
subject: 'example.com',
altnames: ['example.com', 'www.example.com']
};
manager.find = async function({ subject, altnames, renewBefore }) { return site;
if (subject) {
return getSiteConfigBySubject(subject);
} }
if (altnames) {
// may include wildcards
return getSiteConfigByAnyAltname(altnames);
}
if (renewBefore) {
return getSiteConfigsWhereRenewAtIsLessThan(renewBefore);
}
return [];
};
manage.set = function(opts) {
// this is called by greenlock.add({ subject, altnames })
// it's also called by greenlock._update({ subject, renewAt })
return mergSiteConfig(subject, opts);
};
manage.remove = function({ subject, altname }) {
if (subject) {
return removeSiteConfig(subject);
}
return removeFromSiteConfigAndResetRenewAtToZero(altname);
};
// set the global config
manage.defaults = function(options) {
if (!options) {
return getGlobalConfig();
}
return mergeGlobalConfig(options);
}; };
}; };
``` ```
If you want to use wildcards or local domains for a specific domain, you must specify the `dns-01` challenge plugin to use:
```js
'use strict';
module.exports.create = function() {
return {
get: async function({ servername }) {
// do something to fetch the site
var site = {
subject: 'example.com',
altnames: ['example.com', 'www.example.com'],
// dns-01 challenge
challenges: {
'dns-01': {
module: 'acme-dns-01-namedotcom',
apikey: 'xxxx'
}
}
};
return site;
}
};
};
```
### Customer Manager, Complete
See <https://git.rootprojects.org/root/greenlock-manager-test.js#quick-start>
# ACME Challenge Plugins # ACME Challenge Plugins
The ACME challenge plugins are just a few simple callbacks: The ACME challenge plugins are just a few simple callbacks:
@ -419,99 +401,3 @@ They are described here:
- [greenlock store documentation](https://git.rootprojects.org/root/greenlock-store-test.js) - [greenlock store documentation](https://git.rootprojects.org/root/greenlock-store-test.js)
If you are just implenting in-house and are not going to publish a module, you can also do some hack things like this: If you are just implenting in-house and are not going to publish a module, you can also do some hack things like this:
### Custome Store, The hacky / lazy way
`/path/to/project/my-hacky-store.js`:
```js
'use strict';
module.exports.create = function(options) {
// ex: /path/to/account.ecdsa.jwk.json
var accountJwk = require(options.accountJwkPath);
// ex: /path/to/privkey.rsa.pem
var serverPem = fs.readFileSync(options.serverPemPath, 'ascii');
var accounts = {};
var certificates = {};
var store = { accounts, certificates };
// bare essential account callbacks
accounts.checkKeypair = function() {
// ignore all options and just return a single, global keypair
return Promise.resolve({
privateKeyJwk: accountJwk
});
};
accounts.setKeypair = function() {
// this will never get called if checkKeypair always returns
return Promise.resolve({});
};
// bare essential cert and key callbacks
certificates.checkKeypair = function() {
// ignore all options and just return a global server keypair
return {
privateKeyPem: serverPem
};
};
certificates.setKeypair = function() {
// never gets called if checkKeypair always returns an existing key
return Promise.resolve(null);
};
certificates.check = function(args) {
var subject = args.subject;
// make a database call or whatever to get a certificate
return goGetCertBySubject(subject).then(function() {
return {
pems: {
chain: '<PEM>',
cert: '<PEM>'
}
};
});
};
certificates.set = function(args) {
var subject = args.subject;
var cert = args.pems.cert;
var chain = args.pems.chain;
// make a database call or whatever to get a certificate
return goSaveCert({
subject,
cert,
chain
});
};
};
```
### Using the hacky / lazy store plugin
That sort of implementation won't pass the test suite, but it'll work just fine a use case where you only have one subscriber email (most of the time),
you only have one server key (not recommended, but works), and you only really want to worry about storing cetificates.
Then you could assign it as the default for all of your sites:
```json
{
"subscriberEmail": "jon@example.com",
"agreeToTerms": true,
"sites": {
"example.com": {
"subject": "example.com",
"altnames": ["example.com", "www.example.com"]
}
},
"store": {
"module": "/path/to/project/my-hacky-store.js",
"accountJwkPath": "/path/to/account.ecdsa.jwk.json",
"serverPemPath": "/path/to/privkey.rsa.pem"
}
}
```

View File

@ -1,12 +1,10 @@
# New Documentation &amp; [v2/v3 Migration Guide](https://git.rootprojects.org/root/greenlock.js/src/branch/v3/MIGRATION_GUIDE_V2_V3.md) # New Documentation &amp; [v4 Migration Guide](https://git.rootprojects.org/root/greenlock.js/src/branch/master/MIGRATION_GUIDE.md)
Greenlock v3 was just released from private beta **today** (Nov 1st, 2019).
We're still working on the full documentation for this new version, We're still working on the full documentation for this new version,
so please be patient. so please be patient.
To start, check out the To start, check out the
[Migration Guide](https://git.rootprojects.org/root/greenlock.js/src/branch/v3/MIGRATION_GUIDE_V2_V3.md). [Migration Guide](https://git.rootprojects.org/root/greenlock.js/src/branch/master/MIGRATION_GUIDE.md).
!["Greenlock Logo"](https://git.rootprojects.org/root/greenlock.js/raw/branch/master/logo/greenlock-1063x250.png 'Greenlock lock logo and work mark') !["Greenlock Logo"](https://git.rootprojects.org/root/greenlock.js/raw/branch/master/logo/greenlock-1063x250.png 'Greenlock lock logo and work mark')
@ -85,12 +83,10 @@ Certificates are renewed every 45 days by default, and renewal checks will happe
var pkg = require('./package.json'); var pkg = require('./package.json');
var Greenlock = require('greenlock'); var Greenlock = require('greenlock');
var greenlock = Greenlock.create({ var greenlock = Greenlock.create({
configDir: './greenlock.d/config.json',
packageAgent: pkg.name + '/' + pkg.version, packageAgent: pkg.name + '/' + pkg.version,
maintainerEmail: pkg.author, maintainerEmail: pkg.author,
staging: true, staging: true,
manager: require('greenlock-manager-fs').create({
configFile: '~/.config/greenlock/manager.json'
}),
notify: function(event, details) { notify: function(event, details) {
if ('error' === event) { if ('error' === event) {
// `details` is an error object in this case // `details` is an error object in this case
@ -171,7 +167,7 @@ greenlock
--> -->
<details> <details>
<summary>Greenlock.create({ packageAgent, maintainerEmail, staging })</summary> <summary>Greenlock.create({ configDir, packageAgent, maintainerEmail, staging })</summary>
## Greenlock.create() ## Greenlock.create()
@ -181,12 +177,15 @@ Creates an instance of greenlock with _environment_-level values.
var pkg = require('./package.json'); var pkg = require('./package.json');
var gl = Greenlock.create({ var gl = Greenlock.create({
configDir: './greenlock.d/config.json',
// Staging for testing environments // Staging for testing environments
staging: true, staging: true,
// This should be the contact who receives critical bug and security notifications // This should be the contact who receives critical bug and security notifications
// Optionally, you may receive other (very few) updates, such as important new features // Optionally, you may receive other (very few) updates, such as important new features
maintainerEmail: 'jon@example.com', maintainerEmail: 'jon@example.com',
// for an RFC 8555 / RFC 7231 ACME client user agent // for an RFC 8555 / RFC 7231 ACME client user agent
packageAgent: pkg.name + '/' pkg.version packageAgent: pkg.name + '/' pkg.version
}); });
@ -194,6 +193,7 @@ var gl = Greenlock.create({
| Parameter | Description | | Parameter | Description |
| --------------- | ------------------------------------------------------------------------------------ | | --------------- | ------------------------------------------------------------------------------------ |
| configDir | the directory to use for file-based plugins |
| maintainerEmail | the developer contact for critical bug and security notifications | | maintainerEmail | the developer contact for critical bug and security notifications |
| packageAgent | if you publish your package for others to use, `require('./package.json').name` here | | packageAgent | if you publish your package for others to use, `require('./package.json').name` here |
| staging | use the Let's Encrypt staging URL instead of the production URL | | staging | use the Let's Encrypt staging URL instead of the production URL |

View File

@ -55,15 +55,6 @@ G.create = function(gconf) {
gdefaults.notify = _notify; gdefaults.notify = _notify;
} }
/*
if (!gconf.packageRoot) {
gconf.packageRoot = process.cwd();
console.warn(
'`packageRoot` not defined, trying ' + gconf.packageRoot
);
}
*/
gconf = Init._init(gconf); gconf = Init._init(gconf);
// OK: /path/to/blah // OK: /path/to/blah
@ -71,6 +62,12 @@ G.create = function(gconf) {
// NOT OK: ./rel/path/to/blah // NOT OK: ./rel/path/to/blah
// Error: .blah // Error: .blah
if ('.' === (gconf.manager.module || '')[0]) { if ('.' === (gconf.manager.module || '')[0]) {
if (!gconf.packageRoot) {
gconf.packageRoot = process.cwd();
console.warn(
'`packageRoot` not defined, trying ' + gconf.packageRoot
);
}
gconf.manager.module = gconf.manager.module =
gconf.packageRoot + '/' + gconf.manager.module.slice(2); gconf.packageRoot + '/' + gconf.manager.module.slice(2);
} }
@ -426,14 +423,23 @@ G.create = function(gconf) {
storeConf = JSON.parse(JSON.stringify(storeConf)); storeConf = JSON.parse(JSON.stringify(storeConf));
storeConf.packageRoot = gconf.packageRoot; storeConf.packageRoot = gconf.packageRoot;
var path = require('path');
if (!storeConf.basePath) { if (!storeConf.basePath) {
storeConf.basePath = gconf.configDir; storeConf.basePath = gconf.configDir;
} }
storeConf.basePath = path.resolve(
gconf.packageRoot || process.cwd(), if ('.' === (storeConf.basePath || '')[0]) {
storeConf.basePath if (!gconf.packageRoot) {
); gconf.packageRoot = process.cwd();
console.warn(
'`packageRoot` not defined, trying ' + gconf.packageRoot
);
}
storeConf.basePath = require('path').resolve(
gconf.packageRoot || '',
storeConf.basePath
);
}
storeConf.directoryUrl = dirUrl; storeConf.directoryUrl = dirUrl;
var store = await P._loadStore(storeConf); var store = await P._loadStore(storeConf);
var account = await A._getOrCreate( var account = await A._getOrCreate(

View File

@ -492,8 +492,46 @@ function mergeManager(gconf) {
} }
if (mini.get) { if (mini.get) {
mega.get = function(opts) { mega.get = async function(opts) {
return mini.get(opts); if (mini.set) {
return mini.get(opts);
}
if (!mega._get) {
mega._get = m().get;
}
var existing = await mega._get(opts);
var site = await mini.get(opts);
if (!existing) {
// Add
if (!site) {
return;
}
site.renewAt = 1;
site.deletedAt = 0;
await mega.set(site);
existing = await mega._get(opts);
} else if (!site) {
// Delete
existing.deletedAt = site.deletedAt || Date.now();
await mega.set(existing);
existing = null;
} else if (
site.subject !== existing.subject ||
site.altnames.join(' ') !== existing.altnames.join(' ')
) {
// Update
site.renewAt = 1;
site.deletedAt = 0;
await mega.set(site);
existing = await mega._get(opts);
if (!existing) {
throw new Error('failed to `get` after `set`');
}
}
return existing;
}; };
} else if (mini.find) { } else if (mini.find) {
mega.get = function(opts) { mega.get = function(opts) {