A simple test suite for Greenlock manager plugins.
Go to file
AJ ONeal e159a51984 v3.1.1: make executable 2019-11-04 21:15:56 -07:00
bin v3.1.1: make executable 2019-11-04 21:15:56 -07:00
tests v3.1.0: reduce scope of manager API 2019-11-04 21:03:21 -07:00
.gitignore Initial commit 2019-10-31 04:56:52 +00:00
.prettierrc v3.1.0: reduce scope of manager API 2019-11-04 21:03:21 -07:00
LICENSE Initial commit 2019-10-31 04:56:52 +00:00
README.md typo fix 2019-11-04 21:04:39 -07:00
package-lock.json v3.1.1: make executable 2019-11-04 21:15:56 -07:00
package.json v3.1.1: make executable 2019-11-04 21:15:56 -07:00
tester.js v3.1.0: reduce scope of manager API 2019-11-04 21:03:21 -07:00

README.md

greenlock-manager-test.js

A simple test suite for Greenlock v3 manager plugins.

Greenlock Manager

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.

It consists of two required functions:

set({ subject, altnames, renewAt, deletedAt });
get({ servername });

However, if you implement find({ subject, servernames, renewBefore }) (optional), you don't have to implement get().

Usage Details # How to use your plugin

The Right Way:

npm install --save greenlack
npx greenlock init --manager ./path-or-npm-name.js --manager-xxxx 'sets xxxx' --manager-yyyy 'set yyyy'

That creates a .greenlockrc, which is essentially the same as doing this:

var Greenlock = require("greenlock");
var greenlock = Greenlock.create({
    // ...

    manager: "./path-or-npm-name.js",
    xxxx: "sets xxxx",
    yyyy: "sets yyyy",
    packageRoot: __dirname
});

Why no require?

Okay, so you expect it to look like this:

var Greenlock = require("greenlock");
var greenlock = Greenlock.create({
    // WRONG!!
    manager: require("./path-or-npm-name.js").create({
        someOptionYouWant: true
    })
});

NOPE!

Greenlock is designed to so that the CLI tools, Web API, and JavaScript API can all work interdepedently, indpendently.

Therefore the configuration has to go into serializable JSON rather than executable JavaScript.

Quick Start

If you want to write a manager, the best way to start is by using one of the provided templates.

npm install --save-dev greenlock-manager-test
npx greenlock-manager-init

It will generate a bare bones manager that passes the tests, (skipping all optional features), and a test file:

manager.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;
};
manager.test.js
"use strict";

var Tester = require("greenlock-manager-test");

var Manager = require("./manager.js");
var config = {
    configFile: "greenlock-manager-test.delete-me.json"
};

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();
    })
    .catch(function(err) {
        console.error("Oops, you broke it. Here are the details:");
        console.error(err.stack);
        console.error();
        console.error("That's all I know.");
    });
node manager.test.js
PASS:  get({ servername, wildname })
PASS:  set({ subject })

Optional Feature Support:
✘ (NO)  Multiple Domains per Certificate
✘ (NO)  Wildcard Certificates
✘ (NO)  Fully Automatic Renewal

Optional Features

If you're publishing a module to the community, you should implement the full test suite (and it's not that hard).

If you're only halfway through, you should note which features are supported and which aren't.

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.

Additionally, you're manager may need an init or a real delete - rather than just using set({ deletedAt }):

init({ request });
remove({ subject });
Full Implementation

The Right Way™

If you want to publish a module to the community you should do a slightly better job:

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.
        var subject = siteConfig.subject;

        // Cherry pick what you like for indexing / search, and JSONify the rest
        return mergeOrCreateSite(subject, siteConfig);
    };

    // find the things you've saved before

    manager.get = async function({ servername }) {
        return getSiteByAltname(servername);
    }
    manager.find = async function({ subject, servernames, renewBefore }) {
        var results = [];
        var gotten = {};

        if (subject) {
            var site = await getSiteBySubject(subject);
            if (site && site.subject === subject) {
                return [site];
            }
        }

        if (severnames) {
            return await Promise.all(servernames.map(function (altname) {
                var site = getSiteByAltname(subject);
                if (site && !gotten[site.subject]) {
                    gotten[site.subject] = true;
                    return site;
                }
            });
        }

        return getSitesThatShouldBeRenewedBefore(renewBefore || Infinity);
    };

    // 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;
    };
};