-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add d2-app-scripts deploy command (#451)
This adds the d2-app-script deploy command. It takes an existing application bundle (created with d2-app-scripts build) and pushes it to a running DHIS2 instance.
- Loading branch information
Showing
10 changed files
with
410 additions
and
100 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
const { reporter, chalk } = require('@dhis2/cli-helpers-engine') | ||
const path = require('path') | ||
const fs = require('fs-extra') | ||
const FormData = require('form-data') | ||
const inquirer = require('inquirer') | ||
|
||
const makePaths = require('../lib/paths') | ||
const parseConfig = require('../lib/parseConfig') | ||
const { createClient } = require('../lib/httpClient') | ||
const { constructAppUrl } = require('../lib/constructAppUrl') | ||
|
||
const dumpHttpError = (message, response) => { | ||
if (!response) { | ||
reporter.error(message) | ||
return | ||
} | ||
|
||
reporter.error( | ||
message, | ||
response.status, | ||
typeof response.data === 'object' | ||
? response.data.message | ||
: response.statusText | ||
) | ||
reporter.debugErr('Error details', response.data) | ||
} | ||
|
||
const promptForDhis2Config = async params => { | ||
if ( | ||
process.env.CI && | ||
(!params.baseUrl || !params.username || !params.password) | ||
) { | ||
reporter.error( | ||
'Prompt disabled in CI mode - missing baseUrl, username, or password parameter.' | ||
) | ||
process.exit(1) | ||
} | ||
|
||
const isValidUrl = input => | ||
input && input.length && input.match(/^https?:\/\/[^/.]+/) | ||
|
||
const responses = await inquirer.prompt([ | ||
{ | ||
type: 'input', | ||
name: 'baseUrl', | ||
message: 'DHIS2 instance URL:', | ||
when: () => !params.baseUrl, | ||
validate: input => | ||
isValidUrl(input) | ||
? true | ||
: 'Please enter a valid URL, it must start with http:https:// or https://', | ||
}, | ||
{ | ||
type: 'input', | ||
name: 'username', | ||
message: 'DHIS2 instance username:', | ||
when: () => !params.username, | ||
}, | ||
{ | ||
type: 'password', | ||
name: 'password', | ||
message: 'DHIS2 instance password:', | ||
when: () => !params.password, | ||
}, | ||
]) | ||
|
||
return { | ||
baseUrl: responses.baseUrl || params.baseUrl, | ||
auth: { | ||
username: responses.username || params.username, | ||
password: responses.password || params.password, | ||
}, | ||
} | ||
} | ||
|
||
const handler = async ({ cwd = process.cwd(), ...params }) => { | ||
const paths = makePaths(cwd) | ||
const config = parseConfig(paths) | ||
|
||
const dhis2Config = await promptForDhis2Config(params) | ||
|
||
if (config.standalone) { | ||
reporter.error(`Standalone apps cannot be deployed to DHIS2 instances`) | ||
process.exit(1) | ||
} | ||
|
||
const appBundle = path.relative( | ||
cwd, | ||
paths.buildAppBundle | ||
.replace(/{{name}}/, config.name) | ||
.replace(/{{version}}/, config.version) | ||
) | ||
|
||
if (!fs.existsSync(appBundle)) { | ||
reporter.error( | ||
`App bundle does not exist, run ${chalk.bold( | ||
'd2-app-scripts build' | ||
)} before deploying.` | ||
) | ||
process.exit(1) | ||
} | ||
|
||
const client = createClient(dhis2Config) | ||
const formData = new FormData() | ||
formData.append('file', fs.createReadStream(appBundle)) | ||
|
||
let serverVersion | ||
try { | ||
reporter.print(`Pinging server ${dhis2Config.baseUrl}...`) | ||
const rawServerVersion = (await client.get('/api/system/info.json')) | ||
.data.version | ||
const parsedServerVersion = /(\d+)\.(\d+)/.exec(rawServerVersion) | ||
if (!parsedServerVersion) { | ||
reporter.error( | ||
`Invalid server version ${rawServerVersion} found, aborting...` | ||
) | ||
process.exit(1) | ||
} | ||
serverVersion = { | ||
full: parsedServerVersion[0], | ||
major: parsedServerVersion[1], | ||
minor: parsedServerVersion[2], | ||
} | ||
reporter.debug( | ||
'Found server version', | ||
serverVersion.full, | ||
`(${rawServerVersion})` | ||
) | ||
} catch (e) { | ||
dumpHttpError( | ||
`Server ${chalk.bold(dhis2Config.baseUrl)} could not be contacted`, | ||
e.response | ||
) | ||
process.exit(1) | ||
} | ||
|
||
const appUrl = constructAppUrl(dhis2Config.baseUrl, config, serverVersion) | ||
|
||
try { | ||
reporter.print('Uploading app bundle...') | ||
await client.post('/api/apps', formData, { | ||
headers: { | ||
...formData.getHeaders(), | ||
}, | ||
timeout: 30000, // Ensure we have enough time to upload a large zip file | ||
}) | ||
reporter.info( | ||
`Successfully deployed ${config.name} to ${dhis2Config.baseUrl}` | ||
) | ||
} catch (e) { | ||
dumpHttpError('Failed to upload app, HTTP error', e.response) | ||
process.exit(1) | ||
} | ||
|
||
reporter.debug(`Testing app launch url at ${appUrl}...`) | ||
try { | ||
await client.get(appUrl) | ||
} catch (e) { | ||
dumpHttpError(`Uploaded app not responding at ${appUrl}`) | ||
process.exit(1) | ||
} | ||
reporter.print(`App is available at ${appUrl}`) | ||
} | ||
|
||
const command = { | ||
command: 'deploy [baseUrl]', | ||
alias: 'd', | ||
desc: 'Deploy the built application to a specific DHIS2 instance', | ||
builder: { | ||
username: { | ||
alias: 'u', | ||
description: | ||
'The username for authenticating with the DHIS2 instance', | ||
}, | ||
}, | ||
handler, | ||
} | ||
|
||
module.exports = command |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
module.exports.constructAppUrl = (baseUrl, config, serverVersion) => { | ||
let appUrl = baseUrl | ||
|
||
const isModernServer = serverVersion.major >= 2 && serverVersion.minor >= 35 | ||
|
||
// From core version 2.35, short_name is used instead of the human-readable title to generate the url slug | ||
const urlSafeAppSlug = (isModernServer ? config.name : config.title) | ||
.replace(/[^A-Za-z0-9\s-]/g, '') | ||
.replace(/\s+/g, '-') | ||
|
||
// From core version 2.35, core apps are hosted at the server root under the /dhis-web-* namespace | ||
if (config.coreApp && isModernServer) { | ||
appUrl += `/dhis-web-${urlSafeAppSlug}/` | ||
} else { | ||
appUrl += `/api/apps/${urlSafeAppSlug}/` | ||
} | ||
|
||
// Prior to core version 2.35, installed applications did not properly serve "pretty" urls (`/` vs `/index.html`) | ||
if (!isModernServer) { | ||
appUrl += 'index.html' | ||
} | ||
|
||
// Clean up any double slashes | ||
const scheme = appUrl.substr(0, appUrl.indexOf(':https://') + 3) | ||
appUrl = scheme + appUrl.substr(scheme.length).replace(/\/+/g, '/') | ||
return appUrl | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
const axios = require('axios').default | ||
|
||
module.exports.createClient = ({ baseUrl, auth, ...options }) => { | ||
return axios.create({ | ||
baseURL: baseUrl, | ||
auth: auth, | ||
...options, | ||
}) | ||
} |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# d2 app scripts deploy | ||
|
||
Deploys a built application bundle to a running DHIS2 instance. | ||
|
||
Note that you must run `d2 app scripts build` **before** running `deploy` | ||
|
||
This command will prompt the user for the URL of the DHIS2 instance as well as a username and password to authenticate with that instance. The URL can be passed optionally as the first positional argument to the command, and the username with the `--username` or `-u` option. The password can be specified with the `D2_PASSWORD` environment variable. For example, the following will deploy the app without waiting for user input. | ||
|
||
```sh | ||
> d2 app scripts build | ||
> export D2_PASSWORD=district | ||
> d2 app scripts deploy https://play.dhis2.org/dev --username admin | ||
``` | ||
|
||
## Usage | ||
|
||
```sh | ||
> d2 app scripts deploy --help | ||
d2-app-scripts deploy [baseUrl] | ||
|
||
Deploy the built application to a specific DHIS2 instance | ||
|
||
Options: | ||
--cwd working directory to use (defaults to cwd) | ||
--version Show version number [boolean] | ||
--config Path to JSON config file | ||
--username, -u The username for authenticating with the DHIS2 instance | ||
-h, --help Show help [boolean] | ||
``` |
This file contains 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
Oops, something went wrong.