feat: integration support for harness
Signed-off-by: Calvin Lee <cjlee@ualberta.ca>
This commit is contained in:
@@ -513,6 +513,19 @@ export function getGitLabRequestOptions(config: GitLabIntegrationConfig): {
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function getHarnessFileContentsUrl(
|
||||
config: HarnessIntegrationConfig,
|
||||
url: string,
|
||||
): string;
|
||||
|
||||
// @public
|
||||
export function getHarnessRequestOptions(
|
||||
config: HarnessIntegrationConfig,
|
||||
): {
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class GiteaIntegration implements ScmIntegration {
|
||||
constructor(config: GiteaIntegrationConfig);
|
||||
@@ -539,7 +552,7 @@ export type GiteaIntegrationConfig = {
|
||||
host: string;
|
||||
baseUrl?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -674,6 +687,35 @@ export type GoogleGcsIntegrationConfig = {
|
||||
privateKey?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class HarnessIntegration implements ScmIntegration {
|
||||
constructor(config: HarnessIntegrationConfig);
|
||||
// (undocumented)
|
||||
readonly config: HarnessIntegrationConfig;
|
||||
// (undocumented)
|
||||
static factory: ScmIntegrationsFactory<HarnessIntegration>;
|
||||
// (undocumented)
|
||||
resolveEditUrl(url: string): string;
|
||||
// (undocumented)
|
||||
resolveUrl(options: {
|
||||
url: string;
|
||||
base: string;
|
||||
lineNumber?: number | undefined;
|
||||
}): string;
|
||||
// (undocumented)
|
||||
get title(): string;
|
||||
// (undocumented)
|
||||
get type(): string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type HarnessIntegrationConfig = {
|
||||
host: string;
|
||||
baseUrl?: string;
|
||||
username?: string;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface IntegrationsByType {
|
||||
// (undocumented)
|
||||
@@ -696,6 +738,8 @@ export interface IntegrationsByType {
|
||||
github: ScmIntegrationsGroup<GithubIntegration>;
|
||||
// (undocumented)
|
||||
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
|
||||
// (undocumented)
|
||||
harness: ScmIntegrationsGroup<HarnessIntegration>;
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -839,6 +883,11 @@ export function readGoogleGcsIntegrationConfig(
|
||||
config: Config,
|
||||
): GoogleGcsIntegrationConfig;
|
||||
|
||||
// @public
|
||||
export function readHarnessConfig(
|
||||
config: Config,
|
||||
): HarnessIntegrationConfig;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const replaceGitHubUrlType: typeof replaceGithubUrlType;
|
||||
|
||||
@@ -889,6 +938,8 @@ export interface ScmIntegrationRegistry
|
||||
github: ScmIntegrationsGroup<GithubIntegration>;
|
||||
// (undocumented)
|
||||
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
|
||||
// (undocumented)
|
||||
harness: ScmIntegrationsGroup<HarnessIntegration>;
|
||||
resolveEditUrl(url: string): string;
|
||||
resolveUrl(options: {
|
||||
url: string;
|
||||
@@ -927,6 +978,8 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
// (undocumented)
|
||||
get gitlab(): ScmIntegrationsGroup<GitLabIntegration>;
|
||||
// (undocumented)
|
||||
get harness(): ScmIntegrationsGroup<HarnessIntegration>;
|
||||
// (undocumented)
|
||||
list(): ScmIntegration[];
|
||||
// (undocumented)
|
||||
resolveEditUrl(url: string): string;
|
||||
|
||||
Vendored
+24
@@ -345,5 +345,29 @@ export interface Config {
|
||||
*/
|
||||
password?: string;
|
||||
}>;
|
||||
/** Integration configuration for Harness Code */
|
||||
harness?: Array<{
|
||||
/**
|
||||
* The hostname of the given Harness Code instance
|
||||
* @visibility frontend
|
||||
*/
|
||||
host: string;
|
||||
/**
|
||||
* The base url for the Gitea instance.
|
||||
* @visibility frontend
|
||||
*/
|
||||
baseUrl?: string;
|
||||
|
||||
/**
|
||||
* The username to use for authenticated requests.
|
||||
* @visibility secret
|
||||
*/
|
||||
username?: string;
|
||||
/**
|
||||
* Harness Code token used to authenticate requests. This can be either a generated access token.
|
||||
* @visibility secret
|
||||
*/
|
||||
token?: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import { ScmIntegrations } from './ScmIntegrations';
|
||||
import { GiteaIntegration, GiteaIntegrationConfig } from './gitea';
|
||||
import { AwsCodeCommitIntegration } from './awsCodeCommit/AwsCodeCommitIntegration';
|
||||
import { AwsCodeCommitIntegrationConfig } from './awsCodeCommit';
|
||||
import { HarnessIntegration, HarnessIntegrationConfig } from './harness';
|
||||
|
||||
describe('ScmIntegrations', () => {
|
||||
const awsS3 = new AwsS3Integration({
|
||||
@@ -80,6 +81,10 @@ describe('ScmIntegrations', () => {
|
||||
host: 'gitea.local',
|
||||
} as GiteaIntegrationConfig);
|
||||
|
||||
const harness = new HarnessIntegration({
|
||||
host: 'harness.local',
|
||||
} as HarnessIntegrationConfig);
|
||||
|
||||
const i = new ScmIntegrations({
|
||||
awsS3: basicIntegrations([awsS3], item => item.config.host),
|
||||
awsCodeCommit: basicIntegrations([awsCodeCommit], item => item.config.host),
|
||||
@@ -94,6 +99,7 @@ describe('ScmIntegrations', () => {
|
||||
github: basicIntegrations([github], item => item.config.host),
|
||||
gitlab: basicIntegrations([gitlab], item => item.config.host),
|
||||
gitea: basicIntegrations([gitea], item => item.config.host),
|
||||
harness: basicIntegrations([harness], item => item.config.host),
|
||||
});
|
||||
|
||||
it('can get the specifics', () => {
|
||||
@@ -113,6 +119,7 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.github.byUrl('https://github.local')).toBe(github);
|
||||
expect(i.gitlab.byUrl('https://gitlab.local')).toBe(gitlab);
|
||||
expect(i.gitea.byUrl('https://gitea.local')).toBe(gitea);
|
||||
expect(i.harness.byUrl('https://harness.local')).toBe(harness);
|
||||
});
|
||||
|
||||
it('can list', () => {
|
||||
@@ -128,6 +135,7 @@ describe('ScmIntegrations', () => {
|
||||
github,
|
||||
gitlab,
|
||||
gitea,
|
||||
harness,
|
||||
]),
|
||||
);
|
||||
});
|
||||
@@ -143,6 +151,7 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.byUrl('https://github.local')).toBe(github);
|
||||
expect(i.byUrl('https://gitlab.local')).toBe(gitlab);
|
||||
expect(i.byUrl('https://gitea.local')).toBe(gitea);
|
||||
expect(i.byUrl('https://harness.local')).toBe(harness);
|
||||
|
||||
expect(i.byHost('awss3.local')).toBe(awsS3);
|
||||
expect(i.byHost('awscodecommit.local')).toBe(awsCodeCommit);
|
||||
|
||||
@@ -28,6 +28,7 @@ import { defaultScmResolveUrl } from './helpers';
|
||||
import { ScmIntegration, ScmIntegrationsGroup } from './types';
|
||||
import { ScmIntegrationRegistry } from './registry';
|
||||
import { GiteaIntegration } from './gitea';
|
||||
import { HarnessIntegration } from './harness/HarnessIntegration';
|
||||
|
||||
/**
|
||||
* The set of supported integrations.
|
||||
@@ -48,6 +49,7 @@ export interface IntegrationsByType {
|
||||
github: ScmIntegrationsGroup<GithubIntegration>;
|
||||
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
|
||||
gitea: ScmIntegrationsGroup<GiteaIntegration>;
|
||||
harness: ScmIntegrationsGroup<HarnessIntegration>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,6 +72,7 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
github: GithubIntegration.factory({ config }),
|
||||
gitlab: GitLabIntegration.factory({ config }),
|
||||
gitea: GiteaIntegration.factory({ config }),
|
||||
harness: HarnessIntegration.factory({ config }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -120,6 +123,10 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
return this.byType.gitea;
|
||||
}
|
||||
|
||||
get harness(): ScmIntegrationsGroup<HarnessIntegration> {
|
||||
return this.byType.harness;
|
||||
}
|
||||
|
||||
list(): ScmIntegration[] {
|
||||
return Object.values(this.byType).flatMap(
|
||||
i => i.list() as ScmIntegration[],
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { HarnessIntegration } from './HarnessIntegration';
|
||||
|
||||
describe('HarnessIntegration', () => {
|
||||
it('has a working factory', () => {
|
||||
const integrations = HarnessIntegration.factory({
|
||||
config: new ConfigReader({
|
||||
integrations: {
|
||||
harness: [
|
||||
{
|
||||
host: 'app.harness.io',
|
||||
username: 'git',
|
||||
baseUrl: 'https://app.harness.io/route',
|
||||
token: '1234',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(integrations.list().length).toBe(1);
|
||||
expect(integrations.list()[0].config.host).toBe('app.harness.io');
|
||||
expect(integrations.list()[0].config.baseUrl).toBe(
|
||||
'https://app.harness.io/route',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the basics', () => {
|
||||
const integration = new HarnessIntegration({
|
||||
host: 'app.harness.io',
|
||||
});
|
||||
expect(integration.type).toBe('harness');
|
||||
expect(integration.title).toBe('app.harness.io');
|
||||
});
|
||||
|
||||
describe('resolveUrl', () => {
|
||||
it('works for valid urls, ignoring line number', () => {
|
||||
const integration = new HarnessIntegration({
|
||||
host: 'app.harness.io',
|
||||
});
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: 'https://app.harness.io/catalog-info.yaml',
|
||||
base: 'https://app.harness.io/catalog-info.yaml',
|
||||
lineNumber: 9,
|
||||
}),
|
||||
).toBe('https://app.harness.io/catalog-info.yaml');
|
||||
});
|
||||
|
||||
it('handles line numbers', () => {
|
||||
const integration = new HarnessIntegration({
|
||||
host: 'app.harness.io',
|
||||
});
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: '',
|
||||
base: 'https://app.harness.io/catalog-info.yaml#4',
|
||||
lineNumber: 9,
|
||||
}),
|
||||
).toBe('https://app.harness.io/catalog-info.yaml#L9');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolves with a relative url', () => {
|
||||
it('works for valid urls', () => {
|
||||
const integration = new HarnessIntegration({
|
||||
host: 'app.harness.io',
|
||||
});
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: './skeleton',
|
||||
base: 'https://app.harness.io/git/plugins/repo/+/refs/heads/master/template.yaml',
|
||||
}),
|
||||
).toBe(
|
||||
'https://app.harness.io/git/plugins/repo/+/refs/heads/master/skeleton',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolves with an absolute url', () => {
|
||||
it('works for valid urls', () => {
|
||||
const integration = new HarnessIntegration({
|
||||
host: 'app.harness.io',
|
||||
});
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: '/catalog-info.yaml',
|
||||
base: 'https://app.harness.io/git/repo/+/refs/heads/master/',
|
||||
}),
|
||||
).toBe(
|
||||
'https://app.harness.io/git/repo/+/refs/heads/master/catalog-info.yaml',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('resolve edit URL', () => {
|
||||
const integration = new HarnessIntegration({
|
||||
host: 'app.harness.io',
|
||||
});
|
||||
|
||||
expect(
|
||||
integration.resolveEditUrl(
|
||||
'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projName/repos/repoName/edit/refMain/~/all-apis.yaml',
|
||||
),
|
||||
).toBe(
|
||||
'https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName/projName/repoName/+/edit/all-apis.yaml',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { basicIntegrations, defaultScmResolveUrl } from '../helpers';
|
||||
import { ScmIntegration, ScmIntegrationsFactory } from '../types';
|
||||
import { HarnessIntegrationConfig, readHarnessConfig } from './config';
|
||||
import { getHarnessEditContentsUrl } from './core';
|
||||
|
||||
/**
|
||||
* A Harness Code based integration.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class HarnessIntegration implements ScmIntegration {
|
||||
static factory: ScmIntegrationsFactory<HarnessIntegration> = ({ config }) => {
|
||||
const configs = config.getOptionalConfigArray('integrations.harness') ?? [];
|
||||
const harnessConfigs = configs.map(c => readHarnessConfig(c));
|
||||
|
||||
return basicIntegrations(
|
||||
harnessConfigs.map(c => new HarnessIntegration(c)),
|
||||
(harness: HarnessIntegration) => harness.config.host,
|
||||
);
|
||||
};
|
||||
|
||||
constructor(readonly config: HarnessIntegrationConfig) {}
|
||||
|
||||
get type(): string {
|
||||
return 'harness';
|
||||
}
|
||||
|
||||
get title(): string {
|
||||
return this.config.host;
|
||||
}
|
||||
|
||||
resolveUrl(options: {
|
||||
url: string;
|
||||
base: string;
|
||||
lineNumber?: number | undefined;
|
||||
}): string {
|
||||
return defaultScmResolveUrl(options);
|
||||
}
|
||||
|
||||
resolveEditUrl(url: string): string {
|
||||
return getHarnessEditContentsUrl(this.config, url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config, ConfigReader } from '@backstage/config';
|
||||
import { loadConfigSchema } from '@backstage/config-loader';
|
||||
import { HarnessIntegrationConfig, readHarnessConfig } from './config';
|
||||
|
||||
describe('readHarnessConfig', () => {
|
||||
function buildConfig(data: Partial<HarnessIntegrationConfig>): Config {
|
||||
return new ConfigReader(data);
|
||||
}
|
||||
|
||||
async function buildFrontendConfig(
|
||||
data: Partial<HarnessIntegrationConfig>,
|
||||
): Promise<Config> {
|
||||
const fullSchema = await loadConfigSchema({
|
||||
dependencies: ['@backstage/integration'],
|
||||
});
|
||||
const serializedSchema = fullSchema.serialize() as {
|
||||
schemas: { value: { properties?: { integrations?: object } } }[];
|
||||
};
|
||||
const schema = await loadConfigSchema({
|
||||
serialized: {
|
||||
...serializedSchema, // only include schemas that apply to integrations
|
||||
schemas: serializedSchema.schemas.filter(
|
||||
s => s.value?.properties?.integrations,
|
||||
),
|
||||
},
|
||||
});
|
||||
const processed = schema.process(
|
||||
[{ data: { integrations: { harness: [data] } }, context: 'app' }],
|
||||
{ visibility: ['frontend'] },
|
||||
);
|
||||
return new ConfigReader((processed[0].data as any).integrations.harness[0]);
|
||||
}
|
||||
|
||||
it('reads all values', () => {
|
||||
const output = readHarnessConfig(
|
||||
buildConfig({
|
||||
host: 'a.com',
|
||||
baseUrl: 'https://a.com/route/api',
|
||||
username: 'u',
|
||||
token: 'p',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
host: 'a.com',
|
||||
baseUrl: 'https://a.com/route/api',
|
||||
username: 'u',
|
||||
token: 'p',
|
||||
});
|
||||
});
|
||||
|
||||
it('can create a default value if the API base URL is missing', () => {
|
||||
const output = readHarnessConfig(
|
||||
buildConfig({
|
||||
host: 'a.com',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
host: 'a.com',
|
||||
baseUrl: 'https://a.com',
|
||||
username: undefined,
|
||||
token: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects funky configs', () => {
|
||||
const valid: any = {
|
||||
host: 'a.com',
|
||||
};
|
||||
expect(() => readHarnessConfig(buildConfig({ ...valid, host: 2 }))).toThrow(
|
||||
/host/,
|
||||
);
|
||||
expect(() =>
|
||||
readHarnessConfig(buildConfig({ ...valid, baseUrl: 2 })),
|
||||
).toThrow(/baseUrl/);
|
||||
});
|
||||
|
||||
it('works on the frontend', async () => {
|
||||
expect(
|
||||
readHarnessConfig(
|
||||
await buildFrontendConfig({
|
||||
host: 'a.com',
|
||||
baseUrl: 'https://a.com/route',
|
||||
username: 'u',
|
||||
token: 'p',
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
host: 'a.com',
|
||||
baseUrl: 'https://a.com/route',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { trimEnd } from 'lodash';
|
||||
import { isValidHost } from '../helpers';
|
||||
|
||||
/**
|
||||
* The configuration for a single Gitea integration.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type HarnessIntegrationConfig = {
|
||||
/**
|
||||
* The host of the target that this matches on, e.g. "app.harness.io"
|
||||
*/
|
||||
host: string;
|
||||
/**
|
||||
* The optional base URL of the Harness code instance. It is assumed that https
|
||||
* is used and that the base path is "/" on the host. If that is not the
|
||||
* case set the complete base url to the Harness code instance, e.g.
|
||||
* "https://harnesscode.website.com/". This is the url that you would open
|
||||
* in a browser.
|
||||
*/
|
||||
baseUrl?: string;
|
||||
/**
|
||||
* The username to use for requests to harness code.
|
||||
*/
|
||||
username?: string;
|
||||
|
||||
/**
|
||||
* The password or http token to use for authentication.
|
||||
*/
|
||||
token?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a location config block for use in HarnessIntegration
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function readHarnessConfig(config: Config): HarnessIntegrationConfig {
|
||||
const host = config.getString('host');
|
||||
let baseUrl = config.getOptionalString('baseUrl');
|
||||
const username = config.getOptionalString('username');
|
||||
const token = config.getOptionalString('token');
|
||||
if (!isValidHost(host)) {
|
||||
throw new Error(
|
||||
`Invalid Harness Code integration config, '${host}' is not a valid host`,
|
||||
);
|
||||
}
|
||||
|
||||
if (baseUrl) {
|
||||
baseUrl = trimEnd(baseUrl, '/');
|
||||
} else {
|
||||
baseUrl = `https://${host}`;
|
||||
}
|
||||
|
||||
return {
|
||||
host,
|
||||
baseUrl,
|
||||
username,
|
||||
token,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { setupServer } from 'msw/node';
|
||||
import { setupRequestMockHandlers } from '@backstage/test-utils';
|
||||
import { HarnessIntegrationConfig } from './config';
|
||||
import {
|
||||
getHarnessEditContentsUrl,
|
||||
getHarnessFileContentsUrl,
|
||||
getHarnessRequestOptions,
|
||||
} from './core';
|
||||
|
||||
describe('Harness code core', () => {
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
describe('getHarnessFileContentsUrl', () => {
|
||||
it('can create an url from arguments', () => {
|
||||
const config: HarnessIntegrationConfig = {
|
||||
host: 'app.harness.io',
|
||||
};
|
||||
expect(
|
||||
getHarnessFileContentsUrl(
|
||||
config,
|
||||
'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projName/repos/repoName/files/refMain/~/all-apis.yaml',
|
||||
),
|
||||
).toEqual(
|
||||
'https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName/projName/repoName/+/content/all-apis.yaml?routingId=accountId&include_commit=false&ref=refMain',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHarnessEditContentsUrl', () => {
|
||||
it('can create an url from arguments', () => {
|
||||
const config: HarnessIntegrationConfig = {
|
||||
host: 'app.harness.io',
|
||||
};
|
||||
expect(
|
||||
getHarnessEditContentsUrl(
|
||||
config,
|
||||
'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projName/repos/repoName/edit/refMain/~/all-apis.yaml',
|
||||
),
|
||||
).toEqual(
|
||||
'https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName/projName/repoName/+/edit/all-apis.yaml',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGerritRequestOptions', () => {
|
||||
it('adds token header when only a token is specified', () => {
|
||||
const authRequest: HarnessIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
token: 'P',
|
||||
};
|
||||
const anonymousRequest: HarnessIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
};
|
||||
expect(
|
||||
(getHarnessRequestOptions(authRequest).headers as any).Authorization,
|
||||
).toEqual('Bearer P');
|
||||
expect(
|
||||
getHarnessRequestOptions(anonymousRequest).headers,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('adds basic auth when username and token are specified', () => {
|
||||
const authRequest: HarnessIntegrationConfig = {
|
||||
host: 'gerrit.com',
|
||||
username: 'username',
|
||||
token: 'P',
|
||||
};
|
||||
|
||||
const basicAuthentication = `basic ${Buffer.from(
|
||||
`${authRequest.username}:${authRequest.token}`,
|
||||
).toString('base64')}`;
|
||||
|
||||
expect(
|
||||
(getHarnessRequestOptions(authRequest).headers as any).Authorization,
|
||||
).toEqual(basicAuthentication);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { HarnessIntegrationConfig } from './config';
|
||||
|
||||
/**
|
||||
* Given a URL pointing to a file, returns a URL
|
||||
* for editing the contents of the data.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Converts
|
||||
* from: https://app.harness.io/a/b/src/branchname/path/to/c.yaml
|
||||
* or: https://app.harness.io/a/b/_edit/branchname/path/to/c.yaml
|
||||
*
|
||||
* @param url - A URL pointing to a file
|
||||
* @param config - The relevant provider config
|
||||
* @public
|
||||
*/
|
||||
export function getHarnessEditContentsUrl(
|
||||
config: HarnessIntegrationConfig,
|
||||
url: string,
|
||||
) {
|
||||
try {
|
||||
const baseUrl = config.baseUrl ?? `https://${config.host}`;
|
||||
const [
|
||||
_blank,
|
||||
_ng,
|
||||
_account,
|
||||
accountId,
|
||||
_module,
|
||||
_moduleName,
|
||||
_org,
|
||||
orgName,
|
||||
_projects,
|
||||
projectName,
|
||||
_repos,
|
||||
repoName,
|
||||
_files,
|
||||
_ref,
|
||||
_branch,
|
||||
...path
|
||||
] = url.replace(baseUrl, '').split('/');
|
||||
const pathWithoutSlash = path.join('/').replace(/^\//, '');
|
||||
return `${baseUrl}/gateway/code/api/v1/repos/${accountId}/${orgName}/${projectName}/${repoName}/+/edit/${pathWithoutSlash}`;
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect URL: ${url}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a URL pointing to a file, returns an api URL
|
||||
* for fetching the contents of the data.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Converts
|
||||
* from: https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projName/repos/repoName/files/refMain/~/all-apis.yaml
|
||||
* to: https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName/projName/repoName/+/content/all-apis.yaml?routingId=accountId&include_commit=false&ref=refMain
|
||||
*
|
||||
* @param url - A URL pointing to a file
|
||||
* @param config - The relevant provider config
|
||||
* @public
|
||||
*/
|
||||
export function getHarnessFileContentsUrl(
|
||||
config: HarnessIntegrationConfig,
|
||||
url: string,
|
||||
) {
|
||||
try {
|
||||
const baseUrl = config.baseUrl ?? `https://${config.host}`;
|
||||
const [
|
||||
_blank,
|
||||
_ng,
|
||||
_account,
|
||||
accountId,
|
||||
_module,
|
||||
_moduleName,
|
||||
_org,
|
||||
orgName,
|
||||
_projects,
|
||||
projectName,
|
||||
_repos,
|
||||
repoName,
|
||||
_files,
|
||||
ref,
|
||||
_branch,
|
||||
...path
|
||||
] = url.replace(baseUrl, '').split('/');
|
||||
const pathWithoutSlash = path.join('/').replace(/^\//, '');
|
||||
return `${baseUrl}/gateway/code/api/v1/repos/${accountId}/${orgName}/${projectName}/${repoName}/+/content/${pathWithoutSlash}?routingId=${accountId}&include_commit=false&ref=${ref}`;
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect URL: ${url}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return request headers for a Harness Code provider.
|
||||
*
|
||||
* @param config - A Harness Code provider config
|
||||
* @public
|
||||
*/
|
||||
export function getHarnessRequestOptions(config: HarnessIntegrationConfig): {
|
||||
headers?: Record<string, string>;
|
||||
} {
|
||||
const headers: Record<string, string> = {};
|
||||
const { username, token } = config;
|
||||
|
||||
if (!token) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
if (username) {
|
||||
headers.Authorization = `basic ${Buffer.from(
|
||||
`${username}:${token}`,
|
||||
).toString('base64')}`;
|
||||
} else {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { HarnessIntegration } from './HarnessIntegration';
|
||||
export { getHarnessRequestOptions, getHarnessFileContentsUrl } from './core';
|
||||
export { readHarnessConfig } from './config';
|
||||
export type { HarnessIntegrationConfig } from './config';
|
||||
@@ -31,6 +31,7 @@ export * from './gitea';
|
||||
export * from './github';
|
||||
export * from './gitlab';
|
||||
export * from './googleGcs';
|
||||
export * from './harness';
|
||||
export { defaultScmResolveUrl } from './helpers';
|
||||
export { ScmIntegrations } from './ScmIntegrations';
|
||||
export type { IntegrationsByType } from './ScmIntegrations';
|
||||
|
||||
@@ -25,6 +25,7 @@ import { GerritIntegration } from './gerrit/GerritIntegration';
|
||||
import { GithubIntegration } from './github/GithubIntegration';
|
||||
import { GitLabIntegration } from './gitlab/GitLabIntegration';
|
||||
import { GiteaIntegration } from './gitea/GiteaIntegration';
|
||||
import { HarnessIntegration } from './harness/HarnessIntegration';
|
||||
|
||||
/**
|
||||
* Holds all registered SCM integrations, of all types.
|
||||
@@ -46,6 +47,7 @@ export interface ScmIntegrationRegistry
|
||||
github: ScmIntegrationsGroup<GithubIntegration>;
|
||||
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
|
||||
gitea: ScmIntegrationsGroup<GiteaIntegration>;
|
||||
harness: ScmIntegrationsGroup<HarnessIntegration>;
|
||||
/**
|
||||
* Resolves an absolute or relative URL in relation to a base URL.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user