Added support for Confluence Cloud

Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com>
This commit is contained in:
Andre Wanlin
2023-04-20 10:10:26 -05:00
parent 0eea1e0304
commit c59a4b2b9e
8 changed files with 129 additions and 14 deletions
@@ -4,16 +4,18 @@ Welcome to the `confluence:transform:markdown` action for the `scaffolder-backen
## Getting started
You need to configure the action in your backend:
The following sections will help you getting started
## From your Backstage root directory
### Configure Action in Backend
From your Backstage root directory run:
```bash
# From your Backstage root directory
yarn add --cwd packages/backend @backstage/plugin-scaffolder-backend-module-confluence-to-markdown
```
Configure the action:
Then configure the action:
(you can check the [docs](https://backstage.io/docs/features/software-templates/writing-custom-actions#registering-custom-actions) to see all options):
```typescript
@@ -56,6 +58,8 @@ export default async function createPlugin(
}
```
### Configuration
You will also need an access token for authorization with `Read` permissions. You can create a Personal Access Token (PAT) in confluence and add the PAT to your `app-config.yaml`
```yaml
@@ -64,7 +68,34 @@ confluence:
token: ${CONFLUENCE_TOKEN}
```
After that you can use the action in your template:
#### Confluence Cloud
For those using Confluence Cloud you will need to have the following configuration:
```yaml
confluence:
baseUrl: ${CONFLUENCE_BASE_URL}
token: ${CONFLUENCE_TOKEN}
isCloud: true
```
##### baseUrl
The `baseUrl` for Confluence Cloud should include the product name which is `wiki` by default but can be something else if your Org has changed it. An example `baseUrl` for Confluence Cloud would look like this: `https://example.atlassian.net/wiki`
##### token
The `token` for Confluence Cloud needs to be base-64 encoded with your Atlassian account email address. Here's how to do that:
1. First get your token from: `https://<company-name>.atlassian.com/manage-profile/security/api-tokens`
2. Next we need to setup a string in this format: `<your-atlassian-account-mail>:<your-jira-token>`
3. For this example we'll use this: `confluence@backstage.io:wDzAzoXWRGLtvbgHvT0W`
4. Now we can run `echo -n "confluence@backstage.io:wDzAzoXWRGLtvbgHvT0W" | base64`
5. This gives us: `Y29uZmx1ZW5jZUBiYWNrc3RhZ2UuaW86d0R6QXpvWFdSR0x0dmJnSHZUMFc=` which we can now use as the value for the `token` in the configuration
### Template Usage
Here's an example of how you can use the action in your template:
```yaml
apiVersion: scaffolder.backstage.io/v1beta3
@@ -84,7 +115,7 @@ spec:
properties:
confluenceUrls:
type: array
description: Urls for confluence doc to be converted to markdown. In format <CONFLUENCE_BASE_URL>/display/<SPACEKEY>/<PAGE+TITLE>
description: Urls for confluence doc to be converted to markdown. In format <CONFLUENCE_BASE_URL>/display/<SPACEKEY>/<PAGE+TITLE> or <CONFLUENCE_BASE_URL>/spaces/<SPACEKEY>/pages/<PAGEID>/<PAGE+TITLE> for Confluence cloud
items:
type: string
default: confluence url
@@ -57,7 +57,7 @@ export const createConfluenceToMarkdownAction = (options: {
type: 'array',
title: 'Confluence URL',
description:
'Paste your confluence url. Ensure it follows this format: https://{confluence+base+url}/display/{spacekey}/{page+title}',
'Paste your confluence url. Ensure it follows this format: https://{confluence+base+url}/display/{spacekey}/{page+title} or https://{confluence+base+url}/spaces/{spacekey}/pages/1234567/{page+title} for Confluence Cloud',
items: {
type: 'string',
default: 'Confluence URL',
@@ -98,6 +98,7 @@ export const createConfluenceToMarkdownAction = (options: {
const { spacekey, title, titleWithSpaces } =
await createConfluenceVariables(url);
// This calls confluence to get the page html and page id
ctx.logger.info(`Fetching the Confluence content for ${url}`);
const getConfluenceDoc = await fetchConfluence(
`/rest/api/content?title=${title}&spaceKey=${spacekey}&expand=body.export_view`,
config,
@@ -0,0 +1,40 @@
/*
* Copyright 2023 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 { createConfluenceVariables } from './helpers';
describe('createConfluenceVariables', () => {
it('should return values for Confluence Url', async () => {
const url = 'https://confluence.example.com/display/SPACEKEY/Page+Title';
const { spacekey, title, titleWithSpaces } =
await createConfluenceVariables(url);
expect(spacekey).toEqual('SPACEKEY');
expect(title).toEqual('Page+Title');
expect(titleWithSpaces).toEqual('Page Title');
});
it('should return values for Confluence Cloud Url', async () => {
const url =
'https://example.atlassian.net/wiki/spaces/CLOUDSPACEKEY/pages/1234567/Cloud+Page+Title';
const { spacekey, title, titleWithSpaces } =
await createConfluenceVariables(url);
expect(spacekey).toEqual('CLOUDSPACEKEY');
expect(title).toEqual('Cloud+Page+Title');
expect(titleWithSpaces).toEqual('Cloud Page Title');
});
});
@@ -51,10 +51,13 @@ export const readFileAsString = async (fileDir: string) => {
export const fetchConfluence = async (relativeUrl: string, config: Config) => {
const baseUrl = config.getString('confluence.baseUrl');
const token = config.getString('confluence.token');
const response: Response = await fetch(`${baseUrl}${relativeUrl}`, {
const isCloud = config.getOptionalBoolean('confluence.isCloud') || false;
const authToken = isCloud ? `Basic ${token}` : `Bearer ${token}`;
const url = `${baseUrl}${relativeUrl}`;
const response: Response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
Authorization: authToken,
},
});
if (!response.ok) {
@@ -73,6 +76,8 @@ export const getAndWriteAttachments = async (
const productArr: string[][] = [];
const baseUrl = config.getString('confluence.baseUrl');
const token = config.getString('confluence.token');
const isCloud = config.getOptionalBoolean('confluence.isCloud') || false;
const authToken = isCloud ? `Basic ${token}` : `Bearer ${token}`;
await Promise.all(
await arr.results.map(async (result: Result) => {
const downloadLink = result._links.download;
@@ -80,11 +85,11 @@ export const getAndWriteAttachments = async (
if (result.metadata.mediaType !== 'application/gliffy+json') {
productArr.push([result.title.replace(/ /g, '%20'), downloadTitle]);
}
const res = await fetch(`${baseUrl}${downloadLink}`, {
const url = `${baseUrl}${downloadLink}`;
const res = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
Authorization: authToken,
},
});
if (!res.ok) {
@@ -117,12 +122,19 @@ export const createConfluenceVariables = async (url: string) => {
let titleWithSpaces: string | undefined = '';
const params = new URL(url);
if (params.pathname.split('/')[1] === 'display') {
// https://confluence.example.com/display/SPACEKEY/Page+Title
spacekey = params.pathname.split('/')[2];
title = params.pathname.split('/')[3];
titleWithSpaces = title?.replace(/\+/g, ' ');
return { spacekey, title, titleWithSpaces };
} else if (params.pathname.split('/')[2] === 'spaces') {
// https://example.atlassian.net/wiki/spaces/SPACEKEY/pages/1234567/Page+Title
spacekey = params.pathname.split('/')[3];
title = params.pathname.split('/')[6];
titleWithSpaces = title?.replace(/\+/g, ' ');
return { spacekey, title, titleWithSpaces };
}
throw new InputError(
'The Url format for Confluence is incorrect. Acceptable format is `<CONFLUENCE_BASE_URL>/display/<SPACEKEY>/<PAGE+TITLE>`',
'The Url format for Confluence is incorrect. Acceptable format is `<CONFLUENCE_BASE_URL>/display/<SPACEKEY>/<PAGE+TITLE>` or `<CONFLUENCE_BASE_URL>/spaces/<SPACEKEY>/pages/<PAGEID>/<PAGE+TITLE>` for Confluence cloud',
);
};