How to develop external plugins for apigeelint

Create your own plugins for automated static analysis of Apigee proxies and shared flows

ยท

8 min read

I originally posted this article on Apigee community's website in 2020 but after that new features were introduced in apigeelint's releases, including an option to specify an external plugins directory using the -x parameter, which is why I decided to review this article and release an updated version showing how to create and run plugins from an external directory.

Thanks for the heads up kurtkanaskie! ๐Ÿค“ image.png

So here we go!

How to develop, run and test external plugins for apigeelint

In this step-by-step guide, I'll show how to implement new plugins for apigeelint so you can create your own Apigee coding rules. This can be useful in case you or your company wants to enforce some best practices, like having the same naming convention for all proxies, making sure all proxies have versioning in the base path, to guarantee that the proxies are using some mandatory security policies like access token or API key validation, etc. Apigeelint is even more powerful if you include it as a required step in your CI/CD pipeline.

If you don't know what apigeelint is I strongly recommend that you take a look at their GitHub or NPM pages. Here's a short description from the official docs:

Static code analysis for Apigee proxy and sharedflow bundles to encourage API developers to use best practices and avoid anti-patterns.

This utility is intended to capture the best practices knowledge from across Apigee including our Global Support Center team, Customer Success, Engineering, and our product team in a tool that will help developers create more scalable, performant, and stable API bundles using the Apigee DSL.

What is a plugin?

In the apigeelint structure a plugin is where each rule is implemented. A few examples of existing plugins are:

  • checkFileName.js - Check that file names correspond to policy display names.
  • checkUnattachedPolicies.js - Unattached policies are dead code. They should be removed from bundles before releasing the bundle to production.
  • checkForEmptySteps.js - Empty steps clutter a bundle. Performance is not degraded.

The complete list of all plugins can be accessed here.

The main goal of a plugin is to look for problematic patterns or enforcement of conventions. Each plugin should perform its logic using any of the available Apigee entities, like:

  • Proxy Bundle
  • Steps
  • Conditions
  • Proxy Endpoints
  • Target Endpoints
  • Resources
  • Policies
  • Fault Rules
  • Default Fault Rules

For more in-depth technical details check the documentation here and here.

Installing apigeelint

Cloning the source code

If you plan to create a plugin that can be used by other developers then you are encouraged to fork the repository, clone it, create the new plugin and then open a pull request to incorporate it into the official repository.

For example, to clone the official repository open your Git terminal and type:

git clone https://github.com/apigee/apigeelint.git
cd apigeelint
npm install

You need Node.js version >= 12.22.0 and NPM version >= 8.3.0.

With NPM install

In case you need to create a plugin that will be only useful for you or your company then you can create the new plugin and then push the changes to your own personal or internal Git repository.

If you just want to use apigeelint as-is and run your plugins from a specific external directory then type the command below to install it (this is what we'll use here):

npm install -g apigeelint

Apigeelint is a Node.js tool, so you'll need to have at least some basic knowledge of Node.js, NPM, and Javascript.

Creating a new plugin

If you are creating a plugin and don't want to share it with the open-source community then you can just develop it in any directory that you prefer, for example: C:\apigee\apigeelint-plugins. This is what we will do in this guide.

If you do want to share it with all apigeelint users then you'll need to fork the official repository and clone it to your computer. Keep in mind that all plugin files should reside in the lib\package\plugins directory and all of them are executed automatically. For more info about this approach check my original post here.

Example 1 - checkProxyNamePrefix.js

Let's say that we want to enforce that all proxy names start with some kind of identifier, for example, B2B-* for Business to Business proxies and B2C-* for Business to Consumer proxies. In this case, we are going to create a new plugin called checkProxyNamePrefix.js.

Note that I'm developing the plugin in a separate directory called apigeelint-plugins.

Here's the code for apigeelint-plugins\checkProxyNamePrefix.js:

const plugin = {
    ruleId: "MyRule-001",
    name: "Check if the proxy name starts with B2B- or B2C-",
    message: "The proxy name should start with B2B- or B2C-.",
    fatal: false,
    severity: 2, //error
    nodeType: "Bundle",
    enabled: true
  };

const onBundle = function(bundle, cb) {
  var hadError = false;
  var proxyName = bundle.getName();

  if (!proxyName.startsWith("B2B-") && !proxyName.startsWith("B2C-")) {
    bundle.addMessage({
      plugin,
      message: "API Proxy name (" + proxyName + ") should start with B2B-* or B2C-*"
    });
    hadError = true;
  }

  if (typeof(cb) == 'function') {
    cb(null, hadError);
  }
  return hadError;
};

module.exports = {
  plugin,
  onBundle
};

To test it we'll clone apigeelint's repository and use the sample proxy located in the directory test\fixtures\resources\sampleProxy\24Solver using the following command:

Note the -x parameter to specify our external plugins directory.

git clone https://github.com/apigee/apigeelint.git
apigeelint -x ./apigeelint-plugins -f table.js -s apigeelint\test\fixtures\resources\sampleProxy\24Solver\apiproxy

And we can see the error that our new plugin generated in the console output:

image.png

Example 2 - checkPreFlowSpikeArrest.js

In this plugin, we want to make sure that all our proxies are using the Spike Arrest policy in the PreFlow section.

Here's the code for apigeelint-plugins\checkPreFlowSpikeArrest.js:

const plugin = {
    ruleId: "MyRule-002",
    name: "Check if the Spike Arrest policy is being used in the PreFlow section",
    message: "Spike Arrest policy should be included in the PreFlow section.",
    fatal: false,
    severity: 2, //error
    nodeType: "ProxyEndpoint",
    enabled: true
  };

const onProxyEndpoint = function(ep, cb) {
  var hadError = false,
    spikeArrestFound = false;

  if (ep.getPreFlow()) {
    var steps = ep.getPreFlow().getFlowRequest().getSteps();
    steps.forEach(function(step) {
      if (step.getName() && ep.getParent().getPolicies()) {
        var p = ep.getParent().getPolicyByName(step.getName());
        if (p.getType() === "SpikeArrest") {
          spikeArrestFound = true;
        }
      }
    });
  }

  if (!spikeArrestFound) {
    ep.addMessage({
      plugin,
      message: plugin.message
    });
    hadError = true;
  }

  if (typeof(cb) == 'function') {
    cb(null, hadError);
  }
};

module.exports = {
  plugin,
  onProxyEndpoint
};

To test it we can use the same command as before:

apigeelint -x ./apigeelint-plugins -f table.js -s apigeelint\test\fixtures\resources\sampleProxy\24Solver\apiproxy

And we can see the error that our new plugin generated in the console output:

image.png

And finally our custom plugins messages are displayed with all the other standard plugins already provided by apigeelint. How cool is that??

image.png

BTW, this API Proxy definitely needs some improvements... ๐Ÿ˜

Unit test

It's always a best practice to create unit tests for all coding rules that we develop so here's an example of a test for our new plugins.

Note that to run the unit test shown below we need to first clone apigeelint's repository in a directory called apigeelint.

apigeelint-plugins\testMyCustomRules.js

const assert = require("assert"),
  path = require("path"),
  Bundle = require("../apigeelint/lib/package/Bundle.js"),
  bl = require("../apigeelint/lib/package/bundleLinter.js");

const configuration = {
  debug: true,
  source: {
    type: "filesystem",
    path: path.resolve(__dirname, '../apigeelint/test/fixtures/resources/sampleProxy/24Solver/apiproxy'),
    bundleType: "apiproxy"
  },
  profile: 'apigeex',
  excluded: {},
  setExitCode: false,
  output: () => {} // suppress output
};

describe("Check proxy name prefix", function() {
  var pluginFile =  path.resolve(__dirname, "checkProxyNamePrefix.js"),
    ruleId = "MyRule-001";

  it("should show error when proxy name don't start with B2B-* or B2C-*", function() {
    var bundle = new Bundle(configuration);
    bl.executePlugin(pluginFile, bundle);
    var report = getReportByRuleId(ruleId, bundle.getReport());
    assert.equal(report[0].message, "API Proxy name (TwentyFour) should start with B2B-* or B2C-*");
    assert.equal(report[0].severity, 2);
  });

  it("shouldn't show error when proxy name starts with B2B-*", function() {
    var bundle = new Bundle(configuration);
    bundle.getName = function() { return "B2B-TEST"; };
    bl.executePlugin(pluginFile, bundle);
    var report = getReportByRuleId(ruleId, bundle.getReport());
    assert.equal(report.length, 0);
  });

  it("shouldn't show error when proxy name starts with B2C-*", function() {
    var bundle = new Bundle(configuration);
    bundle.getName = function() { return "B2C-TEST"; };
    bl.executePlugin(pluginFile, bundle);
    var report = getReportByRuleId(ruleId, bundle.getReport());
    assert.equal(report.length, 0);
  });

});

describe("Check Spike Arrest", function() {
  var pluginFile =  path.resolve(__dirname, "checkPreFlowSpikeArrest.js"),
    ruleId = "MyRule-002";

  it("should show error when missing Spike Arrest", function() {
    var bundle = new Bundle(configuration);
    bl.executePlugin(pluginFile, bundle);
    var report = getReportByRuleId(ruleId, bundle.getReport());
    assert.equal(report[0].message, "Spike Arrest policy should be included in the PreFlow section.");
    assert.equal(report[0].severity, 2);
  });

});

function getReportByRuleId(ruleId, bundleReport) {
  var jsimpl = bl.getFormatter("json.js"),
    jsonReport = JSON.parse(jsimpl(bundleReport)),
    reports = [];

  for (let r of jsonReport) {
    for (let m of r.messages) {
      if (m.ruleId === ruleId) {
        reports.push(m);
      }
    }
  }
  return reports;
}

To run all tests you can use the following command:

npm test

We can see the output of our new tests in the console:

  Check proxy name prefix
    โˆš should show error when proxy name don't start with B2B-* or B2C-*
    โˆš shouldn't show error when proxy name starts with B2B-*
    โˆš shouldn't show error when proxy name starts with B2C-*

  Check Spike Arrest
    โˆš should show error when missing Spike Arrest

Conclusion

In this guide, I showed how to create 2 simple plugins for Apigeelint, one for checking the proxy name prefix and another to verify that the Spike Arrest policy is being used in the PreFlow. We also learned how to create unit tests for our plugins.

Apigeelint is a powerful tool for everybody working with Apigee proxies, especially if you add it to your CI/CD pipeline. I hope you found this guide useful.

Don't forget to contribute to the official repository if you create something awesome!

You can find all the source code used in this guide here. Feel free to use it!

Thanks and happy coding! ๐Ÿค 

ย