Skip to content

Commit 83ac8bd

Browse files
authored
Merge pull request #37 from atlassian-labs/add-new-relic-script
Add New Relic import script
2 parents cb35f78 + 52fb0e4 commit 83ac8bd

5 files changed

Lines changed: 772 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
.idea/
3+
.vscode/
4+
.env
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
## Bootstrap your Compass Catalog from a New Relic Account
2+
3+
This script
4+
5+
1. Gets all the apps within your New Relic account
6+
2. Creates Compass Components for each of them
7+
8+
## Install
9+
10+
You'll need node and npm to run this script
11+
12+
```
13+
git clone https://github.com/atlassian-labs/compass-examples
14+
cd snippets/scripts/compass-new-relic-importer
15+
npm install
16+
```
17+
18+
## Credentials
19+
20+
Create a `.env` file and fill in the blanks. This file should be ignored by git already since it is in the `.gitignore`
21+
22+
```
23+
# Replace these with your NewRelic instance URL and API token
24+
NEW_RELIC_URL='<new relic GraphQL url: https://api.newrelic.com/graphql or https://api.eu.newrelic.com/graphql>'
25+
NEW_RELIC_API_TOKEN='<an API token for NewRelic>'
26+
USER_EMAIL='<atlassian email>'
27+
# https://id.atlassian.com/manage-profile/security/api-tokens
28+
TOKEN='<atlassian token>'
29+
# Add your subdomain here - find it from the url - e.g. https://<southwest>.atlassian.net
30+
TENANT_SUBDOMAIN='<subdomain>'
31+
# The UUID for your cloud site. This can be found in ARIs - look at the first uuid ari:cloud:compass:{cloud-uuid}
32+
CLOUD_ID='<cloud uuid>'
33+
```
34+
35+
## Do a dry run
36+
37+
Preview what App the script will add to Compass. You don't need to wait until it's done - CTRL-C once you are comfortable.
38+
39+
```
40+
DRY_RUN=1 node index.js
41+
```
42+
43+
Output:
44+
45+
```
46+
New component with external alias for https://one.newrelic.com/redirect/entity/<entity-id> ... added test-app(dry-run)
47+
New component with external alias for https://one.newrelic.com/redirect/entity/<entity-id> ... added test-app-1(dry-run)
48+
....
49+
50+
```
51+
52+
By default, all apps will be added. If you notice any repositories that you don't want to import during the dry run, you can modify `index.js` to add filters accordingly.
53+
54+
```javascript
55+
for (const app of apps) {
56+
const { entityType, guid, name, permalink, tags } = app;
57+
58+
const convertedLabels = newRelicTagsToCompassLabels(tags);
59+
const labelNames = [
60+
"source:newrelic",
61+
`${formatTag("entitytype", entityType)}`,
62+
...convertedLabels,
63+
];
64+
65+
await putComponent(
66+
name,
67+
app.description || "",
68+
permalink,
69+
"new-relic",
70+
guid,
71+
JSON.stringify(labelNames)
72+
);
73+
}
74+
```
75+
76+
## Ready to import?
77+
78+
Remove `DRY_RUN=1` to run the CLI in write mode.
79+
80+
```
81+
node index.js
82+
```
83+
84+
If your connection is interrupted, or you want to re-run after the initial import you can. It checks for duplicates and skips them:
85+
86+
```
87+
Already added https://one.newrelic.com/redirect/entity/<entity-id> ... skipping
88+
```
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
const chalk = require("chalk");
2+
require("dotenv").config();
3+
4+
const {
5+
NEW_RELIC_URL,
6+
NEW_RELIC_API_TOKEN,
7+
USER_EMAIL,
8+
TOKEN,
9+
TENANT_SUBDOMAIN,
10+
CLOUD_ID,
11+
} = process.env;
12+
const dryRun = process.env.DRY_RUN === "1";
13+
14+
const ATLASSIAN_GRAPHQL_URL = `https://${TENANT_SUBDOMAIN}.atlassian.net/gateway/api/graphql`;
15+
16+
function makeGqlRequest(query) {
17+
const header = btoa(`${USER_EMAIL}:${TOKEN}`);
18+
return fetch(ATLASSIAN_GRAPHQL_URL, {
19+
method: "POST",
20+
headers: {
21+
Authorization: `Basic ${header}`,
22+
Accept: "application/json",
23+
"Content-Type": "application/json",
24+
},
25+
body: JSON.stringify(query),
26+
}).then((res) => res.json());
27+
}
28+
29+
function makeNewRelicGqlRequest(query) {
30+
return fetch(NEW_RELIC_URL, {
31+
method: "POST",
32+
headers: {
33+
"Api-Key": NEW_RELIC_API_TOKEN,
34+
"Content-type": "application/json",
35+
},
36+
body: JSON.stringify(query),
37+
}).then((res) => res.json());
38+
}
39+
40+
const formatTag = (key, value) => {
41+
const k = key.toLowerCase().trim().replace(/\s+/, "-");
42+
const v = value.toLowerCase().trim().replace(/\s+/, "-");
43+
return `${k}:${v}`;
44+
};
45+
46+
const newRelicTagsToCompassLabels = (tags) => {
47+
const results = [];
48+
if (!tags || tags.length === 0) {
49+
return results;
50+
}
51+
// Tags need to be lowercase, contain no spaces, for API.
52+
tags
53+
.filter((tag) => tag.key && tag.values && tag.key.trim().length > 0)
54+
.filter((tag) => !tag.key.toLowerCase().includes("account"))
55+
.filter((tag) => !(tag.key.toLowerCase() === "guid"))
56+
.forEach((tag) => {
57+
tag.values.forEach((v) => {
58+
if (v && v.trim().length > 0) {
59+
results.push(formatTag(tag.key, v));
60+
}
61+
});
62+
});
63+
64+
// API requires labels to be shorter than 40 characters.
65+
return results.filter((t) => t.length < 40);
66+
};
67+
68+
// This function looks for a component with a certain name, but if it can't find it, it will create a component.
69+
async function putComponent(
70+
componentName,
71+
description,
72+
url,
73+
externalSource,
74+
externalId,
75+
labels
76+
) {
77+
const response = await makeGqlRequest({
78+
query: `
79+
query componentByExternalAlias {
80+
compass {
81+
componentByExternalAlias(cloudId: "${CLOUD_ID}", externalSource: "${externalSource}", externalID: "${externalId}") {
82+
... on CompassComponent {
83+
id
84+
name
85+
}
86+
... on QueryError {
87+
message
88+
extensions {
89+
statusCode
90+
errorType
91+
}
92+
}
93+
}
94+
}
95+
}
96+
`,
97+
});
98+
99+
const maybeResults = response?.data?.compass?.componentByExternalAlias;
100+
101+
if (
102+
maybeResults?.message &&
103+
maybeResults?.message !== "Component not found"
104+
) {
105+
console.error(
106+
`error fetching component for app ${url} : `,
107+
JSON.stringify(response)
108+
);
109+
throw new Error("Error fetching component");
110+
}
111+
112+
const maybeComponentAri = maybeResults?.id;
113+
114+
if (maybeComponentAri) {
115+
console.log(chalk.gray(`Already added ${url} ... skipping`));
116+
return maybeComponentAri;
117+
} else {
118+
if (!dryRun) {
119+
const response = await makeGqlRequest({
120+
query: `
121+
mutation createComponent {
122+
compass @optIn(to: "compass-beta") {
123+
createComponent(cloudId: "${CLOUD_ID}", input: {name: "${componentName}", description: "${description}" typeId: "SERVICE", labels: ${labels}, links: [{type: DASHBOARD, name: "New Relic", url: "${url}"}]}) {
124+
success
125+
componentDetails {
126+
id
127+
}
128+
}
129+
}
130+
}`,
131+
});
132+
133+
const maybeAri =
134+
response?.data.compass?.createComponent?.componentDetails?.id;
135+
const isSuccess = !!response?.data.compass?.createComponent?.success;
136+
if (!isSuccess || !maybeAri) {
137+
console.error(`error creating component: `, JSON.stringify(response));
138+
throw new Error("Could not create component");
139+
}
140+
141+
const componentDetails =
142+
response?.data.compass?.createComponent.componentDetails;
143+
144+
const createExternalAliasResponse = await makeGqlRequest({
145+
query: `
146+
mutation createCompassComponentExternalAlias {
147+
compass @optIn(to: "compass-beta") {
148+
createComponentExternalAlias(input: {componentId: "${componentDetails.id}", externalAlias: {externalSource:"${externalSource}", externalId:"${externalId}"}}) {
149+
success
150+
errors {
151+
message
152+
}
153+
}
154+
}
155+
}`,
156+
});
157+
158+
const isCreateExternalAliasSuccess =
159+
createExternalAliasResponse?.data.compass?.createComponentExternalAlias
160+
?.success;
161+
if (!isCreateExternalAliasSuccess) {
162+
console.error(
163+
`error creating component's external alias: `,
164+
JSON.stringify(createExternalAliasResponse)
165+
);
166+
throw new Error("Could not create component's external alias");
167+
}
168+
169+
console.log(
170+
chalk.green(
171+
`New component with external alias for ${url} ... added ${componentName}`
172+
)
173+
);
174+
175+
return maybeAri;
176+
} else {
177+
console.log(
178+
chalk.yellow(
179+
`New component for ${url} ... would be added ${componentName} (dry-run)`
180+
)
181+
);
182+
return;
183+
}
184+
}
185+
}
186+
187+
async function listAllApps() {
188+
const apps = [];
189+
190+
const response = await makeNewRelicGqlRequest({
191+
query: `
192+
{
193+
actor {
194+
entitySearch(queryBuilder: {type: APPLICATION}) {
195+
count
196+
results {
197+
nextCursor
198+
entities {
199+
name
200+
entityType
201+
guid
202+
permalink
203+
... on ApmApplicationEntityOutline {
204+
language
205+
}
206+
tags {
207+
key
208+
values
209+
}
210+
}
211+
}
212+
}
213+
}
214+
}`,
215+
});
216+
217+
let cursor = response?.data?.actor?.entitySearch?.results?.nextCursor;
218+
const entities = response?.data?.actor?.entitySearch?.results?.entities;
219+
apps.push(...entities);
220+
221+
while (cursor) {
222+
const response = await makeNewRelicGqlRequest({
223+
query: `
224+
{
225+
actor {
226+
entitySearch(queryBuilder: {type: APPLICATION}) {
227+
count
228+
results (cursor: "${cursor}) {
229+
nextCursor
230+
entities {
231+
name
232+
entityType
233+
guid
234+
permalink
235+
... on ApmApplicationEntityOutline {
236+
language
237+
}
238+
tags {
239+
key
240+
values
241+
}
242+
}
243+
}
244+
}
245+
}
246+
}`,
247+
});
248+
249+
const entites = response?.data?.actor?.entitySearch?.results?.entities;
250+
apps.push(...entites);
251+
cursor = response?.data?.actor?.entitySearch?.results?.nextCursor;
252+
}
253+
254+
return apps;
255+
}
256+
257+
async function importAllApps() {
258+
const apps = await listAllApps();
259+
260+
try {
261+
for (const app of apps) {
262+
const { entityType, guid, name, permalink, tags } = app;
263+
264+
const convertedLabels = newRelicTagsToCompassLabels(tags);
265+
const labelNames = [
266+
"source:newrelic",
267+
`${formatTag("entitytype", entityType)}`,
268+
...convertedLabels,
269+
];
270+
271+
await putComponent(
272+
name,
273+
app.description || "",
274+
permalink,
275+
"new-relic",
276+
guid,
277+
JSON.stringify(labelNames)
278+
);
279+
}
280+
} catch (e) {
281+
console.error(`Error importing apps`, e);
282+
}
283+
}
284+
285+
importAllApps();

0 commit comments

Comments
 (0)