diff --git a/.changeset/quick-hornets-call.md b/.changeset/quick-hornets-call.md new file mode 100644 index 000000000..57cf843de --- /dev/null +++ b/.changeset/quick-hornets-call.md @@ -0,0 +1,5 @@ +--- +"@monokle/synchronizer": patch +--- + +Improved project matching logic. diff --git a/packages/synchronizer/src/__tests__/fixtures/githubcom-kubeshop-monokle-core.policy.yaml b/packages/synchronizer/src/__tests__/fixtures/github-kubeshop-monokle-core.policy.yaml similarity index 100% rename from packages/synchronizer/src/__tests__/fixtures/githubcom-kubeshop-monokle-core.policy.yaml rename to packages/synchronizer/src/__tests__/fixtures/github-kubeshop-monokle-core.policy.yaml diff --git a/packages/synchronizer/src/__tests__/synchronizer.spec.ts b/packages/synchronizer/src/__tests__/synchronizer.spec.ts index c506adc1f..17a4d4bd2 100644 --- a/packages/synchronizer/src/__tests__/synchronizer.spec.ts +++ b/packages/synchronizer/src/__tests__/synchronizer.spec.ts @@ -42,7 +42,7 @@ describe('Synchronizer Tests', () => { const synchronizer = createDefaultMonokleSynchronizer(new StorageHandlerPolicy(storagePath)); const policy = await synchronizer.getPolicy({ - provider: 'github.com', + provider: 'GITHUB', remote: 'origin', owner: 'kubeshop', name: 'monokle-core', @@ -55,7 +55,7 @@ describe('Synchronizer Tests', () => { }); it('returns policy if there is policy file (from path)', async () => { - const storagePath = await createTmpConfigDir('githubcom-kubeshop-monokle-core.policy.yaml'); + const storagePath = await createTmpConfigDir('github-kubeshop-monokle-core.policy.yaml'); const synchronizer = createDefaultMonokleSynchronizer(new StorageHandlerPolicy(storagePath)); const policy = await synchronizer.getPolicy(storagePath); @@ -63,16 +63,16 @@ describe('Synchronizer Tests', () => { assert.isObject(policy); assert.isTrue(policy.valid); assert.isNotEmpty(policy.path); - assert.match(policy.path, /githubcom-kubeshop-monokle-core.policy.yaml$/); + assert.match(policy.path, /github-kubeshop-monokle-core.policy.yaml$/); assert.isNotEmpty(policy.policy); }); it('returns policy if there is policy file (from git data)', async () => { - const storagePath = await createTmpConfigDir('githubcom-kubeshop-monokle-core.policy.yaml'); + const storagePath = await createTmpConfigDir('github-kubeshop-monokle-core.policy.yaml'); const synchronizer = createDefaultMonokleSynchronizer(new StorageHandlerPolicy(storagePath)); const policy = await synchronizer.getPolicy({ - provider: 'github.com', + provider: 'GITHUB', remote: 'origin', owner: 'kubeshop', name: 'monokle-core', @@ -81,7 +81,7 @@ describe('Synchronizer Tests', () => { assert.isObject(policy); assert.isTrue(policy.valid); assert.isNotEmpty(policy.path); - assert.match(policy.path, /githubcom-kubeshop-monokle-core.policy.yaml$/); + assert.match(policy.path, /github-kubeshop-monokle-core.policy.yaml$/); assert.isNotEmpty(policy.policy); assert.isNotEmpty(policy.policy.plugins); }); @@ -172,7 +172,7 @@ describe('Synchronizer Tests', () => { stubs.push(queryApiStub); const repoData = { - provider: 'github.com', + provider: 'GITHUB', remote: 'origin', owner: 'kubeshop', name: 'monokle-core', @@ -190,7 +190,7 @@ describe('Synchronizer Tests', () => { assert.isObject(newPolicy); assert.isTrue(newPolicy.valid); assert.isNotEmpty(newPolicy.path); - assert.match(newPolicy.path, /githubcom-kubeshop-monokle-core.policy.yaml$/); + assert.match(newPolicy.path, /github-kubeshop-monokle-core.policy.yaml$/); assert.isNotEmpty(newPolicy.policy); assert.isNotEmpty(newPolicy.policy.plugins); assert.isNotEmpty(newPolicy.policy.rules); @@ -270,6 +270,204 @@ describe('Synchronizer Tests', () => { assert.deepEqual(newPolicy, getPolicyResult); }); + it('fetches and returns valid policy based on repo data (GITHUB provider)', async () => { + const storagePath = await createTmpConfigDir(); + const synchronizer = createDefaultMonokleSynchronizer(new StorageHandlerPolicy(storagePath)); + + const queryApiStub = sinon.stub((synchronizer as any)._apiHandler, 'queryApi').callsFake(async (...args) => { + const query = args[0] as string; + + if (query.includes('query getUser')) { + return { + data: { + me: { + id: 7, + email: 'user7@kubeshop.io', + projects: [ + { + project: { + id: 7000, + slug: 'user7-proj', + name: 'User7 Project', + repositories: [ + { + id: 'user7-proj-policy-id', + projectId: 7000, + provider: 'GITHUB', + owner: 'kubeshop', + name: 'monokle-core', + prChecks: false, + canEnablePrChecks: true, + }, + ], + }, + }, + ], + }, + }, + }; + } + + if (query.includes('query getPolicy')) { + return { + data: { + getProject: { + id: 7000, + name: 'User7 Project', + policy: { + id: 'user7-proj-policy-id', + json: { + plugins: { + 'pod-security-standards': true, + 'yaml-syntax': false, + 'resource-links': false, + 'kubernetes-schema': false, + practices: true, + }, + rules: { + 'pod-security-standards/host-process': 'err', + }, + settings: { + 'kubernetes-schema': { + schemaVersion: 'v1.27.1', + }, + }, + }, + }, + }, + }, + }; + } + + return {}; + }); + stubs.push(queryApiStub); + + const repoData = { + provider: 'GITHUB', + remote: 'origin', + owner: 'kubeshop', + name: 'monokle-core', + }; + + const policy = await synchronizer.getPolicy(repoData); + + assert.isFalse(policy.valid); + + const newPolicy = await synchronizer.getPolicy(repoData, true, { + accessToken: 'SAMPLE_ACCESS_TOKEN', + tokenType: 'ApiKey', + }); + + assert.isObject(newPolicy); + assert.isTrue(newPolicy.valid); + assert.isNotEmpty(newPolicy.path); + assert.match(newPolicy.path, /github-kubeshop-monokle-core.policy.yaml$/); + assert.isNotEmpty(newPolicy.policy); + assert.isNotEmpty(newPolicy.policy.plugins); + assert.isNotEmpty(newPolicy.policy.rules); + assert.isNotEmpty(newPolicy.policy.settings); + }); + + it('fetches and returns valid policy based on repo data (HTTPS provider)', async () => { + const storagePath = await createTmpConfigDir(); + const synchronizer = createDefaultMonokleSynchronizer(new StorageHandlerPolicy(storagePath)); + + const queryApiStub = sinon.stub((synchronizer as any)._apiHandler, 'queryApi').callsFake(async (...args) => { + const query = args[0] as string; + + if (query.includes('query getUser')) { + return { + data: { + me: { + id: 8, + email: 'user8@kubeshop.io', + projects: [ + { + project: { + id: 8000, + slug: 'user8-proj', + name: 'User8 Project', + repositories: [ + { + id: 'user8-proj-policy-id', + projectId: 8000, + provider: 'HTTPS', + owner: 'sample-org', + name: 'demo', + prChecks: false, + canEnablePrChecks: true, + }, + ], + }, + }, + ], + }, + }, + }; + } + + if (query.includes('query getPolicy')) { + return { + data: { + getProject: { + id: 8000, + name: 'User8 Project', + policy: { + id: 'user8-proj-policy-id', + json: { + plugins: { + 'pod-security-standards': true, + 'yaml-syntax': false, + 'resource-links': false, + 'kubernetes-schema': false, + practices: true, + }, + rules: { + 'pod-security-standards/host-process': 'err', + }, + settings: { + 'kubernetes-schema': { + schemaVersion: 'v1.27.1', + }, + }, + }, + }, + }, + }, + }; + } + + return {}; + }); + stubs.push(queryApiStub); + + const repoData = { + provider: 'HTTPS', + remote: 'origin', + owner: 'sample-org', + name: 'demo', + }; + + const policy = await synchronizer.getPolicy(repoData); + + assert.isFalse(policy.valid); + + const newPolicy = await synchronizer.getPolicy(repoData, true, { + accessToken: 'SAMPLE_ACCESS_TOKEN', + tokenType: 'ApiKey', + }); + + assert.isObject(newPolicy); + assert.isTrue(newPolicy.valid); + assert.isNotEmpty(newPolicy.path); + assert.match(newPolicy.path, /https-sample-org-demo.policy.yaml$/); + assert.isNotEmpty(newPolicy.policy); + assert.isNotEmpty(newPolicy.policy.plugins); + assert.isNotEmpty(newPolicy.policy.rules); + assert.isNotEmpty(newPolicy.policy.settings); + }); + it('throws unauthorized error when invalid auth credentials passed', async () => { const storagePath = await createTmpConfigDir(); const synchronizer = createDefaultMonokleSynchronizer(new StorageHandlerPolicy(storagePath)); @@ -433,7 +631,7 @@ describe('Synchronizer Tests', () => { stubs.push(queryApiStub); const repoData = { - provider: 'github.com', + provider: 'GITHUB', remote: 'origin', owner: 'kubeshop', name: 'monokle-core', @@ -445,7 +643,7 @@ describe('Synchronizer Tests', () => { assert.isObject(policy); assert.isTrue(policy.valid); assert.isNotEmpty(policy.path); - assert.match(policy.path, /githubcom-kubeshop-monokle-core.policy.yaml$/); + assert.match(policy.path, /github-kubeshop-monokle-core.policy.yaml$/); assert.isNotEmpty(policy.policy); assert.isNotEmpty(policy.policy.plugins); assert.isNotEmpty(policy.policy.rules); @@ -507,7 +705,7 @@ describe('Synchronizer Tests', () => { stubs.push(queryApiStub); const repoData = { - provider: 'github.com', + provider: 'GITHUB', remote: 'origin', owner: 'kubeshop', name: 'monokle-core', @@ -608,7 +806,7 @@ describe('Synchronizer Tests', () => { stubs.push(queryApiStub); const repoData = { - provider: 'github.com', + provider: 'GITHUB', remote: 'origin', owner: 'kubeshop', name: 'monokle-core', diff --git a/packages/synchronizer/src/handlers/gitHandler.ts b/packages/synchronizer/src/handlers/gitHandler.ts index 450625121..36b3414ff 100644 --- a/packages/synchronizer/src/handlers/gitHandler.ts +++ b/packages/synchronizer/src/handlers/gitHandler.ts @@ -7,8 +7,15 @@ export type RepoRemoteData = { remote: string; owner: string; name: string; + details?: gitUrlParse.GitUrl; }; +export const KNOWN_GIT_PROVIDERS: Record = { + 'github.com': 'GITHUB', +}; + +export const GENERIC_GIT_PROVIDER = 'HTTPS'; + export class GitHandler { async getRepoRemoteData(folderPath: string): Promise { if (!(await this.isGitRepo(folderPath))) { @@ -25,10 +32,11 @@ export class GitHandler { const urlParts = gitUrlParse(url); return { - provider: urlParts.source, + provider: this.matchProvider(urlParts), remote: remote.name, owner: urlParts.owner, name: urlParts.name, + details: urlParts, }; } catch (err: any) { return undefined; @@ -61,4 +69,41 @@ export class GitHandler { } } } + + // Sample `gitUrlParse.GitUrl` object (as it's not fully consistent with TS typings): + // { + // protocols: [ 'ssh' ], + // protocol: 'ssh', + // port: '', + // resource: 'github.com', + // host: 'github.com', + // user: 'git', + // password: '', + // pathname: '/kubeshop/monokle-demo.git', + // hash: '', + // search: '', + // href: 'git@github.com:kubeshop/monokle-demo.git', + // query: {}, + // parse_failed: false, + // token: '', + // toString: [Function (anonymous)], + // source: 'github.com', + // git_suffix: true, + // name: 'monokle-demo', + // owner: 'kubeshop', + // commit: undefined, + // ref: '', + // filepathtype: '', + // filepath: '', + // organization: 'kubeshop', + // full_name: 'kubeshop/monokle-demo' + // } + private matchProvider(repoMetadata: gitUrlParse.GitUrl) { + return ( + KNOWN_GIT_PROVIDERS[repoMetadata.source] || + KNOWN_GIT_PROVIDERS[repoMetadata.resource] || + KNOWN_GIT_PROVIDERS[(repoMetadata as any).host] || + GENERIC_GIT_PROVIDER + ); + } } diff --git a/packages/synchronizer/src/utils/synchronizer.ts b/packages/synchronizer/src/utils/synchronizer.ts index cac97fbca..83adac238 100644 --- a/packages/synchronizer/src/utils/synchronizer.ts +++ b/packages/synchronizer/src/utils/synchronizer.ts @@ -353,17 +353,16 @@ export class Synchronizer extends EventEmitter { return project.project.slug === repoData.ownerProjectSlug; }); - const repoMainProject = userData.data.me.projects.find(project => { + const repoFirstProject = userData.data.me.projects.find(project => { return project.project.repositories.find( - repo => repo.owner === repoData.owner && repo.name === repoData.name && repo.prChecks + repo => + repo.owner.toLowerCase() === repoData.owner.toLowerCase() && + repo.name.toLowerCase() === repoData.name.toLowerCase() && + repo.provider.toLowerCase() === repoData.provider.toLowerCase() ); }); - const repoFirstProject = userData.data.me.projects.find(project => { - return project.project.repositories.find(repo => repo.owner === repoData.owner && repo.name === repoData.name); - }); - - const matchingProject = repoMatchingProjectBySlug ?? repoMainProject ?? repoFirstProject; + const matchingProject = repoMatchingProjectBySlug ?? repoFirstProject; if (matchingProject?.project) { const cacheId = this.getRepoCacheId(repoData, tokenInfo.accessToken);