Created
October 28, 2021 12:07
-
-
Save igrek8/40a2968551799b1d3260308a0653dfcf to your computer and use it in GitHub Desktop.
Migrate to Self Hosted Redash
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable no-param-reassign */ | |
const fetch = require("node-fetch"); | |
const _ = require("lodash"); | |
const fs = require("fs"); | |
const { format } = require("util"); | |
const SOURCE_REDASH = "https://app.redash.io/domain"; | |
const SOURCE_REDASH_API_KEY = "XXXXXXXXXXXXXXXXXXXXXXXXXXX"; | |
const TARGET_REDASH = "https://redash.domain.io"; | |
const TARGET_REDASH_API_KEY = "XXXXXXXXXXXXXXXXXXXXXXXXXXX"; | |
function log(level, fmt, ...values) { | |
return console[level](format(fmt, ...values)); | |
} | |
async function req(method, url, key, body) { | |
const res = await fetch(url, { | |
method, | |
headers: { Authorization: key }, | |
body: body ? JSON.stringify(body) : undefined, | |
}); | |
if (res.status !== 200) { | |
throw new Error(await res.text()); | |
} | |
return res.json(); | |
} | |
let cache = { | |
source: { | |
users: {}, | |
queries: {}, | |
dashboards: {}, | |
usersQueries: {}, | |
usersDashboards: {}, | |
dataSources: {}, | |
visualizations: {}, | |
}, | |
target: { | |
users: {}, | |
queries: {}, | |
dashboards: {}, | |
usersQueries: {}, | |
usersDashboards: {}, | |
dataSources: {}, | |
visualizations: {}, | |
}, | |
migration: { | |
users: {}, | |
queries: {}, | |
dashboards: {}, | |
dataSources: {}, | |
visualizations: {}, | |
}, | |
}; | |
try { | |
cache = JSON.parse(fs.readFileSync("cache.json", { encoding: "utf-8" })); | |
} catch (err) { | |
log("warn", err.message); | |
} | |
function onShutdown() { | |
log("debug", "shutdown..."); | |
fs.writeFileSync("cache.json", JSON.stringify(cache)); | |
} | |
async function pullData({ baseUrl, apiKey, store }) { | |
log("debug", `pulling data sources from ${baseUrl}`); | |
const dataSources = await req("GET", `${baseUrl}/api/data_sources`, apiKey); | |
for (const { id } of dataSources) { | |
if (id in store.dataSources) { | |
log("debug", "skip pulling data source %s from %s", id, baseUrl); | |
continue; | |
} | |
log("debug", "pulling data source %s from %s", id, baseUrl); | |
const dataSource = await req("GET", `${baseUrl}/api/data_sources/${id}`, apiKey); | |
store.dataSources[id] = dataSource; | |
} | |
log("debug", `pulling active users from ${baseUrl}`); | |
const { results: activeUsers } = await req( | |
"GET", | |
`${baseUrl}/api/users?page_size=250&order=created_at`, | |
apiKey | |
); | |
for (const user of activeUsers) { | |
if (user.id in store.users) { | |
log("debug", "skiping user %s", user.id); | |
continue; | |
} | |
log("debug", "pulling user's data %s from %s", user.id, baseUrl); | |
store.users[user.id] = await req("GET", `${baseUrl}/api/users/${user.id}`, apiKey); | |
} | |
log("debug", "pulling disabled users from %s", baseUrl); | |
const { results: disabledUsers } = await req( | |
"GET", | |
`${baseUrl}/api/users?page_size=250&order=created_at&disabled=true`, | |
apiKey | |
); | |
for (const user of disabledUsers) { | |
if (user.id in store.users) { | |
log("debug", "skiping user %s", user.id); | |
continue; | |
} | |
log("debug", "pulling user's data %s from %s", user.id, baseUrl); | |
store.users[user.id] = await req("GET", `${baseUrl}/api/users/${user.id}`, apiKey); | |
} | |
for (const user of Object.values(store.users)) { | |
if (user.id in store.usersQueries) { | |
log("debug", "skip user's queries %s from %s", user.id, baseUrl); | |
continue; | |
} | |
if (user.is_disabled) { | |
log("debug", "skip user's queries %s from %s", user.id, baseUrl); | |
continue; | |
} | |
log("debug", "pulling user's queries %s from %s", user.id, baseUrl); | |
const { results: queries } = await req( | |
"GET", | |
`${baseUrl}/api/queries?page_size=250&order=created_at`, | |
user.api_key | |
); | |
for (const query of queries) { | |
if (query.id in store.queries) { | |
log("debug", "skip query %s from %s", query.id, baseUrl); | |
continue; | |
} | |
log("debug", "pulling query data %s from %s", query.id, baseUrl); | |
store.queries[query.id] = await req( | |
"GET", | |
`${baseUrl}/api/queries/${query.id}`, | |
user.api_key | |
); | |
} | |
store.usersQueries[user.id] = queries; | |
} | |
for (const user of Object.values(store.users)) { | |
if (user.id in store.usersDashboards) { | |
log("debug", "skip pulling user's dashboards %s from %s", user.id, baseUrl); | |
continue; | |
} | |
if (user.is_disabled) { | |
log("debug", "skip pulling user's dashboards %s from %s", user.id, baseUrl); | |
continue; | |
} | |
log("debug", "pulling user's dashboards %s from %s", user.id, baseUrl); | |
const { results: dashboards } = await req( | |
"GET", | |
`${baseUrl}/api/dashboards?page_size=250&order=created_at`, | |
user.api_key | |
); | |
for (const dashboard of dashboards) { | |
if (dashboard.id in store.dashboards) { | |
log("debug", "skip pulling dashboard's data %s from %s", dashboard.id, baseUrl); | |
continue; | |
} | |
log("debug", "pulling dashboard's data %s from %s", dashboard.id, baseUrl); | |
store.dashboards[dashboard.id] = await req( | |
"GET", | |
`${baseUrl}/api/dashboards/${dashboard.slug}`, | |
user.api_key | |
); | |
} | |
store.usersDashboards[user.id] = dashboards; | |
} | |
return store; | |
} | |
(async () => { | |
try { | |
await pullData({ | |
baseUrl: SOURCE_REDASH, | |
apiKey: SOURCE_REDASH_API_KEY, | |
store: cache.source, | |
}); | |
await pullData({ | |
baseUrl: TARGET_REDASH, | |
apiKey: TARGET_REDASH_API_KEY, | |
store: cache.target, | |
}); | |
// Migrate data from source to target | |
for (const sUser of Object.values(cache.source.users)) { | |
if (sUser.id in cache.migration.users) { | |
log("debug", "skip migrating user %s from %s", sUser.id, TARGET_REDASH); | |
continue; | |
} | |
log("debug", "migrating user %s to %s", sUser.id, TARGET_REDASH); | |
const tUser = await req("POST", `${TARGET_REDASH}/api/users`, TARGET_REDASH_API_KEY, { | |
...sUser, | |
id: undefined, | |
api_key: sUser.api_key, | |
groups: [], | |
}); | |
cache.target.users[tUser.id] = await req( | |
"GET", | |
`${TARGET_REDASH}/api/users/${tUser.id}`, | |
TARGET_REDASH_API_KEY | |
); | |
cache.migration.users[sUser.id] = tUser.id; | |
} | |
for (const sDataSource of Object.values(cache.source.dataSources)) { | |
if (sDataSource.id in cache.migration.dataSources) { | |
log("debug", "skip migrating data source %s from %s", sDataSource.id, SOURCE_REDASH); | |
continue; | |
} | |
log("debug", "migrating data source %s from %s", sDataSource.id, SOURCE_REDASH); | |
const tDataSource = await req( | |
"POST", | |
`${TARGET_REDASH}/api/data_sources`, | |
TARGET_REDASH_API_KEY, | |
{ | |
...sDataSource, | |
id: undefined, | |
groups: [], | |
} | |
); | |
cache.target.dataSources[tDataSource.id] = tDataSource; | |
cache.migration.dataSources[sDataSource.id] = tDataSource.id; | |
} | |
for (const sQuery of Object.values(cache.source.queries)) { | |
try { | |
if (sQuery.id in cache.migration.queries) { | |
continue; | |
} | |
const tUserId = cache.migration.users[sQuery.user.id]; | |
if (!tUserId) { | |
throw new Error(`Couldn't find ${sQuery.user.id} in cache.migration.users`); | |
} | |
const tUser = cache.target.users[tUserId]; | |
if (!tUser) { | |
throw new Error(`Couldn't find ${tUserId} in cache.target.users`); | |
} | |
const tDataSourceId = cache.migration.dataSources[sQuery.data_source_id]; | |
if (!tDataSourceId) { | |
throw new Error(`Couldn't find ${sQuery.data_source_id} in cache.migration.dataSources`); | |
} | |
log("debug", "migrating query %s from %s", sQuery.id, SOURCE_REDASH); | |
const tQuery = await req("POST", `${TARGET_REDASH}/api/queries`, tUser.api_key, { | |
...sQuery, | |
data_source_id: tDataSourceId, | |
last_modified_by: tUser, | |
user: tUser, | |
id: undefined, | |
is_favorite: undefined, | |
can_edit: undefined, | |
api_key: undefined, | |
latest_query_data_id: undefined, | |
is_safe: undefined, | |
visualizations: undefined, | |
}); | |
const [defaultVisualization] = tQuery.visualizations; | |
await req( | |
"DELETE", | |
`${TARGET_REDASH}/api/visualizations/${defaultVisualization.id}`, | |
tUser.api_key | |
); | |
for (const sVisualization of sQuery.visualizations) { | |
if (sVisualization.id in cache.migration.visualizations) { | |
continue; | |
} | |
const tVisualization = await req( | |
"POST", | |
`${TARGET_REDASH}/api/visualizations`, | |
tUser.api_key, | |
{ | |
...sVisualization, | |
id: undefined, | |
query_id: tQuery.id, | |
} | |
); | |
cache.target.visualizations[tVisualization.id] = tVisualization; | |
cache.migration.visualizations[sVisualization.id] = tVisualization.id; | |
} | |
if (sQuery.is_draft === false) { | |
await req("POST", `${TARGET_REDASH}/api/queries/${tQuery.id}`, tUser.api_key, { | |
id: tQuery.id, | |
is_draft: false, | |
version: 1, | |
}); | |
} | |
cache.target.queries[tQuery.id] = tQuery; | |
cache.migration.queries[sQuery.id] = tQuery.id; | |
} catch (err) { | |
log("error", err.message); | |
} | |
} | |
for (const sDashboard of Object.values(cache.source.dashboards)) { | |
if (sDashboard.id in cache.migration.dashboards) { | |
log("debug", "skip migrating dashboard %s from %s", sDashboard.id, TARGET_REDASH); | |
continue; | |
} | |
const tUserId = cache.migration.users[sDashboard.user.id]; | |
if (!tUserId) { | |
throw new Error(`Couldn't find ${sDashboard.user.id} in cache.migration.users`); | |
} | |
const tUser = cache.target.users[tUserId]; | |
if (!tUser) { | |
throw new Error(`Couldn't find ${tUserId} in cache.target.users`); | |
} | |
log("debug", "migrating dashboard %s to %s", sDashboard.id, TARGET_REDASH); | |
const tDashboard = await req("POST", `${TARGET_REDASH}/api/dashboards`, tUser.api_key, { | |
...sDashboard, | |
id: undefined, | |
user: tUser, | |
user_id: tUser.id, | |
widgets: [], | |
is_favorite: undefined, | |
is_safe: undefined, | |
can_edit: undefined, | |
}); | |
for (const sWidget of sDashboard.widgets) { | |
const tVisualizationId = cache.migration.visualizations[sWidget.visualization.id]; | |
if (!tVisualizationId) { | |
throw new Error( | |
`Failed to find ${sWidget.visualization.id} in cache.migration.visualizations` | |
); | |
} | |
await req("POST", `${TARGET_REDASH}/api/widgets`, tUser.api_key, { | |
...sWidget, | |
id: undefined, | |
dashboard_id: tDashboard.id, | |
visualization_id: tVisualizationId, | |
}); | |
} | |
if (sDashboard.is_draft === false) { | |
await req("POST", `${TARGET_REDASH}/api/dashboards/${tDashboard.id}`, tUser.api_key, { | |
is_draft: false, | |
name: tDashboard.name, | |
slug: tDashboard.id, | |
}); | |
} | |
cache.target.dashboards[tDashboard.id] = tDashboard; | |
cache.migration.dashboards[sDashboard.id] = tDashboard.id; | |
} | |
for (const sUser of Object.values(cache.source.users)) { | |
const tUserId = cache.migration.users[sUser.id]; | |
if (!tUserId) { | |
throw new Error(`Failed to find ${sUser.id} in cache.migration.users`); | |
} | |
const tUser = cache.target.users[tUserId]; | |
if (!tUser) { | |
throw new Error(`Failed to find ${tUserId} in cache.target.users`); | |
} | |
if (sUser.is_disabled) { | |
await req("POST", `${TARGET_REDASH}/api/users/${tUser.id}/disable`, TARGET_REDASH_API_KEY); | |
} | |
} | |
} finally { | |
onShutdown(); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment