Skip to content

Commit

Permalink
Internal/e2e automated build (#989)
Browse files Browse the repository at this point in the history
* [internal/e2e-automated-builds] Internal work for fully automating release
  • Loading branch information
rvowles authored May 3, 2023
1 parent dd9d864 commit fd7cacc
Show file tree
Hide file tree
Showing 57 changed files with 435 additions and 277 deletions.
4 changes: 4 additions & 0 deletions adks/e2e-sdk/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
target
logs

7 changes: 7 additions & 0 deletions adks/e2e-sdk/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM node:18-buster-slim as build

WORKDIR /app
ADD app /app/app/
ADD features /app/features/
COPY package.json package-lock.json tsconfig.json run.sh /app/
RUN npm install && npm run build
3 changes: 3 additions & 0 deletions adks/e2e-sdk/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,7 @@ If you want to override the portfolio name when using ./create-demo.sh, use
`PORTFOLIO_NAME=pancakes ./create-demo.sh` (replace pancakes with whatever you
like, it needs to be unique for each run).

`WRITE_CONFIG` is used to allow writing the config generated by setting up the demo
example in a specific location. By default, it will drop into this folder.


7 changes: 5 additions & 2 deletions adks/e2e-sdk/app/steps/flag_steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,15 @@ Then(/^the feature flag is (locked|unlocked) and (off|on)$/, async function (loc
await waitForExpect(() => {
expect(world.repository).to.not.be.undefined;
expect(world.repository.readyness).to.eq(Readyness.Ready);
}, 2000, 500);

await waitForExpect(() => {
// const f = this.featureState(this.feature.key) as FeatureStateHolder;
const f = this.featureState(world.feature.key) as FeatureStateHolder;
console.log('key is val', world.feature.key, f.getBoolean(), value, f.isLocked(), lockedStatus);
logger.info('the feature %s is value %s and locked status %s', this.feature.key, f.getBoolean(), f.locked);
expect(f.getBoolean()).to.eq(value === 'on');
expect(f.isLocked()).to.eq(lockedStatus === 'locked');
expect(f.getBoolean(), `${f.key} flag is >${f.flag}< and expected to have a value >${value == 'on'}<`).to.eq(value === 'on');
expect(f.isLocked(), `${f.key} locked ${f.locked} and expected ${lockedStatus}`).to.eq(lockedStatus === 'locked');
}, 4000, 500);
});

Expand Down
4 changes: 3 additions & 1 deletion adks/e2e-sdk/app/steps/general_steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ Then('I write out a feature-examples config file', function () {
const world = this as SdkWorld;
const buf = `#!/bin/sh\nexport FEATUREHUB_SERVER_API_KEY=${world.sdkUrlServerEval}\nexport FEATUREHUB_CLIENT_API_KEY="${world.sdkUrlClientEval}"\nexport FEATUREHUB_EDGE_URL=${world.featureUrl}\nexport FEATUREHUB_BASE_URL=${this.adminUrl}\n`;

fs.writeFileSync('./example-test.sh', buf);
// we allow the config to be written in a specific location to share it in the docker overrides
// for automated testing
fs.writeFileSync((process.env.WRITE_CONFIG || '.') + '/example-test.sh', buf);
});

Then(/^there are (\d+) features$/, async function (numberOfFeatures) {
Expand Down
15 changes: 10 additions & 5 deletions adks/e2e-sdk/app/steps/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import {
ServiceAccountPermission, ServiceAccountServiceApi, UpdateEnvironment
} from 'featurehub-javascript-admin-sdk';
import { makeid, sleep } from '../support/random';
import { EdgeFeatureHubConfig, FeatureHubPollingClient, FHLog } from 'featurehub-javascript-node-sdk';
import { EdgeFeatureHubConfig, FeatureHubPollingClient, Readyness } from 'featurehub-javascript-node-sdk';
import waitForExpect from 'wait-for-expect';
import { logger } from '../support/logging';
import { SdkWorld } from '../support/world';
import { getWebserverExternalAddress } from '../support/make_me_a_webserver';
import { timeout } from 'nats/lib/nats-base-client/util';

Given(/^I create a new portfolio$/, async function () {
const world = this as SdkWorld;
Expand Down Expand Up @@ -157,7 +158,7 @@ Given('the edge connection is no longer available', async function () {
} catch (ignored) {}

expect(found).to.be.false;
});
}, 10000, 1000);
});

Given(/^I connect to the feature server$/, async function () {
Expand All @@ -182,9 +183,6 @@ Given(/^I connect to the feature server$/, async function () {
expect(found, `${serviceAccountPerm.sdkUrlClientEval} failed to connect`).to.be.true;
logger.info('Successfully completed poll');
const edge = new EdgeFeatureHubConfig(world.featureUrl, serviceAccountPerm.sdkUrlClientEval);
FHLog.fhLog.trace = (...args: any[]) => {
console.error(args);
};
this.sdkUrlClientEval = serviceAccountPerm.sdkUrlClientEval;
this.sdkUrlServerEval = serviceAccountPerm.sdkUrlServerEval;
//edge.edgeServiceProvider((repo, config) => new FeatureHubPollingClient(repo, config, 200));
Expand All @@ -193,6 +191,13 @@ Given(/^I connect to the feature server$/, async function () {
// give it time to connect
sleep(200);
world.repository = edge.repository();
// its important we wait for it to become ready before stuffing data into it otherwise we can create features at the same
// time we are waiting for a result back and miss the 1st feature
await waitForExpect(() => {
expect(world.repository).to.not.be.undefined;
expect(world.repository.readyness).to.eq(Readyness.Ready);
logger.info('repository is ready for action');
}, 2000, 500);
}, 4000, 200);
});

Expand Down
19 changes: 14 additions & 5 deletions adks/e2e-sdk/app/steps/webhook_steps.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { Given, Then } from '@cucumber/cucumber';
import { Given, Then, When } from '@cucumber/cucumber';
import { SdkWorld } from '../support/world';
import { WebhookCheck } from 'featurehub-javascript-admin-sdk';
import waitForExpect from 'wait-for-expect';
import { getWebhookData } from '../support/make_me_a_webserver';
import { expect } from 'chai';
import { sleep } from '../support/random';


When('I wait for {int} seconds', async function (seconds: number) {
await sleep(seconds * 1000);
});

Given(/^I test the webhook$/, async function () {
const world = this as SdkWorld;
Expand Down Expand Up @@ -34,11 +40,14 @@ Then(/^we receive a webhook with (.*) flag that is (locked|unlocked) and (off|on
}, 10000, 200);
});

Then(/^we should have 3 messages in the list of webhooks$/, async function() {
Then(/^we should have (\d+) messages in the list of webhooks$/, async function(resultCount: number) {
if (!process.env.EXTERNAL_NGROK && process.env.REMOTE_BACKEND) {
return;
}
const world = this as SdkWorld;
const hookResults = (await world.webhookApi.listWebhooks(world.environment.id)).data;
expect(hookResults.results.length).to.eq(3);

await waitForExpect(async () => {
const world = this as SdkWorld;
const hookResults = (await world.webhookApi.listWebhooks(world.environment.id)).data;
expect(hookResults.results.length).to.eq(3);
}, 2000, 200);
});
12 changes: 9 additions & 3 deletions adks/e2e-sdk/app/support/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export abstract class BackendDiscovery {
}

static async discover(): Promise<void> {
if (process.env.REMOTE_BACKEND) {
if (process.env.REMOTE_BACKEND || process.env.FEATUREHUB_BASE_URL) {
return;
}
if (!BackendDiscovery._discovered) {
Expand All @@ -108,20 +108,26 @@ export async function discover() {
}

export function mrHost() {
return process.env.REMOTE_BACKEND || `http://localhost:${BackendDiscovery.mrPort}`;
return process.env.FEATUREHUB_BASE_URL || process.env.REMOTE_BACKEND || `http://localhost:${BackendDiscovery.mrPort}`;
}

export function edgeHost() {
if (process.env.FEATUREHUB_EDGE_URL) {
return process.env.FEATUREHUB_EDGE_URL;
}

if (process.env.REMOTE_BACKEND) {
const backend = process.env.REMOTE_BACKEND;

if (backend.includes('/pistachio/')) {
return backend.substring(0, backend.lastIndexOf('/'));
}

return backend;
}
return `http://localhost:${BackendDiscovery.featuresPort}`;
}

export function supportsSSE() {
return process.env.REMOTE_BACKEND ? true : BackendDiscovery.supportsSSE;
return (process.env.REMOTE_BACKEND || process.env.FEATUREHUB_BASE_URL) ? true : BackendDiscovery.supportsSSE;
}
20 changes: 6 additions & 14 deletions adks/e2e-sdk/app/support/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
import { After, AfterAll, Before, BeforeAll } from '@cucumber/cucumber';
import {
Application,
AuthServiceApi,
Portfolio,
PortfolioServiceApi,
RoleType,
ServiceAccount,
ServiceAccountPermission,
SetupServiceApi,
UserCredentials
} from 'featurehub-javascript-admin-sdk';
import { After, Before, BeforeAll } from '@cucumber/cucumber';
import { AuthServiceApi, PortfolioServiceApi, SetupServiceApi, UserCredentials } from 'featurehub-javascript-admin-sdk';
import { makeid } from './random';
import { expect } from 'chai';
import { SdkWorld } from './world';
import { discover } from './discovery';
import { startWebServer, terminateServer } from './make_me_a_webserver';
import { lstat } from 'fs';

const superuserEmailAddress = '[email protected]';
// const superuserEmailAddress = '[email protected]';
Expand Down Expand Up @@ -65,6 +53,10 @@ Before(async function () {
await ensureLoggedIn(this as SdkWorld);
});

Before(function () {
this.setScenarioId(makeid(30));
});

After(function () {
if (this.edgeServer) {
console.log('shutting down edge connection');
Expand Down
29 changes: 25 additions & 4 deletions adks/e2e-sdk/app/support/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import * as winston from 'winston';
import { MESSAGE } from 'triple-beam';
import stringify from 'safe-stable-stringify';
import { fhLog } from 'featurehub-javascript-client-sdk';

const httpAwareJsonFormatter = winston.format((info, opts) => {
const json = {};
Expand All @@ -27,6 +28,8 @@ const httpAwareJsonFormatter = winston.format((info, opts) => {
delete info.level;
}

json['@timestamp'] = new Date().toISOString()

// any extra fields
json['@fields'] = info;

Expand All @@ -47,17 +50,31 @@ export const logger = winston.createLogger({
winston.format.splat(),
httpAwareJsonFormatter()
),
defaultMeta: { service: 'e2e-sdk-testing' },
defaultMeta: {service: 'e2e-sdk-testing'},
transports: [
//
// - Write all logs with level `error` and below to `error.log`
// - Write all logs with level `verbose` and below to `combined.log`
//
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log', level: 'verbose' }),
new winston.transports.File({filename: 'logs/error.log', level: 'error'}),
new winston.transports.File({filename: 'logs/combined.log', level: 'verbose'}),
],
});

fhLog.error = (...args: any[]) => {
logger.log({level: 'error', message: args.join(" "), sdkTrace: true});
};

fhLog.log = (...args: any[]) => {
logger.log({level: 'info', message: args.join(" "), sdkTrace: true});
};

fhLog.trace = (...args: any[]) => {
logger.log({level: 'error', message: args.map(o => (typeof o === 'string') ? o : JSON.stringify(o).replace('"', "'")).join(" "), sdkTrace: true});
};

fhLog.trace('this is a message');

if (process.env.DEBUG) {
logger.add(new winston.transports.Console({
format: winston.format.combine(
Expand Down Expand Up @@ -101,7 +118,11 @@ export function axiosLoggingAttachment(axiosInstances: Array<AxiosInstance>) {
return resp;
}, (error) => {
if (error.response) {
logger.log({level: 'error', message: 'response rejected:', http: JSON.stringify(responseToRecord(error.response), undefined, 2) });
logger.log({
level: 'error',
message: 'response rejected:',
http: JSON.stringify(responseToRecord(error.response), undefined, 2)
});
}
return Promise.reject(error);
});
Expand Down
8 changes: 4 additions & 4 deletions adks/e2e-sdk/app/support/make_me_a_webserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,26 +70,26 @@ export function startWebServer(): Promise<void> {

try {
server.listen(3000, function () {
console.log('%s listening at %s', server.name, server.url);
logger.info(`${server.name} listening at ${server.url}`);
resolve();
});
} catch (e) {
server = undefined;
console.error("Failed to listen", e);
logger.error("Failed to listen", e);
reject(e);
}
});
}

export function terminateServer(): Promise<void> {
console.log("terminating webserver");
logger.debug("terminating webserver");
return new Promise((resolve, reject) => {
if (server) {
try {
server.close(() => { resolve(); });
server = undefined;
} catch (e) {
console.error("Failed to close server", e);
logger.error("Failed to close server", e);
reject(e);
}
} else {
Expand Down
39 changes: 36 additions & 3 deletions adks/e2e-sdk/app/support/world.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
WebhookServiceApi
} from 'featurehub-javascript-admin-sdk';
import { axiosLoggingAttachment, logger } from './logging';
import globalAxios from 'axios';
import globalAxios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
import {
ClientContext,
EdgeFeatureHubConfig,
Expand Down Expand Up @@ -55,7 +55,7 @@ export class SdkWorld extends World {
private _clientContext: ClientContext;
public sdkUrlClientEval: string;
public sdkUrlServerEval: string;

private scenarioId: string;

constructor(props) {
super(props);
Expand All @@ -80,6 +80,40 @@ export class SdkWorld extends World {
this.webhookApi = new WebhookServiceApi(this.adminApiConfig);

axiosLoggingAttachment([this.adminApiConfig.axiosInstance]);
const self = this;
this.attachBaggageInterceptors(() => {
return self.baggageHeader();
}, [this.adminApiConfig.axiosInstance])
}

private attachBaggageInterceptors(baggageHeader: () => string | undefined, axiosInstances: Array<AxiosInstance>): void {
axiosInstances.forEach((axios) => {
axios.interceptors.request.use((reqConfig: InternalAxiosRequestConfig) => {
const header = baggageHeader();

if (header) {
reqConfig.headers['baggage'] = header;
}

return reqConfig;
}, (error) => Promise.reject(error));
});
}

private baggageHeader(): string | undefined {
const headers = [];

if (this.scenarioId) {
headers.push(`cucumberScenarioId=${this.scenarioId}`);
}

return headers.length == 0 ? undefined : headers.join(',');
}

public setScenarioId(id: string) {
this.scenarioId = id;
logger.info('session id is %s', this.scenarioId);
this.attach(`scenarioId=${id}`, 'text/plain');
}

public reset(): void {
Expand Down Expand Up @@ -146,7 +180,6 @@ export class SdkWorld extends World {
const fValueResult = await this.featureValueApi.getFeatureForEnvironment(this.environment.id, this.feature.key);
return fValueResult.data;
} catch (e) {
console.log(e);
expect(e.response.status).to.eq(404); // null value

if (e.response.status === 404) {
Expand Down
2 changes: 2 additions & 0 deletions adks/e2e-sdk/features/webhooks.feature
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ Feature: Webhooks work as expected
Given I create a new portfolio
And I create an application
When There is a feature flag with the key FEATURE_TITLE_TO_UPPERCASE
And I wait for 5 seconds
And I update the environment for feature webhooks
And I wait for 5 seconds
And I test the webhook
Then we receive a webhook with FEATURE_TITLE_TO_UPPERCASE flag that is locked and off
And I set the feature flag to unlocked and on
Expand Down
2 changes: 2 additions & 0 deletions adks/e2e-sdk/make-image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/sh
docker build -t featurehub/e2e-adk:1.0 .
2 changes: 2 additions & 0 deletions adks/e2e-sdk/run.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#!/bin/sh
mkdir -p logs
rm -rf logs/*
if [ $# -eq 0 ]
then
echo DEBUG=true npm run test
Expand Down
Loading

0 comments on commit fd7cacc

Please sign in to comment.