Skip to content

Commit

Permalink
Feature Flag Guard
Browse files Browse the repository at this point in the history
  • Loading branch information
NickPhura committed Feb 23, 2024
1 parent 95e86f6 commit ba07abf
Show file tree
Hide file tree
Showing 21 changed files with 372 additions and 98 deletions.
6 changes: 3 additions & 3 deletions api/.pipeline/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ const phases = {
backboneIntakePath: '/api/dwc/submission/queue',
backboneArtifactIntakePath: '/api/artifact/intake',
biohubTaxonTsnPath: '/api/taxonomy/taxon/tsn',
backboneIntakeEnabled: false,
bctwApiHost: 'https://moe-bctw-api-dev.apps.silver.devops.gov.bc.ca',
critterbaseApiHost: 'https://moe-critterbase-api-dev.apps.silver.devops.gov.bc.ca/api',
nodeEnv: 'development',
s3KeyPrefix: (isStaticDeployment && 'sims') || `local/${deployChangeId}/sims`,
tz: config.timezone.api,
sso: config.sso.dev,
featureFlags: 'API_FF_SUBMIT_BIOHUB',
logLevel: 'info',
nodeOptions: '--max_old_space_size=2250', // 75% of memoryLimit (bytes)
cpuRequest: '50m',
Expand All @@ -116,13 +116,13 @@ const phases = {
backboneIntakePath: '/api/dwc/submission/queue',
backboneArtifactIntakePath: '/api/artifact/intake',
biohubTaxonTsnPath: '/api/taxonomy/taxon/tsn',
backboneIntakeEnabled: false,
bctwApiHost: 'https://moe-bctw-api-test.apps.silver.devops.gov.bc.ca',
critterbaseApiHost: 'https://moe-critterbase-api-test.apps.silver.devops.gov.bc.ca/api',
nodeEnv: 'production',
s3KeyPrefix: 'sims',
tz: config.timezone.api,
sso: config.sso.test,
featureFlags: 'API_FF_SUBMIT_BIOHUB',
logLevel: 'info',
nodeOptions: '--max_old_space_size=2250', // 75% of memoryLimit (bytes)
cpuRequest: '50m',
Expand All @@ -149,13 +149,13 @@ const phases = {
backboneIntakePath: '/api/dwc/submission/queue',
backboneArtifactIntakePath: '/api/artifact/intake',
biohubTaxonTsnPath: '/api/taxonomy/taxon/tsn',
backboneIntakeEnabled: false,
bctwApiHost: 'https://moe-bctw-api-prod.apps.silver.devops.gov.bc.ca',
critterbaseApiHost: 'https://moe-critterbase-api-prod.apps.silver.devops.gov.bc.ca/api',
nodeEnv: 'production',
s3KeyPrefix: 'sims',
tz: config.timezone.api,
sso: config.sso.prod,
featureFlags: 'API_FF_SUBMIT_BIOHUB',
logLevel: 'warn',
nodeOptions: '--max_old_space_size=2250', // 75% of memoryLimit (bytes)
cpuRequest: '50m',
Expand Down
3 changes: 2 additions & 1 deletion api/.pipeline/lib/api.deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ const apiDeploy = async (settings) => {
BACKBONE_INTERNAL_API_HOST: phases[phase].backboneInternalApiHost,
BACKBONE_INTAKE_PATH: phases[phase].backboneIntakePath,
BACKBONE_ARTIFACT_INTAKE_PATH: phases[phase].backboneArtifactIntakePath,
BACKBONE_INTAKE_ENABLED: phases[phase].backboneIntakeEnabled,
BIOHUB_TAXON_PATH: phases[phase].biohubTaxonPath,
BIOHUB_TAXON_TSN_PATH: phases[phase].biohubTaxonTsnPath,
// BCTW / Critterbase
Expand Down Expand Up @@ -66,6 +65,8 @@ const apiDeploy = async (settings) => {
KEYCLOAK_API_ENVIRONMENT: phases[phase].sso.cssApi.cssApiEnvironment,
// Log Level
LOG_LEVEL: phases[phase].logLevel || 'info',
// Feature Flags
FEATURE_FLAGS: phases[phase].featureFlags,
// Openshift Resources
CPU_REQUEST: phases[phase].cpuRequest,
CPU_LIMIT: phases[phase].cpuLimit,
Expand Down
11 changes: 6 additions & 5 deletions api/.pipeline/templates/api.dc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,6 @@ parameters:
- name: BACKBONE_ARTIFACT_INTAKE_PATH
required: true
description: API path for BioHub Platform Backbone artifact submission intake endpoint. Example "/api/path/to/artifact/intake".
- name: BACKBONE_INTAKE_ENABLED
required: true
description: Controls whether or not SIMS will submit DwCA datasets to the BioHub Platform Backbone. Set to "true" to enable it, will be disabled by default otherwise.
- name: BIOHUB_TAXON_TSN_PATH
required: true
description: API path for BioHub Platform Backbone taxon TSN endpoint. Example "/api/path/to/taxon/tsn".
Expand Down Expand Up @@ -121,6 +118,10 @@ parameters:
# Log level
- name: LOG_LEVEL
value: 'info'
# Feature Flags
- name: FEATURE_FLAGS
description: Used to identify features that should be temporarily disabled/hidden. Must be a comma delimited list of keywords.
value: ''
# GCNotify
- name: GCNOTIFY_API_SECRET
description: Secret for gcnotify api key
Expand Down Expand Up @@ -227,8 +228,6 @@ objects:
value: ${BACKBONE_INTAKE_PATH}
- name: BACKBONE_ARTIFACT_INTAKE_PATH
value: ${BACKBONE_ARTIFACT_INTAKE_PATH}
- name: BACKBONE_INTAKE_ENABLED
value: ${BACKBONE_INTAKE_ENABLED}
- name: BIOHUB_TAXON_TSN_PATH
value: ${BIOHUB_TAXON_TSN_PATH}
- name: BIOHUB_TAXON_PATH
Expand Down Expand Up @@ -342,6 +341,8 @@ objects:
value: ${GCNOTIFY_EMAIL_URL}
- name: GCNOTIFY_SMS_URL
value: ${GCNOTIFY_SMS_URL}
- name: FEATURE_FLAGS
value: ${FEATURE_FLAGS}
image: ' '
imagePullPolicy: Always
ports:
Expand Down
9 changes: 4 additions & 5 deletions api/src/services/platform-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { describe } from 'mocha';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import { ObservationRecord } from '../repositories/observation-repository';
import * as featureFlagUtils from '../utils/feature-flag-utils';
import { getMockDBConnection } from '../__mocks__/db';
import { AttachmentService } from './attachment-service';
import { HistoryPublishService } from './history-publish-service';
Expand All @@ -24,8 +25,8 @@ describe('PlatformService', () => {
sinon.restore();
});

it('throws an error if BioHub intake is not enabled', async () => {
process.env.BACKBONE_INTAKE_ENABLED = 'false';
it('throws an error if publishing to BioHub is not currently enabled.', async () => {
sinon.stub(featureFlagUtils, 'isFeatureFlagPresent').returns(true);

const mockDBConnection = getMockDBConnection();
const platformService = new PlatformService(mockDBConnection);
Expand All @@ -34,12 +35,11 @@ describe('PlatformService', () => {
await platformService.submitSurveyToBioHub(1, { submissionComment: 'test' });
expect.fail();
} catch (error) {
expect((error as Error).message).to.equal('BioHub intake is not enabled');
expect((error as Error).message).to.equal('Publishing to BioHub is not currently enabled.');
}
});

it('throws error when axios request fails', async () => {
process.env.BACKBONE_INTAKE_ENABLED = 'true';
process.env.BACKBONE_INTERNAL_API_HOST = 'http://backbone-host.dev/';

const mockDBConnection = getMockDBConnection();
Expand Down Expand Up @@ -69,7 +69,6 @@ describe('PlatformService', () => {
});

it('should submit survey to BioHub successfully', async () => {
process.env.BACKBONE_INTAKE_ENABLED = 'true';
process.env.BACKBONE_INTERNAL_API_HOST = 'http://backbone-host.dev/';

const mockDBConnection = getMockDBConnection();
Expand Down
6 changes: 3 additions & 3 deletions api/src/services/platform-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { IDBConnection } from '../database/db';
import { ApiError, ApiErrorType, ApiGeneralError } from '../errors/api-error';
import { PostSurveySubmissionToBioHubObject } from '../models/biohub-create';
import { ISurveyAttachment, ISurveyReportAttachment } from '../repositories/attachment-repository';
import { isFeatureFlagPresent } from '../utils/feature-flag-utils';
import { getFileFromS3 } from '../utils/file-utils';
import { getLogger } from '../utils/logger';
import { AttachmentService } from './attachment-service';
Expand Down Expand Up @@ -54,7 +55,6 @@ export interface ITaxonomy {
scientificName: string;
}

const getBackboneIntakeEnabled = () => process.env.BACKBONE_INTAKE_ENABLED === 'true' || false;
const getBackboneInternalApiHost = () => process.env.BACKBONE_INTERNAL_API_HOST || '';
const getBackboneArtifactIntakePath = () => process.env.BACKBONE_ARTIFACT_INTAKE_PATH || '';
const getBackboneSurveyIntakePath = () => process.env.BACKBONE_INTAKE_PATH || '';
Expand Down Expand Up @@ -120,8 +120,8 @@ export class PlatformService extends DBService {
): Promise<{ submission_uuid: string }> {
defaultLog.debug({ label: 'submitSurveyToBioHub', message: 'params', surveyId });

if (!getBackboneIntakeEnabled()) {
throw new ApiGeneralError('BioHub intake is not enabled');
if (isFeatureFlagPresent(['API_FF_SUBMIT_BIOHUB'])) {
throw new ApiGeneralError('Publishing to BioHub is not currently enabled.');
}

const keycloakService = new KeycloakService();
Expand Down
99 changes: 99 additions & 0 deletions api/src/utils/feature-flag-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { expect } from 'chai';
import sinon from 'sinon';
import { getFeatureFlags, getFeatureFlagsString, isFeatureFlagPresent } from './feature-flag-utils';

describe('getFeatureFlagsString', () => {
describe('returns the env var `FEATURE_FLAGS`', () => {
const env = Object.assign({}, process.env);

afterEach(() => {
sinon.restore();
process.env = env;
});

it('when the FEATURE_FLAGS env var is undefined', () => {
process.env.FEATURE_FLAGS = undefined;

expect(getFeatureFlagsString()).to.equal(undefined);
});

it('when the FEATURE_FLAGS env var is defined', () => {
process.env.FEATURE_FLAGS = 'flag1,flag2,flag3';

expect(getFeatureFlagsString()).to.equal('flag1,flag2,flag3');
});
});
});

describe('getFeatureFlags', () => {
describe('returns an array of flags', () => {
const env = Object.assign({}, process.env);

afterEach(() => {
sinon.restore();
process.env = env;
});

it('when the FEATURE_FLAGS env var is undefined', () => {
process.env.FEATURE_FLAGS = undefined;

expect(getFeatureFlags()).to.eql([]);
});

it('when the FEATURE_FLAGS env var is defined', () => {
process.env.FEATURE_FLAGS = 'flag1,flag2,flag3';

expect(getFeatureFlags()).to.eql(['flag1', 'flag2', 'flag3']);
});
});
});

describe('isFeatureFlagPresent', () => {
describe('returns true', () => {
const env = Object.assign({}, process.env);

afterEach(() => {
sinon.restore();
process.env = env;
});

it('when one flag matches', () => {
process.env.FEATURE_FLAGS = 'flag1,flag2,flag3';

expect(isFeatureFlagPresent(['flag2'])).to.equal(true);
});

it('when two flags match', () => {
process.env.FEATURE_FLAGS = 'flag1,flag2,flag3';

expect(isFeatureFlagPresent(['flag2', 'flag1'])).to.equal(true);
});

it('when some, but not all, flags match', () => {
process.env.FEATURE_FLAGS = 'flag1,flag2,flag3';

expect(isFeatureFlagPresent(['flag4', 'flag3'])).to.equal(true);
});
});

describe('returns false', () => {
const env = Object.assign({}, process.env);

afterEach(() => {
sinon.restore();
process.env = env;
});

it('when no flags match', () => {
process.env.FEATURE_FLAGS = 'flag1,flag2,flag3';

expect(isFeatureFlagPresent(['flag4'])).to.equal(false);
});

it('when no flags specified', () => {
process.env.FEATURE_FLAGS = 'flag1,flag2,flag3';

expect(isFeatureFlagPresent([])).to.equal(false);
});
});
});
34 changes: 34 additions & 0 deletions api/src/utils/feature-flag-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Returns the raw feature flag string from the environment variable `REACT_APP_FEATURE_FLAGS`.
*
* @return {*} {(string | undefined)}
*/
export const getFeatureFlagsString = (): string | undefined => {
return process.env.FEATURE_FLAGS;
};

/**
* Returns a parsed array of feature flag strings from the environment variable `REACT_APP_FEATURE_FLAGS`.
*
* @return {*} {string[]}
*/
export const getFeatureFlags = (): string[] => {
const featureFlagsString = getFeatureFlagsString();

if (!featureFlagsString) {
return [];
}

return featureFlagsString.split(',');
};

/**
* Returns `true` if at least one of the provided `featureFlags` is present in the environment variable
* `REACT_APP_FEATURE_FLAGS`.
*
* @param {string[]} featureFlags
* @return {*} {boolean}
*/
export const isFeatureFlagPresent = (featureFlags: string[]): boolean => {
return getFeatureFlags().some((flag) => featureFlags.includes(flag));
};
6 changes: 3 additions & 3 deletions app/.pipeline/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ const phases = {
maxUploadFileSize,
nodeEnv: 'development',
sso: config.sso.dev,
featureFlags: '',
cpuRequest: '50m',
cpuLimit: '300m',
memoryRequest: '100Mi',
memoryLimit: '500Mi',
replicas: (isStaticDeployment && '1') || '1',
replicasMax: (isStaticDeployment && '2') || '1',
biohubFeatureFlag: 'true',
backbonePublicApiHost: 'https://api-dev-biohub-platform.apps.silver.devops.gov.bc.ca',
biohubTaxonPath: '/api/taxonomy/taxon',
biohubTaxonTsnPath: '/api/taxonomy/taxon/tsn'
Expand All @@ -110,13 +110,13 @@ const phases = {
maxUploadFileSize,
nodeEnv: 'production',
sso: config.sso.test,
featureFlags: 'APP_FF_PUBLISH_BIOHUB',
cpuRequest: '50m',
cpuLimit: '500m',
memoryRequest: '100Mi',
memoryLimit: '500Mi',
replicas: '2',
replicasMax: '3',
biohubFeatureFlag: 'false',
backbonePublicApiHost: 'https://api-test-biohub-platform.apps.silver.devops.gov.bc.ca',
biohubTaxonPath: '/api/taxonomy/taxon',
biohubTaxonTsnPath: '/api/taxonomy/taxon/tsn'
Expand All @@ -137,13 +137,13 @@ const phases = {
maxUploadFileSize,
nodeEnv: 'production',
sso: config.sso.prod,
featureFlags: 'APP_FF_PUBLISH_BIOHUB',
cpuRequest: '50m',
cpuLimit: '500m',
memoryRequest: '100Mi',
memoryLimit: '500Mi',
replicas: '2',
replicasMax: '3',
biohubFeatureFlag: 'false',
backbonePublicApiHost: 'https://api-biohub-platform.apps.silver.devops.gov.bc.ca',
biohubTaxonPath: '/api/taxonomy/taxon',
biohubTaxonTsnPath: '/api/taxonomy/taxon/tsn'
Expand Down
3 changes: 2 additions & 1 deletion app/.pipeline/lib/app.deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,15 @@ const appDeploy = async (settings) => {
REACT_APP_KEYCLOAK_HOST: phases[phase].sso.host,
REACT_APP_KEYCLOAK_REALM: phases[phase].sso.realm,
REACT_APP_KEYCLOAK_CLIENT_ID: phases[phase].sso.clientId,
// Feature Flags
REACT_APP_FEATURE_FLAGS: phases[phase].featureFlags,
// Openshift Resources
CPU_REQUEST: phases[phase].cpuRequest,
CPU_LIMIT: phases[phase].cpuLimit,
MEMORY_REQUEST: phases[phase].memoryRequest,
MEMORY_LIMIT: phases[phase].memoryLimit,
REPLICAS: phases[phase].replicas,
REPLICAS_MAX: phases[phase].replicasMax,
REACT_APP_BIOHUB_FEATURE_FLAG: phases[phase].biohubFeatureFlag,
REACT_APP_BACKBONE_PUBLIC_API_HOST: phases[phase].backbonePublicApiHost,
REACT_APP_BIOHUB_TAXON_PATH: phases[phase].biohubTaxonPath,
REACT_APP_BIOHUB_TAXON_TSN_PATH: phases[phase].biohubTaxonTsnPath
Expand Down
Loading

0 comments on commit ba07abf

Please sign in to comment.