Skip to content

Commit 0583fce

Browse files
committed
chore:[CSENG-68] add ffg call
chore:[CSENG-68] correct ffg path in fake server chore:[CSENG-68] update ffg hidden end point chore: [CSENG-68] plug in ffg call chore: [CSENG-68] update package chore: [CSENG-68] correct org chore: code tidy for CSENG-68 chore: update to use makeRequest chore: update tap tests [CSENG-68] chore: remove status check for CSENG-68
1 parent fec68c6 commit 0583fce

17 files changed

+550
-30
lines changed

package-lock.json

Lines changed: 69 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@
121121
"snyk-go-plugin": "1.28.0",
122122
"snyk-gradle-plugin": "5.1.1",
123123
"snyk-module": "3.1.0",
124-
"snyk-mvn-plugin": "4.3.3",
124+
"snyk-mvn-plugin": "4.5.0",
125125
"snyk-nodejs-lockfile-parser": "2.4.3",
126126
"snyk-nodejs-plugin": "1.4.5",
127127
"snyk-nuget-plugin": "2.12.0",
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import config from '../config';
2+
import { getAuthHeader } from '../api-token';
3+
import { FeatureFlagsEvaluationResponse } from './types';
4+
import { makeRequest } from '../request';
5+
import { AuthFailedError, ValidationError } from '../errors';
6+
import { TestLimitReachedError } from '../../cli/commands/test/iac/local-execution/usage-tracking';
7+
8+
export const SHOW_MAVEN_BUILD_SCOPE = 'show-maven-build-scope';
9+
10+
export async function evaluateFeatureFlagsForOrg(
11+
flags: string[],
12+
orgId: string,
13+
version = '2024-10-15',
14+
): Promise<FeatureFlagsEvaluationResponse> {
15+
if (!orgId.trim()) {
16+
throw new Error('orgId is required');
17+
}
18+
19+
const url =
20+
`${config.API_HIDDEN_URL}/orgs/${encodeURIComponent(orgId)}` +
21+
`/feature_flags/evaluation?version=${encodeURIComponent(version)}`;
22+
23+
const payload = {
24+
data: {
25+
type: 'feature_flags_evaluation',
26+
attributes: { flags },
27+
},
28+
};
29+
30+
const { res, body } = await makeRequest({
31+
url,
32+
method: 'POST',
33+
headers: {
34+
'Content-Type': 'application/vnd.api+json',
35+
Authorization: getAuthHeader(),
36+
},
37+
body: payload,
38+
json: true,
39+
noCompression: true,
40+
});
41+
42+
switch (res.statusCode) {
43+
case 200:
44+
break;
45+
case 401:
46+
throw AuthFailedError();
47+
case 429:
48+
throw new TestLimitReachedError();
49+
default:
50+
throw new ValidationError(
51+
res.body?.error ?? 'An error occurred, please contact Snyk support',
52+
);
53+
}
54+
55+
if (body?.errors?.length) {
56+
const first = body.errors[0];
57+
throw new Error(
58+
`Feature flag evaluation failed: ${first?.status ?? res.statusCode} ${
59+
first?.detail ?? res.statusMessage
60+
}`,
61+
);
62+
}
63+
64+
return body as FeatureFlagsEvaluationResponse;
65+
}
66+
67+
export async function getFeatureFlagValue(
68+
flag: string,
69+
orgId: string,
70+
): Promise<boolean> {
71+
try {
72+
const result = await evaluateFeatureFlagsForOrg([flag], orgId);
73+
74+
return (
75+
result.data.attributes.evaluations.find((e) => e.key === flag)?.value ??
76+
false
77+
);
78+
} catch (err) {
79+
return false;
80+
}
81+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export type FeatureFlagsEvaluationResponse = {
2+
jsonapi?: { version: string };
3+
data: {
4+
id: string;
5+
type: 'feature_flags_evaluation';
6+
attributes: {
7+
evaluations: Array<{
8+
key: string;
9+
value: boolean;
10+
reason: string;
11+
}>;
12+
evaluatedAt: string;
13+
};
14+
};
15+
};

src/lib/plugins/get-deps-from-plugin.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
AUTO_DETECTABLE_FILES,
1717
detectPackageManagerFromFile,
1818
} from '../detect';
19-
import analytics = require('../analytics');
19+
import * as analytics from '../analytics';
2020
import { convertSingleResultToMultiCustom } from './convert-single-splugin-res-to-multi-custom';
2121
import { convertMultiResultToMultiCustom } from './convert-multi-plugin-res-to-multi-custom';
2222
import { processYarnWorkspaces } from './nodejs-plugin/yarn-workspaces-parser';
@@ -102,7 +102,12 @@ export async function getDepsFromPlugin(
102102
if (!options.docker && !(options.file || options.packageManager)) {
103103
throw NoSupportedManifestsFoundError([...root]);
104104
}
105-
const inspectRes = await getSinglePluginResult(root, options);
105+
const inspectRes = await getSinglePluginResult(
106+
root,
107+
options,
108+
'',
109+
featureFlags,
110+
);
106111

107112
if (!pluginApi.isMultiResult(inspectRes)) {
108113
if (!inspectRes.package && !inspectRes.dependencyGraph) {

src/lib/plugins/get-multi-plugin-result.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export async function getMultiPluginResult(
116116
root,
117117
optionsClone,
118118
optionsClone.file,
119+
featureFlags,
119120
);
120121
let resultWithScannedProjects: cliInterface.legacyPlugin.MultiProjectResult;
121122

src/lib/plugins/get-single-plugin-result.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
1-
import plugins = require('.');
1+
import * as plugins from '.';
22
import { ModuleInfo } from '../module-info';
33
import { legacyPlugin as pluginApi } from '@snyk/cli-interface';
44
import { TestOptions, Options, MonitorOptions } from '../types';
55
import { snykHttpClient } from '../request/snyk-http-client';
66
import * as types from './types';
7+
import { SHOW_MAVEN_BUILD_SCOPE } from '../feature-flag-gateway';
78

89
export async function getSinglePluginResult(
910
root: string,
1011
options: Options & (TestOptions | MonitorOptions),
1112
targetFile?: string,
13+
featureFlags: Set<string> = new Set<string>(),
1214
): Promise<pluginApi.InspectResult> {
1315
const plugin: types.Plugin = plugins.loadPlugin(options.packageManager);
1416
const moduleInfo = ModuleInfo(plugin, options.policy);
17+
1518
const inspectRes: pluginApi.InspectResult = await moduleInfo.inspect(
1619
root,
1720
targetFile || options.file,
18-
{ ...options },
21+
{
22+
...options,
23+
showMavenBuildScope: featureFlags.has(SHOW_MAVEN_BUILD_SCOPE),
24+
},
1925
snykHttpClient,
2026
);
2127
return inspectRes;

src/lib/snyk-test/index.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module.exports = test;
22

33
const detect = require('../detect');
44
const { runTest } = require('./run-test');
5+
56
const chalk = require('chalk');
67
const pm = require('../package-managers');
78
const { UnsupportedPackageManagerError } = require('../errors');
@@ -12,6 +13,11 @@ const {
1213
DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG,
1314
MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF,
1415
} = require('../package-managers');
16+
const {
17+
getFeatureFlagValue,
18+
SHOW_MAVEN_BUILD_SCOPE,
19+
} = require('../feature-flag-gateway');
20+
const { getOrganizationID } = require('../organization');
1521

1622
async function test(root, options, callback) {
1723
if (typeof options === 'function') {
@@ -76,6 +82,14 @@ async function executeTest(root, options) {
7682
? new Set([PNPM_FEATURE_FLAG])
7783
: new Set([]);
7884

85+
const showScope = await getFeatureFlagValue(
86+
SHOW_MAVEN_BUILD_SCOPE,
87+
getOrganizationID(),
88+
);
89+
if (showScope) {
90+
featureFlags.add(SHOW_MAVEN_BUILD_SCOPE);
91+
}
92+
7993
if (!options.allProjects) {
8094
options.packageManager = detect.detectPackageManager(
8195
root,

test/acceptance/fake-server.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ export const fakeServer = (basePath: string, snykToken: string): FakeServer => {
214214

215215
if (
216216
req.url?.includes('/iac-org-settings') ||
217+
req.url?.includes('/feature_flags/evaluation') ||
217218
req.url?.includes('/cli-config/feature-flags/') ||
218219
(!nextResponse && !nextStatusCode && !statusCode)
219220
) {
@@ -887,6 +888,56 @@ export const fakeServer = (basePath: string, snykToken: string): FakeServer => {
887888
});
888889
});
889890

891+
app.post('/api/hidden/orgs/:orgId/feature_flags/evaluation', (req, res) => {
892+
const { orgId } = req.params;
893+
const flags: string[] = req.body?.data?.attributes?.flags ?? [];
894+
895+
// Example: org that has no flags at all
896+
if (orgId === 'no-flag') {
897+
return res.send({
898+
data: {
899+
type: 'feature_flags_evaluation',
900+
attributes: {
901+
evaluations: flags.map((flag) => ({
902+
key: flag,
903+
value: false,
904+
reason: 'not_available',
905+
})),
906+
evaluatedAt: new Date().toISOString(),
907+
},
908+
},
909+
});
910+
}
911+
912+
const evaluations = flags.map((flag) => {
913+
if (!featureFlags.has(flag)) {
914+
return {
915+
key: flag,
916+
value: false,
917+
reason: 'not_available',
918+
};
919+
}
920+
921+
const enabled = featureFlags.get(flag);
922+
923+
return {
924+
key: flag,
925+
value: Boolean(enabled),
926+
reason: enabled ? 'found' : 'not_available',
927+
};
928+
});
929+
930+
res.send({
931+
data: {
932+
type: 'feature_flags_evaluation',
933+
attributes: {
934+
evaluations,
935+
evaluatedAt: new Date().toISOString(),
936+
},
937+
},
938+
});
939+
});
940+
890941
app.get(basePath + '/iac-org-settings', (req, res) => {
891942
const baseResponse = {
892943
meta: {

0 commit comments

Comments
 (0)