diff --git a/lib/commands/instance/index.ts b/lib/commands/instance/index.ts new file mode 100644 index 00000000..5506b191 --- /dev/null +++ b/lib/commands/instance/index.ts @@ -0,0 +1,49 @@ +/** + * @license + * Copyright 2016-2020 Balena Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { flags } from '@oclif/command'; +import Command from '../../command'; +import * as cf from '../../utils/common-flags'; +import { stripIndent } from '../../utils/lazy'; + +interface FlagsDef { + help: void; + v13: boolean; +} + +export default class InstanceCmd extends Command { + public static description = stripIndent` + Initialize a new cloud instance running balenaOS + + A config.json must first be generated using the 'balena config generate' command + `; + public static examples = ['$ balena instance init']; + + public static usage = 'instance [COMMAND]'; + + public static flags: flags.Input = { + help: cf.help, + v13: cf.v13, + }; + + public static authenticated = true; + public static primary = true; + + public async run() { + + } +} diff --git a/lib/commands/instance/init.ts b/lib/commands/instance/init.ts new file mode 100644 index 00000000..41120445 --- /dev/null +++ b/lib/commands/instance/init.ts @@ -0,0 +1,175 @@ +/** + * @license + * Copyright 2016-2020 Balena Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { IArg } from '@oclif/parser/lib/args'; +import Command from '../../command'; +import { stripIndent } from '../../utils/lazy'; +import { + applicationIdInfo, +} from '../../utils/messages'; + +import * as fs from 'fs' +import * as fetch from 'isomorphic-fetch' +import * as cf from '../../utils/common-flags'; +import { flags } from '@oclif/command'; +import { uniqueId } from 'lodash'; +import { json } from 'body-parser'; + +interface FlagsDef { + help: void; + v13: boolean; + apiKey?: string; +} + +export default class InstanceInitCmd extends Command { + public static description = stripIndent` + Initialize an instance with balenaOS. + + Initialize a device by downloading the OS image of the specified fleet + and writing it to an SD Card. + + If the --fleet option is omitted, it will be prompted for interactively. + + ${applicationIdInfo.split('\n').join('\n\t\t')} + `; + + public static examples = [ + '$ balena instance init', + '$ balena instance init --fleet MyFleet', + '$ balena instance init -f myorg/myfleet', + ]; + + public static usage = 'instance init'; + + public static args: Array> = [ + { + name: 'configFile', + description: 'the config.json file path', + required: true, + }, + ]; + + public static flags: flags.Input = { + help: cf.help, + v13: cf.v13, + apiKey: flags.string({ + description: 'digitalocean api key', + }), + }; + + public static authenticated = true; + + public async run() { + const { args: params, flags: options } = this.parse(InstanceInitCmd); + + // Check if the config file exists + console.log('Reading config file') + const exists = fs.existsSync(params.configFile) + if (!exists) { + console.log('Config file does not exist, exiting...') + return + } + + const configFile = JSON.parse(fs.readFileSync(params.configFile).toString()) + + console.log('Creating digitalocean image') + + if (!options.apiKey) { + console.log('Missing digitalocean api key, please provide with --apiKey ') + } + + console.log('Uploading image...') + let res = await fetch('https://api.digitalocean.com/v2/images', { + method: 'POST', + headers: { + 'content-type': 'application/json', + authorization: `Bearer ${options.apiKey}` + }, + body: JSON.stringify({ + name: 'balenaOS', + url: `https://api.balena-cloud.com/download?fileType=.gz&appId=${configFile.applicationID}&deviceType=qemux86-64`, + distribution: 'Unknown', + region: 'nyc1', + description: 'balenaOS', + tags: [ + 'balenaOS' + ] + }) + }) + console.log('Image sent.') + + let responseBody = await res.json() + const imageID = responseBody.image.id + do { + console.log('Checking image status...') + await new Promise((r) => setTimeout(() => r(null), 2000)) // Sleep for 2 seconds + res = await fetch(`https://api.digitalocean.com/v2/images/${imageID}`, { + headers: { + authorization: `Bearer ${options.apiKey}` + } + }) + responseBody = await res.json() + } while (responseBody.image.status !== 'available') + console.log('Image available.') + + console.log('Getting ssh key info') + res = await fetch('https://api.digitalocean.com/v2/account/keys', { + headers: { + authorization: `Bearer ${options.apiKey}` + } + }) + responseBody = await res.json() + + const sshKeyID = responseBody.ssh_keys[0].id + + console.log('Creating droplet...') + res = await fetch('https://api.digitalocean.com/v2/droplets', { + method: 'POST', + body: JSON.stringify({ + name: uniqueId(), + region: 'nyc1', + size: 's-2vcpu-4gb', + image: imageID, + ssh_keys: [sshKeyID], + user_data: JSON.stringify(configFile), + tags: [ + 'balenaOS' + ] + }) + }) + + responseBody = await res.json() + const createURL = responseBody.links.actions.filter((link: any) => link.rel === 'created')[0] + if (!createURL) { + console.error('Failed to get a create url!') + } + + do { + console.log('Checking droplet creation status...') + await new Promise((r) => setTimeout(() => r(null), 2000)) // Sleep for 2 seconds + res = await fetch(createURL, { + headers: { + authorization: `Bearer ${options.apiKey}` + } + }) + responseBody = await res.json() + } while (responseBody.action.status !== 'completed') + + console.log('Done! the device should show soon!') + + } +} diff --git a/lib/help.ts b/lib/help.ts index ff78d206..6f9262d5 100644 --- a/lib/help.ts +++ b/lib/help.ts @@ -232,5 +232,6 @@ See: https://git.io/JRHUW#deprecation-policy`, 'join', 'leave', 'scan', + 'instance', ]; } diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 9d41ea04..e5de0bb6 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2583,6 +2583,12 @@ "is-root": "*" } }, + "@types/isomorphic-fetch": { + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/@types/isomorphic-fetch/-/isomorphic-fetch-0.0.35.tgz", + "integrity": "sha512-DaZNUvLDCAnCTjgwxgiL1eQdxIKEpNLOlTNtAgnZc50bG2copGhRrFN9/PxPBuJe+tZVLCbQ7ls0xveXVRPkvw==", + "dev": true + }, "@types/js-yaml": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.2.tgz", @@ -10365,6 +10371,15 @@ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, + "isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "requires": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -18492,6 +18507,11 @@ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.1.1.tgz", "integrity": "sha512-Czi3fG883e96T4DLEPRvufrF2ydhOOW1+1a6c3gNjH2aIh50DNFBdfwh2AKoOf1rXvpvavAoA11Qdq9+BKjE0Q==" }, + "whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 65987a76..e6089bf8 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,7 @@ "@types/http-proxy": "^1.17.7", "@types/intercept-stdout": "^0.1.0", "@types/is-root": "^2.1.2", + "@types/isomorphic-fetch": "0.0.35", "@types/js-yaml": "^4.0.2", "@types/jsonwebtoken": "^8.5.4", "@types/klaw": "^3.0.2", @@ -244,6 +245,7 @@ "inquirer": "^7.3.3", "is-elevated": "^3.0.0", "is-root": "^2.1.0", + "isomorphic-fetch": "^3.0.0", "js-yaml": "^4.0.0", "klaw": "^3.0.0", "livepush": "^3.5.0", diff --git a/test.json b/test.json new file mode 100644 index 00000000..e69de29b