From f450dbc2527ecd0a60a061343a2d3ca65e7f579d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Thu, 12 Sep 2024 11:24:12 +0200 Subject: [PATCH] feat(cli): refactoring and error handling in commands using apps-service --- packages/cli/src/bin/create-export-config.ts | 80 ++++- packages/cli/src/bin/publish-application.ts | 121 ++++++-- packages/cli/src/bin/tag-application.ts | 81 ++++-- packages/cli/src/bin/upload-application.ts | 78 +++-- packages/cli/src/bin/utils/app-api.ts | 274 ------------------ ...{execute-commant.ts => execute-command.ts} | 0 packages/cli/src/bin/utils/getEndpointUrl.ts | 50 ++++ packages/cli/src/bin/utils/index.ts | 18 ++ packages/cli/src/bin/utils/isAppRegistered.ts | 27 ++ .../cli/src/bin/utils/publishAppConfig.ts | 31 ++ .../cli/src/bin/utils/requireValidToken.ts | 23 ++ packages/cli/src/bin/utils/tagAppBundle.ts | 24 ++ packages/cli/src/bin/utils/uploadAppBundle.ts | 57 ++++ 13 files changed, 507 insertions(+), 357 deletions(-) delete mode 100644 packages/cli/src/bin/utils/app-api.ts rename packages/cli/src/bin/utils/{execute-commant.ts => execute-command.ts} (100%) create mode 100644 packages/cli/src/bin/utils/getEndpointUrl.ts create mode 100644 packages/cli/src/bin/utils/index.ts create mode 100644 packages/cli/src/bin/utils/isAppRegistered.ts create mode 100644 packages/cli/src/bin/utils/publishAppConfig.ts create mode 100644 packages/cli/src/bin/utils/requireValidToken.ts create mode 100644 packages/cli/src/bin/utils/tagAppBundle.ts create mode 100644 packages/cli/src/bin/utils/uploadAppBundle.ts diff --git a/packages/cli/src/bin/create-export-config.ts b/packages/cli/src/bin/create-export-config.ts index e05b66481..6b0922b68 100644 --- a/packages/cli/src/bin/create-export-config.ts +++ b/packages/cli/src/bin/create-export-config.ts @@ -6,11 +6,19 @@ import semverValid from 'semver/functions/valid.js'; import { chalk, formatPath } from './utils/format.js'; import { Spinner } from './utils/spinner.js'; -import { loadPackage } from './utils/load-package.js'; -import { loadAppConfig } from './utils/load-app-config.js'; -import { publishAppConfig, validateToken, type FusionEnv } from './utils/app-api.js'; +import { + getEndpointUrl, + loadAppConfig, + loadPackage, + isAppRegistered, + requireValidToken, + publishAppConfig, +} from './utils/index.js'; +import type { FusionEnv } from './utils/index.js'; + import { ConfigExecuterEnv } from '../lib/utils/config.js'; import { resolveAppKey } from '../lib/app-package.js'; +import { exit } from 'node:process'; export const createExportConfig = async (options: { command?: ConfigExecuterEnv['command']; @@ -55,13 +63,9 @@ export const createExportConfig = async (options: { } if (publish) { - spinner.info('Publishing config'); - - const validToken = validateToken(); - if (!validToken) { - return; - } + spinner.info('Preparing to publishing config'); + /* Make sure version is valid */ const version = publish === 'current' ? pkg.packageJson.version : publish; if (!version || (!semverValid(version) && !['latest', 'preview'].includes(version))) { spinner.fail( @@ -70,12 +74,64 @@ export const createExportConfig = async (options: { chalk.redBright(version), '', ); - return; + exit(1); + } + + /** make sure user has a valid token */ + try { + spinner.info('Validating FUSION_TOKEN'); + await requireValidToken(env); + spinner.succeed('Found valid FUSION_TOKEN'); + } catch (e) { + const err = e as Error; + spinner.fail(chalk.bgRed(err.message)); + exit(1); + } + + try { + spinner.info('Verifying that App is registered'); + + const state = { endpoint: '' }; + try { + state.endpoint = await getEndpointUrl(`apps/${appKey}`, env, service); + } catch (e) { + const err = e as Error; + throw new Error( + `Could not get endpoint from service discovery while verifying app. service-discovery status: ${err.message}`, + ); + } + + await isAppRegistered(state.endpoint, appKey); + spinner.succeed(`${appKey} is registered`); + } catch (e) { + const err = e as Error; + spinner.fail('🙅‍♂️', chalk.bgRed(err.message)); + exit(1); } - const published = await publishAppConfig(appKey, version, config, env, service); - if (published) { + try { + spinner.info(`Publishing config to "${appKey}@${version}"`); + + const state = { endpoint: '' }; + try { + state.endpoint = await getEndpointUrl( + `apps/${appKey}/builds/${version}/config`, + env, + service, + ); + } catch (e) { + const err = e as Error; + throw new Error( + `Could not get endpoint from service discovery while publishig config. service-discovery status: ${err.message}`, + ); + } + + await publishAppConfig(state.endpoint, appKey, config); spinner.succeed('✅', 'Published config to version', chalk.yellowBright(version)); + } catch (e) { + const err = e as Error; + spinner.fail('🙅‍♂️', chalk.bgRed(err.message)); + exit(1); } } diff --git a/packages/cli/src/bin/publish-application.ts b/packages/cli/src/bin/publish-application.ts index 02fe28f69..dda5b0032 100644 --- a/packages/cli/src/bin/publish-application.ts +++ b/packages/cli/src/bin/publish-application.ts @@ -4,12 +4,15 @@ import { bundleApplication } from './bundle-application.js'; import { resolveAppPackage, resolveAppKey } from '../lib/app-package.js'; import { - uploadAppBundle, - appRegistered, - validateToken, + isAppRegistered, + getEndpointUrl, + requireValidToken, tagAppBundle, - type FusionEnv, -} from './utils/app-api.js'; + uploadAppBundle, +} from './utils/index.js'; +import type { FusionEnv } from './utils/index.js'; + +import { exit } from 'node:process'; export const publishApplication = async (options: { tag: string; @@ -20,46 +23,110 @@ export const publishApplication = async (options: { const spinner = Spinner.Global({ prefixText: chalk.dim('Publish') }); - const validToken = validateToken(); - if (!validToken) { - return; + try { + spinner.info('Validating FUSION_TOKEN'); + await requireValidToken(env); + spinner.succeed('Found valid FUSION_TOKEN'); + } catch (e) { + const err = e as Error; + spinner.fail(chalk.bgRed(err.message)); + exit(1); } const pkg = await resolveAppPackage(); const appKey = resolveAppKey(pkg.packageJson); - spinner.info(`Publishing app: "${appKey}" with tag: "${tag}"`); + try { + spinner.info('Verifying that App is registered'); + const state = { endpoint: '' }; + try { + state.endpoint = await getEndpointUrl(`apps/${appKey}`, env, service); + } catch (e) { + const err = e as Error; + throw new Error( + `Could not get endpoint from service discovery while verifying app is registered. service-discovery status: ${err.message}`, + ); + } - spinner.info('Verifying that App is registered in api.'); - const appResponse = await appRegistered(appKey, env, service); - if (!appResponse) { - return; + await isAppRegistered(state.endpoint, appKey); + spinner.succeed(`${appKey} is registered`); + } catch (e) { + const err = e as Error; + spinner.fail('🙅‍♂️', chalk.bgRed(err.message)); + exit(1); } + const bundle = 'app-bundle.zip'; + /* Zip app bundle */ - spinner.info('Create bundle'); + spinner.info('Creating zip bundle'); + await bundleApplication({ - archive: 'app-bundle.zip', + archive: bundle, outDir: 'dist', }); - spinner.info('Uploading bundle'); - const uploadedBundle = await uploadAppBundle(appKey, 'app-bundle.zip', env, service); - if (!uploadedBundle) { - return; + const state = { + uploadedBundle: { version: '' }, + endpoint: '', + }; + + spinner.info(`Publishing app: "${appKey}" with tag: "${tag}"`); + + /* Upload app bundle */ + try { + spinner.info( + `Uploading bundle ${chalk.yellowBright(bundle)} to appKey ${chalk.yellowBright(appKey)}`, + ); + + try { + state.endpoint = await getEndpointUrl(`bundles/apps/${appKey}`, env, service); + } catch (e) { + const err = e as Error; + throw new Error( + `Could not get endpoint from service discovery while uploading app bundle. service-discovery status: ${err.message}`, + ); + } + + spinner.info(`Posting bundle to => ${state.endpoint}`); + + state.uploadedBundle = await uploadAppBundle(state.endpoint, bundle); + + spinner.succeed( + '✅', + `Uploaded bundle: "${chalk.greenBright(bundle)}" with version: ${chalk.greenBright(state.uploadedBundle.version)}"`, + ); + } catch (e) { + const err = e as Error; + spinner.fail('🙅‍♂️', chalk.bgRed(err.message)); + exit(1); } - spinner.info(`Tag app bundle with: ${tag}`); - const tagged = await tagAppBundle(tag, appKey, uploadedBundle.version, env, service); + try { + spinner.info(`Tagging ${state.uploadedBundle.version} with ${tag}`); + + try { + state.endpoint = await getEndpointUrl(`apps/${appKey}/tags/${tag}`, env, service); + } catch (e) { + const err = e as Error; + throw new Error( + `Could not get endpoint from service discovery while tagging app. service-discovery status: ${err.message}`, + ); + } - if (!tagged) { - return; + const tagged = await tagAppBundle(state.endpoint, state.uploadedBundle.version); + spinner.succeed( + '✅', + `Tagged version ${chalk.greenBright(tagged.version)} with ${chalk.greenBright(tagged.tagName)}`, + ); + } catch (e) { + const err = e as Error; + spinner.fail('🙅‍♂️', chalk.bgRed(err.message)); + exit(1); } spinner.succeed( - '✅', - `Published app: "${chalk.greenBright(appKey)}"`, - `With version: "${chalk.greenBright(tagged.version)}"`, - `Tag: "${chalk.greenBright(tagged.tagName)}"`, + '⭐️', + `Published app: "${chalk.greenBright(appKey)}" version: "${chalk.greenBright(state.uploadedBundle.version)}" with tagg: "${chalk.greenBright(tag)}"`, ); }; diff --git a/packages/cli/src/bin/tag-application.ts b/packages/cli/src/bin/tag-application.ts index a079172af..70753a5a6 100644 --- a/packages/cli/src/bin/tag-application.ts +++ b/packages/cli/src/bin/tag-application.ts @@ -1,7 +1,9 @@ +import { exit } from 'node:process'; import { chalk } from './utils/format.js'; import { Spinner } from './utils/spinner.js'; import { resolveAppPackage, resolveAppKey } from '../lib/app-package.js'; -import { appRegistered, validateToken, tagAppBundle, type FusionEnv } from './utils/app-api.js'; +import { isAppRegistered, getEndpointUrl, requireValidToken, tagAppBundle } from './utils/index.js'; +import type { FusionEnv } from './utils/index.js'; enum Tags { preview = 'preview', @@ -18,38 +20,67 @@ export const tagApplication = async (options: { const spinner = Spinner.Global({ prefixText: chalk.dim('Tag') }); - const validToken = validateToken(); - if (!validToken) { - return; + if (!Object.values(Tags).includes(tag as Tags)) { + spinner.fail('😞', `Tag must match (${Tags.latest} | ${Tags.preview})`); + exit(1); + } + + /** make sure user has a valid token */ + try { + spinner.info('Validating FUSION_TOKEN'); + await requireValidToken(env); + spinner.succeed('Found valid FUSION_TOKEN'); + } catch (e) { + const err = e as Error; + spinner.fail(chalk.bgRed(err.message)); + exit(1); } const pkg = await resolveAppPackage(); const appKey = resolveAppKey(pkg.packageJson); - // if (!['preview', 'latest'].includes(tag)) { - if (!Object.values(Tags).includes(tag as Tags)) { - spinner.fail('😞', `Tag must match (${Tags.latest} | ${Tags.preview})`); - return; - } + try { + spinner.info('Verifying that App is registered'); + const state = { endpoint: '' }; + try { + state.endpoint = await getEndpointUrl(`apps/${appKey}`, env, service); + } catch (e) { + const err = e as Error; + throw new Error( + `Could not get endpoint from service discovery while verifying app is registered. service-discovery status: ${err.message}`, + ); + } - spinner.info(`Tag app "${appKey}@${version}" with: "${tag}"`); - - spinner.info('Verifying that App is registered'); - const registered = await appRegistered(appKey, env, service); - if (!registered) { - return; + await isAppRegistered(state.endpoint, appKey); + spinner.succeed(`${appKey} is registered`); + } catch (e) { + const err = e as Error; + spinner.fail('🙅‍♂️', chalk.bgRed(err.message)); + exit(1); } - const tagged = await tagAppBundle(tag, appKey, version, env, service); + try { + spinner.info(`Tagging "${appKey}@${version}" with: "${tag}"`); + const state = { endpoint: '' }; + try { + state.endpoint = await getEndpointUrl(`apps/${appKey}/tags/${tag}`, env, service); + } catch (e) { + const err = e as Error; + throw new Error( + `Could not get endpoint from service discovery while tagging app. service-discovery status: ${err.message}`, + ); + } - if (!tagged) { - return; + const tagged = await tagAppBundle(state.endpoint, version); + spinner.succeed( + '✅', + `Tagged app: "${chalk.greenBright(appKey)}"`, + `version: "${chalk.greenBright(tagged.version)}"`, + `with tag: "${chalk.greenBright(tagged.tagName)}"`, + ); + } catch (e) { + const err = e as Error; + spinner.fail('🙅‍♂️', chalk.bgRed(err.message)); + exit(1); } - - spinner.succeed( - '✅', - `App: "${chalk.greenBright(appKey)}"`, - `Version: "${chalk.greenBright(tagged.version)}"`, - `Tag: "${chalk.greenBright(tagged.tagName)}"`, - ); }; diff --git a/packages/cli/src/bin/upload-application.ts b/packages/cli/src/bin/upload-application.ts index 8a2031d62..1d15f71b2 100644 --- a/packages/cli/src/bin/upload-application.ts +++ b/packages/cli/src/bin/upload-application.ts @@ -1,7 +1,14 @@ +import { exit } from 'node:process'; import { Spinner } from './utils/spinner.js'; import { chalk } from './utils/format.js'; import { resolveAppPackage, resolveAppKey } from '../lib/app-package.js'; -import { uploadAppBundle, appRegistered, validateToken, type FusionEnv } from './utils/app-api.js'; +import { + isAppRegistered, + getEndpointUrl, + requireValidToken, + uploadAppBundle, +} from './utils/index.js'; +import type { FusionEnv } from './utils/index.js'; export const uploadApplication = async (options: { bundle: string; @@ -12,32 +19,65 @@ export const uploadApplication = async (options: { const spinner = Spinner.Global({ prefixText: chalk.dim('Upload') }); - const validToken = validateToken(); - if (!validToken) { - return; + /** make sure user has a valid token */ + try { + spinner.info('Validating FUSION_TOKEN'); + await requireValidToken(env); + spinner.succeed('Found valid FUSION_TOKEN'); + } catch (e) { + const err = e as Error; + spinner.fail(chalk.bgRed(err.message)); + exit(1); } /* get package.json */ const pkg = await resolveAppPackage(); const appKey = resolveAppKey(pkg.packageJson); - spinner.info('Verifying App is registered'); - const appResponse = await appRegistered(appKey, env, service); - if (!appResponse) { - return; + try { + spinner.info('Verifying that App is registered'); + const state = { endpoint: '' }; + + try { + state.endpoint = await getEndpointUrl(`apps/${appKey}`, env, service); + } catch (e) { + const err = e as Error; + throw new Error( + `Could not get endpoint from service discovery while verifying app. service-discovery status: ${err.message}`, + ); + } + + await isAppRegistered(state.endpoint, appKey); + spinner.succeed(`${appKey} is registered`); + } catch (e) { + const err = e as Error; + spinner.fail('🙅‍♂️', chalk.bgRed(err.message)); + exit(1); } /* Upload app bundle */ - spinner.info(`Uploading appkey: ${chalk.yellowBright(appKey)}`); - spinner.info(`Uploading bundle ${chalk.yellowBright(bundle)}`); - const uploadedBundle = await uploadAppBundle(appKey, bundle, env, service); - if (!uploadedBundle) { - return; - } + try { + spinner.info( + `Uploading bundle ${chalk.yellowBright(bundle)} to appKey ${chalk.yellowBright(appKey)}` + ); + + const endpoint = await getEndpointUrl(`bundles/apps/${appKey}`, env, service); + if (!endpoint) { + throw new Error('Could not get endpoint from service discovery'); + } - spinner.succeed( - '✅', - `Uploaded app: "${chalk.greenBright(appKey)}"`, - `Version: "${chalk.greenBright(uploadedBundle.version)}"`, - ); + spinner.info(`Posting bundle to => ${endpoint}`); + + const uploadedBundle = await uploadAppBundle(endpoint, bundle); + + spinner.succeed( + '✅', + `Uploaded app: "${chalk.greenBright(appKey)}"`, + `Version: "${chalk.greenBright(uploadedBundle.version)}"`, + ); + } catch (e) { + const err = e as Error; + spinner.fail('🙅‍♂️', chalk.bgRed(err.message)); + exit(1); + } }; diff --git a/packages/cli/src/bin/utils/app-api.ts b/packages/cli/src/bin/utils/app-api.ts deleted file mode 100644 index be837e0d2..000000000 --- a/packages/cli/src/bin/utils/app-api.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { Spinner } from './spinner.js'; -import { chalk } from './format.js'; -import type { AppConfig } from '@equinor/fusion-framework-module-app'; - -export type FusionEnv = 'ci' | 'fqa' | 'tr' | 'fprd'; - -/* build api endpoint url */ -export const getEndpointUrl = async ( - endpoint: string, - fusionEnv: FusionEnv, - service: string, - version: string = '1.0', -) => { - const { CUSTOM_APPAPI, FUSION_CLI_ENV, FUSION_TOKEN } = process.env; - - const spinner = Spinner.Current; - - /* use consumer provided api url */ - if (service || CUSTOM_APPAPI) { - return service ?? CUSTOM_APPAPI; - } - - /* Env has changed get new api url */ - if (FUSION_CLI_ENV !== fusionEnv || !process.env.FUSION_CLI_APPAPI) { - process.env.FUSION_CLI_ENV = fusionEnv; - - const requestService = await fetch( - `https://discovery.ci.fusion-dev.net/service-registry/environments/${fusionEnv}/services/apps`, - { - headers: { - Authorization: `Bearer ${FUSION_TOKEN}`, - }, - }, - ); - if (!requestService.ok) { - spinner.fail( - '😞', - chalk.redBright('Could not resolve app api endpoint from service discovery api'), - ); - spinner.info( - '🤯', - chalk.yellowBright( - `HTTP status ${requestService.status}, ${requestService.statusText}`, - ), - ); - return; - } - - const responseService = await requestService.json(); - process.env.FUSION_CLI_APPAPI = responseService.uri; - } - - const uri = new URL(`${process.env.FUSION_CLI_APPAPI}/${endpoint}`); - uri.searchParams.set('api-version', version); - - /* return fresh/cached endpoint url */ - return uri.href; -}; - -export const validateToken = () => { - const spinner = Spinner.Current; - spinner.info('Validating FUSION_TOKEN'); - - if (!process?.env?.FUSION_TOKEN) { - spinner.fail( - '😞', - `Missing environment variable "${chalk.yellowBright('FUSION_TOKEN')}"`, - `\n\nThe "${chalk.yellowBright('FUSION_TOKEN')}" variable is required to perform any actions towards the app api. \nDefine the variable with a valid accessToken in you pipeline|shell before running commands using the app api.`, - ); - return false; - } - - const tokenPayload = process.env.FUSION_TOKEN.split('.')[1]; - const buffer = Buffer.from(tokenPayload, 'base64'); - const userToken = JSON.parse(buffer.toString('utf-8').trim()); - if (Number(userToken.exp) < Date.now() / 1000) { - spinner.fail( - '😞', - chalk.yellowBright( - 'FUSION_TOKEN expired', - Math.round(Date.now() / 1000 - Number(userToken.exp)), - 'seconds ago', - ), - ); - return false; - } - - spinner.succeed('Token is valid'); - return true; -}; - -export const appRegistered = async (appKey: string, env: FusionEnv, service: string) => { - const spinner = Spinner.Current; - - const endpoint = await getEndpointUrl(`apps/${appKey}`, env, service); - if (!endpoint) { - return; - } - - const requestApp = await fetch(endpoint, { - headers: { - Authorization: `Bearer ${process.env.FUSION_TOKEN}`, - }, - }); - - if (requestApp.status === 404) { - spinner.fail( - '🙅‍♂️', - chalk.bgRed( - `The appkey '${appKey}' is not registered, visit the app-admin app and register the application there.`, - ), - ); - return; - } - - if (!requestApp.ok) { - spinner.fail('😞', chalk.redBright('Could not connect to apps API')); - spinner.info( - '🤯', - chalk.yellowBright(`HTTP status ${requestApp.status}, ${requestApp.statusText}`), - ); - return; - } - - spinner.succeed('✅', 'app is registered'); - - return await requestApp.json(); -}; - -export const uploadAppBundle = async ( - appKey: string, - bundle: string, - env: FusionEnv, - service: string, -) => { - const spinner = Spinner.Current; - - const state: { buffer: Buffer | null } = { - buffer: null, - }; - - try { - state.buffer = readFileSync(bundle); - } catch { - spinner.fail('😞', 'Could not retreive app bundle, does it exist?'); - return; - } - - const endpointUrl = await getEndpointUrl(`bundles/apps/${appKey}`, env, service); - if (!endpointUrl) { - return; - } - - spinner.info(`Posting bundle to => ${endpointUrl}`); - - const requestBundle = await fetch(endpointUrl, { - method: 'POST', - body: state.buffer, - headers: { - Authorization: `Bearer ${process.env.FUSION_TOKEN}`, - 'Content-Type': 'application/zip', - }, - }); - - if (requestBundle.status === 409) { - spinner.info('🤯', chalk.yellowBright(`This app version is already published`)); - spinner.fail('😞', chalk.redBright('Failed to publish bundle')); - return; - } - - if (!requestBundle.ok) { - spinner.info( - '🤯', - chalk.yellowBright(`HTTP status ${requestBundle.status}, ${requestBundle.statusText}`), - ); - spinner.fail('😞', chalk.redBright('Failed to publish bundle')); - return; - } - - return await requestBundle.json(); -}; - -export const tagAppBundle = async ( - tag: string, - appKey: string, - version: string, - env: FusionEnv, - service: string, -) => { - const spinner = Spinner.Current; - - const endpointUrl = await getEndpointUrl(`apps/${appKey}/tags/${tag}`, env, service); - if (!endpointUrl) { - return; - } - - spinner.info(`Tagging in app api => ${endpointUrl}`); - - const requestTag = await fetch(endpointUrl, { - method: 'PUT', - body: JSON.stringify({ version }), - headers: { - Authorization: `Bearer ${process.env.FUSION_TOKEN}`, - 'Content-Type': 'application/json', - }, - }); - - if (!requestTag.ok) { - spinner.fail('😞', chalk.redBright('Failed to tag bundle')); - spinner.info( - '🤯', - chalk.yellowBright(`HTTP status ${requestTag.status}, ${requestTag.statusText}`), - ); - return; - } - - return await requestTag.json(); -}; - -export const publishAppConfig = async ( - appKey: string, - version: string, - config: AppConfig, - env: FusionEnv, - service: string, -) => { - const spinner = Spinner.Current; - - const isAppRegistered = appRegistered(appKey, env, service); - if (!isAppRegistered) { - return; - } - - const endpointUrl = await getEndpointUrl( - `apps/${appKey}/builds/${version}/config`, - env, - service, - ); - if (!endpointUrl) { - return; - } - - spinner.info(`Publishing app config to => ${endpointUrl}`); - - const requestConfig = await fetch(endpointUrl, { - method: 'PUT', - body: JSON.stringify(config), - headers: { - Authorization: `Bearer ${process.env.FUSION_TOKEN}`, - 'Content-Type': 'application/json', - }, - }); - - if (requestConfig.status === 404) { - spinner.fail('😞', chalk.redBright('App version is not published')); - spinner.info( - '🤯', - chalk.yellowBright(`HTTP status ${requestConfig.status}, ${requestConfig.statusText}`), - ); - return; - } - - if (!requestConfig.ok) { - spinner.fail('😞', chalk.redBright('Failed to upload config')); - spinner.info( - '🤯', - chalk.yellowBright(`HTTP status ${requestConfig.status}, ${requestConfig.statusText}`), - ); - return; - } - - return await requestConfig.json(); -}; diff --git a/packages/cli/src/bin/utils/execute-commant.ts b/packages/cli/src/bin/utils/execute-command.ts similarity index 100% rename from packages/cli/src/bin/utils/execute-commant.ts rename to packages/cli/src/bin/utils/execute-command.ts diff --git a/packages/cli/src/bin/utils/getEndpointUrl.ts b/packages/cli/src/bin/utils/getEndpointUrl.ts new file mode 100644 index 000000000..a0291be2b --- /dev/null +++ b/packages/cli/src/bin/utils/getEndpointUrl.ts @@ -0,0 +1,50 @@ +export type FusionEnv = 'ci' | 'fqa' | 'tr' | 'fprd'; + +/** + * Retreive full endpoint URI to env in service-discovery + * @param endpoint The endpoint to call in+ uri + * @param fusionEnv The Fusion env to get uri for + * @param service Custom service uri to use insted of Fusion + * @param version The version of the api to use + * @returns The uri with endpoint + */ +export const getEndpointUrl = async ( + endpoint: string, + fusionEnv: FusionEnv, + service: string, + version: string = '1.0', +): Promise => { + const { CUSTOM_APPAPI, FUSION_CLI_ENV, FUSION_TOKEN } = process.env; + + /* use consumer provided api url */ + if (service || CUSTOM_APPAPI) { + return service ?? CUSTOM_APPAPI; + } + + /* Env has changed get new api url */ + if (FUSION_CLI_ENV !== fusionEnv || !process.env.FUSION_CLI_APPAPI) { + process.env.FUSION_CLI_ENV = fusionEnv; + + const requestService = await fetch( + `https://discovery.ci.fusion-dev.net/service-registry/environments/${fusionEnv}/services/apps`, + { + headers: { + Authorization: `Bearer ${FUSION_TOKEN}`, + }, + }, + ); + + if (!requestService.ok) { + throw new Error(`${requestService.status}`); + } + + const responseService = await requestService.json(); + process.env.FUSION_CLI_APPAPI = responseService.uri; + } + + const uri = new URL(`${process.env.FUSION_CLI_APPAPI}/${endpoint}`); + uri.searchParams.set('api-version', version); + + /* return fresh/cached endpoint url */ + return uri.href; +}; diff --git a/packages/cli/src/bin/utils/index.ts b/packages/cli/src/bin/utils/index.ts new file mode 100644 index 000000000..1b0aef8d1 --- /dev/null +++ b/packages/cli/src/bin/utils/index.ts @@ -0,0 +1,18 @@ +export { getEndpointUrl } from './getEndpointUrl.js'; +export type { FusionEnv } from './getEndpointUrl.js'; +export { requireValidToken } from './requireValidToken.js'; +export { isAppRegistered } from './isAppRegistered.js'; + +export { loadAppConfig } from './load-app-config.js'; +export { loadAppManifest } from './load-manifest.js'; +export { loadPackage } from './load-package.js'; +export { loadViteConfig } from './load-vite-config.js'; + +export { formatPath, formatByteSize } from './format.js'; +export { executeCommand } from './execute-command.js'; + +export { publishAppConfig } from './publishAppConfig.js'; +export { tagAppBundle } from './tagAppBundle.js'; +export { uploadAppBundle } from './uploadAppBundle.js'; + +export { Spinner } from './spinner.js'; diff --git a/packages/cli/src/bin/utils/isAppRegistered.ts b/packages/cli/src/bin/utils/isAppRegistered.ts new file mode 100644 index 000000000..c6de30a67 --- /dev/null +++ b/packages/cli/src/bin/utils/isAppRegistered.ts @@ -0,0 +1,27 @@ +/** + * Make sure the app is registerred in the app-service + * @param endpoint The endpoint to make a call to + * @param appKey The appkey to check for + * @returns response object as json + */ +export const isAppRegistered = async (endpoint: string, appKey: string) => { + const requestApp = await fetch(endpoint, { + headers: { + Authorization: `Bearer ${process.env.FUSION_TOKEN}`, + }, + }); + + if (requestApp.status === 404) { + throw new Error( + `The appkey '${appKey}' is not registered, visit the app-admin app and register the application there.`, + ); + } + + if (!requestApp.ok) { + throw new Error( + `Could not connect to apps-service. HTTP status ${requestApp.status}, ${requestApp.statusText}`, + ); + } + + return await requestApp.json(); +}; diff --git a/packages/cli/src/bin/utils/publishAppConfig.ts b/packages/cli/src/bin/utils/publishAppConfig.ts new file mode 100644 index 000000000..4823a0d77 --- /dev/null +++ b/packages/cli/src/bin/utils/publishAppConfig.ts @@ -0,0 +1,31 @@ +import type { AppConfig } from '@equinor/fusion-framework-module-app'; + +/** + * Publishes app config to the apps-service endpoint + * @param endpoint string The endpoint to upload to + * @param appKey The application key + * @param config Object with app config + * @returns HTTP response as json + */ +export const publishAppConfig = async (endpoint: string, appKey: string, config: AppConfig) => { + const requestConfig = await fetch(endpoint, { + method: 'PUT', + body: JSON.stringify(config), + headers: { + Authorization: `Bearer ${process.env.FUSION_TOKEN}`, + 'Content-Type': 'application/json', + }, + }); + + if (requestConfig.status === 410) { + throw new Error( + `App ${appKey} is deleted from apps-service. HTTP status ${requestConfig.status}, ${requestConfig.statusText}`, + ); + } else if (!requestConfig.ok || requestConfig.status > 399) { + throw new Error( + `Failed to upload config. HTTP status ${requestConfig.status}, ${requestConfig.statusText}`, + ); + } + + return await requestConfig.json(); +}; diff --git a/packages/cli/src/bin/utils/requireValidToken.ts b/packages/cli/src/bin/utils/requireValidToken.ts new file mode 100644 index 000000000..0058cf34e --- /dev/null +++ b/packages/cli/src/bin/utils/requireValidToken.ts @@ -0,0 +1,23 @@ +import { FusionEnv, getEndpointUrl } from './index.js'; + +/** + * Make sure the user has a valid azure token. + * @param env The environment to validate token in + */ +export const requireValidToken = async (env: FusionEnv) => { + if (!process?.env?.FUSION_TOKEN) { + throw new Error( + 'Missing required environment variable FUSION_TOKEN. Please set it before running this command.', + ); + } + + try { + await getEndpointUrl('apps', env, ''); + } catch (e) { + const err = e as Error; + if (err.message === '401') { + throw new Error(`😞 FUSION_TOKEN is not valid in environment : ${env}`); + } + throw new Error('😞 Failed to call service-discovery while validating FUSION_TOKEN'); + } +}; diff --git a/packages/cli/src/bin/utils/tagAppBundle.ts b/packages/cli/src/bin/utils/tagAppBundle.ts new file mode 100644 index 000000000..4b6a1d6e6 --- /dev/null +++ b/packages/cli/src/bin/utils/tagAppBundle.ts @@ -0,0 +1,24 @@ +/** + * Send request to apps-service to tag a bundle. + * @param endpoint string The endpoint to send request to. + * @param version string The version to tag the bundle with. + * @returns Response object as json. + */ +export const tagAppBundle = async (endpoint: string, version: string) => { + const requestTag = await fetch(endpoint, { + method: 'PUT', + body: JSON.stringify({ version }), + headers: { + Authorization: `Bearer ${process.env.FUSION_TOKEN}`, + 'Content-Type': 'application/json', + }, + }); + + if (requestTag.status !== 200) { + throw new Error( + `Failed to tag bundle. HTTP status ${requestTag.status}, ${requestTag.statusText}`, + ); + } + + return await requestTag.json(); +}; diff --git a/packages/cli/src/bin/utils/uploadAppBundle.ts b/packages/cli/src/bin/utils/uploadAppBundle.ts new file mode 100644 index 000000000..85a723237 --- /dev/null +++ b/packages/cli/src/bin/utils/uploadAppBundle.ts @@ -0,0 +1,57 @@ +import { readFileSync } from 'node:fs'; + +/** + * Function that uploads a zip bundle to the endpoint. + * + * @param endpoint string The endpoint to upload to + * @param bundle string The filename to upload + * @returns Object + */ +export const uploadAppBundle = async (endpoint: string, bundle: string) => { + const state: { buffer: Buffer | null } = { + buffer: null, + }; + + try { + state.buffer = readFileSync(bundle); + } catch { + throw new Error(`😞 Could not read bundle ${bundle}, does it exist?`); + } + + const requestBundle = await fetch(endpoint, { + method: 'POST', + body: state.buffer, + headers: { + Authorization: `Bearer ${process.env.FUSION_TOKEN}`, + 'Content-Type': 'application/zip', + }, + }); + + if (requestBundle.status === 401 || requestBundle.status === 403) { + throw new Error( + `This is not allowed for this role on this app. HTTP message: ${requestBundle.statusText}` + ); + } + + if (requestBundle.status === 404) { + throw new Error(`This app do not exist. HTTP message: ${requestBundle.statusText}`); + } + + if (requestBundle.status === 409) { + throw new Error( + `This version is already published. HTTP message: ${requestBundle.statusText}`, + ); + } + + if (requestBundle.status === 410) { + throw new Error(`This app is deleted. HTTP message: ${requestBundle.statusText}`); + } + + if (!requestBundle.ok) { + throw new Error( + `Failed to publish bundle. HTTP status ${requestBundle.status}, ${requestBundle.statusText}`, + ); + } + + return await requestBundle.json(); +};