Skip to content

Commit

Permalink
Merge pull request #22 from wyvern8/feature/restoreFeatureFlags
Browse files Browse the repository at this point in the history
feat(flags): restoreFeatureFlags mode
  • Loading branch information
wyvern8 authored Mar 14, 2018
2 parents 310e66f + c5baa90 commit 4c7dad4
Show file tree
Hide file tree
Showing 6 changed files with 424 additions and 19 deletions.
24 changes: 23 additions & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ as api grouping util classes are attached to this class.
* [.toggleFeatureFlag(projectKey, featureFlagKey, environmentKeyQuery, value)](#LaunchDarklyUtilsFlags+toggleFeatureFlag) ⇒ <code>Promise</code>
* [.migrateFeatureFlag(projectKey, featureFlagKey, fromEnv, toEnv, includeState)](#LaunchDarklyUtilsFlags+migrateFeatureFlag) ⇒ <code>Promise</code>
* [.bulkMigrateFeatureFlags(projectKey, featureFlagKeys, fromEnv, toEnv, includeState)](#LaunchDarklyUtilsFlags+bulkMigrateFeatureFlags) ⇒ <code>Promise</code>
* [.restoreFeatureFlags(projectKey, featureFlagKeys, targetEnv, backupJsonFile, includeState)](#LaunchDarklyUtilsFlags+restoreFeatureFlags) ⇒ <code>Promise</code>

<a name="new_LaunchDarklyUtilsFlags_new"></a>

Expand Down Expand Up @@ -153,7 +154,7 @@ Get the boolean state of a single feature flag by key, and optional environment

**Example**
```js
getFeatureFlagState ldutils my-project my-flag dev
ldutils getFeatureFlagState my-project my-flag dev
```
<a name="LaunchDarklyUtilsFlags+updateFeatureFlag"></a>

Expand Down Expand Up @@ -238,6 +239,27 @@ targets, rules, fallthrough, offVariation, prerequisites and optionally the flag
```js
ldutils bulkMigrateFeatureFlags my-project my-flag,my-flag-two dev test
```
<a name="LaunchDarklyUtilsFlags+restoreFeatureFlags"></a>

### launchDarklyUtilsFlags.restoreFeatureFlags(projectKey, featureFlagKeys, targetEnv, backupJsonFile, includeState) ⇒ <code>Promise</code>
Restore feature flags to state captured in a backup json file generated by getFeatureFlags(proj).

**Kind**: instance method of [<code>LaunchDarklyUtilsFlags</code>](#LaunchDarklyUtilsFlags)
**Fulfil**: <code>Object</code> updated feature flag json
**Reject**: <code>Error</code> object with message

| Param | Type | Description |
| --- | --- | --- |
| projectKey | <code>string</code> | project identifier |
| featureFlagKeys | <code>string</code> | feature flag identifiers comma separated |
| targetEnv | <code>string</code> | environment to restore flag attributes to |
| backupJsonFile | <code>string</code> | file to restore from from getFeatureFlags(proj) |
| includeState | <code>boolean</code> | optionally restore boolean state true/false |

**Example**
```js
ldutils restoreFeatureFlags my-project my-flag,my-flag-two prod ./preReleaseBackup.json true
```
<a name="LaunchDarklyUtilsMembers"></a>

## LaunchDarklyUtilsMembers
Expand Down
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,34 @@ In addition we expose apis as a commandline tool.
1. `npm install launchdarkly-nodeutils --save` or clone this repo.
2. Generate an access token with the permissions for the operations you will use. Please read: https://docs.launchdarkly.com/v2.0/docs/api-access-tokens

## commandline usage
## Use cases
The command line tool or nodejs module can be used for many things. Here are some examples.

***Automated management of ACLs***

Users of Enterprise LaunchDarkly can create Custom Roles for fine-grained RBAC. These can be maintained in git, and applied to LaunchDarkly when changes are committed via `bulkUpsertCustomRoles` or `bulkUpsertCustomRoleFolder`.

***Setting flag state for automated tests***

Automated test suites can call the apis to toggle features and set targetting rules before test executions. With care, a matrix of flag based test scenarios could defined and executed.

***Releasing flags in bulk***

A typical release workflow to move a set of flag attributes from test to prod may consist of CI jobs calling ldutils to:
1. create a backup of flags to be migrated using `getFeatureFlags my-proj > ./backup.json`
2. use `bulkMigrateFeatureFlags my-proj flag-x,flag-y test prod` to copy flag attrs from test to prod
3. if issues, to rollback the flags use `restoreFeatureFlags my-proj flag-x,flag-y prod ./backup.json`

***Scheduled backup of flags***

A cron/ci job could call getFeatureFlags for each project, and commit the json to private git repo in order to keep a versioned record of change. This could also be used to recover environment state.

***Refreshing Environments***

Copying flag attributes from prod back to preprod for testing. Also newly created environments can be primed with flag targetting rules using `bulkMigrateFeatureFlags`.


## Command line usage
After cloning this repo you can make `ldutils` executable, and use it to make api calls based on passed in parameters.

> please read the [API documentation](API.md) for examples.
Expand Down Expand Up @@ -78,6 +105,7 @@ The following modes are supported. This info is also available via: `ldutils -h
| toggleFeatureFlag | projectKey, featureFlagKey, environmentKeyQuery, enabled |
| migrateFeatureFlag | projectKey, featureFlagKey, fromEnv, toEnv, includeState |
| bulkMigrateFeatureFlags | projectKey, featureFlagKeys, fromEnv, toEnv, includeState |
| restoreFeatureFlags | projectKey, featureFlagKeys, targetEnv, backupJsonFile, includeState |

- `migrateFeatureFlag` mode is used to copy flag attributes between environments. This covers: targets, rules, fallthrough, offVariation, prerequisites and optionally the flag on/off state. eg. to migrate a flag from dev to test env.

Expand Down
9 changes: 9 additions & 0 deletions ldutils
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ new LaunchDarklyUtils().create(process.env.LAUNCHDARKLY_API_TOKEN, log).then(fun
});
});

program
.command(`restoreFeatureFlags <projectKey> <featureFlagKeys> <targetEnv> <backupJsonFile> (includeState)`)
.description('restore environment flag settings from a backup file - backup from getFeatureFlags')
.action(function(projectKey, featureFlagKeys, targetEnv, backupJsonFile, includeState) {
ldUtils.flags.restoreFeatureFlags(projectKey, featureFlagKeys, targetEnv, backupJsonFile, includeState).then(function(response) {
console.log(json.plain(response));
});
});

program
.command('getCustomRoles')
.description('get all custom roles in account')
Expand Down
89 changes: 72 additions & 17 deletions src/LaunchDarklyUtilsFlags.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { default as json } from 'format-json';
import { default as jsonPatch } from 'fast-json-patch';
import { default as fs } from 'fs';
import { default as _ } from 'lodash';

// Class representing Feature flag functionality
export class LaunchDarklyUtilsFlags {
Expand Down Expand Up @@ -90,7 +92,7 @@ export class LaunchDarklyUtilsFlags {
* @returns {Promise}
* @fulfil {boolean} true/false
* @reject {Error} object with message
* @example getFeatureFlagState ldutils my-project my-flag dev
* @example ldutils getFeatureFlagState my-project my-flag dev
*/
async getFeatureFlagState(projectKey, featureFlagKey, environmentKeyQuery) {
return this.getFeatureFlag(projectKey, featureFlagKey, environmentKeyQuery).then(result => {
Expand Down Expand Up @@ -164,31 +166,36 @@ export class LaunchDarklyUtilsFlags {
return this.getFeatureFlag(projectKey, featureFlagKey)
.then(flag => {
let patchDelta = jsonPatch.compare(flag.environments[toEnv], flag.environments[fromEnv]);
that.log.debug(`envFlagDiff for '${featureFlagKey}' ${json.plain(patchDelta)}`);
that.log.debug(`flagDelta for '${featureFlagKey}' ${json.plain(patchDelta)}`);
return patchDelta;
})
.then(patchDelta => {
let patchComment = [];
patchDelta.forEach(patch => {
if (
patch.path.startsWith('/targets') ||
patch.path.startsWith('/rules') ||
patch.path.startsWith('/fallthrough') ||
patch.path.startsWith('/offVariation') ||
patch.path.startsWith('/prerequisites') ||
(includeState && patch.path.startsWith('/on'))
) {
// add target env obj path and push
patch.path = `/environments/${toEnv}${patch.path}`;
patchComment.push(patch);
}
});
let patchComment = this.assembleFlagPatch(patchDelta, toEnv, includeState);

that.log.debug(`patchComment for '${featureFlagKey}' in ${toEnv} : ${json.plain(patchComment)}`);
return this.updateFeatureFlag(projectKey, featureFlagKey, patchComment);
});
}

assembleFlagPatch(patchDelta, targetEnv, includeState) {
let patchComment = [];
patchDelta.forEach(patch => {
if (
patch.path.startsWith('/targets') ||
patch.path.startsWith('/rules') ||
patch.path.startsWith('/fallthrough') ||
patch.path.startsWith('/offVariation') ||
patch.path.startsWith('/prerequisites') ||
(includeState && patch.path.startsWith('/on'))
) {
// add target env obj path and push
patch.path = `/environments/${targetEnv}${patch.path}`;
patchComment.push(patch);
}
});
return patchComment;
}

/**
* Migrate multiple feature flags properties between environments in a project. this includes:
* targets, rules, fallthrough, offVariation, prerequisites and optionally the flags on/off state.
Expand All @@ -211,4 +218,52 @@ export class LaunchDarklyUtilsFlags {

return Promise.all(promises);
}

/**
* Restore feature flags to state captured in a backup json file generated by getFeatureFlags(proj).
* @param {string} projectKey - project identifier
* @param {string} featureFlagKeys - feature flag identifiers comma separated
* @param {string} targetEnv - environment to restore flag attributes to
* @param {string} backupJsonFile - file to restore from from getFeatureFlags(proj)
* @param {boolean} includeState - optionally restore boolean state true/false
* @returns {Promise}
* @fulfil {Object} updated feature flag json
* @reject {Error} object with message
* @example ldutils restoreFeatureFlags my-project my-flag,my-flag-two prod ./preReleaseBackup.json true
*/
async restoreFeatureFlags(projectKey, featureFlagKeys, targetEnv, backupJsonFile, includeState) {
let that = this;
let promises = [];

const backupJson = JSON.parse(fs.readFileSync(process.cwd() + '/' + backupJsonFile, 'utf-8'));

//foreach flag, lookup node in env branch
featureFlagKeys.split(',').forEach(key => {
promises.push(
this.getFeatureFlag(projectKey, key)
.then(flag => {
let backupFlagArr = _.filter(backupJson.items, { key: key });
if (backupFlagArr.length === 0) {
that.log.error(`flag does not exist in backup: ${key}`);
}
let backupFlag = backupFlagArr[0];
that.log.debug(`backupFlag: ${json.plain(backupFlag)}`);
let patchDelta = jsonPatch.compare(
flag.environments[targetEnv],
backupFlag.environments[targetEnv]
);
that.log.debug(`flagDelta for '${key}' ${json.plain(patchDelta)}`);
return patchDelta;
})
.then(patchDelta => {
let patchComment = this.assembleFlagPatch(patchDelta, targetEnv, includeState);

that.log.debug(`patchComment for '${key}' in ${targetEnv} : ${json.plain(patchComment)}`);
return this.updateFeatureFlag(projectKey, key, patchComment);
})
);
});

return Promise.all(promises);
}
}
41 changes: 41 additions & 0 deletions test/LaunchDarklyUtilsFlags.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,45 @@ describe('LaunchDarklyUtilsFlags', function() {
});
});
});

describe('restoreFeatureFlags', function() {
before(done => {
let scope = nock('https://app.launchdarkly.com')
.get('/api/v2/flags/sample-project/sort.order')
.replyWithFile(200, __dirname + '/fixtures/feature-flags-get.json', {
'Content-Type': 'application/json'
})
.patch('/api/v2/flags/sample-project/sort.order')
.replyWithFile(200, __dirname + '/fixtures/feature-flags-update.json', {
'Content-Type': 'application/json'
})
.get('/api/v2/flags/sample-project/sort.order2')
.replyWithFile(200, __dirname + '/fixtures/feature-flags-get-two.json', {
'Content-Type': 'application/json'
})
.patch('/api/v2/flags/sample-project/sort.order2')
.replyWithFile(200, __dirname + '/fixtures/feature-flags-update-two.json', {
'Content-Type': 'application/json'
});
assert(scope);
done();
});

it('should make expected api call and return results', async function() {
let expected = [];
expected.push(JSON.parse(fs.readFileSync(__dirname + '/fixtures/feature-flags-update.json', 'utf-8')));
expected.push(JSON.parse(fs.readFileSync(__dirname + '/fixtures/feature-flags-update-two.json', 'utf-8')));
return ldutils.flags
.restoreFeatureFlags(
'sample-project',
'sort.order,sort.order2',
'test',
'./test/fixtures/sampleBackup.json',
true
)
.then(actual => {
expect(actual).to.deep.equal(expected);
});
});
});
});
Loading

0 comments on commit 4c7dad4

Please sign in to comment.