diff --git a/.changeset/eleven-tables-tease.md b/.changeset/eleven-tables-tease.md
new file mode 100644
index 0000000000..3f95fbb9d2
--- /dev/null
+++ b/.changeset/eleven-tables-tease.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-import': patch
+---
+
+Modifying import functionality to register existing catalog-info.yaml if one exists in given GitHub repository
diff --git a/.changeset/fair-geckos-collect.md b/.changeset/fair-geckos-collect.md
new file mode 100644
index 0000000000..204041b195
--- /dev/null
+++ b/.changeset/fair-geckos-collect.md
@@ -0,0 +1,16 @@
+---
+'@backstage/create-app': patch
+---
+
+Due to a package name change from `@kyma-project/asyncapi-react` to
+`@asyncapi/react-component` the jest configuration in the root `package.json`
+has to be updated:
+
+```diff
+ "jest": {
+ "transformModules": [
+- "@kyma-project/asyncapi-react
++ "@asyncapi/react-component"
+ ]
+ }
+```
diff --git a/.changeset/friendly-flies-fly.md b/.changeset/friendly-flies-fly.md
new file mode 100644
index 0000000000..cfccf621fe
--- /dev/null
+++ b/.changeset/friendly-flies-fly.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-backend': patch
+---
+
+Change AWS Account type from Component to Resource
diff --git a/.changeset/purple-olives-destroy.md b/.changeset/purple-olives-destroy.md
new file mode 100644
index 0000000000..3578f5fdf5
--- /dev/null
+++ b/.changeset/purple-olives-destroy.md
@@ -0,0 +1,5 @@
+---
+'@backstage/backend-common': patch
+---
+
+URL Reader: Use API response headers for archive filename in readTree. Fixes bug for users with hosted Bitbucket.
diff --git a/.changeset/rich-geckos-lie.md b/.changeset/rich-geckos-lie.md
new file mode 100644
index 0000000000..61ff7600f6
--- /dev/null
+++ b/.changeset/rich-geckos-lie.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-api-docs': patch
+---
+
+Update `@asyncapi/react-component` to 0.18.2
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 11a756a597..9a5ef09554 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -6,6 +6,7 @@
* @backstage/maintainers
/docs/features/techdocs @backstage/techdocs-core
+/docs/features/search @backstage/techdocs-core
/plugins/cost-insights @backstage/silver-lining
/plugins/cloudbuild @trivago/ebarrios
/plugins/search @backstage/techdocs-core
diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml
new file mode 100644
index 0000000000..f3fe2bbc5f
--- /dev/null
+++ b/.github/workflows/fossa.yml
@@ -0,0 +1,44 @@
+name: FOSSA
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2.3.4
+
+ # We use this to modify the generated .fossa.yml
+ - name: Install yq
+ run: sudo snap install yq
+
+ - name: Install Fossa
+ run: "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash"
+
+ - name: Configure Fossa
+ # The --option flag for fossa init does not work yet, see https://github.com/fossas/fossa-cli/issues/614
+ run: |
+ fossa init
+ yq eval -i '.analyze.modules[].options.strategy = "yarn-list"' .fossa.yml
+
+ # This deletes entries for template and example packages found within packages and plugins
+ # Seems like yq has a bug that causes only a subset of all matches to be deleted each run
+ yq eval -i 'del(.analyze.modules[] | select(.path == "*/*/**"))' .fossa.yml
+ yq eval -i 'del(.analyze.modules[] | select(.path == "*/*/**"))' .fossa.yml
+ yq eval -i 'del(.analyze.modules[] | select(.path == "*/*/**"))' .fossa.yml
+ yq eval -i 'del(.analyze.modules[] | select(.path == "*/*/**"))' .fossa.yml
+ yq eval -i 'del(.analyze.modules[] | select(.path == "*/*/**"))' .fossa.yml
+
+ - name: Show config
+ run: cat .fossa.yml
+
+ - name: Fossa Analyze
+ env:
+ # FOSSA Push-Only API Token
+ FOSSA_API_KEY: 9ee7e8893660832a7387dcc32377fb61
+ run: fossa analyze --branch "$GITHUB_REF"
diff --git a/app-config.yaml b/app-config.yaml
index 741991877e..71c3310713 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -185,33 +185,46 @@ catalog:
# groupFilter: securityEnabled eq false and mailEnabled eq true and groupTypes/any(c:c+eq+'Unified')
locations:
+ # Add a location here to ingest it, for example from an URL:
+ #
+ # - type: url
+ # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-components.yaml
+ #
+ # For local development you can use a file location instead:
+ #
+ # - type: file
+ # target: ../catalog-model/examples/all-components.yaml
+ #
+ # File locations are relative to the current working directory of the
+ # backend, for example packages/backend/.
+
# Backstage example components
- - type: url
- target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-components.yaml
+ - type: file
+ target: ../catalog-model/examples/all-components.yaml
# Example component for github-actions
- - type: url
- target: https://github.com/backstage/backstage/blob/master/plugins/github-actions/examples/sample.yaml
+ - type: file
+ target: ../../plugins/github-actions/examples/sample.yaml
# Example component for TechDocs
- - type: url
- target: https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml
+ - type: file
+ target: ../../plugins/techdocs-backend/examples/documented-component/catalog-info.yaml
# Backstage example APIs
- - type: url
- target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-apis.yaml
+ - type: file
+ target: ../catalog-model/examples/all-apis.yaml
# Backstage example resources
- - type: url
- target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-resources.yaml
+ - type: file
+ target: ../catalog-model/examples/all-resources.yaml
# Backstage example systems
- - type: url
- target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-systems.yaml
+ - type: file
+ target: ../catalog-model/examples/all-systems.yaml
# Backstage example domains
- - type: url
- target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-domains.yaml
+ - type: file
+ target: ../catalog-model/examples/all-domains.yaml
# Backstage example templates
- - type: url
- target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/all-templates.yaml
+ - type: file
+ target: ../../plugins/scaffolder-backend/sample-templates/all-templates.yaml
# Backstage example groups and users
- - type: url
- target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml
+ - type: file
+ target: ../catalog-model/examples/acme-corp.yaml
scaffolder:
github:
diff --git a/docs/architecture-decisions/adr006-avoid-react-fc.md b/docs/architecture-decisions/adr006-avoid-react-fc.md
index 51dcf042da..eecb4ad87a 100644
--- a/docs/architecture-decisions/adr006-avoid-react-fc.md
+++ b/docs/architecture-decisions/adr006-avoid-react-fc.md
@@ -43,6 +43,15 @@ const GoodComponent = ({ text, children }: GoodProps) => (
{children}
);
+
+/* Or as a shorthand, if no specifc child type is required */
+type GoodProps = PropsWithChildren<{ text: string }>;
+const GoodComponent = ({ text, children }: GoodProps) => (
+
+);
```
## Consequences
diff --git a/package.json b/package.json
index aaa68e67de..758a8ba884 100644
--- a/package.json
+++ b/package.json
@@ -75,7 +75,7 @@
},
"jest": {
"transformModules": [
- "@kyma-project/asyncapi-react"
+ "@asyncapi/react-component"
]
}
}
diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts
index 59e437607f..578db2ac92 100644
--- a/packages/backend-common/src/reading/AzureUrlReader.ts
+++ b/packages/backend-common/src/reading/AzureUrlReader.ts
@@ -76,6 +76,8 @@ export class AzureUrlReader implements UrlReader {
url: string,
options?: ReadTreeOptions,
): Promise {
+ // TODO: Support filepath based reading tree feature like other providers
+
// Get latest commit SHA
const commitsAzureResponse = await fetch(
diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts
index 3542e822e1..9661368b5e 100644
--- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts
+++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts
@@ -95,6 +95,10 @@ describe('BitbucketUrlReader', () => {
res(
ctx.status(200),
ctx.set('Content-Type', 'application/zip'),
+ ctx.set(
+ 'content-disposition',
+ 'attachment; filename=backstage-mock-12ab34cd56ef.zip',
+ ),
ctx.body(repoBuffer),
),
),
@@ -114,6 +118,10 @@ describe('BitbucketUrlReader', () => {
res(
ctx.status(200),
ctx.set('Content-Type', 'application/zip'),
+ ctx.set(
+ 'content-disposition',
+ 'attachment; filename=backstage-mock.zip',
+ ),
ctx.body(privateBitbucketRepoBuffer),
),
),
diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts
index 869870f248..e9727e04cf 100644
--- a/packages/backend-common/src/reading/BitbucketUrlReader.ts
+++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts
@@ -101,17 +101,13 @@ export class BitbucketUrlReader implements UrlReader {
url: string,
options?: ReadTreeOptions,
): Promise {
- const { name: repoName, owner: project, resource, filepath } = parseGitUrl(
- url,
- );
+ const { filepath } = parseGitUrl(url);
const lastCommitShortHash = await this.getLastCommitShortHash(url);
if (options?.etag && options.etag === lastCommitShortHash) {
throw new NotModifiedError();
}
- const isHosted = resource === 'bitbucket.org';
-
const downloadUrl = await getBitbucketDownloadUrl(url, this.config);
const archiveBitbucketResponse = await fetch(
downloadUrl,
@@ -125,14 +121,31 @@ export class BitbucketUrlReader implements UrlReader {
throw new Error(message);
}
- let folderPath = `${project}-${repoName}`;
- if (isHosted) {
- folderPath = `${project}-${repoName}-${lastCommitShortHash}`;
+ // Get the filename of archive from the header of the response
+ const contentDispositionHeader = archiveBitbucketResponse.headers.get(
+ 'content-disposition',
+ ) as string;
+ if (!contentDispositionHeader) {
+ throw new Error(
+ `Failed to read tree from ${url}. ` +
+ 'Bitbucket API response for downloading archive does not contain content-disposition header ',
+ );
+ }
+ const fileNameRegEx = new RegExp(
+ /^attachment; filename=(?.*).zip$/,
+ );
+ const archiveFileName = contentDispositionHeader.match(fileNameRegEx)
+ ?.groups?.fileName;
+ if (!archiveFileName) {
+ throw new Error(
+ `Failed to read tree from ${url}. Bitbucket API response for downloading archive has an unexpected ` +
+ `format of content-disposition header ${contentDispositionHeader} `,
+ );
}
return await this.treeResponseFactory.fromZipArchive({
stream: (archiveBitbucketResponse.body as unknown) as Readable,
- path: `${folderPath}/${filepath}`,
+ path: `${archiveFileName}/${filepath}`,
etag: lastCommitShortHash,
filter: options?.filter,
});
diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts
index 0b7d56480f..080e8b1d5b 100644
--- a/packages/backend-common/src/reading/GithubUrlReader.test.ts
+++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts
@@ -165,6 +165,10 @@ describe('GithubUrlReader', () => {
res(
ctx.status(200),
ctx.set('Content-Type', 'application/x-gzip'),
+ ctx.set(
+ 'content-disposition',
+ 'attachment; filename=backstage-mock-etag123.tar.gz',
+ ),
ctx.body(repoBuffer),
),
),
@@ -178,6 +182,10 @@ describe('GithubUrlReader', () => {
res(
ctx.status(200),
ctx.set('Content-Type', 'application/x-gzip'),
+ ctx.set(
+ 'content-disposition',
+ 'attachment; filename=backstage-mock-etag123.tar.gz',
+ ),
ctx.body(repoBuffer),
),
),
@@ -244,6 +252,10 @@ describe('GithubUrlReader', () => {
return res(
ctx.status(200),
ctx.set('Content-Type', 'application/x-gzip'),
+ ctx.set(
+ 'content-disposition',
+ 'attachment; filename=backstage-mock-etag123.tar.gz',
+ ),
ctx.body(repoBuffer),
);
},
diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts
index b5b4e9c1a7..6c7cefe2ef 100644
--- a/packages/backend-common/src/reading/GithubUrlReader.ts
+++ b/packages/backend-common/src/reading/GithubUrlReader.ts
@@ -166,18 +166,31 @@ export class GithubUrlReader implements UrlReader {
throw new Error(message);
}
- // Note that repoResponseJson.full_name must be used over full_name because the path
- // is case sensitive and full_name may not be inq the correct case.
- // TODO(OrkoHunter): The directory name inside the tarball should be retrieved from the tar
- // instead of being constructed here. Same goes for GitLab, Bitbucket and Azure.
- const extractedDirName = `${repoResponseJson.full_name.replace(
- '/',
- '-',
- )}-${commitSha.substr(0, 7)}`;
+ // Get the filename of archive from the header of the response
+ const contentDispositionHeader = archive.headers.get(
+ 'content-disposition',
+ ) as string;
+ if (!contentDispositionHeader) {
+ throw new Error(
+ `Failed to read tree from ${url}. ` +
+ 'GitHub API response for downloading archive does not contain content-disposition header ',
+ );
+ }
+ const fileNameRegEx = new RegExp(
+ /^attachment; filename=(?.*).tar.gz$/,
+ );
+ const archiveFileName = contentDispositionHeader.match(fileNameRegEx)
+ ?.groups?.fileName;
+ if (!archiveFileName) {
+ throw new Error(
+ `Failed to read tree from ${url}. GitHub API response for downloading archive has an unexpected ` +
+ `format of content-disposition header ${contentDispositionHeader} `,
+ );
+ }
// The path includes the name of the directory inside the tarball and a sub path
// if requested in readTree.
- const path = `${extractedDirName}/${filepath}`;
+ const path = `${archiveFileName}/${filepath}`;
return await this.deps.treeResponseFactory.fromTarArchive({
// TODO(Rugvip): Underlying implementation of fetch will be node-fetch, we probably want
diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts
index 2e66794397..c0736f769d 100644
--- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts
+++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts
@@ -176,6 +176,10 @@ describe('GitlabUrlReader', () => {
res(
ctx.status(200),
ctx.set('Content-Type', 'application/zip'),
+ ctx.set(
+ 'content-disposition',
+ 'attachment; filename="mock-main-sha123abc.zip"',
+ ),
ctx.body(archiveBuffer),
),
),
@@ -225,6 +229,10 @@ describe('GitlabUrlReader', () => {
res(
ctx.status(200),
ctx.set('Content-Type', 'application/zip'),
+ ctx.set(
+ 'content-disposition',
+ 'attachment; filename="mock-main-sha123abc.zip"',
+ ),
ctx.body(archiveBuffer),
),
),
@@ -254,6 +262,10 @@ describe('GitlabUrlReader', () => {
res(
ctx.status(200),
ctx.set('Content-Type', 'application/zip'),
+ ctx.set(
+ 'content-disposition',
+ 'attachment; filename="mock-main-sha123abc.zip"',
+ ),
ctx.body(archiveBuffer),
),
),
diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts
index 72db901ac1..654f4f9a85 100644
--- a/packages/backend-common/src/reading/GitlabUrlReader.ts
+++ b/packages/backend-common/src/reading/GitlabUrlReader.ts
@@ -78,7 +78,7 @@ export class GitlabUrlReader implements UrlReader {
url: string,
options?: ReadTreeOptions,
): Promise {
- const { name: repoName, ref, full_name, filepath } = parseGitUrl(url);
+ const { ref, full_name, filepath } = parseGitUrl(url);
// Use GitLab API to get the default branch
// encodeURIComponent is required for GitLab API
@@ -140,9 +140,29 @@ export class GitlabUrlReader implements UrlReader {
throw new Error(message);
}
- const path = filepath
- ? `${repoName}-${branch}-${commitSha}/${filepath}/`
- : '';
+ // Get the filename of archive from the header of the response
+ const contentDispositionHeader = archiveGitLabResponse.headers.get(
+ 'content-disposition',
+ ) as string;
+ if (!contentDispositionHeader) {
+ throw new Error(
+ `Failed to read tree from ${url}. ` +
+ 'GitLab API response for downloading archive does not contain content-disposition header ',
+ );
+ }
+ const fileNameRegEx = new RegExp(
+ /^attachment; filename="(?.*).zip"$/,
+ );
+ const archiveFileName = contentDispositionHeader.match(fileNameRegEx)
+ ?.groups?.fileName;
+ if (!archiveFileName) {
+ throw new Error(
+ `Failed to read tree from ${url}. GitLab API response for downloading archive has an unexpected ` +
+ `format of content-disposition header ${contentDispositionHeader} `,
+ );
+ }
+
+ const path = filepath ? `${archiveFileName}/${filepath}/` : '';
return await this.treeResponseFactory.fromZipArchive({
stream: (archiveGitLabResponse.body as unknown) as Readable,
diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs
index 1353e33072..3fa350fa7c 100644
--- a/packages/create-app/templates/default-app/package.json.hbs
+++ b/packages/create-app/templates/default-app/package.json.hbs
@@ -45,7 +45,7 @@
},
"jest": {
"transformModules": [
- "@kyma-project/asyncapi-react"
+ "@asyncapi/react-component"
]
}
}
diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json
index 39e202eef8..571b4a3067 100644
--- a/plugins/api-docs/package.json
+++ b/plugins/api-docs/package.json
@@ -29,11 +29,11 @@
"clean": "backstage-cli clean"
},
"dependencies": {
+ "@asyncapi/react-component": "^0.18.2",
"@backstage/catalog-model": "^0.6.0",
"@backstage/core": "^0.4.4",
"@backstage/plugin-catalog": "^0.2.9",
"@backstage/theme": "^0.2.2",
- "@kyma-project/asyncapi-react": "^0.14.2",
"@material-icons/font": "^1.0.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
diff --git a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx
index 86f524d099..a908a91b14 100644
--- a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx
+++ b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-import AsyncApi from '@kyma-project/asyncapi-react';
+import AsyncApi from '@asyncapi/react-component';
+import '@asyncapi/react-component/lib/styles/fiori.css';
+import { fade, makeStyles } from '@material-ui/core/styles';
import React from 'react';
-import { makeStyles, fade } from '@material-ui/core/styles';
-import '@kyma-project/asyncapi-react/lib/styles/fiori.css';
const useStyles = makeStyles(theme => ({
root: {
diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts
index 5044a16aea..6eb7a45c63 100644
--- a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts
@@ -45,7 +45,7 @@ describe('AwsOrganizationCloudAccountProcessor', () => {
location,
entity: {
apiVersion: 'backstage.io/v1alpha1',
- kind: 'Component',
+ kind: 'Resource',
metadata: {
annotations: {
'amazonaws.com/arn':
@@ -58,7 +58,6 @@ describe('AwsOrganizationCloudAccountProcessor', () => {
},
spec: {
type: 'cloud-account',
- lifecycle: 'unknown',
owner: 'unknown',
},
},
@@ -91,7 +90,7 @@ describe('AwsOrganizationCloudAccountProcessor', () => {
location,
entity: {
apiVersion: 'backstage.io/v1alpha1',
- kind: 'Component',
+ kind: 'Resource',
metadata: {
annotations: {
'amazonaws.com/arn':
@@ -104,7 +103,6 @@ describe('AwsOrganizationCloudAccountProcessor', () => {
},
spec: {
type: 'cloud-account',
- lifecycle: 'unknown',
owner: 'unknown',
},
},
diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts
index 298fcb82ec..40516f88f7 100644
--- a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts
@@ -13,10 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {
- ComponentEntityV1alpha1,
- LocationSpec,
-} from '@backstage/catalog-model';
+import { LocationSpec, ResourceEntityV1alpha1 } from '@backstage/catalog-model';
import {
Account,
Organizations,
@@ -82,13 +79,13 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor {
return awsAccounts;
}
- mapAccountToComponent(account: Account): ComponentEntityV1alpha1 {
+ mapAccountToComponent(account: Account): ResourceEntityV1alpha1 {
const { accountId, organizationId } = this.extractInformationFromArn(
account.Arn as string,
);
return {
apiVersion: 'backstage.io/v1alpha1',
- kind: 'Component',
+ kind: 'Resource',
metadata: {
annotations: {
[ACCOUNTID_ANNOTATION]: accountId,
@@ -100,7 +97,6 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor {
},
spec: {
type: 'cloud-account',
- lifecycle: 'unknown',
owner: 'unknown',
},
};
@@ -129,7 +125,7 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor {
}
return true;
})
- .forEach((entity: ComponentEntityV1alpha1) => {
+ .forEach((entity: ResourceEntityV1alpha1) => {
emit(results.entity(location, entity));
});
diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json
index 5e601ad896..c4e8d918d1 100644
--- a/plugins/catalog-import/package.json
+++ b/plugins/catalog-import/package.json
@@ -32,8 +32,8 @@
"dependencies": {
"@backstage/catalog-model": "^0.6.1",
"@backstage/core": "^0.4.4",
- "@backstage/plugin-catalog": "^0.2.11",
"@backstage/integration": "^0.2.0",
+ "@backstage/plugin-catalog": "^0.2.11",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
diff --git a/plugins/catalog-import/src/api/CatalogImportApi.ts b/plugins/catalog-import/src/api/CatalogImportApi.ts
index 99526a3eaa..5abb0e3e53 100644
--- a/plugins/catalog-import/src/api/CatalogImportApi.ts
+++ b/plugins/catalog-import/src/api/CatalogImportApi.ts
@@ -30,6 +30,11 @@ export interface CatalogImportApi {
fileContent: string;
githubIntegrationConfig: GitHubIntegrationConfig;
}): Promise<{ link: string; location: string }>;
+ checkForExistingCatalogInfo(options: {
+ owner: string;
+ repo: string;
+ githubIntegrationConfig: GitHubIntegrationConfig;
+ }): Promise<{ exists: boolean; url?: string }>;
createRepositoryLocation(options: { location: string }): Promise;
generateEntityDefinitions(options: {
repo: string;
diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts
new file mode 100644
index 0000000000..0e0cf4b323
--- /dev/null
+++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts
@@ -0,0 +1,64 @@
+/*
+ * 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 { CatalogImportClient } from './CatalogImportClient';
+
+jest.mock('@octokit/rest', () => ({
+ Octokit: jest.fn().mockImplementation(() => {
+ return {
+ repos: {
+ get: () =>
+ Promise.resolve({
+ data: {
+ default_branch: 'main',
+ },
+ }),
+ },
+ search: {
+ code: () =>
+ Promise.resolve({
+ data: {
+ total_count: 2,
+ items: [
+ { path: 'simple/path/catalog-info.yaml' },
+ { path: 'co/mple/x/path/catalog-info.yaml' },
+ { path: 'catalog-info.yaml' },
+ ],
+ },
+ }),
+ },
+ };
+ }),
+}));
+
+describe('CatalogImportClient', () => {
+ describe('checkForExistingCatalogInfo', () => {
+ const cic = new CatalogImportClient({
+ discoveryApi: { getBaseUrl: () => Promise.resolve('base') },
+ githubAuthApi: { getAccessToken: (_, __) => Promise.resolve('token') },
+ configApi: {} as any,
+ });
+ it('should return the closest-to-root catalog-info from multiple responses', async () => {
+ const respo = await cic.checkForExistingCatalogInfo({
+ owner: 'test-user',
+ repo: 'rest-repo',
+ githubIntegrationConfig: { host: 'https://github.com' },
+ });
+ expect(respo.exists).toBe(true);
+ expect(respo.url).toBe('blob/main/catalog-info.yaml');
+ });
+ });
+});
diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts
index a578f37a96..d1509c2105 100644
--- a/plugins/catalog-import/src/api/CatalogImportClient.ts
+++ b/plugins/catalog-import/src/api/CatalogImportClient.ts
@@ -90,6 +90,52 @@ export class CatalogImportClient implements CatalogImportApi {
}
}
+ async checkForExistingCatalogInfo({
+ owner,
+ repo,
+ githubIntegrationConfig,
+ }: {
+ owner: string;
+ repo: string;
+ githubIntegrationConfig: GitHubIntegrationConfig;
+ }): Promise<{ exists: boolean; url?: string }> {
+ const token = await this.githubAuthApi.getAccessToken(['repo']);
+ const octo = new Octokit({
+ auth: token,
+ baseUrl: githubIntegrationConfig.apiBaseUrl,
+ });
+ const catalogFileName = 'catalog-info.yaml';
+ const query = `repo:${owner}/${repo}+filename:${catalogFileName}`;
+
+ const searchResult = await octo.search.code({ q: query }).catch(e => {
+ throw new Error(
+ formatHttpErrorMessage(
+ "Couldn't search repository for metadata file.",
+ e,
+ ),
+ );
+ });
+ const exists = searchResult.data.total_count > 0;
+ if (exists) {
+ const repoInformation = await octo.repos.get({ owner, repo }).catch(e => {
+ throw new Error(formatHttpErrorMessage("Couldn't fetch repo data", e));
+ });
+ const defaultBranch = repoInformation.data.default_branch;
+
+ // Github search sorts returned values with 'best match' using 'multiple factors to boost the most relevant item',
+ // aka magic.
+ // Sorting to use the shortest item, closest to the repository root.
+ const catalogInfoItem = searchResult.data.items
+ .map(it => it.path)
+ .sort((a, b) => a.length - b.length)[0];
+ return {
+ url: `blob/${defaultBranch}/${catalogInfoItem}`,
+ exists,
+ };
+ }
+ return { exists };
+ }
+
async submitPrToRepo({
owner,
repo,
diff --git a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx
new file mode 100644
index 0000000000..91a5208bee
--- /dev/null
+++ b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2021 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 React from 'react';
+import { renderInTestApp } from '@backstage/test-utils';
+import { RegisterComponentForm } from './ImportComponentForm';
+import {
+ ApiProvider,
+ ApiRegistry,
+ DiscoveryApi,
+ errorApiRef,
+} from '@backstage/core';
+import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog';
+import { catalogImportApiRef, CatalogImportClient } from '../api';
+import { fireEvent, waitFor, screen } from '@testing-library/react';
+
+describe('', () => {
+ let apis: ApiRegistry;
+
+ const mockErrorApi: jest.Mocked = {
+ post: jest.fn(),
+ error$: jest.fn(),
+ };
+
+ beforeEach(() => {
+ apis = ApiRegistry.from([
+ [catalogApiRef, new CatalogClient({ discoveryApi: {} as DiscoveryApi })],
+ [
+ catalogImportApiRef,
+ new CatalogImportClient({
+ discoveryApi: { getBaseUrl: () => Promise.resolve('base') },
+ githubAuthApi: {
+ getAccessToken: (_, __) => Promise.resolve('token'),
+ },
+ configApi: {} as any,
+ }),
+ ],
+ [errorApiRef, mockErrorApi],
+ ]);
+ });
+
+ async function renderSUT(
+ nextStep: () => void = () => {},
+ saveConfig: () => void = () => {},
+ ) {
+ return await renderInTestApp(
+
+
+ ,
+ );
+ }
+
+ it('Renders without exploding', async () => {
+ await renderSUT();
+ expect(
+ screen.getByPlaceholderText('https://github.com/backstage/backstage'),
+ ).toBeInTheDocument();
+ });
+
+ it('Should have basic URL validation for input', async () => {
+ await renderSUT();
+ await waitFor(() => {
+ fireEvent.input(
+ screen.getByPlaceholderText('https://github.com/backstage/backstage'),
+ { target: { value: 'not a url' } },
+ );
+ });
+ await waitFor(() => {
+ fireEvent.click(screen.getByText('Next'));
+ });
+ expect(screen.getByText('Must start with https://.')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/catalog-import/src/components/ImportComponentForm.tsx b/plugins/catalog-import/src/components/ImportComponentForm.tsx
index 7434f07b10..c233be5a8c 100644
--- a/plugins/catalog-import/src/components/ImportComponentForm.tsx
+++ b/plugins/catalog-import/src/components/ImportComponentForm.tsx
@@ -64,27 +64,45 @@ export const RegisterComponentForm = ({
const isMounted = useMountedState();
const errorApi = useApi(errorApiRef);
- const { generateEntityDefinitions } = useGithubRepos();
+ const {
+ generateEntityDefinitions,
+ checkForExistingCatalogInfo,
+ } = useGithubRepos();
const onSubmit = async (formData: Record) => {
const { componentLocation: target } = formData;
- try {
- if (!isMounted()) return;
- const type = urlType(target);
+ async function saveCatalogFileConfig(target: string) {
+ const data = await catalogApi.addLocation({ target });
+ saveConfig({
+ type: 'file',
+ location: data.location.target,
+ config: data.entities,
+ });
+ }
- if (type === 'tree') {
+ async function trySaveRepositoryConfig(target: string) {
+ const existingCatalog = await checkForExistingCatalogInfo(target);
+ if (existingCatalog.exists) {
+ const targetUrl = target.endsWith('/')
+ ? `${target}${existingCatalog.url}`
+ : `${target}/${existingCatalog.url}`;
+ await saveCatalogFileConfig(targetUrl);
+ } else {
saveConfig({
- type,
+ type: 'tree',
location: target,
config: await generateEntityDefinitions(target),
});
+ }
+ }
+
+ try {
+ if (!isMounted()) return;
+ const type = urlType(target);
+ if (type === 'tree') {
+ await trySaveRepositoryConfig(target);
} else {
- const data = await catalogApi.addLocation({ target });
- saveConfig({
- type,
- location: data.location.target,
- config: data.entities,
- });
+ await saveCatalogFileConfig(target);
}
nextStep();
} catch (e) {
diff --git a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx
new file mode 100644
index 0000000000..932f9aafa0
--- /dev/null
+++ b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2021 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 React from 'react';
+import { msw, renderInTestApp } from '@backstage/test-utils';
+import { ImportComponentPage } from './ImportComponentPage';
+import {
+ ApiProvider,
+ ApiRegistry,
+ configApiRef,
+ errorApiRef,
+} from '@backstage/core';
+import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog';
+import { catalogImportApiRef, CatalogImportClient } from '../api';
+
+import { fireEvent, screen, waitFor } from '@testing-library/react';
+
+import { rest } from 'msw';
+import { setupServer } from 'msw/node';
+
+let codeSearchMockResponse: () => Promise<{
+ data: {
+ total_count: number;
+ items: Array<{ path: string }>;
+ };
+}>;
+
+let findGithubConfigMockResponse = () => ({
+ host: 'test.localhost',
+ owner: 'someuser',
+});
+
+jest.mock('@backstage/integration', () => ({
+ readGitHubIntegrationConfigs: () => ({
+ find: findGithubConfigMockResponse,
+ }),
+}));
+
+jest.mock('@octokit/rest', () => ({
+ Octokit: jest.fn().mockImplementation(() => {
+ return {
+ repos: {
+ get: () =>
+ Promise.resolve({
+ data: {
+ default_branch: 'main',
+ },
+ }),
+ },
+ search: {
+ code: codeSearchMockResponse,
+ },
+ };
+ }),
+}));
+
+const OUR_GITHUB_TEST_REPO = 'https://github.com/someuser/somerepo';
+describe('', () => {
+ const server = setupServer();
+ msw.setupDefaultHandlers(server);
+
+ beforeEach(() => {
+ server.use(
+ rest.post('https://backend.localhost/locations', (_, res, ctx) => {
+ return res(
+ ctx.status(201),
+ ctx.json(require('../mocks/locations-POST-response.json')),
+ );
+ }),
+ rest.post('https://backend.localhost/analyze-location', (_, res, ctx) => {
+ return res(
+ ctx.json(require('../mocks/analyze-location-POST-response.json')),
+ );
+ }),
+ );
+ });
+ beforeAll(() => server.listen());
+ afterEach(() => server.resetHandlers());
+ afterAll(() => server.close());
+
+ let apis: ApiRegistry;
+
+ const mockErrorApi: jest.Mocked = {
+ post: jest.fn(),
+ error$: jest.fn(),
+ };
+
+ beforeEach(() => {
+ const getBaseUrl = () => Promise.resolve('https://backend.localhost');
+ apis = ApiRegistry.from([
+ [
+ catalogApiRef,
+ new CatalogClient({
+ discoveryApi: { getBaseUrl },
+ }),
+ ],
+ [
+ catalogImportApiRef,
+ new CatalogImportClient({
+ discoveryApi: { getBaseUrl },
+ githubAuthApi: {
+ getAccessToken: (_, __) => Promise.resolve('token'),
+ },
+ configApi: {} as any,
+ }),
+ ],
+ [
+ configApiRef,
+ {
+ getOptional: () => 'Title',
+ getOptionalConfigArray: () => [],
+ has: () => true,
+ getConfig: () => ({
+ has: () => true,
+ }),
+ },
+ ],
+ [errorApiRef, mockErrorApi],
+ ]);
+ });
+
+ async function renderSUT() {
+ return await renderInTestApp(
+
+
+ ,
+ );
+ }
+
+ it('Should not explode on non-Github URLs', async () => {
+ findGithubConfigMockResponse = () => undefined!!;
+ await renderSUT();
+ await waitFor(() => {
+ fireEvent.input(
+ screen.getByPlaceholderText('https://github.com/backstage/backstage'),
+ {
+ target: {
+ value: 'https://test-git-provider.localhost/someuser/somerepo',
+ },
+ },
+ );
+ });
+
+ fireEvent.click(screen.getByText('Next'));
+ await waitFor(() => {
+ const firstStepInput = screen.queryByPlaceholderText(
+ 'https://github.com/backstage/backstage',
+ );
+ expect(firstStepInput).not.toBeInTheDocument();
+ });
+ });
+
+ it('Should offer direct file import from non-Github URLs', async () => {
+ findGithubConfigMockResponse = () => undefined!!;
+ await renderSUT();
+ await waitFor(() => {
+ fireEvent.input(
+ screen.getByPlaceholderText('https://github.com/backstage/backstage'),
+ {
+ target: {
+ value:
+ 'https://test-git-provider.localhost/someuser/somerepo/catalog-info.yaml',
+ },
+ },
+ );
+ });
+
+ fireEvent.click(screen.getByText('Next'));
+ await waitFor(() => {
+ const firstStepInput = screen.queryByPlaceholderText(
+ 'https://github.com/backstage/backstage',
+ );
+ expect(firstStepInput).not.toBeInTheDocument();
+ });
+ expect(
+ screen.getByText(
+ 'https://test-git-provider.localhost/someuser/somerepo/catalog-info.yaml',
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it('Should use found yaml file directly and not create a pull request if GitHub api returns one', async () => {
+ findGithubConfigMockResponse = () => ({
+ host: 'test.localhost',
+ owner: 'someuser',
+ });
+ codeSearchMockResponse = () =>
+ Promise.resolve({
+ data: {
+ total_count: 3,
+ items: [
+ { path: 'simple/path/catalog-info.yaml' },
+ { path: 'co/mple/x/path/catalog-info.yaml' },
+ { path: 'catalog-info.yaml' },
+ ],
+ },
+ });
+ await renderSUT();
+ await waitFor(() => {
+ fireEvent.input(
+ screen.getByPlaceholderText('https://github.com/backstage/backstage'),
+ { target: { value: OUR_GITHUB_TEST_REPO } },
+ );
+ });
+
+ fireEvent.click(screen.getByText('Next'));
+ await waitFor(() => {
+ expect(
+ screen.getByText(
+ 'https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml',
+ ),
+ ).toBeInTheDocument();
+
+ const pullReqText = screen.queryByText('pull request');
+ expect(pullReqText).not.toBeInTheDocument();
+ });
+ });
+
+ it('Should indicate a pull request creation when no yaml file found in the repo', async () => {
+ findGithubConfigMockResponse = () => ({
+ host: 'test.localhost',
+ owner: 'someuser',
+ });
+ codeSearchMockResponse = () =>
+ Promise.resolve({
+ data: {
+ total_count: 0,
+ items: [],
+ },
+ });
+ const { container } = await renderSUT();
+ await waitFor(() => {
+ fireEvent.input(
+ screen.getByPlaceholderText('https://github.com/backstage/backstage'),
+ { target: { value: OUR_GITHUB_TEST_REPO } },
+ );
+ });
+
+ fireEvent.click(screen.getByText('Next'));
+ await waitFor(() => {
+ expect(screen.getByText(OUR_GITHUB_TEST_REPO)).toBeInTheDocument();
+ });
+ const textNode = container
+ .querySelector('a[href="https://github.com/someuser/somerepo"]')
+ ?.closest('p');
+ expect(textNode?.innerHTML).toContain(
+ 'Following config object will be submitted in a pull request to the repository',
+ );
+ expect(
+ screen.queryByText(
+ 'https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml',
+ ),
+ ).not.toBeInTheDocument();
+ });
+});
diff --git a/plugins/catalog-import/src/components/ImportComponentPage.tsx b/plugins/catalog-import/src/components/ImportComponentPage.tsx
index 7e0d0ff92e..e284e95456 100644
--- a/plugins/catalog-import/src/components/ImportComponentPage.tsx
+++ b/plugins/catalog-import/src/components/ImportComponentPage.tsx
@@ -109,10 +109,11 @@ export const ImportComponentPage = ({
GitHub Repo
- If you already have code in a GitHub repository, enter the
- full URL to your repo and a new pull request with a sample
- Backstage metadata Entity File (
- catalog-info.yaml) will be opened for you.
+ If you already have code in a GitHub repository without
+ Backstage metadata file set up for it, enter the full URL to
+ your repo and a new pull request with a sample Backstage
+ metadata Entity File (catalog-info.yaml) will
+ be opened for you.
)}
diff --git a/plugins/catalog-import/src/mocks/analyze-location-POST-response.json b/plugins/catalog-import/src/mocks/analyze-location-POST-response.json
new file mode 100644
index 0000000000..eaadf1054a
--- /dev/null
+++ b/plugins/catalog-import/src/mocks/analyze-location-POST-response.json
@@ -0,0 +1,22 @@
+{
+ "existingEntityFiles": [],
+ "generateEntities": [
+ {
+ "entity": {
+ "apiVersion": "backstage.io/v1alpha1",
+ "kind": "Component",
+ "metadata": {
+ "name": "somerepo",
+ "annotations": {
+ "github.com/project-slug": "someuser/somerepo"
+ }
+ },
+ "spec": {
+ "type": "other",
+ "lifecycle": "unknown"
+ }
+ },
+ "fields": []
+ }
+ ]
+}
diff --git a/plugins/catalog-import/src/mocks/locations-POST-response.json b/plugins/catalog-import/src/mocks/locations-POST-response.json
new file mode 100644
index 0000000000..c44a0f4d61
--- /dev/null
+++ b/plugins/catalog-import/src/mocks/locations-POST-response.json
@@ -0,0 +1,39 @@
+{
+ "location": {
+ "id": "d4a64359-a709-4c91-a9de-0905a033bf22",
+ "type": "url",
+ "target": "https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml"
+ },
+ "entities": [
+ {
+ "metadata": {
+ "namespace": "default",
+ "annotations": {
+ "backstage.io/managed-by-location": "url:https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml",
+ "github.com/project-slug": "someusername/somerepo"
+ },
+ "name": "somerepo",
+ "uid": "e992d5ee-7c70-4316-90cf-325f1a0a5146",
+ "etag": "YWE2M2Q5MzgtNjdkNi00N2QwLWJkZjYtNDM0MTMzMDI4Y2I0",
+ "generation": 1
+ },
+ "apiVersion": "backstage.io/v1alpha1",
+ "kind": "Component",
+ "spec": {
+ "type": "other",
+ "lifecycle": "unknown",
+ "owner": "unknown"
+ },
+ "relations": [
+ {
+ "target": {
+ "kind": "group",
+ "namespace": "default",
+ "name": "unknown"
+ },
+ "type": "ownedBy"
+ }
+ ]
+ }
+ ]
+}
diff --git a/plugins/catalog-import/src/setupTests.ts b/plugins/catalog-import/src/setupTests.ts
index 825bcd4115..aea2220869 100644
--- a/plugins/catalog-import/src/setupTests.ts
+++ b/plugins/catalog-import/src/setupTests.ts
@@ -15,3 +15,4 @@
*/
import '@testing-library/jest-dom';
+import 'cross-fetch/polyfill';
diff --git a/plugins/catalog-import/src/util/useGithubRepos.ts b/plugins/catalog-import/src/util/useGithubRepos.ts
index 89448a1be1..a2d299b9f4 100644
--- a/plugins/catalog-import/src/util/useGithubRepos.ts
+++ b/plugins/catalog-import/src/util/useGithubRepos.ts
@@ -22,18 +22,21 @@ import parseGitUrl from 'git-url-parse';
// TODO: (O5ten) Refactor into a core API instead of direct usage like this
// https://github.com/backstage/backstage/pull/3613#issuecomment-7408929430
-import { readGitHubIntegrationConfigs } from '@backstage/integration';
+import {
+ GitHubIntegrationConfig,
+ readGitHubIntegrationConfigs,
+} from '@backstage/integration';
export function useGithubRepos() {
const api = useApi(catalogImportApiRef);
const config = useApi(configApiRef);
- const submitPrToRepo = async (selectedRepo: ConfigSpec) => {
+ const getGithubIntegrationConfig = (location: string) => {
const {
name: repoName,
owner: ownerName,
resource: hostname,
- } = parseGitUrl(selectedRepo.location);
+ } = parseGitUrl(location);
const configs = readGitHubIntegrationConfigs(
config.getOptionalConfigArray('integrations.github') ?? [],
@@ -41,9 +44,22 @@ export function useGithubRepos() {
const githubIntegrationConfig = configs.find(v => v.host === hostname);
if (!githubIntegrationConfig) {
throw new Error(
- `Unable to locate github-integration for repo-location: ${selectedRepo.location}`,
+ `Unable to locate github-integration for repo-location, ${location}`,
);
}
+ return {
+ repoName,
+ ownerName,
+ githubIntegrationConfig,
+ };
+ };
+
+ const submitPrToRepo = async (selectedRepo: ConfigSpec) => {
+ const {
+ repoName,
+ ownerName,
+ githubIntegrationConfig,
+ } = getGithubIntegrationConfig(selectedRepo.location);
const submitPRResponse = await api
.submitPrToRepo({
owner: ownerName,
@@ -54,7 +70,7 @@ export function useGithubRepos() {
githubIntegrationConfig,
})
.catch(e => {
- throw new Error(`Failed to submit PR to repo:\n${e.message}`);
+ throw new Error(`Failed to submit PR to repo, ${e.message}`);
});
await api
@@ -62,14 +78,41 @@ export function useGithubRepos() {
location: submitPRResponse.location,
})
.catch(e => {
- throw new Error(`Failed to create repository location:\n${e.message}`);
+ throw new Error(`Failed to create repository location, ${e.message}`);
});
return submitPRResponse;
};
+ const checkForExistingCatalogInfo = async (
+ location: string,
+ ): Promise<{ exists: boolean; url?: string }> => {
+ let githubConfig: {
+ repoName: string;
+ ownerName: string;
+ githubIntegrationConfig: GitHubIntegrationConfig;
+ };
+ try {
+ githubConfig = getGithubIntegrationConfig(location);
+ } catch (e) {
+ return { exists: false };
+ }
+ return await api
+ .checkForExistingCatalogInfo({
+ owner: githubConfig.ownerName,
+ repo: githubConfig.repoName,
+ githubIntegrationConfig: githubConfig.githubIntegrationConfig,
+ })
+ .catch(e => {
+ throw new Error(
+ `Failed to inspect repository for existing catalog-info.yaml, ${e.message}`,
+ );
+ });
+ };
+
return {
submitPrToRepo,
+ checkForExistingCatalogInfo,
generateEntityDefinitions: (repo: string) =>
api.generateEntityDefinitions({ repo }),
addLocation: (location: string) =>
diff --git a/plugins/jenkins/README.md b/plugins/jenkins/README.md
index 5e0a701caf..154fcf913d 100644
--- a/plugins/jenkins/README.md
+++ b/plugins/jenkins/README.md
@@ -84,6 +84,5 @@ YWRtaW46MTFlYzI1NmU0Mzg1MDFjM2Y1Yzc2Yjc1MWE3ZTQ3YWY4Mw== is the base64 of user a
## Limitations
-- Only works with projects that use the Git SCM
-- It requires jobs to be organised into folders
-- No pagination support currently - don't run this on a Jenkins with lots of builds
+- Only works with organization folder jobs backed by GitHub
+- No pagination support currently, limited to 50 jobs - don't run this on a Jenkins with lots of builds
diff --git a/plugins/kafka/src/assets/screenshot-1.png b/plugins/kafka/src/assets/screenshot-1.png
index 0afae0b5d3..f10a8918cc 100644
Binary files a/plugins/kafka/src/assets/screenshot-1.png and b/plugins/kafka/src/assets/screenshot-1.png differ
diff --git a/plugins/kubernetes/src/assets/emptystate.svg b/plugins/kubernetes/src/assets/emptystate.svg
index f01a74f374..fa7f19123e 100644
--- a/plugins/kubernetes/src/assets/emptystate.svg
+++ b/plugins/kubernetes/src/assets/emptystate.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 5b5e2a2c44..ff91348489 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -68,10 +68,10 @@
dependencies:
"@openapi-contrib/openapi-schema-to-json-schema" "^3.0.0"
-"@asyncapi/parser@^1.0.1":
- version "1.1.0"
- resolved "https://registry.npmjs.org/@asyncapi/parser/-/parser-1.1.0.tgz#b366c85a6322e182e040d60f36400a0b5c26393c"
- integrity sha512-0+NeTiW5sPNYaFf4P2VCcy7Z7MLMP7H969cgOp3gvVTKroI5idkYnWo/C16tKAxs+2oOQ9WnPolsNgc+jb/qtg==
+"@asyncapi/parser@^1.3.2":
+ version "1.3.2"
+ resolved "https://registry.npmjs.org/@asyncapi/parser/-/parser-1.3.2.tgz#6ff22680ff0e9aaf732121040e01e1f5836a305d"
+ integrity sha512-YDGdxoVA3+gQ3jvWDp+J3IV5XsvAe+nSHSytPg9m5hfJ48R+mBPMnO7Bnh//gR17LjbmyebPj27h4Ceyaa4d3Q==
dependencies:
"@apidevtools/json-schema-ref-parser" "^9.0.6"
"@asyncapi/specs" "^2.7.5"
@@ -82,6 +82,21 @@
node-fetch "^2.6.0"
tiny-merge-patch "^0.1.2"
+"@asyncapi/react-component@^0.18.2":
+ version "0.18.2"
+ resolved "https://registry.npmjs.org/@asyncapi/react-component/-/react-component-0.18.2.tgz#74882b4c2a5ecd00c156898f38d0a5c9e8c4384d"
+ integrity sha512-vsD3acSM0hTxxSsTJoU79kpI9FXVA64OqNCu8xrawfaU4wdsSQWWn0y7SD8pUfwe4gohwmms0xNcAaMzYIhGLw==
+ dependencies:
+ "@asyncapi/avro-schema-parser" "^0.2.0"
+ "@asyncapi/openapi-schema-parser" "^2.0.0"
+ "@asyncapi/parser" "^1.3.2"
+ constate "^1.2.0"
+ dompurify "^2.1.1"
+ markdown-it "^11.0.1"
+ merge "^2.1.0"
+ openapi-sampler "^1.0.0-beta.15"
+ react-use "^12.2.0"
+
"@asyncapi/specs@^2.7.5":
version "2.7.5"
resolved "https://registry.npmjs.org/@asyncapi/specs/-/specs-2.7.5.tgz#3a516d198fc41a1103695bd889fdd4fbbebe7f5d"
@@ -3770,21 +3785,6 @@
dependencies:
stream "^0.0.2"
-"@kyma-project/asyncapi-react@^0.14.2":
- version "0.14.2"
- resolved "https://registry.npmjs.org/@kyma-project/asyncapi-react/-/asyncapi-react-0.14.2.tgz#6d99ef878f0481b05db0f3be7c94f4534176675f"
- integrity sha512-lf3zcIuCTaVEIkCpF7Vwulrucgdfp1zXUIvkqjYPteOJjOuT6BAvnYwgvFUYibitmD/ADkvK8ybIeKdNJcyaPw==
- dependencies:
- "@asyncapi/avro-schema-parser" "^0.2.0"
- "@asyncapi/openapi-schema-parser" "^2.0.0"
- "@asyncapi/parser" "^1.0.1"
- constate "^1.2.0"
- dompurify "^2.1.1"
- markdown-it "^11.0.1"
- merge "^1.2.1"
- openapi-sampler "^1.0.0-beta.15"
- react-use "^12.2.0"
-
"@lerna/add@3.21.0":
version "3.21.0"
resolved "https://registry.npmjs.org/@lerna/add/-/add-3.21.0.tgz#27007bde71cc7b0a2969ab3c2f0ae41578b4577b"
@@ -18348,10 +18348,10 @@ merge2@^1.2.3, merge2@^1.3.0:
resolved "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81"
integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==
-merge@^1.2.1:
- version "1.2.1"
- resolved "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
- integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
+merge@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/merge/-/merge-2.1.0.tgz#91fff62458ba2eca378dd395fa85f1690bf87f60"
+ integrity sha512-TcuhVDV+e6X457MQAm7xIb19rWhZuEDEho7RrwxMpQ/3GhD5sDlnP188gjQQuweXHy9igdke5oUtVOXX1X8Sxg==
methods@^1.0.0, methods@^1.1.1, methods@^1.1.2, methods@~1.1.2:
version "1.1.2"