Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SIMSBIOHUB-79: Feature Flag Guard #1219

Merged
merged 15 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions api/.pipeline/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,13 @@ const phases = {
backboneArtifactIntakePath: '/api/artifact/intake',
biohubTaxonPath: '/api/taxonomy/taxon',
biohubTaxonTsnPath: '/api/taxonomy/taxon/tsn',
backboneIntakeEnabled: true,
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: '',
logLevel: 'info',
apiResponseValidationEnabled: true,
databaseResponseValidationEnabled: true,
Expand Down Expand Up @@ -120,14 +120,14 @@ const phases = {
backboneArtifactIntakePath: '/api/artifact/intake',
biohubTaxonPath: '/api/taxonomy/taxon',
biohubTaxonTsnPath: '/api/taxonomy/taxon/tsn',
backboneIntakeEnabled: true,
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,
logLevel: 'info',
featureFlags: '',
apiResponseValidationEnabled: true,
databaseResponseValidationEnabled: true,
nodeOptions: '--max_old_space_size=3000', // 75% of memoryLimit (bytes)
Expand Down Expand Up @@ -156,13 +156,13 @@ const phases = {
backboneArtifactIntakePath: '/api/artifact/intake',
biohubTaxonPath: '/api/taxonomy/taxon',
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',
apiResponseValidationEnabled: false,
databaseResponseValidationEnabled: false,
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 @@ -68,6 +67,8 @@ const apiDeploy = async (settings) => {
LOG_LEVEL: phases[phase].logLevel,
API_RESPONSE_VALIDATION_ENABLED: phases[phase].apiResponseValidationEnabled,
DATABASE_RESPONSE_VALIDATION_ENABLED: phases[phase].databaseResponseValidationEnabled,
// 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: The publishing intake endpoint path for BioHub Platform artifact submissions. 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 @@ -125,6 +122,10 @@ parameters:
value: 'false'
- name: DATABASE_RESPONSE_VALIDATION_ENABLED
value: 'false'
# 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 @@ -231,8 +232,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 @@ -344,6 +343,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/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 @@ -56,7 +57,6 @@ export interface ITaxonomy {
kingdom: 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 @@ -126,8 +126,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: '1',
replicasMax: '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: '',
cpuRequest: '50m',
cpuLimit: '500m',
memoryRequest: '100Mi',
memoryLimit: '500Mi',
replicas: '2',
replicasMax: '2',
biohubFeatureFlag: 'true',
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_SUBMIT_BIOHUB',
cpuRequest: '50m',
cpuLimit: '1000m',
memoryRequest: '100Mi',
memoryLimit: '1Gi',
replicas: '2',
replicasMax: '2',
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