Skip to content

Commit

Permalink
feat: sync all projects in a given Org for Github
Browse files Browse the repository at this point in the history
Sync all projects in a given org for Github.com

Co-authored-by: mathild3r <[email protected]>
  • Loading branch information
lili2311 and mathild3r committed Oct 20, 2022
1 parent 1aac046 commit 38d7bf4
Show file tree
Hide file tree
Showing 12 changed files with 926 additions and 17 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@
"semantic-release": "17.3.0",
"ts-jest": "27.0.3",
"tsc-watch": "^4.1.0",
"typescript": "4.3.5"
"typescript": "4.3.5",
"uuid": "9.0.0"
},
"pkg": {
"scripts": [
Expand Down
9 changes: 7 additions & 2 deletions src/lib/api/feature-flags/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@ export async function getFeatureFlag(
url: url,
useRESTApi: false,
});

debug(`Feature flag ${featureFlagName} is enabled for Org ${orgId}`);
return res.data['ok'];

const enabled: boolean = res.data['ok'];

return enabled;
} catch (err) {
if (err instanceof Error) {
//Currently this is the only way to distinguish between an actual 403 and a 403 that is returned when an org hasn't got that FF enabled
if (JSON.stringify(err).search('"ok":false') > 0) {
const errorMessage = JSON.stringify(err);
if (errorMessage.includes('"ok":false')) {
debug(
`Feature flag ${featureFlagName} is not enabled for Org ${orgId}, please advise with your Snyk representative`,
);
Expand Down
12 changes: 6 additions & 6 deletions src/lib/api/org/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export async function deleteOrg(
return res.data;
}

interface ProjectsResponse {
export interface ProjectsResponse {
org: {
id: string;
};
Expand All @@ -166,6 +166,7 @@ interface ProjectsFilters {
origin?: string; //If supplied, only projects that exactly match this origin will be returned
type?: string; //If supplied, only projects that exactly match this type will be returned
isMonitored?: boolean; // If set to true, only include projects which are monitored, if set to false, only include projects which are not monitored
targetId?: string; // The target ID to be used in sunc functions
}

export async function listProjects(
Expand All @@ -175,8 +176,7 @@ export async function listProjects(
): Promise<ProjectsResponse> {
getApiToken();
getSnykHost();
debug(`Listing all projects for org: ${orgId}`);

debug(`Listing all projects for org: ${orgId} with filter ${JSON.stringify(filters)}`);
if (!orgId) {
throw new Error(
`Missing required parameters. Please ensure you have set: orgId, settings.
Expand Down Expand Up @@ -280,7 +280,7 @@ function convertToSnykProject(projectData: RESTProjectData[]): SnykProject[] {

return projects;
}
interface TargetFilters {
export interface TargetFilters {
remoteUrl?: string;
limit?: number;
isPrivate?: boolean;
Expand All @@ -304,12 +304,12 @@ export async function listTargets(
);
}

const targets = await listAllSnykTarget(requestManager, orgId, config);
const targets = await listAllSnykTargets(requestManager, orgId, config);

return { targets };
}

export async function listAllSnykTarget(
export async function listAllSnykTargets(
requestManager: requestsManager,
orgId: string,
config?: TargetFilters,
Expand Down
11 changes: 8 additions & 3 deletions src/lib/project/compare-branches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,20 @@ export async function compareAndUpdateBranches(
},
defaultBranch: string,
orgId: string,
dryRun = false,
): Promise<{ updated: boolean }> {
const {branch, projectPublicId} = project;
const { branch, projectPublicId } = project;
let updated = false
try {

if (branch != defaultBranch) {
debug(`Default branch has changed for Snyk project ${projectPublicId}`);
await updateProject(requestManager, orgId, projectPublicId, { branch: defaultBranch });
updated = true
if (!dryRun) {
await updateProject(requestManager, orgId, project.projectPublicId, {
branch: defaultBranch,
});
}
updated = true;
}

return { updated }
Expand Down
4 changes: 4 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export enum SupportedIntegrationTypesImportOrgData {
BITBUCKET_CLOUD = 'bitbucket-cloud',
}

export enum SupportedIntegrationTypesUpdateProject {
GITHUB = 'github',
}

// used to generate imported targets that exist in Snyk
// when we need to grab the integrationId from Snyk
export enum SupportedIntegrationTypesToListSnykTargets {
Expand Down
174 changes: 174 additions & 0 deletions src/scripts/sync/sync-org-projects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import pMap = require('p-map');
import * as debugLib from 'debug';
import * as path from 'path';
import { requestsManager } from 'snyk-request-manager';
import { UPDATED_PROJECTS_LOG_NAME } from '../../common';
import type {
TargetFilters,
} from '../../lib';
import {
getLoggingPath,
listProjects,
listTargets,
} from '../../lib';
import { getFeatureFlag } from '../../lib/api/feature-flags';
import type {
SnykProject,
SnykTarget,
} from '../../lib/types';
import {
SupportedIntegrationTypesUpdateProject,
} from '../../lib/types';
import { logUpdatedProjects } from '../../loggers/log-updated-project';
import { updateProjectForTarget as updateProjectForTarget } from './sync-projects-per-target';

const debug = debugLib('snyk:sync-org-projects');

export async function updateOrgTargets(
publicOrgId: string,
sources: SupportedIntegrationTypesUpdateProject[],
dryRun = false,
): Promise<{
fileName: string;
processedTargets: number;
meta: {
projects: {
branchUpdated: string[];
};
};
}> {
const branchUpdated: string[] = [];
const logFile = path.resolve(getLoggingPath(), UPDATED_PROJECTS_LOG_NAME);
const res = {
fileName: logFile,
processedTargets: 0,
meta: {
projects: {
branchUpdated,
},
},
};

// ensure source is enabled for sync
const allowedSources = sources.filter((source) =>
Object.values(SupportedIntegrationTypesUpdateProject).includes(source),
);
if (!allowedSources.length) {
console.warn(
`The organization (${publicOrgId}) does not have any projects that are supported for sync. Currently supported projects are ${Object.values(
SupportedIntegrationTypesUpdateProject,
).join(',')}`,
);
return res;
}

const requestManager = new requestsManager({
userAgentPrefix: 'snyk-api-import',
period: 1000,
maxRetryCount: 3,
});

if (await getFeatureFlag(requestManager, 'customBranch', publicOrgId)) {
console.warn(
'Detected custom branches are used in this organization. Skipping syncing organization ${publicOrgId}',
);
return res;
}

await pMap(
allowedSources,
async (source: SupportedIntegrationTypesUpdateProject) => {
const filters: TargetFilters = {
limit: 100,
origin: source,
excludeEmpty: true,
};
debug(`Listing all targets for source ${source}`);
const { targets } = await listTargets(
requestManager,
publicOrgId,
filters,
);
debug(`Syncing targets for source ${source}`);
const updated = await updateTargets(
requestManager,
publicOrgId,
targets,
dryRun,
);
res.processedTargets += updated.processedTargets;
res.meta.projects.branchUpdated.push(...updated.meta.projects.branchUpdated);
debug(`Logging updated targets for source ${source}`);
// TODO: add a test to ensure a log was created & is the expected format
await logUpdatedProjects(publicOrgId, branchUpdated, logFile);
},
{ concurrency: 3 },
);
return res;
}

export async function updateTargets(
requestManager: requestsManager,
orgId: string,
targets: SnykTarget[],
dryRun = false,
): Promise<{
processedTargets: number;
meta: {
projects: {
branchUpdated: string[];
};
};
}> {
let processedTargets = 0;
const updated: string[] = [];

await pMap(
targets,
async (target: SnykTarget) => {
try {
const filters = { targetId: target.id };
debug(`Listing projects for target ${target.attributes.displayName}`);
const { projects } = await listProjects(requestManager, orgId, filters);
debug(`Syncing projects for target ${target.attributes.displayName}`);
const { updatedProjects } = await syncAllProjects(requestManager, orgId, projects, dryRun);
updated.push(...updatedProjects);
processedTargets += 1;
} catch (e) {
debug(e);
console.warn(`Failed to sync target ${target.attributes.displayName}. ERROR: ${e.message}`)
}
},
{ concurrency: 10 },
);
return {
processedTargets,
// TODO: collect failed targets & log them with reason?
meta: {
projects: {
branchUpdated: updated,
},
},
};
}

async function syncAllProjects(
requestManager: requestsManager,
orgId: string,
projects: SnykProject[],
dryRun = false): Promise<{ updatedProjects: string[] }> {
const updatedProjects: string[] = [];
await pMap(projects, async (project) => {
const { updated } = await updateProjectForTarget(
requestManager,
orgId,
project,
dryRun,
);
if (updated) {
updatedProjects.push(project.id);
}
});

return { updatedProjects };
}
56 changes: 56 additions & 0 deletions src/scripts/sync/sync-projects-per-target.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { requestsManager } from 'snyk-request-manager';
import * as debugLib from 'debug';

import { getGithubReposDefaultBranch } from '../../lib/source-handlers/github';
import { compareAndUpdateBranches } from '../../lib/project/compare-branches';
import type {
SnykProject, Target,
} from '../../lib/types';
import {
SupportedIntegrationTypesUpdateProject,
} from '../../lib/types';
import { targetGenerators } from '../generate-imported-targets-from-snyk';
const debug = debugLib('snyk:sync-projects-per-target');

export function getBranchGenerator(
origin: SupportedIntegrationTypesUpdateProject,
): (target: Target, host?: string | undefined) => Promise<string> {
const getDefaultBranchGenerators = {
[SupportedIntegrationTypesUpdateProject.GITHUB]: getGithubReposDefaultBranch,
};
return getDefaultBranchGenerators[origin];
}

export async function updateProjectForTarget(
requestManager: requestsManager,
orgId: string,
project: SnykProject,
dryRun = false, // TODO: add a test for this function + this param
): Promise<{ updated: boolean }> {
let defaultBranch;
const origin = project.origin as SupportedIntegrationTypesUpdateProject;

try {
const target = targetGenerators[origin](project);
defaultBranch = await getBranchGenerator(origin)(target);
} catch (e) {
debug(`Getting default branch failed with error: ${e}`);
}

if (!defaultBranch) {
return { updated: false };
}

const { updated } = await compareAndUpdateBranches(
requestManager,
{
branch: project.branch!,
projectPublicId: project.id,
},
defaultBranch,
orgId,
dryRun,
);

return { updated };
}
1 change: 0 additions & 1 deletion test/lib/org.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ describe('listTargets', () => {
jest.restoreAllMocks();
});


it('list the targets in a given Org without pagination - mock', async () => {
jest.spyOn(requestManager, 'request').mockResolvedValue({
statusCode: 200,
Expand Down
Loading

0 comments on commit 38d7bf4

Please sign in to comment.