inline.js (353 lines of code) (raw):

const crypto = require("crypto"); const fs = require("fs"); const axios = require("axios"); const baseUrl = process.env.KIBANA || "http://elastic:changeme@localhost:5601"; const folderPath = process.argv[2]; function cleanupAttributes(attributes) { if ( attributes.kibanaSavedObjectMeta?.searchSourceJSON && typeof attributes.kibanaSavedObjectMeta.searchSourceJSON !== "string" ) { attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify( attributes.kibanaSavedObjectMeta.searchSourceJSON ); } if (attributes.visState && typeof attributes.visState !== "string") { attributes.visState = JSON.stringify(attributes.visState); } if (attributes.uiStateJSON && typeof attributes.uiStateJSON !== "string") { attributes.uiStateJSON = JSON.stringify(attributes.uiStateJSON); } if (attributes.panelsJSON && typeof attributes.panelsJSON !== "string") { attributes.panelsJSON = JSON.stringify(attributes.panelsJSON); } if (attributes.optionsJSON && typeof attributes.optionsJSON !== "string") { attributes.optionsJSON = JSON.stringify(attributes.optionsJSON); } if (attributes.mapStateJSON && typeof attributes.mapStateJSON !== "string") { attributes.mapStateJSON = JSON.stringify(attributes.mapStateJSON); } if ( attributes.layerListJSON && typeof attributes.layerListJSON !== "string" ) { attributes.layerListJSON = JSON.stringify(attributes.layerListJSON); } return attributes; } function rehydrateAttributes(attributes) { if ( attributes.kibanaSavedObjectMeta?.searchSourceJSON && typeof attributes.kibanaSavedObjectMeta.searchSourceJSON === "string" ) { attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.parse( attributes.kibanaSavedObjectMeta.searchSourceJSON ); } if (attributes.panelsJSON && typeof attributes.panelsJSON === "string") { attributes.panelsJSON = JSON.parse(attributes.panelsJSON); } if (attributes.optionsJSON && typeof attributes.optionsJSON === "string") { attributes.optionsJSON = JSON.parse(attributes.optionsJSON); } return attributes; } (async function () { const migratedVisualizations = await migrateSavedObjects("visualization"); const migratedLens = await migrateSavedObjects("lens"); const migratedMap = await migrateSavedObjects("map"); const migratedSearch = await migrateSavedObjects("search"); const dashboardPath = `${folderPath}/dashboard`; const dExists = fs.existsSync(dashboardPath); if (!dExists) throw new Error("no dashboard folder found"); const dashboardPaths = fs.readdirSync(dashboardPath); const dashboards = dashboardPaths.map((d) => JSON.parse(fs.readFileSync(`${dashboardPath}/${d}`, { encoding: "utf8" })) ); const response3 = await axios.post( `${baseUrl}/api/saved_objects/_bulk_create?overwrite=true`, dashboards.map( ({ type, id, attributes, references, migrationVersion }) => ({ type, id, attributes: cleanupAttributes(attributes), references, migrationVersion, }) ), { headers: { "kbn-xsrf": "abc", }, } ); if (response3.data.saved_objects.some((s) => s.error)) { throw new Error("error loading dashboards"); } const response4 = await axios.post( `${baseUrl}/api/saved_objects/_bulk_get`, dashboards.map((d) => ({ type: "dashboard", id: d.id })), { headers: { "kbn-xsrf": "abc", }, } ); let counter = { visualization: new Set(), lens: new Set(), map: new Set(), search: new Set(), }; const inlinedDashboards = response4.data.saved_objects.map((d) => { console.log(`Processing dashboard ${d.attributes.title}`); const attributes = d.attributes; const references = d.references; const panels = JSON.parse(attributes.panelsJSON); panels.forEach((p) => { const ref = references.find( (r) => r.name === `${p.panelIndex}:panel_${p.panelIndex}` ) || references.find((r) => r.name === `${p.panelRefName}`); if (ref && migratedVisualizations.has(ref.id)) { const visToInline = migratedVisualizations.get(ref.id); const visState = JSON.parse(visToInline.attributes.visState); p.version = visToInline.migrationVersion.visualization; p.type = "visualization"; p.embeddableConfig.savedVis = { title: visToInline.attributes.title, description: visToInline.attributes.description, uiState: typeof visToInline.attributes.uiStateJSON === "string" ? JSON.parse(visToInline.attributes.uiStateJSON) : visToInline.attributes.uiStateJSON, params: visState.params, type: visState.type, data: { aggs: !visState.aggs ? undefined : visState.aggs, searchSource: JSON.parse( visToInline.attributes.kibanaSavedObjectMeta.searchSourceJSON ), }, }; delete p.panelRefName; references.splice(references.indexOf(ref), 1); references.push( ...visToInline.references.map((r) => ({ type: r.type, name: `${p.panelIndex}:${r.name}`, id: r.id, })) ); console.log( `Inlined a vis, pushed ${visToInline.references.length} inner references` ); counter.visualization.add(ref.id); } else if (ref && migratedLens.has(ref.id)) { const visToInline = migratedLens.get(ref.id); p.version = visToInline.migrationVersion.lens; p.type = "lens"; p.embeddableConfig.attributes = { ...visToInline.attributes, references: visToInline.references, }; delete p.panelRefName; references.splice(references.indexOf(ref), 1); references.push( ...visToInline.references.map((r) => ({ type: r.type, name: `${p.panelIndex}:${r.name}`, id: r.id, })) ); console.log( `Inlined a lens, pushed ${visToInline.references.length} inner references` ); counter.lens.add(ref.id); } else if (ref && migratedMap.has(ref.id)) { const visToInline = migratedMap.get(ref.id); p.version = visToInline.migrationVersion.map; p.type = "map"; p.embeddableConfig.attributes = { title: visToInline.attributes.title, description: visToInline.attributes.description, uiStateJSON: visToInline.attributes.uiStateJSON, mapStateJSON: visToInline.attributes.mapStateJSON, layerListJSON: visToInline.attributes.layerListJSON, }; delete p.panelRefName; references.splice(references.indexOf(ref), 1); references.push( ...visToInline.references.map((r) => ({ type: r.type, name: `${p.panelIndex}:${r.name}`, id: r.id, })) ); console.log( `Inlined a map, pushed ${visToInline.references.length} inner references` ); counter.map.add(ref.id); } else if (ref && migratedSearch.has(ref.id)) { const searchToInline = migratedSearch.get(ref.id); p.version = searchToInline.migrationVersion.search; p.type = "search"; p.embeddableConfig.attributes = { ...searchToInline.attributes, references: searchToInline.references, }; delete p.panelRefName; references.splice(references.indexOf(ref), 1); references.push( ...searchToInline.references.map((r) => ({ type: r.type, name: `${p.panelIndex}:${r.name}`, id: r.id, })) ); console.log( `Inlined a search, pushed ${searchToInline.references.length} inner references` ); counter.search.add(ref.id); } else { if (!ref) { if (p.type === undefined) { console.log(d.references); throw new Error("Could not match reference"); } console.log( `Leaving panel of type ${p.type}, seems to be inlined already` ); } else { console.log(`Leaving panel of type ${p.type}`); } } }); attributes.panelsJSON = JSON.stringify(panels); return d; }); console.log(`Inlined ${counter.visualization.size} visualizations`); console.log(`Inlined ${counter.map.size} maps`); console.log(`Inlined ${counter.lens.size} lenses`); console.log(`Inlined ${counter.search.size} searches`); if (counter.visualization.size !== migratedVisualizations.size) { console.log( `Some legacy visualizations did not get inlined! ${counter.visualization.size}/${migratedVisualizations.size}` ); [...migratedVisualizations.values()].map((v) => { if (!counter.visualization.has(v.id)) { console.log(`Did not inline ${v.id} anywhere`); } }); } if (counter.map.size !== migratedMap.size) { console.log("Some maps did not get inlined!"); [...migratedMap.values()].map((v) => { if (!counter.map.has(v.id)) { console.log(`Did not inline ${v.id} anywhere`); } }); } if (counter.lens.size !== migratedLens.size) { console.log("Some lens did not get inlined!"); [...migratedLens.values()].map((v) => { if (!counter.lens.has(v.id)) { console.log(`Did not inline ${v.id} anywhere`); } }); } if (counter.search.size !== migratedSearch.size) { console.log("Some searches did not get inlined!"); [...migratedSearch.values()].map((v) => { if (!counter.search.has(v.id)) { console.log(`Did not inline ${v.id} anywhere`); } }); } if (fs.existsSync(`${folderPath}/visualization`)) { console.log("Removing visualization folder"); fs.rmSync(`${folderPath}/visualization`, { force: true, recursive: true }); } if (fs.existsSync(`${folderPath}/map`)) { console.log("Removing maps folder"); fs.rmSync(`${folderPath}/map`, { force: true, recursive: true }); } if (fs.existsSync(`${folderPath}/lens`)) { console.log("Removing lens folder"); fs.rmSync(`${folderPath}/lens`, { force: true, recursive: true }); } if (fs.existsSync(`${folderPath}/search`)) { console.log("Removing search folder"); fs.rmSync(`${folderPath}/search`, { force: true, recursive: true }); } console.log("Writing back dashboards"); inlinedDashboards.forEach((d) => { fs.writeFileSync( `${dashboardPath}/${d.id}.json`, JSON.stringify( { ...d, attributes: rehydrateAttributes(d.attributes) }, null, 2 ) ); }); if (fs.existsSync("./result.json")) { fs.rmSync("./result.json"); } fs.writeFileSync("./result.json", JSON.stringify(inlinedDashboards, null, 2)); })(); async function migrateSavedObjects(subFolder) { const visPath = `${folderPath}/${subFolder}`; const exists = fs.existsSync(visPath); if (!exists) return new Map(); const visualizationPaths = fs.readdirSync(visPath); const visualizations = visualizationPaths.map((vis) => JSON.parse(fs.readFileSync(`${visPath}/${vis}`, { encoding: "utf8" })) ); let response; try { response = await axios.post( `${baseUrl}/api/saved_objects/_bulk_create?overwrite=true`, visualizations.map( ({ type, id, attributes, references, migrationVersion }) => ({ type, id, attributes: cleanupAttributes(attributes), references, // sometimes the migration version is not set migrationVersion: migrationVersion || { [subFolder]: "7.0.0" }, }) ), { headers: { "kbn-xsrf": "abc", }, } ); } catch (e){ // Some machines are converting localhost to IPv6 address that doesn't work with axios // so providing some helpful message here to help debug the issue // on the first axios call if(/ECONNREFUSED ::1/.test(e.message)){ console.log('Either Kibana is not running or try to use a IPv4 address (i.e. 127.0.0.1)'); } throw Error(e) } if (response.data.saved_objects.some((s) => s.error)) { throw new Error(`error loading ${subFolder}`); } const response2 = await axios.post( `${baseUrl}/api/saved_objects/_bulk_get`, visualizations.map((v) => ({ type: subFolder, id: v.id })), { headers: { "kbn-xsrf": "abc", }, } ); const migratedVisualizations = new Map(); response2.data.saved_objects.forEach((s) => { if (s.error) throw new Error(s.error); migratedVisualizations.set(s.id, s); }); console.log( `Prepared ${response2.data.saved_objects.length} ${subFolder}s to be inlined` ); return migratedVisualizations; }