mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
14 Commits
os-configu
...
droplet-cr
Author | SHA1 | Date | |
---|---|---|---|
08653aeee2 | |||
bb011ff1b9 | |||
4cd8af904e | |||
19b6d194ea | |||
bb4c2b1f82 | |||
2a8bd18e9b | |||
6265e03d80 | |||
03d59f9e2b | |||
cd7014b293 | |||
9a562ce1f4 | |||
6890e9c5b4 | |||
cb0336d1de | |||
dcbba23d80 | |||
e10a974a9f |
2
.gitignore
vendored
2
.gitignore
vendored
@ -34,3 +34,5 @@
|
||||
/package-lock.json
|
||||
/resinrc.yml
|
||||
/tmp/
|
||||
|
||||
config.json
|
||||
|
60
lib/commands/instance/index.ts
Normal file
60
lib/commands/instance/index.ts
Normal 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() {
|
||||
|
||||
}
|
||||
}
|
259
lib/commands/instance/init.ts
Normal file
259
lib/commands/instance/init.ts
Normal 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!')
|
||||
|
||||
}
|
||||
}
|
@ -232,5 +232,6 @@ See: https://git.io/JRHUW#deprecation-policy`,
|
||||
'join',
|
||||
'leave',
|
||||
'scan',
|
||||
'instance',
|
||||
];
|
||||
}
|
||||
|
37
npm-shrinkwrap.json
generated
37
npm-shrinkwrap.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
|
Reference in New Issue
Block a user