@basaldev/nodeblocks-cloud-sdk
Advanced tools
| const fetch = require('node-fetch'); | ||
| const _ = require('lodash'); | ||
| function getAuthHeaders(makeSession, fingerprint) { | ||
| const { accessToken } = makeSession(); | ||
| return { | ||
| Authorization: `Bearer ${accessToken}`, | ||
| 'x-nb-fingerprint': fingerprint | ||
| }; | ||
| } | ||
| function createClient(endpoint, makeBaseHeaders) { | ||
| return async (path, { method, headers, body }) => { | ||
| // console.log(`Requesting: ${method} ${path}`, { headers, body }, makeBaseHeaders?.()); | ||
| const res = await fetch(`${endpoint}${path}`, { | ||
| method, | ||
| headers: { | ||
| ...makeBaseHeaders?.(), | ||
| ...headers | ||
| }, | ||
| body | ||
| }); | ||
| const data = await res.json(); | ||
| // console.log(`Response: ${res.status} ${res.statusText}`, data); | ||
| if (!res.ok) { | ||
| throw new Error(data.message); | ||
| } | ||
| return data; | ||
| } | ||
| } | ||
| function createPost(makeClient) { | ||
| const client = makeClient(); | ||
| return async (path, body) => await client(path, { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json' | ||
| }, | ||
| body: JSON.stringify(body) | ||
| }); | ||
| } | ||
| function createPatch(makeClient) { | ||
| const client = makeClient(); | ||
| return async (path, body) => await client(path, { | ||
| method: 'PATCH', | ||
| headers: { | ||
| 'Content-Type': 'application/json' | ||
| }, | ||
| body: JSON.stringify(body) | ||
| }); | ||
| } | ||
| function createGet(makeClient) { | ||
| const client = makeClient(); | ||
| return async (path) => await client(path, { | ||
| method: 'GET', | ||
| }); | ||
| } | ||
| function createLogin(makePost) { | ||
| const post = makePost(); | ||
| return async (email, password, fingerprint) => await post('/login', { email, password, fingerprint }); | ||
| } | ||
| function templateProjectPath(projectId) { | ||
| const id = projectId ? `/${projectId}` : ''; | ||
| return `/projects${id}`; | ||
| } | ||
| function createPostProject(makePost) { | ||
| const post = makePost(); | ||
| const path = templateProjectPath(); | ||
| return async (values) => await post(path, values); | ||
| } | ||
| function createPatchProject(makePatch, projectId) { | ||
| const patch = makePatch(); | ||
| const path = templateProjectPath(projectId); | ||
| return async (values) => await patch(path, values); | ||
| } | ||
| function createGetProject(makeGet, projectId) { | ||
| const get = makeGet(); | ||
| const path = templateProjectPath(projectId); | ||
| return async () => await get(path); | ||
| } | ||
| function templateServicePath(projectId, serviceId) { | ||
| const projectPath = templateProjectPath(projectId); | ||
| const id = serviceId ? `/${serviceId}` : ''; | ||
| return `${projectPath}/services${id}`; | ||
| } | ||
| function createPostService(makePost, projectId) { | ||
| const post = makePost(); | ||
| const path = templateServicePath(projectId); | ||
| return async (type, values) => await post(`${path}/${type}`, values); | ||
| } | ||
| function createPatchService(makePatch, projectId, serviceId) { | ||
| const patch = makePatch(); | ||
| const path = templateServicePath(projectId, serviceId); | ||
| return async (values) => await patch(path, values); | ||
| } | ||
| function createGetService(makeGet, projectId, serviceId) { | ||
| const get = makeGet(); | ||
| const path = templateServicePath(projectId, serviceId); | ||
| return async () => await get(path); | ||
| } | ||
| function createApi({ endpoints, auth }) { | ||
| const fingerprint = auth.fingerprint; | ||
| const user = auth.user; | ||
| const session = { | ||
| userId: '', | ||
| accessToken: '' | ||
| }; | ||
| const getSession = () => session; | ||
| const authHeaders = () => getAuthHeaders(getSession, fingerprint); | ||
| // auth client | ||
| const authClient = () => createClient(endpoints.auth); | ||
| const authPost = () => createPost(authClient); | ||
| const login = () => createLogin(authPost)(user.email, user.password, fingerprint) | ||
| .then((res) => _.assign(session, res)); | ||
| // backend client | ||
| const backendClient = () => createClient(endpoints.backend, authHeaders); | ||
| const backendPost = () => createPost(backendClient); | ||
| const backendPatch = () => createPatch(backendClient); | ||
| const backendGet = () => createGet(backendClient); | ||
| // projects | ||
| const postProject = () => createPostProject(backendPost); | ||
| const patchProject = projectId => createPatchProject(backendPatch, projectId); | ||
| const getProject = projectId => createGetProject(backendGet, projectId); | ||
| // services | ||
| const postService = projectId => createPostService(backendPost, projectId); | ||
| const patchService = (projectId, serviceId) => createPatchService(backendPatch, projectId, serviceId); | ||
| const getService = (projectId, serviceId) => createGetService(backendGet, projectId, serviceId); | ||
| return { | ||
| login, | ||
| projects: { | ||
| create: postProject(), | ||
| id: projectId => ({ | ||
| update: patchProject(projectId), | ||
| get: getProject(projectId), | ||
| services: { | ||
| create: postService(projectId), | ||
| id: serviceId => ({ | ||
| update: patchService(projectId, serviceId), | ||
| get: getService(projectId, serviceId), | ||
| }), | ||
| }, | ||
| }) | ||
| }, | ||
| }; | ||
| } | ||
| exports.createApi = createApi; |
| const _ = require('lodash'); | ||
| const utils = require('./utils'); | ||
| const apis = require('./apis'); | ||
| /** | ||
| * Apply command | ||
| */ | ||
| async function resourceApply(options) { | ||
| console.log('🚧 Applying resources...'); | ||
| // Read template and state | ||
| const template = utils.readJSON(options.template); | ||
| const state = utils.readJSON(options.state); | ||
| const profile = utils.readJSON(options.profile); | ||
| state.env = template.env ?? {}; | ||
| state.resources = state.resources ?? {}; | ||
| state.resources.infrastructures = state.resources.infrastructures ?? {}; | ||
| state.resources.projects = state.resources.projects ?? {}; | ||
| state.resources.services = state.resources.services ?? {}; | ||
| // Setup apis | ||
| const api = apis.createApi(profile.settings); | ||
| // get session | ||
| await getSession(api); | ||
| // TODO: Apply infrastructures | ||
| // Apply projects | ||
| state.resources.projects = await applyProjects(api, template, state); | ||
| // Apply services | ||
| state.resources.services = await applyServices(api, template, state); | ||
| // Write state | ||
| utils.writeJSON(options.state, state); | ||
| } | ||
| function parseTemplate(template, state) { | ||
| const templateString = JSON.stringify(template); | ||
| const parsedTemplateString = templateString.replace(/{{\s*?([^\s]*?)\s*?}}/g, (_match, key) => { | ||
| const value = _.get(state, key); | ||
| if (!value) { | ||
| // console.log(`Key ${key} is not found in the state`); | ||
| return ''; | ||
| } | ||
| return value; | ||
| }); | ||
| return JSON.parse(parsedTemplateString); | ||
| } | ||
| async function getSession(api) { | ||
| console.log('Getting session...'); | ||
| return await api.login(); | ||
| } | ||
| function getCreateProjectFieldMask() { | ||
| return [ | ||
| 'name', | ||
| 'infrastructureId', | ||
| 'budgetAlertEmails', | ||
| 'budgetAmount', | ||
| 'enableExternalIpAddress' | ||
| ]; | ||
| } | ||
| function getUpdateProjectFieldMask() { | ||
| return [ | ||
| 'name', | ||
| ]; | ||
| } | ||
| async function createProject(api, value) { | ||
| const mask = getCreateProjectFieldMask(); | ||
| const data = _.pick(value, mask); | ||
| const body = _.omitBy(data, _.isUndefined); | ||
| return await api.projects.create(body); | ||
| } | ||
| async function updateProject(api, projectId, value) { | ||
| const mask = getUpdateProjectFieldMask(); | ||
| const data = _.pick(value, mask); | ||
| const body = _.omitBy(data, _.isUndefined); | ||
| return await api.projects | ||
| .id(projectId) | ||
| .update(body); | ||
| } | ||
| async function applyProjects(api, template, state) { | ||
| const parsed = parseTemplate(template, state); | ||
| const results = {}; | ||
| const create = async (key, value) => { | ||
| console.log(`- Creating project ${key}...`); | ||
| return await createProject(api, value); | ||
| } | ||
| const get = async (projectId) => { | ||
| console.log(`- Getting project:${projectId}...`); | ||
| return await api.projects.id(projectId).get(); | ||
| } | ||
| const isDrifted = (projectInState, projectInDb) => { | ||
| const mask = ['budgetAmount', 'status', 'createdAt', 'updatedAt']; | ||
| return !_.isEqual( | ||
| _.omit(projectInState, mask), | ||
| _.omit(projectInDb, mask) | ||
| ); | ||
| } | ||
| const hasUpdate = (projectInTemplate, projectInState) => { | ||
| const updateFieldMask = getUpdateProjectFieldMask(); | ||
| return !_.isEqual( | ||
| _.pick(projectInTemplate, updateFieldMask), | ||
| _.pick(projectInState, updateFieldMask) | ||
| ); | ||
| } | ||
| for (const [key, projectInTemplate] of Object.entries(parsed.resources.projects)) { | ||
| console.log(`Apply project:${key}...`); | ||
| const projectInState = state.resources.projects[key]; | ||
| const projectId = projectInState?.id | ||
| if (!projectId) { | ||
| const res = await create(key, projectInTemplate); | ||
| results[key] = res; | ||
| continue; | ||
| } | ||
| const projectInDb = await get(projectId); | ||
| if (!projectInDb) { | ||
| throw new Error(`Project:${key} is not found in DB!`); | ||
| } | ||
| const drifted = isDrifted(projectInState, projectInDb); | ||
| if (drifted) { | ||
| console.log('# in state:', projectInState); | ||
| console.log('# in db:', projectInDb); | ||
| throw new Error(`Project:${key} is drifted!`); | ||
| } | ||
| const noUpdates = !hasUpdate(projectInTemplate, projectInState); | ||
| if (noUpdates) { | ||
| console.log(`- Skipping project:${projectId} as it is up to date...`); | ||
| results[key] = projectInState; | ||
| continue; | ||
| } | ||
| console.log(`- Updating project:${projectId} as it is changed...`); | ||
| const res = await updateProject(api, projectId, projectInTemplate); | ||
| results[key] = _.merge(projectInState, res); | ||
| } | ||
| return results; | ||
| } | ||
| function getCreateCustomServiceFieldMask() { | ||
| return [ | ||
| 'name', | ||
| 'serviceRepo', | ||
| 'serviceRepoBranch' | ||
| ]; | ||
| } | ||
| function getCreatePredefinedServiceFieldMask() { | ||
| return [ | ||
| 'name', | ||
| 'serviceVersion', | ||
| ]; | ||
| } | ||
| function getUpdateServiceFieldMask() { | ||
| return [ | ||
| 'adapterRepo', | ||
| 'adapterRepoBranch', | ||
| 'envVariables', | ||
| 'healthCheckPath', | ||
| 'instance', | ||
| 'selectedAdapterName', | ||
| 'serviceRepo', | ||
| 'serviceRepoBranch', | ||
| ]; | ||
| } | ||
| function isCustomService(value) { | ||
| return value.type === 'custom'; | ||
| } | ||
| async function createService(api, value) { | ||
| const projectId = value.projectId; | ||
| const type = value.type; | ||
| const mask = isCustomService(value) | ||
| ? getCreateCustomServiceFieldMask() | ||
| : getCreatePredefinedServiceFieldMask(); | ||
| const data = _.pick(value, mask); | ||
| const body = _.omitBy(data, _.isUndefined); | ||
| return await api.projects | ||
| .id(projectId) | ||
| .services | ||
| .create(type, body); | ||
| } | ||
| async function updateService(api, serviceId, value) { | ||
| const projectId = value.projectId; | ||
| const mask = getUpdateServiceFieldMask(); | ||
| const data = _.pick(value, mask); | ||
| const body = _.omitBy(data, _.isUndefined); | ||
| /** | ||
| * workaround for the constraint of max props = 10 in envVariables | ||
| * make a chunk of 10 props and update them one by one | ||
| */ | ||
| if (body.envVariables) { | ||
| const chunkedKeys = _.chunk(Object.keys(body.envVariables), 10); | ||
| for (const keys of chunkedKeys) { | ||
| console.log(` - Updating chunked envVariables...`); | ||
| await api.projects | ||
| .id(projectId) | ||
| .services | ||
| .id(serviceId) | ||
| .update({ envVariables: _.pick(body.envVariables, keys) }); | ||
| } | ||
| delete body.envVariables; | ||
| } | ||
| return await api.projects | ||
| .id(projectId) | ||
| .services | ||
| .id(serviceId) | ||
| .update(body); | ||
| } | ||
| async function applyServices(api, template, state) { | ||
| const parsed = parseTemplate(template, state); | ||
| const results = {}; | ||
| const create = async (key, value) => { | ||
| console.log(`- Creating service ${key}...`); | ||
| return await createService(api, value); | ||
| } | ||
| const get = async (projectId, serviceId) => { | ||
| console.log(`- Getting service:${serviceId}...`); | ||
| return await api.projects.id(projectId).services.id(serviceId).get(); | ||
| } | ||
| const isDrifted = (serviceInState, serviceInDb) => { | ||
| const mask = ['serviceRepoDeployKey', 'lastDeployment', 'envVariables', 'status', 'createdAt', 'updatedAt']; | ||
| return !_.isEqual( | ||
| _.omit(serviceInState, mask), | ||
| _.omit(serviceInDb, mask) | ||
| ); | ||
| } | ||
| const hasUpdates = (serviceInTemplate, serviceInState) => { | ||
| const updateFieldMask = getUpdateServiceFieldMask(); | ||
| return !_.isEqual( | ||
| _.pick(serviceInTemplate, updateFieldMask), | ||
| _.pick(serviceInState, updateFieldMask) | ||
| ); | ||
| } | ||
| for (const [key, serviceInTemplate] of Object.entries(parsed.resources.services)) { | ||
| console.log(`Apply service:${key}...`); | ||
| const serviceInState = state.resources.services[key]; | ||
| const serviceId = serviceInState?.id | ||
| if (!serviceId) { | ||
| const created = await create(key, serviceInTemplate); | ||
| const updated = await updateService(api, created.id, serviceInTemplate); | ||
| results[key] = _.merge(created, updated); | ||
| continue; | ||
| } | ||
| const projectId = serviceInTemplate.projectId; | ||
| const serviceInDb = await get(projectId, serviceId); | ||
| if (!serviceInDb) { | ||
| throw new Error(`Service:${key} is not found in DB!`); | ||
| } | ||
| const drifted = isDrifted(serviceInState, serviceInDb); | ||
| if (drifted) { | ||
| console.log('# in state:', serviceInState); | ||
| console.log('# in db:', serviceInDb); | ||
| throw new Error(`Service:${key} is drifted!`); | ||
| } | ||
| const noUpdates = !hasUpdates(serviceInTemplate, serviceInState); | ||
| if (noUpdates) { | ||
| console.log(`- Skipping service:${key} as it is up to date...`); | ||
| results[key] = serviceInState; | ||
| continue; | ||
| } | ||
| console.log(`- Updating service:${serviceId}...`); | ||
| const res = await updateService(api, serviceId, serviceInTemplate); | ||
| results[key] = _.merge(serviceInState, res); | ||
| } | ||
| return results; | ||
| } | ||
| exports.resourceApply = resourceApply; |
| const fs = require('fs'); | ||
| function readJSON(path) { | ||
| return fs.existsSync(path) ? JSON.parse(fs.readFileSync(path, 'utf8')) : {}; | ||
| } | ||
| function writeJSON(path, data) { | ||
| return fs.writeFileSync(path, JSON.stringify(data, null, 2)); | ||
| } | ||
| exports.readJSON = readJSON; | ||
| exports.writeJSON = writeJSON; |
+10
-0
@@ -10,2 +10,3 @@ #! /usr/bin/env node | ||
| const dotenv = require('dotenv'); | ||
| const { resourceApply } = require('./resource'); | ||
@@ -32,2 +33,4 @@ const packageDir = path.join(__dirname, '..'); | ||
| .description('Manage the custom adapter'); | ||
| const resourceCommand = program.command('resource') | ||
| .description('Manage the NBC resource'); | ||
@@ -43,2 +46,9 @@ adapterCommand.command('start') | ||
| .action(adapterDev); | ||
| resourceCommand.command('apply') | ||
| .description('Apply the resource') | ||
| .requiredOption('-t --template <value>', 'The path to the template file') | ||
| .requiredOption('-s --state <value>', 'The path to the state file') | ||
| .requiredOption('-p --profile <value>', 'The path to the profile file') | ||
| .action(resourceApply); | ||
| await program.parseAsync(process.argv); | ||
@@ -45,0 +55,0 @@ } |
+5
-2
| { | ||
| "name": "@basaldev/nodeblocks-cloud-sdk", | ||
| "version": "0.2.5", | ||
| "version": "0.3.0", | ||
| "description": "Nodeblocks cloud SDK", | ||
@@ -12,5 +12,6 @@ "main": "./lib/index.js", | ||
| "adapter:dev": "node ./lib/index.js adapter dev", | ||
| "resource:apply": "node ./lib/index.js resource apply", | ||
| "templates:fetch": "node ./lib/clone.js", | ||
| "prepack": "zip -r assets.zip assets -x \"**/node_modules/*\"", | ||
| "postinstall": "unzip -o assets.zip" | ||
| "postinstall": "[ -f \"assets.zip\" ] && unzip -o assets.zip || echo \"skipping unzip assets.zip\"" | ||
| }, | ||
@@ -27,2 +28,4 @@ "files": [ | ||
| "dotenv": "^16.4.5", | ||
| "lodash": "^4.17.21", | ||
| "node-fetch": "^2.7.0", | ||
| "prompts": "^2.4.2", | ||
@@ -29,0 +32,0 @@ "zx": "^8.1.0" |
Sorry, the diff of this file is not supported yet
Explicitly Unlicensed Item
LicenseSomething was found which is explicitly marked as unlicensed.
Found 1 instance in 1 package
Install scripts
Supply chain riskInstall scripts are run when the package is installed or built. Malicious packages often use scripts that run automatically to execute payloads or fetch additional code.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Explicitly Unlicensed Item
LicenseSomething was found which is explicitly marked as unlicensed.
Found 1 instance in 1 package
Install scripts
Supply chain riskInstall scripts are run when the package is installed or built. Malicious packages often use scripts that run automatically to execute payloads or fetch additional code.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
1470364
82.78%8
60%771
117.18%7
40%8
14.29%3
200%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added