2019-10-31 06:12:01 +00:00
|
|
|
# [greenlock-manager-test.js](https://git.rootprojects.org/root/greenlock-manager-test.js)
|
2019-10-31 04:56:52 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
A simple test suite for Greenlock v3 manager plugins.
|
2019-10-31 06:07:33 +00:00
|
|
|
|
|
|
|
# Greenlock Manager
|
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
A Greenlock Manager is responsible for tracking which domains
|
|
|
|
belong on a certificate, when they are scheduled for renewal,
|
|
|
|
and if they have been deleted.
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
It consists of two required functions:
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
```js
|
|
|
|
set({ subject, altnames, renewAt, deletedAt });
|
|
|
|
```
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
```js
|
|
|
|
get({ servername });
|
|
|
|
```
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
However, if you implement `find({ subject, servernames, renewBefore })` (optional),
|
|
|
|
you don't have to implement `get()`.
|
2019-10-31 06:09:09 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
<details>
|
|
|
|
<summary>Usage Details</summary>
|
|
|
|
# How to use your plugin
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
The **Right Way**:
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
```bash
|
|
|
|
npm install --save greenlack
|
|
|
|
npx greenlock init --manager ./path-or-npm-name.js --manager-xxxx 'sets xxxx' --manager-yyyy 'set yyyy'
|
|
|
|
```
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
That creates a `.greenlockrc`, which is essentially the same as doing this:
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
```js
|
|
|
|
var Greenlock = require("greenlock");
|
|
|
|
var greenlock = Greenlock.create({
|
|
|
|
// ...
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
manager: "./path-or-npm-name.js",
|
|
|
|
xxxx: "sets xxxx",
|
|
|
|
yyyy: "sets yyyy",
|
|
|
|
packageRoot: __dirname
|
|
|
|
});
|
|
|
|
```
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
## Why no require?
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
Okay, so you **expect** it to look like this:
|
2019-10-31 06:07:33 +00:00
|
|
|
|
|
|
|
```js
|
2019-11-05 04:03:21 +00:00
|
|
|
var Greenlock = require("greenlock");
|
|
|
|
var greenlock = Greenlock.create({
|
|
|
|
// WRONG!!
|
|
|
|
manager: require("./path-or-npm-name.js").create({
|
|
|
|
someOptionYouWant: true
|
|
|
|
})
|
|
|
|
});
|
|
|
|
```
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
**NOPE**!
|
2019-10-31 06:09:09 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
Greenlock is designed to so that the CLI tools, Web API, and JavaScript API
|
|
|
|
can all work interdepedently, indpendently.
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
Therefore the configuration has to go into serializable JSON rather than
|
|
|
|
executable JavaScript.
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
</details>
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
# Quick Start
|
|
|
|
|
|
|
|
If you want to write a manager,
|
|
|
|
the best way to start is by using one of the provided templates.
|
|
|
|
|
|
|
|
```bash
|
2019-10-31 06:07:33 +00:00
|
|
|
npm install --save-dev greenlock-manager-test
|
2019-11-05 04:03:21 +00:00
|
|
|
npx greenlock-manager-init
|
2019-10-31 06:07:33 +00:00
|
|
|
```
|
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
It will generate a bare bones manager that passes the tests,
|
|
|
|
(skipping all optional features), and a test file:
|
|
|
|
|
|
|
|
<details>
|
|
|
|
<summary>manager.js</summary>
|
|
|
|
```js
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
var Manager = module.exports;
|
|
|
|
var db = {};
|
|
|
|
|
|
|
|
Manager.create = function(opts) {
|
|
|
|
var manager = {};
|
|
|
|
|
|
|
|
//
|
|
|
|
// REQUIRED (basic issuance)
|
|
|
|
//
|
|
|
|
|
|
|
|
// Get
|
|
|
|
manager.get = async function({ servername, wildname }) {
|
|
|
|
// Required: find the certificate with the subject of `servername`
|
|
|
|
// Optional (multi-domain certs support): find a certificate with `servername` as an altname
|
|
|
|
// Optional (wildcard support): find a certificate with `wildname` as an altname
|
|
|
|
|
|
|
|
// { subject, altnames, renewAt, deletedAt, challenges, ... }
|
|
|
|
return db[servername] || db[wildname];
|
|
|
|
};
|
|
|
|
|
|
|
|
// Set
|
|
|
|
manager.set = async function(opts) {
|
|
|
|
// { subject, altnames, renewAt, deletedAt }
|
|
|
|
// Required: updated `renewAt` and `deletedAt` for certificate matching `subject`
|
|
|
|
|
|
|
|
var site = db[opts.subject] || {};
|
|
|
|
db[opts.subject] = Object.assign(site, opts);
|
|
|
|
return null;
|
|
|
|
};
|
|
|
|
|
|
|
|
//
|
|
|
|
// Optional (Fully Automatic Renewal)
|
|
|
|
//
|
|
|
|
/*
|
|
|
|
manager.find = async function(opts) {
|
|
|
|
// { subject, servernames, altnames, renewBefore }
|
|
|
|
|
|
|
|
return [{ subject, altnames, renewAt, deletedAt }];
|
|
|
|
};
|
|
|
|
//*/
|
|
|
|
|
|
|
|
//
|
|
|
|
// Optional (Special Remove Functionality)
|
|
|
|
// The default behavior is to set `deletedAt`
|
|
|
|
//
|
|
|
|
/*
|
|
|
|
manager.remove = async function(opts) {
|
|
|
|
return mfs.remove(opts);
|
|
|
|
};
|
|
|
|
//*/
|
|
|
|
|
|
|
|
//
|
|
|
|
// Optional (special settings save)
|
|
|
|
// Implemented here because this module IS the fallback
|
|
|
|
//
|
|
|
|
/*
|
|
|
|
manager.defaults = async function(opts) {
|
|
|
|
if (opts) {
|
|
|
|
return setDefaults(opts);
|
|
|
|
}
|
|
|
|
return getDefaults();
|
|
|
|
};
|
|
|
|
//*/
|
|
|
|
|
|
|
|
//
|
|
|
|
// Optional (for common deps and/or async initialization)
|
|
|
|
//
|
|
|
|
/*
|
|
|
|
manager.init = async function(deps) {
|
|
|
|
manager.request = deps.request;
|
|
|
|
return null;
|
|
|
|
};
|
|
|
|
//*/
|
|
|
|
|
|
|
|
return manager;
|
|
|
|
|
|
|
|
};
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
````
|
|
|
|
</details>
|
|
|
|
<details>
|
|
|
|
<summary>manager.test.js</summary>
|
2019-10-31 06:07:33 +00:00
|
|
|
```js
|
2019-11-05 04:03:21 +00:00
|
|
|
"use strict";
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
var Tester = require("greenlock-manager-test");
|
|
|
|
|
|
|
|
var Manager = require("./manager.js");
|
|
|
|
var config = {
|
|
|
|
configFile: "greenlock-manager-test.delete-me.json"
|
2019-10-31 06:07:33 +00:00
|
|
|
};
|
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
Tester.test(Manager, config)
|
|
|
|
.then(function(features) {
|
|
|
|
console.info("PASS");
|
|
|
|
console.info();
|
|
|
|
console.info("Optional Feature Support:");
|
|
|
|
features.forEach(function(feature) {
|
|
|
|
console.info(feature.supported ? "✓ (YES)" : "✘ (NO) ", feature.description);
|
|
|
|
});
|
|
|
|
console.info();
|
2019-10-31 06:07:33 +00:00
|
|
|
})
|
|
|
|
.catch(function(err) {
|
2019-11-05 04:03:21 +00:00
|
|
|
console.error("Oops, you broke it. Here are the details:");
|
|
|
|
console.error(err.stack);
|
|
|
|
console.error();
|
|
|
|
console.error("That's all I know.");
|
2019-10-31 06:07:33 +00:00
|
|
|
});
|
2019-11-05 04:03:21 +00:00
|
|
|
````
|
|
|
|
|
|
|
|
</details>
|
|
|
|
|
|
|
|
```bash
|
|
|
|
node manager.test.js
|
2019-10-31 06:07:33 +00:00
|
|
|
```
|
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
```txt
|
|
|
|
PASS: get({ servername, wildname })
|
|
|
|
PASS: set({ subject })
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
Optional Feature Support:
|
|
|
|
✘ (NO) Multiple Domains per Certificate
|
|
|
|
✘ (NO) Wildcard Certificates
|
|
|
|
✘ (NO) Fully Automatic Renewal
|
|
|
|
```
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
# Optional Features
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
If you're publishing a module to the community,
|
|
|
|
you should implement the full test suite (and it's not that hard).
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
If you're only halfway through, you should note
|
|
|
|
which features are supported and which aren't.
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
```js
|
|
|
|
find({ subject, servernames, renewBefore });
|
|
|
|
defaults({ subscriberEmail, agreeToTerms, challenges, store, ... });
|
|
|
|
defaults(); // as getter
|
|
|
|
```
|
|
|
|
|
|
|
|
- `find()` is used to get the full list of sites, for continuous fully automatic renewal.
|
|
|
|
- `defaults()` exists so that the global config can be saved in the same place as the per-site config.
|
|
|
|
- a proper `get()` should be able to search not just primary domains, but altnames as well.
|
2019-10-31 06:07:33 +00:00
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
Additionally, you're manager may need an init or a _real_ delete - rather than just using `set({ deletedAt })`:
|
2019-10-31 06:07:33 +00:00
|
|
|
|
|
|
|
```js
|
2019-11-05 04:03:21 +00:00
|
|
|
init({ request });
|
|
|
|
remove({ subject });
|
2019-10-31 06:07:33 +00:00
|
|
|
```
|
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
<details>
|
|
|
|
<summary>Full Implementation</summary>
|
2019-10-31 06:07:33 +00:00
|
|
|
|
|
|
|
# The Right Way™
|
|
|
|
|
|
|
|
If you want to publish a module to the community you should do a slightly better job:
|
|
|
|
|
|
|
|
```js
|
|
|
|
module.exports.create = function(options) {
|
|
|
|
var manager = {};
|
|
|
|
|
|
|
|
// add some things to... wherever you save things
|
|
|
|
|
|
|
|
manager.set = async function(siteConfig) {
|
|
|
|
// You can see in the tests a sample of common values,
|
|
|
|
// but you don't really need to worry about it.
|
2019-11-05 04:03:21 +00:00
|
|
|
var subject = siteConfig.subject;
|
2019-10-31 06:07:33 +00:00
|
|
|
|
|
|
|
// Cherry pick what you like for indexing / search, and JSONify the rest
|
|
|
|
return mergeOrCreateSite(subject, siteConfig);
|
|
|
|
};
|
|
|
|
|
|
|
|
// find the things you've saved before
|
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
manager.get = async function({ servername }) {
|
|
|
|
return getSiteByAltname(servername);
|
|
|
|
}
|
|
|
|
manager.find = async function({ subject, servernames, renewBefore }) {
|
2019-10-31 06:07:33 +00:00
|
|
|
var results = [];
|
|
|
|
var gotten = {};
|
|
|
|
|
|
|
|
if (subject) {
|
|
|
|
var site = await getSiteBySubject(subject);
|
2019-11-05 04:03:21 +00:00
|
|
|
if (site && site.subject === subject) {
|
|
|
|
return [site];
|
2019-10-31 06:07:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
if (severnames) {
|
|
|
|
return await Promise.all(servernames.map(function (altname) {
|
|
|
|
var site = getSiteByAltname(subject);
|
|
|
|
if (site && !gotten[site.subject]) {
|
2019-10-31 06:07:33 +00:00
|
|
|
gotten[site.subject] = true;
|
2019-11-05 04:03:21 +00:00
|
|
|
return site;
|
2019-10-31 06:07:33 +00:00
|
|
|
}
|
2019-11-05 04:03:21 +00:00
|
|
|
});
|
2019-10-31 06:07:33 +00:00
|
|
|
}
|
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
return getSitesThatShouldBeRenewedBefore(renewBefore || Infinity);
|
2019-10-31 06:07:33 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// delete a site config
|
|
|
|
|
|
|
|
manager.remove = async function({ subject }) {
|
|
|
|
// set deletedAt to a value, or actually delete it - however you like
|
|
|
|
return mergeOrCreateSite(subject, { deletedAt: Date.now() });
|
|
|
|
};
|
|
|
|
|
|
|
|
// get / set global things
|
|
|
|
|
|
|
|
manager.defaults = async function(options) {
|
|
|
|
if (!options) {
|
|
|
|
return getDefaultConfigValues();
|
|
|
|
}
|
|
|
|
|
|
|
|
return mergeDefaultConfigValues(options);
|
|
|
|
};
|
|
|
|
|
|
|
|
// optional, if you need it
|
|
|
|
|
|
|
|
manager.init = async function(deps) {
|
|
|
|
// a place to do some init, if you need it
|
|
|
|
|
|
|
|
return doMyInit();
|
|
|
|
|
|
|
|
// Also, `deps` will have some common dependencies
|
|
|
|
// than many modules need, such as `request`.
|
|
|
|
// This cuts down on stray dependencies, and helps
|
|
|
|
// with browser compatibility.
|
|
|
|
|
|
|
|
request = deps.request;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
```
|
|
|
|
|
2019-11-05 04:03:21 +00:00
|
|
|
</details>
|