Merge branch 'master' of github.com:spotify/backstage into migrate-to-msw

* 'master' of github.com:spotify/backstage:
  Read git auth token from backend config (#2992)
  chore(catalog-model): add the petstore into the default entities
  chore(deps-dev): bump @storybook/addon-actions from 6.0.21 to 6.0.26
  chore(deps): bump eslint-config-prettier from 6.10.0 to 6.14.0
  Add API docs link to About card
  adds a sample organization set of users and groups (#2963)
  Make sure hasCostsWithinTimeframe is a boolean
  fix(catalog-backend): fix codeowners processor to handle users
  feat(catalog): add simple client side paging
  default to last 30 days
  Removed default-branch library since it's no longer used (#3019)
  useRealTimers in  onCssReady test case
  move type dependecy
  remove unused import
  changeset
  simpler query params
This commit is contained in:
blam
2020-10-22 11:02:32 +02:00
35 changed files with 851 additions and 208 deletions
@@ -123,6 +123,10 @@ describe('CodeOwnersProcessor', () => {
});
describe('normalizeCodeOwner', () => {
it('should remove the @ symbol', () => {
expect(normalizeCodeOwner('@yoda')).toBe('yoda');
});
it('should remove org from org/team format', () => {
expect(normalizeCodeOwner('@acme/foo')).toBe('foo');
});
@@ -127,6 +127,8 @@ export function findPrimaryCodeOwner(
export function normalizeCodeOwner(owner: string) {
if (owner.match(/^@.*\/.*/)) {
return owner.split('/')[1];
} else if (owner.match(/^@.*/)) {
return owner.substring(1);
} else if (owner.match(/^.*@.*\..*$/)) {
return owner.split('@')[0];
}
@@ -26,6 +26,7 @@ import {
makeStyles,
Typography,
} from '@material-ui/core';
import BrightnessAutoIcon from '@material-ui/icons/BrightnessAuto';
import DocsIcon from '@material-ui/icons/Description';
import EditIcon from '@material-ui/icons/Edit';
import GitHubIcon from '@material-ui/icons/GitHub';
@@ -120,6 +121,14 @@ export function AboutCard({ entity, variant }: AboutCardProps) {
entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE
}/${entity.kind}/${entity.metadata.name}`}
/>
<IconLinkVertical
disabled={!entity.spec?.implementsApis}
label="View API"
icon={<BrightnessAutoIcon />}
href={`./${
entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE
}:${entity.metadata.name}/api`}
/>
</nav>
}
/>
@@ -155,7 +155,8 @@ export const CatalogTable = ({
isLoading={loading}
columns={columns}
options={{
paging: false,
paging: true,
pageSize: 10,
actionsColumnIndex: -1,
loadingType: 'linear',
showEmptyDataSourceMessage: !loading,
+3 -1
View File
@@ -40,7 +40,8 @@
"react-dom": "^16.13.1",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^15.3.3",
"recharts": "^1.8.5"
"recharts": "^1.8.5",
"yup": "^0.29.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
@@ -50,6 +51,7 @@
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"@types/yup": "^0.29.8",
"@types/recharts": "^1.8.14",
"msw": "^0.21.2",
"cross-fetch": "^3.0.6",
@@ -51,7 +51,7 @@ const ProductInsightsCard = ({ product }: ProductInsightsCardProps) => {
const dispatchLoadingProduct = useCallback(dispatchLoading, [product.kind]);
const amount = resource?.entities?.length || 0;
const hasCostsWithinTimeframe = resource?.change && amount;
const hasCostsWithinTimeframe = resource?.change && !!amount;
const subheader = amount
? `${amount} ${pluralOf(amount, 'entity', 'entities')}, sorted by cost`
-1
View File
@@ -20,5 +20,4 @@ export * from './useFilters';
export * from './useCurrency';
export * from './useGroups';
export * from './useLoading';
export * from './useQueryParams';
export * from './useScroll';
+60 -86
View File
@@ -16,114 +16,98 @@
import React, {
Dispatch,
ReactNode,
PropsWithChildren,
SetStateAction,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import {
getDefaultPageFilters,
PageFilters,
ProductFilters,
Duration,
Group,
} from '../types';
import { Alert } from '@material-ui/lab';
import { Maybe, PageFilters, ProductFilters } from '../types';
import { useLocation, useNavigate } from 'react-router-dom';
import { useQueryParams } from './useQueryParams';
import { stringify } from '../utils/history';
import {
stringify,
validate,
getInitialPageState,
getInitialProductState,
} from '../utils/history';
import { useGroups } from './useGroups';
import { useConfig } from './useConfig';
const getInitialPageState = (
groups: Group[],
queryParams?: Partial<PageFilters>,
) => {
// The group is written initially to queryParams as null, since user groups are asynchronously
// loaded. We preserve nulls in queryParams for other parameters where null is meaningful; for
// group, avoid overwriting the default with null after groups are loaded.
const { group, ...otherParams } = queryParams || {};
return {
...getDefaultPageFilters(groups),
...otherParams,
...(group ? { group: group } : {}),
};
};
export type FilterContextProps = {
pageFilters: PageFilters;
productFilters: ProductFilters;
setPageFilters: Dispatch<SetStateAction<PageFilters>>;
setProductFilters: Dispatch<SetStateAction<ProductFilters>>;
setPageFilters: Dispatch<SetStateAction<Maybe<PageFilters>>>;
setProductFilters: Dispatch<SetStateAction<Maybe<ProductFilters>>>;
};
export type MapFiltersToProps<T> = (props: FilterContextProps) => T;
export type FilterProviderProps = {
children: ReactNode;
};
export const FilterContext = React.createContext<
FilterContextProps | undefined
>(undefined);
export const FilterProvider = ({ children }: FilterProviderProps) => {
export const FilterProvider = ({ children }: PropsWithChildren<{}>) => {
const config = useConfig();
const navigate = useNavigate();
const location = useLocation();
const queryParams = useQueryParams();
const qsRef = useRef('');
const groups = useGroups();
const defaultProductFilters = config.products.map(product => ({
productType: product.kind,
duration: Duration.P1M,
}));
const getInitialProductState = (productFilters?: ProductFilters) => {
if (!productFilters) return defaultProductFilters;
return defaultProductFilters.map(product => {
return (
productFilters.find(
param => param.productType === product.productType,
) || product
);
});
};
const [productFilters, setProductFilters] = useState(
getInitialProductState(queryParams.productFilters),
);
const [pageFilters, setPageFilters] = useState(
getInitialPageState(groups, queryParams.pageFilters),
const [error, setError] = useState<Maybe<Error>>(null);
const [pageFilters, setPageFilters] = useState<Maybe<PageFilters>>(null);
const [productFilters, setProductFilters] = useState<Maybe<ProductFilters>>(
null,
);
// TODO: Figure out why pageFilters doesn't get updated by the above when groups are loaded.
useEffect(() => {
const initialState = getInitialPageState(groups, queryParams.pageFilters);
const defaultMetric = config.metrics.find(m => m.default);
setPageFilters({ ...initialState, metric: defaultMetric?.kind ?? null });
async function setPageFiltersFromLocation() {
try {
// strip extraneous parameters, validate and transform
const queryParams = await validate(location.search);
const defaultMetric = config.metrics.find(m => m.default)?.kind ?? null;
// Group or project parameters should override defaults
const initialPageState = getInitialPageState(groups, queryParams);
const initialProductState = getInitialProductState(config);
setProductFilters(initialProductState);
setPageFilters({ ...initialPageState, metric: defaultMetric });
} catch (e) {
setError(e);
}
}
setPageFiltersFromLocation();
}, [groups]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
const queryString = stringify({ ...pageFilters, products: productFilters });
if (queryString === qsRef.current) return;
qsRef.current = queryString;
// TODO Remove workaround once issue is resolved in react-router
// (https://github.com/ReactTraining/react-router/issues/7496)
// navigate({ ...location, search: queryString });
navigate({ ...location, search: `?${queryString}` });
}, [pageFilters, productFilters]); // eslint-disable-line react-hooks/exhaustive-deps
function setLocationFromPageFilters(filters: PageFilters) {
const queryString = stringify({
group: filters.group,
...(filters.project ? { project: filters.project } : {}),
});
// TODO Remove workaround once issue is resolved in react-router
// (https://github.com/ReactTraining/react-router/issues/7496)
// navigate({ ...location, search: queryString });
navigate({ ...location, search: `?${queryString}` });
}
if (pageFilters) {
setLocationFromPageFilters(pageFilters);
}
}, [pageFilters]); // eslint-disable-line react-hooks/exhaustive-deps
if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
// Wait for filters to load
if (!pageFilters || !productFilters) {
return null;
}
return (
<FilterContext.Provider
value={{
pageFilters: pageFilters,
productFilters: productFilters,
setPageFilters: setPageFilters,
setProductFilters: setProductFilters,
}}
value={{ pageFilters, productFilters, setPageFilters, setProductFilters }}
>
{children}
</FilterContext.Provider>
@@ -132,17 +116,7 @@ export const FilterProvider = ({ children }: FilterProviderProps) => {
export function useFilters<T>(mapFiltersToProps: MapFiltersToProps<T>): T {
const context = useContext(FilterContext);
if (!context) {
assertNever();
}
return mapFiltersToProps({
pageFilters: context.pageFilters,
productFilters: context.productFilters,
setPageFilters: context.setPageFilters,
setProductFilters: context.setProductFilters,
});
return context ? mapFiltersToProps(context) : assertNever();
}
function assertNever(): never {
+30 -21
View File
@@ -14,11 +14,17 @@
* limitations under the License.
*/
import React, { ReactNode, useContext, useEffect, useState } from 'react';
import React, {
PropsWithChildren,
useContext,
useEffect,
useState,
} from 'react';
import { Alert } from '@material-ui/lab';
import { useApi, identityApiRef } from '@backstage/core';
import { costInsightsApiRef } from '../api';
import { MapLoadingToProps, useLoading } from './useLoading';
import { DefaultLoadingAction, Group } from '../types';
import { DefaultLoadingAction, Group, Maybe } from '../types';
type GroupsProviderLoadingProps = {
dispatchLoadingGroups: (isLoading: boolean) => void;
@@ -35,33 +41,41 @@ type GroupsContextProps = {
groups: Group[];
};
export type GroupsProviderProps = {
children: ReactNode;
};
export const GroupsContext = React.createContext<
GroupsContextProps | undefined
>(undefined);
export const GroupsContext = React.createContext<GroupsContextProps>({
groups: [],
});
export const GroupsProvider = ({ children }: GroupsProviderProps) => {
export const GroupsProvider = ({ children }: PropsWithChildren<{}>) => {
const userId = useApi(identityApiRef).getUserId();
const client = useApi(costInsightsApiRef);
const [error, setError] = useState<Maybe<Error>>(null);
const { dispatchLoadingGroups } = useLoading(mapLoadingToProps);
const [groups, setGroups] = useState<Group[]>([]);
const [groups, setGroups] = useState<Maybe<Group[]>>(null);
useEffect(() => {
dispatchLoadingGroups(true);
async function getUserGroups() {
const g = await client.getUserGroups(userId);
setGroups(g);
dispatchLoadingGroups(false);
try {
const g = await client.getUserGroups(userId);
setGroups(g);
} catch (e) {
setError(e);
} finally {
dispatchLoadingGroups(false);
}
}
getUserGroups();
}, [userId, client]); // eslint-disable-line react-hooks/exhaustive-deps
if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
if (!groups) return null;
return (
<GroupsContext.Provider value={{ groups: groups }}>
{children}
@@ -70,13 +84,8 @@ export const GroupsProvider = ({ children }: GroupsProviderProps) => {
};
export function useGroups(): Group[] {
const { groups } = useContext(GroupsContext);
if (!groups) {
assertNever();
}
return groups;
const context = useContext(GroupsContext);
return context ? context.groups : assertNever();
}
function assertNever(): never {
@@ -1,35 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { useLocation } from 'react-router-dom';
import { Location } from 'history';
import { PageFilters, ProductFilters } from '../types';
import { parse } from '../utils/history';
export type FilterParams = {
pageFilters?: Partial<PageFilters>;
productFilters?: ProductFilters;
};
export function useQueryParams(): FilterParams {
const location: Location = useLocation();
const { products: productFilters, ...pageFilters } = parse(location.search);
return {
productFilters: productFilters,
pageFilters: pageFilters,
};
}
@@ -27,8 +27,6 @@ export interface PageFilters {
export type ProductFilters = Array<ProductPeriod>;
export type QueryParams = PageFilters & { products: ProductFilters };
export interface ProductPeriod {
duration: Duration;
productType: string;
@@ -0,0 +1,80 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { validate, getInitialPageState } from './history';
describe('getInitialPageState', () => {
describe('groups', () => {
it('should set defaults if params or group is not provided', () => {
const initialState = getInitialPageState([]);
expect(initialState.group).toBe(null);
});
it('should set defaults if a group is fetched but no group is present on query params', () => {
const initialState = getInitialPageState([{ id: 'group' }]);
expect(initialState.group).toMatch(/group/);
});
it('group param should always override fetched group', () => {
const initialState = getInitialPageState(
[{ id: 'group' }, { id: 'second-group' }],
{ group: 'other-group' },
);
expect(initialState.group).toMatch(/other-group/);
});
it('first group should be set as default group if user belongs to multiple groups', () => {
const initialState = getInitialPageState([
{ id: 'group' },
{ id: 'other-group' },
]);
expect(initialState.group).toMatch(/group/);
});
});
describe('projects', () => {
it("should set defaults if project param doesn't exist", () => {
const initialState = getInitialPageState([], {});
expect(initialState.project).toBeNull();
});
it('should override defaults if project param exists', () => {
const initialState = getInitialPageState([], { project: 'some-project' });
expect(initialState.project).toMatch(/some-project/);
});
});
});
describe.each`
params | expected
${''} | ${{}}
${'?foo=bar'} | ${{}}
${'?project'} | ${{ project: null }}
${'?group=some-group'} | ${{ group: 'some-group' }}
${'?group=some-group&project'} | ${{ group: 'some-group', project: null }}
${'?group=some-group&project=some-project'} | ${{ group: 'some-group', project: 'some-project' }}
${'?group=some-group&project=some-project&foo=bar'} | ${{ group: 'some-group', project: 'some-project' }}
`('validate', ({ params, expected }) => {
it(`should validate ${params}`, async () => {
const pageFilters = await validate(params);
expect(pageFilters).toMatchObject(expected);
});
});
describe('invalidate', () => {
it("should throw an error if param values don't match schema", async () => {
await expect(validate('?group')).rejects.toThrowError();
});
});
+38 -3
View File
@@ -15,9 +15,44 @@
*/
import qs from 'qs';
import { QueryParams } from '../types';
import * as yup from 'yup';
import { Group, PageFilters, getDefaultPageFilters, Duration } from '../types';
import { ConfigContextProps } from '../hooks/useConfig';
export const stringify = (queryParams: Partial<QueryParams>) =>
const schema = yup
.object()
.shape({
group: yup.string(),
project: yup.string().nullable(),
})
.required();
export const stringify = (queryParams: Partial<PageFilters>) =>
qs.stringify(queryParams, { strictNullHandling: true });
export const parse = (queryString: string): Partial<QueryParams> =>
export const parse = (queryString: string): Partial<PageFilters> =>
qs.parse(queryString, { ignoreQueryPrefix: true, strictNullHandling: true });
export const validate = (queryString: string): Promise<PageFilters> => {
return schema.validate(parse(queryString), {
stripUnknown: true,
strict: true,
}) as Promise<PageFilters>;
};
export const getInitialPageState = (
groups: Group[],
queryParams: Partial<PageFilters> = {},
) => {
return {
...getDefaultPageFilters(groups),
...(queryParams.project ? { project: queryParams.project } : {}),
...(queryParams.group ? { group: queryParams.group } : {}),
};
};
export const getInitialProductState = (config: ConfigContextProps) =>
config.products.map(product => ({
productType: product.kind,
duration: Duration.P30D,
}));
+3 -2
View File
@@ -16,6 +16,7 @@
import React, { Dispatch, ReactNode, SetStateAction } from 'react';
import {
getDefaultPageFilters,
Maybe,
PageFilters,
ProductFilters,
Group,
@@ -29,8 +30,8 @@ import { MockProductFilters } from './mockData';
export const MockGroups: Group[] = [{ id: 'tech' }, { id: 'mock-group' }];
type MockFilterProviderProps = {
setPageFilters: Dispatch<SetStateAction<PageFilters>>;
setProductFilters: Dispatch<SetStateAction<ProductFilters>>;
setPageFilters: Dispatch<SetStateAction<Maybe<PageFilters>>>;
setProductFilters: Dispatch<SetStateAction<Maybe<ProductFilters>>>;
children: ReactNode;
};
-1
View File
@@ -26,7 +26,6 @@
"@types/dockerode": "^2.5.34",
"@types/express": "^4.17.6",
"command-exists-promise": "^2.0.2",
"default-branch": "^1.0.8",
"dockerode": "^3.2.1",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
+20 -27
View File
@@ -17,6 +17,13 @@ import fetch from 'cross-fetch';
import parseGitUrl from 'git-url-parse';
import { Config } from '@backstage/config';
import { getRootLogger, loadBackendConfig } from '@backstage/backend-common';
import {
getAzureHostToken,
getGitHost,
getGithubHostToken,
getGitlabHostToken,
getGitRepoType,
} from './git-auth';
interface IGitlabBranch {
name: string;
@@ -73,15 +80,12 @@ function getAzureApiUrl(url: string): URL {
);
}
function getGithubRequestOptions(config: Config): RequestInit {
function getGithubRequestOptions(config: Config, host: string): RequestInit {
const headers: HeadersInit = {
Accept: 'application/vnd.github.v3.raw',
};
const token =
config.getOptionalString('catalog.processors.github.privateToken') ??
config.getOptionalString('catalog.processors.githubApi.privateToken') ??
process.env.GITHUB_TOKEN;
const token = getGithubHostToken(config, host);
if (token) {
headers.Authorization = `token ${token}`;
@@ -92,16 +96,12 @@ function getGithubRequestOptions(config: Config): RequestInit {
};
}
function getGitlabRequestOptions(config: Config): RequestInit {
function getGitlabRequestOptions(config: Config, host: string): RequestInit {
const headers: HeadersInit = {
'PRIVATE-TOKEN': '',
};
const token =
config.getOptionalString('catalog.processors.gitlab.privateToken') ??
config.getOptionalString('catalog.processors.gitlabApi.privateToken') ??
process.env.GITLAB_TOKEN;
const token = getGitlabHostToken(config, host);
if (token) {
headers['PRIVATE-TOKEN'] = token;
}
@@ -111,12 +111,10 @@ function getGitlabRequestOptions(config: Config): RequestInit {
};
}
function getAzureRequestOptions(config: Config): RequestInit {
function getAzureRequestOptions(config: Config, host: string): RequestInit {
const headers: HeadersInit = {};
const token =
config.getOptionalString('catalog.processors.azureApi.privateToken') ??
process.env.AZURE_TOKEN;
const token = getAzureHostToken(config, host);
if (token !== '') {
headers.Authorization = `Basic ${Buffer.from(`:${token}`, 'utf8').toString(
@@ -136,7 +134,8 @@ async function getGithubDefaultBranch(
config: Config,
): Promise<string> {
const path = getGithubApiUrl(config, repositoryUrl).toString();
const options = getGithubRequestOptions(config);
const host = getGitHost(repositoryUrl);
const options = getGithubRequestOptions(config, host);
try {
const raw = await fetch(path, options);
@@ -165,7 +164,8 @@ async function getGitlabDefaultBranch(
): Promise<string> {
const path = getGitlabApiUrl(repositoryUrl).toString();
const options = getGitlabRequestOptions(config);
const gitlabHost = getGitHost(repositoryUrl);
const options = getGitlabRequestOptions(config, gitlabHost);
try {
const raw = await fetch(path, options);
@@ -196,8 +196,8 @@ async function getAzureDefaultBranch(
config: Config,
): Promise<string> {
const path = getAzureApiUrl(repositoryUrl).toString();
const options = getAzureRequestOptions(config);
const host = getGitHost(repositoryUrl);
const options = getAzureRequestOptions(config, host);
try {
const urlResponse = await fetch(path, options);
@@ -232,14 +232,7 @@ export const getDefaultBranch = async (
): Promise<string> => {
// TODO(Rugvip): Config should not be loaded here, pass it in instead
const config = await loadBackendConfig({ logger: getRootLogger() });
const typeMapping = [
{ url: /github*/g, type: 'github' },
{ url: /gitlab*/g, type: 'gitlab' },
{ url: /azure*/g, type: 'azure/api' },
];
const type = typeMapping.filter(item => item.url.test(repositoryUrl))[0]
?.type;
const type = getGitRepoType(repositoryUrl);
try {
switch (type) {
+116
View File
@@ -0,0 +1,116 @@
/*
* Copyright 2020 Spotify AB
*
* 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 parseGitUrl from 'git-url-parse';
import { Config } from '@backstage/config';
import { getRootLogger, loadBackendConfig } from '@backstage/backend-common';
export function getGitHost(url: string): string {
const { resource } = parseGitUrl(url);
return resource;
}
export function getGitRepoType(url: string): string {
const typeMapping = [
{ url: /github*/g, type: 'github' },
{ url: /gitlab*/g, type: 'gitlab' },
{ url: /azure*/g, type: 'azure/api' },
];
const type = typeMapping.filter(item => item.url.test(url))[0]?.type;
return type;
}
export function getGithubHostToken(
config: Config,
host: string,
): string | undefined {
const providerConfigs =
config.getOptionalConfigArray('integrations.github') ?? [];
const hostConfig = providerConfigs.filter(
providerConfig => providerConfig.getOptionalString('host') === host,
);
const token =
hostConfig[0]?.getOptionalString('token') ??
config.getOptionalString('catalog.processors.github.privateToken') ??
config.getOptionalString('catalog.processors.githubApi.privateToken') ??
process.env.GITHUB_TOKEN;
return token;
}
export function getGitlabHostToken(
config: Config,
host: string,
): string | undefined {
const providerConfigs =
config.getOptionalConfigArray('integrations.gitlab') ?? [];
const hostConfig = providerConfigs.filter(
providerConfig => providerConfig.getOptionalString('host') === host,
);
const token =
hostConfig[0]?.getOptionalString('token') ??
config.getOptionalString('catalog.processors.gitlab.privateToken') ??
config.getOptionalString('catalog.processors.gitlabApi.privateToken') ??
process.env.GITLAB_TOKEN;
return token;
}
export function getAzureHostToken(
config: Config,
host: string,
): string | undefined {
const providerConfigs =
config.getOptionalConfigArray('integrations.azure') ?? [];
const hostConfig = providerConfigs.filter(
providerConfig => providerConfig.getOptionalString('host') === host,
);
const token =
hostConfig[0]?.getOptionalString('token') ??
config.getOptionalString('catalog.processors.azureApi.privateToken') ??
process.env.AZURE_TOKEN;
return token;
}
export const getTokenForGitRepo = async (
repositoryUrl: string,
): Promise<string | undefined> => {
const config = await loadBackendConfig({ logger: getRootLogger() });
const host = getGitHost(repositoryUrl);
const type = getGitRepoType(repositoryUrl);
try {
switch (type) {
case 'github':
return getGithubHostToken(config, host);
case 'gitlab':
return getGitlabHostToken(config, host);
case 'azure/api':
return getAzureHostToken(config, host);
default:
throw new Error('Failed to get repository type');
}
} catch (error) {
throw error;
}
};
+4 -6
View File
@@ -20,6 +20,7 @@ import parseGitUrl from 'git-url-parse';
import NodeGit, { Clone, Repository } from 'nodegit';
import fs from 'fs-extra';
import { getDefaultBranch } from './default-branch';
import { getTokenForGitRepo } from './git-auth';
import { Entity } from '@backstage/catalog-model';
import { InputError } from '@backstage/backend-common';
import { RemoteProtocol } from './techdocs/stages/prepare/types';
@@ -127,11 +128,8 @@ export const checkoutGitRepository = async (
process.env.GITLAB_PRIVATE_TOKEN_USER ||
process.env.AZURE_PRIVATE_TOKEN_USER ||
'';
const token =
process.env.GITHUB_TOKEN ||
process.env.GITLAB_PRIVATE_TOKEN_USER ||
process.env.AZURE_TOKEN ||
'';
const token = await getTokenForGitRepo(repoUrl);
if (fs.existsSync(repositoryTmpPath)) {
try {
@@ -153,7 +151,7 @@ export const checkoutGitRepository = async (
}
}
if (user && token) {
if (token) {
parsedGitLocation.token = `${user}:${token}`;
}
@@ -25,14 +25,20 @@ import { onCssReady } from '../transformers';
const docStorageUrl: string =
'https://techdocs-mock-sites.storage.googleapis.com';
jest.useFakeTimers();
const fixture = `
<link rel="stylesheet" href="${docStorageUrl}/test.css" />
<link rel="stylesheet" href="http://example.com/test.css" />
`;
describe('onCssReady', () => {
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
beforeEach(() => {
mockStylesheetEventListener(100);
});