Compare commits

...

14 Commits

Author SHA1 Message Date
08653aeee2 Add waiting for created droplets and throw error if error on creation 2021-10-26 12:29:04 -07:00
bb011ff1b9 Clean up logging and add spinner
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-10-26 12:29:03 -07:00
4cd8af904e Add multiple nodes 2021-10-26 12:29:03 -07:00
19b6d194ea Add provider argument 2021-10-26 12:29:03 -07:00
bb4c2b1f82 Working name generation 2021-10-26 12:29:03 -07:00
2a8bd18e9b Make image deduplication by name work 2021-10-26 12:29:03 -07:00
6265e03d80 Add more flags 2021-10-26 12:29:03 -07:00
03d59f9e2b git ignore 2021-10-26 12:29:03 -07:00
cd7014b293 Add random droplet id, region and size flag options 2021-10-26 12:29:03 -07:00
9a562ce1f4 fix verything it works 2021-10-26 12:29:03 -07:00
6890e9c5b4 fix creation 2021-10-26 12:29:02 -07:00
cb0336d1de fix variable 2021-10-26 12:29:02 -07:00
dcbba23d80 remove import 2021-10-26 12:29:02 -07:00
e10a974a9f Initial version of command 2021-10-26 12:29:02 -07:00
7 changed files with 356 additions and 6 deletions

2
.gitignore vendored
View File

@ -34,3 +34,5 @@
/package-lock.json
/resinrc.yml
/tmp/
config.json

View File

@ -0,0 +1,60 @@
/**
* @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 { IArg } from '@oclif/parser/lib/args';
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 args: Array<IArg<any>> = [
{
name: 'provider',
description: 'the cloud provider',
required: true,
},
];
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 digitalocean'];
public static usage = 'instance [COMMAND]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
v13: cf.v13,
};
public static authenticated = true;
public static primary = true;
public async run() {
}
}

View File

@ -0,0 +1,259 @@
/**
* @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, getVisuals } 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 { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator'
function randomName() {
return uniqueNamesGenerator({
dictionaries: [adjectives, colors, animals],
separator: '-'
})
}
interface FlagsDef {
help: void;
v13: boolean;
apiKey?: string;
region?: string;
size?: string;
imageName?: string;
num?: number;
}
export default class InstanceInitCmd extends Command {
public static description = stripIndent`
Initialize a new balenaOS device in the cloud.
This will upload a balenaOS image to your specific cloud provider (if it does not already exist), create a new cloud instance, and join it to the fleet with the provided config.json
Note: depending on the instance size this can take 5-15 minutes after image upload
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena instance init digitalocean config.json --apiKey <api key>',
];
public static usage = 'instance init <provider> <config.json path>';
public static args: Array<IArg<any>> = [
{
name: 'provider',
description: 'the cloud provider: do | digitalocean | aws | gcp',
required: true,
},
{
name: 'configFile',
description: 'the config.json file path',
required: true,
},
];
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
v13: cf.v13,
apiKey: flags.string({
description: 'DigitalOcean api key',
}),
region: flags.string({
description: 'DigitalOcean region',
}),
size: flags.string({
description: 'DigitalOcean droplet size',
}),
num: flags.integer({
description: 'Number of instances to create',
}),
imageName: flags.string({
description: 'custom image name',
})
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, { configFile: string, provider: string }>(InstanceInitCmd);
if (!['do', 'digitalocean'].includes(params.provider)) {
console.error('Only DigitalOcean is supported as a provider, please use "do" or "digitalocean" as your provider positional argument.')
return
}
// 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())
const imageName = options.imageName || 'balenaOS-qemux86-64'
let skipUpload = false
let imageID = 0
let page = 1
let res
let responseBody
let images = []
const num = options.num || 1
console.log(`Checking if image '${imageName}' already exists...`)
do {
res = await fetch(`https://api.digitalocean.com/v2/images?per_page=200&page=${page}`, {
headers: {
authorization: `Bearer ${options.apiKey}`
}
})
responseBody = await res.json()
for (const image of responseBody.images) {
if (image.name === imageName) {
console.log('Image exists, skipping upload.')
skipUpload = true
imageID = image.id
break
}
}
page++
images = responseBody.images
} while (images.length === 200)
if (!skipUpload) {
if (!options.apiKey) {
console.log('DigitalOcean API key is required, please provide with --apiKey <api_key>')
}
console.log('Uploading image to DigitalOcean...')
res = await fetch('https://api.digitalocean.com/v2/images', {
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${options.apiKey}`
},
body: JSON.stringify({
name: imageName,
url: `https://api.balena-cloud.com/download?fileType=.gz&appId=1833771&deviceType=qemux86-64`,
distribution: 'Unknown',
region: options.region || 'nyc1',
description: 'balenaOS custom image',
tags: [
'balenaOS'
]
})
})
console.log('Image uploaded.')
const visuals = getVisuals();
const spinner = new visuals.Spinner(
`Waiting for image to be ready`,
);
responseBody = await res.json()
imageID = responseBody.image.id
spinner.start();
do {
// console.log('Waiting for image to be ready...')
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.')
spinner.stop();
}
console.log('Getting DigitalOcean SSH keys...')
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
const dropletNames = []
for (let i = 0; i < num; i++) {
dropletNames.push(randomName())
}
console.log('Creating DigitalOcean droplets:', dropletNames.join(', '))
res = await fetch('https://api.digitalocean.com/v2/droplets', {
method: 'POST',
body: JSON.stringify({
names: dropletNames,
region: options.region || 'nyc1',
size: options.size || 's-2vcpu-4gb',
image: imageID,
ssh_keys: [sshKeyID],
user_data: JSON.stringify(configFile),
tags: [
'balenaOS'
]
}),
headers: {
authorization: `Bearer ${options.apiKey}`,
'content-type': 'application/json'
}
})
responseBody = await res.json()
const visuals = getVisuals();
const spinner = new visuals.Spinner(
`Waiting for droplets to be created`,
);
spinner.start();
// God tier code incoming
await Promise.all(responseBody.links.actions.map(async (action: any) => {
let respBody: any
do {
await new Promise((r) => setTimeout(() => r(null), 2000)) // Sleep for 2 seconds
const waitResp = await fetch(action.href, {
headers: {
authorization: `Bearer ${options.apiKey}`
}
})
respBody = await waitResp.json()
if (respBody.action.status === 'errored') {
throw new Error('Error creating droplet')
}
} while (respBody.action.status !== 'completed')
}))
spinner.stop();
console.log('Done! The device should appear in your Dashboard in a few minutes!')
}
}

View File

@ -232,5 +232,6 @@ See: https://git.io/JRHUW#deprecation-policy`,
'join',
'leave',
'scan',
'instance',
];
}

37
npm-shrinkwrap.json generated
View File

@ -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",
@ -12136,6 +12151,12 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"nanoid": {
"version": "3.1.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
"integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==",
"dev": true
},
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@ -12418,12 +12439,6 @@
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
"integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw=="
},
"nanoid": {
"version": "3.1.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
"integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==",
"dev": true
},
"nanomatch": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@ -18077,6 +18092,11 @@
"set-value": "^2.0.1"
}
},
"unique-names-generator": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/unique-names-generator/-/unique-names-generator-4.6.0.tgz",
"integrity": "sha512-m0fke1emBeT96UYn2psPQYwljooDWRTKt9oUZ5vlt88ZFMBGxqwPyLHXwCfkbgdm8jzioCp7oIpo6KdM+fnUlQ=="
},
"unique-stream": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz",
@ -18492,6 +18512,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",

View File

@ -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",
@ -281,6 +283,7 @@
"through2": "^2.0.3",
"tmp": "^0.2.1",
"typed-error": "^3.2.1",
"unique-names-generator": "^4.6.0",
"update-notifier": "^4.1.0",
"which": "^2.0.2",
"window-size": "^1.1.0"

0
test.json Normal file
View File