diff --git a/MIGRATION_GUIDE_V2_V3.md b/MIGRATION_GUIDE_V2_V3.md index f969018..1ba88d8 100644 --- a/MIGRATION_GUIDE_V2_V3.md +++ b/MIGRATION_GUIDE_V2_V3.md @@ -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. @@ -195,11 +227,11 @@ as well as a set of callbacks for easy configurability. ### 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: -`~/.config/greenlock/manager.json`: +`./greenlock.d/config.json`: ```json { @@ -256,28 +288,19 @@ The same is true with `greenlock-store-*` plugins: ### Customer Manager, the lazy way -At the very least you have to implement `find({ servername })`. - -Since this is a very common use case, it's supported out of the box as part of the default manager plugin: +At the very least you have to implement `get({ servername, wildname })`. ```js var greenlock = Greenlock.create({ packageAgent: pkg.name + '/' + pkg.version, maintainerEmail: 'jon@example.com', notify: notify, - find: find -}); - -// 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'] } - ]); -} + packageRoot: __dirname, + manager: { + module: './manager.js' + } +}); function notify(ev, args) { if ('error' === ev || 'warning' === ev) { @@ -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: - -```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: +In the simplest case you can ignore all incoming options +and return a single site config in the same format as the config file -- find(options) -- set(siteConfig) -- remove(options) -- defaults(globalOptions) (as setter) - - defaults() => globalOptions (as getter) - -`/path/to/manager.js`: +`./manager.js`: ```js 'use strict'; module.exports.create = function() { - var manager = {}; - - manager.find = async function({ subject, altnames, renewBefore }) { - if (subject) { - return getSiteConfigBySubject(subject); - } - - if (altnames) { - // may include wildcards - return getSiteConfigByAnyAltname(altnames); - } + return { + get: async function({ servername }) { + // do something to fetch the site + var site = { + subject: 'example.com', + altnames: ['example.com', 'www.example.com'] + }; - if (renewBefore) { - return getSiteConfigsWhereRenewAtIsLessThan(renewBefore); + return site; } - - 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); - }; +If you want to use wildcards or local domains for a specific domain, you must specify the `dns-01` challenge plugin to use: - manage.remove = function({ subject, altname }) { - if (subject) { - return removeSiteConfig(subject); - } +```js +'use strict'; - return removeFromSiteConfigAndResetRenewAtToZero(altname); - }; +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' + } + } + }; - // set the global config - manage.defaults = function(options) { - if (!options) { - return getGlobalConfig(); + return site; } - return mergeGlobalConfig(options); }; }; ``` +### Customer Manager, Complete + +See + # ACME Challenge Plugins 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) 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: '', - cert: '' - } - }; - }); - }; - 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" - } -} -``` diff --git a/README.md b/README.md index 573d2cb..5ebd7fe 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ -# New Documentation & [v2/v3 Migration Guide](https://git.rootprojects.org/root/greenlock.js/src/branch/v3/MIGRATION_GUIDE_V2_V3.md) - -Greenlock v3 was just released from private beta **today** (Nov 1st, 2019). +# New Documentation & [v4 Migration Guide](https://git.rootprojects.org/root/greenlock.js/src/branch/master/MIGRATION_GUIDE.md) We're still working on the full documentation for this new version, so please be patient. 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') @@ -85,12 +83,10 @@ Certificates are renewed every 45 days by default, and renewal checks will happe var pkg = require('./package.json'); var Greenlock = require('greenlock'); var greenlock = Greenlock.create({ + configDir: './greenlock.d/config.json', packageAgent: pkg.name + '/' + pkg.version, maintainerEmail: pkg.author, staging: true, - manager: require('greenlock-manager-fs').create({ - configFile: '~/.config/greenlock/manager.json' - }), notify: function(event, details) { if ('error' === event) { // `details` is an error object in this case @@ -171,7 +167,7 @@ greenlock -->
-Greenlock.create({ packageAgent, maintainerEmail, staging }) +Greenlock.create({ configDir, packageAgent, maintainerEmail, staging }) ## Greenlock.create() @@ -181,12 +177,15 @@ Creates an instance of greenlock with _environment_-level values. var pkg = require('./package.json'); var gl = Greenlock.create({ + configDir: './greenlock.d/config.json', + // Staging for testing environments staging: true, // 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 maintainerEmail: 'jon@example.com', + // for an RFC 8555 / RFC 7231 ACME client user agent packageAgent: pkg.name + '/' pkg.version }); @@ -194,6 +193,7 @@ var gl = Greenlock.create({ | Parameter | Description | | --------------- | ------------------------------------------------------------------------------------ | +| configDir | the directory to use for file-based plugins | | 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 | | staging | use the Let's Encrypt staging URL instead of the production URL | diff --git a/greenlock.js b/greenlock.js index 077517d..b346f38 100644 --- a/greenlock.js +++ b/greenlock.js @@ -55,15 +55,6 @@ G.create = function(gconf) { gdefaults.notify = _notify; } - /* - if (!gconf.packageRoot) { - gconf.packageRoot = process.cwd(); - console.warn( - '`packageRoot` not defined, trying ' + gconf.packageRoot - ); - } - */ - gconf = Init._init(gconf); // OK: /path/to/blah @@ -71,6 +62,12 @@ G.create = function(gconf) { // NOT OK: ./rel/path/to/blah // Error: .blah if ('.' === (gconf.manager.module || '')[0]) { + if (!gconf.packageRoot) { + gconf.packageRoot = process.cwd(); + console.warn( + '`packageRoot` not defined, trying ' + gconf.packageRoot + ); + } gconf.manager.module = gconf.packageRoot + '/' + gconf.manager.module.slice(2); } @@ -426,14 +423,23 @@ G.create = function(gconf) { storeConf = JSON.parse(JSON.stringify(storeConf)); storeConf.packageRoot = gconf.packageRoot; - var path = require('path'); if (!storeConf.basePath) { storeConf.basePath = gconf.configDir; } - storeConf.basePath = path.resolve( - gconf.packageRoot || process.cwd(), - storeConf.basePath - ); + + if ('.' === (storeConf.basePath || '')[0]) { + 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; var store = await P._loadStore(storeConf); var account = await A._getOrCreate( diff --git a/lib/manager-wrapper.js b/lib/manager-wrapper.js index 24f421c..b4e7c97 100644 --- a/lib/manager-wrapper.js +++ b/lib/manager-wrapper.js @@ -492,8 +492,46 @@ function mergeManager(gconf) { } if (mini.get) { - mega.get = function(opts) { - return mini.get(opts); + mega.get = async function(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) { mega.get = function(opts) {