mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
46 Commits
add-update
...
v16.7.9
Author | SHA1 | Date | |
---|---|---|---|
1671e46d99 | |||
507333c463 | |||
8b320d3e9e | |||
e1be268749 | |||
1a0019e6d0 | |||
e79cdb671f | |||
f38e643cf0 | |||
b8e190cd1d | |||
9cca654bd5 | |||
35177e2d2f | |||
1a24b193e7 | |||
272915192b | |||
96774f4c52 | |||
a034f585ba | |||
365d95c36b | |||
c6313c08ae | |||
f5764c4659 | |||
aff094575b | |||
4aaaf64f8d | |||
7b88ce273f | |||
b011af89ad | |||
1bf8c1bfe7 | |||
2b39d5d111 | |||
98663af7f6 | |||
5628824bee | |||
d12d7996bc | |||
0dcf4cbff6 | |||
884e37d242 | |||
f4a24e26c3 | |||
122eccf3dc | |||
bd598788dc | |||
406482b4da | |||
a381c97ca9 | |||
8ce78ba33c | |||
f53f148c89 | |||
0086feb645 | |||
4ee55b049f | |||
90c6f121cc | |||
d3c27ae859 | |||
8f39c1de6c | |||
4df1831187 | |||
2bce761ace | |||
d78b76aceb | |||
f07f6b84d4 | |||
d297a10570 | |||
9d0b82122a |
4
.gitattributes
vendored
4
.gitattributes
vendored
@ -4,6 +4,10 @@
|
||||
*.* -eol
|
||||
|
||||
*.sh text eol=lf
|
||||
.dockerignore eol=lf
|
||||
Dockerfile eol=lf
|
||||
Dockerfile.* eol=lf
|
||||
* text=auto eol=lf
|
||||
|
||||
# lf for the docs as it's auto-generated and will otherwise trigger an uncommited error on windows
|
||||
docs/balena-cli.md text eol=lf
|
||||
|
2
.github/actions/test/action.yml
vendored
2
.github/actions/test/action.yml
vendored
@ -49,7 +49,7 @@ runs:
|
||||
|
||||
- name: Compress custom source
|
||||
shell: pwsh
|
||||
run: tar -acf ${{ runner.temp }}/custom.tgz .
|
||||
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
|
||||
|
||||
- name: Upload custom artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
|
2
.github/workflows/flowzone.yml
vendored
2
.github/workflows/flowzone.yml
vendored
@ -11,7 +11,7 @@ on:
|
||||
jobs:
|
||||
flowzone:
|
||||
name: Flowzone
|
||||
uses: product-os/flowzone/.github/workflows/flowzone.yml@v4.7.1
|
||||
uses: product-os/flowzone/.github/workflows/flowzone.yml@master
|
||||
# prevent duplicate workflow executions for pull_request and pull_request_target
|
||||
if: |
|
||||
(
|
||||
|
File diff suppressed because it is too large
Load Diff
1223
CHANGELOG.md
1223
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -1276,7 +1276,6 @@ Examples:
|
||||
$ balena envs --fleet myorg/myfleet
|
||||
$ balena envs --fleet MyFleet --json
|
||||
$ balena envs --fleet MyFleet --service MyService
|
||||
$ balena envs --fleet MyFleet --service MyService
|
||||
$ balena envs --fleet MyFleet --config
|
||||
$ balena envs --device 7cf02a6
|
||||
$ balena envs --device 7cf02a6 --json
|
||||
|
@ -62,7 +62,7 @@ export default class ApiKeysCmd extends Command {
|
||||
$select: 'actor',
|
||||
})
|
||||
).actor
|
||||
: await getBalenaSdk().auth.getUserActorId();
|
||||
: await getBalenaSdk().auth.getActorId();
|
||||
const keys = await getBalenaSdk().pine.get({
|
||||
resource: 'api_key',
|
||||
options: {
|
||||
|
@ -18,19 +18,9 @@
|
||||
import { flags } from '@oclif/command';
|
||||
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
organization?: string;
|
||||
type?: string; // application device type
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
import { stripIndent } from '../../utils/lazy';
|
||||
import { ArgsDef, FlagsDef } from '../../utils/application-create';
|
||||
|
||||
export default class AppCreateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
@ -90,61 +80,8 @@ export default class AppCreateCmd extends Command {
|
||||
AppCreateCmd,
|
||||
);
|
||||
|
||||
// Ascertain device type
|
||||
const deviceType =
|
||||
options.type ||
|
||||
(await (await import('../../utils/patterns')).selectDeviceType());
|
||||
|
||||
// Ascertain organization
|
||||
const organization =
|
||||
options.organization?.toLowerCase() || (await this.getOrganization());
|
||||
|
||||
// Create application
|
||||
try {
|
||||
const application = await getBalenaSdk().models.application.create({
|
||||
name: params.name,
|
||||
deviceType,
|
||||
organization,
|
||||
applicationClass: 'app',
|
||||
});
|
||||
|
||||
// Output
|
||||
console.log(
|
||||
`App created: slug "${application.slug}", device type "${deviceType}"`,
|
||||
);
|
||||
} catch (err) {
|
||||
if ((err.message || '').toLowerCase().includes('unique')) {
|
||||
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
|
||||
throw new ExpectedError(
|
||||
`Error: An app or block or fleet with the name "${params.name}" already exists in organization "${organization}".`,
|
||||
);
|
||||
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
|
||||
// BalenaRequestError: Request error: Unauthorized
|
||||
throw new ExpectedError(
|
||||
`Error: You are not authorized to create apps in organization "${organization}".`,
|
||||
);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async getOrganization() {
|
||||
const { getOwnOrganizations } = await import('../../utils/sdk');
|
||||
const organizations = await getOwnOrganizations(getBalenaSdk(), {
|
||||
$select: ['name', 'handle'],
|
||||
});
|
||||
|
||||
if (organizations.length === 0) {
|
||||
// User is not a member of any organizations (should not happen).
|
||||
throw new Error('This account is not a member of any organizations');
|
||||
} else if (organizations.length === 1) {
|
||||
// User is a member of only one organization - use this.
|
||||
return organizations[0].handle;
|
||||
} else {
|
||||
// User is a member of multiple organizations -
|
||||
const { selectOrganization } = await import('../../utils/patterns');
|
||||
return selectOrganization(organizations);
|
||||
}
|
||||
await (
|
||||
await import('../../utils/application-create')
|
||||
).applicationCreateBase('app', options, params);
|
||||
}
|
||||
}
|
||||
|
@ -18,9 +18,8 @@
|
||||
import { flags } from '@oclif/command';
|
||||
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
organization?: string;
|
||||
@ -90,61 +89,8 @@ export default class BlockCreateCmd extends Command {
|
||||
BlockCreateCmd,
|
||||
);
|
||||
|
||||
// Ascertain device type
|
||||
const deviceType =
|
||||
options.type ||
|
||||
(await (await import('../../utils/patterns')).selectDeviceType());
|
||||
|
||||
// Ascertain organization
|
||||
const organization =
|
||||
options.organization?.toLowerCase() || (await this.getOrganization());
|
||||
|
||||
// Create application
|
||||
try {
|
||||
const application = await getBalenaSdk().models.application.create({
|
||||
name: params.name,
|
||||
deviceType,
|
||||
organization,
|
||||
applicationClass: 'block',
|
||||
});
|
||||
|
||||
// Output
|
||||
console.log(
|
||||
`Block created: slug "${application.slug}", device type "${deviceType}"`,
|
||||
);
|
||||
} catch (err) {
|
||||
if ((err.message || '').toLowerCase().includes('unique')) {
|
||||
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
|
||||
throw new ExpectedError(
|
||||
`Error: An app or block or fleet with the name "${params.name}" already exists in organization "${organization}".`,
|
||||
);
|
||||
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
|
||||
// BalenaRequestError: Request error: Unauthorized
|
||||
throw new ExpectedError(
|
||||
`Error: You are not authorized to create blocks in organization "${organization}".`,
|
||||
);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async getOrganization() {
|
||||
const { getOwnOrganizations } = await import('../../utils/sdk');
|
||||
const organizations = await getOwnOrganizations(getBalenaSdk(), {
|
||||
$select: ['name', 'handle'],
|
||||
});
|
||||
|
||||
if (organizations.length === 0) {
|
||||
// User is not a member of any organizations (should not happen).
|
||||
throw new Error('This account is not a member of any organizations');
|
||||
} else if (organizations.length === 1) {
|
||||
// User is a member of only one organization - use this.
|
||||
return organizations[0].handle;
|
||||
} else {
|
||||
// User is a member of multiple organizations -
|
||||
const { selectOrganization } = await import('../../utils/patterns');
|
||||
return selectOrganization(organizations);
|
||||
}
|
||||
await (
|
||||
await import('../../utils/application-create')
|
||||
).applicationCreateBase('block', options, params);
|
||||
}
|
||||
}
|
||||
|
@ -346,9 +346,9 @@ ${dockerignoreHelp}
|
||||
);
|
||||
logger.logWarn(msg);
|
||||
|
||||
const [token, username, url, options] = await Promise.all([
|
||||
const [token, { username }, url, options] = await Promise.all([
|
||||
sdk.auth.getToken(),
|
||||
sdk.auth.whoami(),
|
||||
sdk.auth.getUserInfo(),
|
||||
sdk.settings.get('balenaUrl'),
|
||||
{
|
||||
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
||||
@ -371,8 +371,8 @@ ${dockerignoreHelp}
|
||||
$select: ['commit'],
|
||||
});
|
||||
} else {
|
||||
const [userId, auth, apiEndpoint] = await Promise.all([
|
||||
sdk.auth.getUserId(),
|
||||
const [{ id: userId }, auth, apiEndpoint] = await Promise.all([
|
||||
sdk.auth.getUserInfo(),
|
||||
sdk.auth.getToken(),
|
||||
sdk.settings.get('apiUrl'),
|
||||
]);
|
||||
|
@ -20,7 +20,6 @@ import type { IArg } from '@oclif/parser/lib/args';
|
||||
import type {
|
||||
BalenaSDK,
|
||||
Device,
|
||||
DeviceType,
|
||||
PineOptions,
|
||||
PineTypedResult,
|
||||
} from 'balena-sdk';
|
||||
@ -138,7 +137,6 @@ export default class DeviceMoveCmd extends Command {
|
||||
balena: BalenaSDK,
|
||||
devices: Awaited<ReturnType<typeof this.getDevices>>,
|
||||
) {
|
||||
const { getExpandedProp } = await import('../../utils/pine');
|
||||
// deduplicate the slugs
|
||||
const deviceCpuArchs = Array.from(
|
||||
new Set(
|
||||
@ -148,48 +146,44 @@ export default class DeviceMoveCmd extends Command {
|
||||
),
|
||||
);
|
||||
|
||||
const deviceTypeOptions = {
|
||||
$select: 'slug',
|
||||
$expand: {
|
||||
is_of__cpu_architecture: {
|
||||
$select: 'slug',
|
||||
},
|
||||
const allCpuArches = await balena.pine.get({
|
||||
resource: 'cpu_architecture',
|
||||
options: {
|
||||
$select: ['id', 'slug'],
|
||||
},
|
||||
} satisfies PineOptions<DeviceType>;
|
||||
const deviceTypes = (await balena.models.deviceType.getAllSupported(
|
||||
deviceTypeOptions,
|
||||
)) as Array<PineTypedResult<DeviceType, typeof deviceTypeOptions>>;
|
||||
});
|
||||
|
||||
const compatibleDeviceTypeSlugs = new Set(
|
||||
deviceTypes
|
||||
.filter((deviceType) => {
|
||||
const deviceTypeArch = getExpandedProp(
|
||||
deviceType.is_of__cpu_architecture,
|
||||
'slug',
|
||||
)!;
|
||||
return deviceCpuArchs.every((deviceCpuArch) =>
|
||||
balena.models.os.isArchitectureCompatibleWith(
|
||||
deviceCpuArch,
|
||||
deviceTypeArch,
|
||||
),
|
||||
);
|
||||
})
|
||||
.map((deviceType) => deviceType.slug),
|
||||
);
|
||||
const compatibleCpuArchIds = allCpuArches
|
||||
.filter((cpuArch) => {
|
||||
return deviceCpuArchs.every((deviceCpuArch) =>
|
||||
balena.models.os.isArchitectureCompatibleWith(
|
||||
deviceCpuArch,
|
||||
cpuArch.slug,
|
||||
),
|
||||
);
|
||||
})
|
||||
.map((deviceType) => deviceType.id);
|
||||
|
||||
const patterns = await import('../../utils/patterns');
|
||||
try {
|
||||
const application = await patterns.selectApplication(
|
||||
(app) =>
|
||||
compatibleDeviceTypeSlugs.has(app.is_for__device_type[0].slug) &&
|
||||
devices.some(
|
||||
(device) => device.belongs_to__application.__id !== app.id,
|
||||
),
|
||||
{
|
||||
is_for__device_type: {
|
||||
$any: {
|
||||
$alias: 'dt',
|
||||
$expr: {
|
||||
dt: {
|
||||
is_of__cpu_architecture: { $in: compatibleCpuArchIds },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
return application;
|
||||
} catch (err) {
|
||||
if (!compatibleDeviceTypeSlugs.size) {
|
||||
if (!compatibleCpuArchIds.length) {
|
||||
throw new ExpectedError(
|
||||
`${err.message}\nDo all devices have a compatible architecture?`,
|
||||
);
|
||||
|
1
lib/commands/env/add.ts
vendored
1
lib/commands/env/add.ts
vendored
@ -268,6 +268,7 @@ async function getServiceIdForApp(
|
||||
): Promise<number> {
|
||||
let serviceId: number | undefined;
|
||||
const services = await sdk.models.service.getAllByApplication(appSlug, {
|
||||
$select: 'id',
|
||||
$filter: { service_name: serviceName },
|
||||
});
|
||||
if (services.length > 0) {
|
||||
|
@ -93,7 +93,6 @@ export default class EnvsCmd extends Command {
|
||||
'$ balena envs --fleet myorg/myfleet',
|
||||
'$ balena envs --fleet MyFleet --json',
|
||||
'$ balena envs --fleet MyFleet --service MyService',
|
||||
'$ balena envs --fleet MyFleet --service MyService',
|
||||
'$ balena envs --fleet MyFleet --config',
|
||||
'$ balena envs --device 7cf02a6',
|
||||
'$ balena envs --device 7cf02a6 --json',
|
||||
@ -209,6 +208,7 @@ async function validateServiceName(
|
||||
fleetSlug: string,
|
||||
) {
|
||||
const services = await sdk.models.service.getAllByApplication(fleetSlug, {
|
||||
$select: 'id',
|
||||
$filter: { service_name: serviceName },
|
||||
});
|
||||
if (services.length === 0) {
|
||||
|
@ -18,19 +18,9 @@
|
||||
import { flags } from '@oclif/command';
|
||||
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
organization?: string;
|
||||
type?: string; // application device type
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
import { stripIndent } from '../../utils/lazy';
|
||||
import { ArgsDef, FlagsDef } from '../../utils/application-create';
|
||||
|
||||
export default class FleetCreateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
@ -90,60 +80,8 @@ export default class FleetCreateCmd extends Command {
|
||||
FleetCreateCmd,
|
||||
);
|
||||
|
||||
// Ascertain device type
|
||||
const deviceType =
|
||||
options.type ||
|
||||
(await (await import('../../utils/patterns')).selectDeviceType());
|
||||
|
||||
// Ascertain organization
|
||||
const organization =
|
||||
options.organization?.toLowerCase() || (await this.getOrganization());
|
||||
|
||||
// Create application
|
||||
try {
|
||||
const application = await getBalenaSdk().models.application.create({
|
||||
name: params.name,
|
||||
deviceType,
|
||||
organization,
|
||||
});
|
||||
|
||||
// Output
|
||||
console.log(
|
||||
`Fleet created: slug "${application.slug}", device type "${deviceType}"`,
|
||||
);
|
||||
} catch (err) {
|
||||
if ((err.message || '').toLowerCase().includes('unique')) {
|
||||
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
|
||||
throw new ExpectedError(
|
||||
`Error: An app or block or fleet with the name "${params.name}" already exists in organization "${organization}".`,
|
||||
);
|
||||
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
|
||||
// BalenaRequestError: Request error: Unauthorized
|
||||
throw new ExpectedError(
|
||||
`Error: You are not authorized to create fleets in organization "${organization}".`,
|
||||
);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async getOrganization() {
|
||||
const { getOwnOrganizations } = await import('../../utils/sdk');
|
||||
const organizations = await getOwnOrganizations(getBalenaSdk(), {
|
||||
$select: ['name', 'handle'],
|
||||
});
|
||||
|
||||
if (organizations.length === 0) {
|
||||
// User is not a member of any organizations (should not happen).
|
||||
throw new Error('This account is not a member of any organizations');
|
||||
} else if (organizations.length === 1) {
|
||||
// User is a member of only one organization - use this.
|
||||
return organizations[0].handle;
|
||||
} else {
|
||||
// User is a member of multiple organizations -
|
||||
const { selectOrganization } = await import('../../utils/patterns');
|
||||
return selectOrganization(organizations);
|
||||
}
|
||||
await (
|
||||
await import('../../utils/application-create')
|
||||
).applicationCreateBase('fleet', options, params);
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import { flags } from '@oclif/command';
|
||||
|
||||
import Command from '../command';
|
||||
@ -22,7 +23,7 @@ import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface ExtendedApplication extends ApplicationWithDeviceType {
|
||||
interface ExtendedApplication extends ApplicationWithDeviceTypeSlug {
|
||||
device_count: number;
|
||||
online_devices: number;
|
||||
device_type?: string;
|
||||
@ -60,15 +61,20 @@ export default class FleetsCmd extends Command {
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const pineOptions = {
|
||||
$select: ['id', 'app_name', 'slug'],
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
owns__device: { $select: 'is_online' },
|
||||
},
|
||||
} satisfies BalenaSdk.PineOptions<BalenaSdk.Application>;
|
||||
// Get applications
|
||||
const applications =
|
||||
(await balena.models.application.getAllDirectlyAccessible({
|
||||
$select: ['id', 'app_name', 'slug'],
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
owns__device: { $select: 'is_online' },
|
||||
},
|
||||
})) as ExtendedApplication[];
|
||||
(await balena.models.application.getAllDirectlyAccessible(
|
||||
pineOptions,
|
||||
)) as Array<
|
||||
BalenaSdk.PineTypedResult<BalenaSdk.Application, typeof pineOptions>
|
||||
> as ExtendedApplication[];
|
||||
|
||||
// Add extended properties
|
||||
applications.forEach((application) => {
|
||||
|
@ -137,7 +137,7 @@ export default class LoginCmd extends Command {
|
||||
console.log(`\nLogging in to ${balenaUrl}`);
|
||||
await this.doLogin(options, balenaUrl, params.token);
|
||||
|
||||
const username = await balena.auth.whoami();
|
||||
const { username } = await balena.auth.getUserInfo();
|
||||
|
||||
console.info(`Successfully logged in as: ${username}`);
|
||||
console.info(`\
|
||||
@ -165,7 +165,12 @@ ${messages.reachingOut}`);
|
||||
}
|
||||
const balena = getBalenaSdk();
|
||||
await balena.auth.loginWithToken(token!);
|
||||
if (!(await balena.auth.whoami())) {
|
||||
try {
|
||||
await balena.auth.getUserInfo();
|
||||
} catch (err) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`Get user info failed with: ${err.message}`);
|
||||
}
|
||||
throw new ExpectedError('Token authentication failed');
|
||||
}
|
||||
return;
|
||||
|
@ -204,7 +204,7 @@ export default class OsConfigureCmd extends Command {
|
||||
const helpers = await import('../../utils/helpers');
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
let app: ApplicationWithDeviceType | undefined;
|
||||
let app: ApplicationWithDeviceTypeSlug | undefined;
|
||||
let device;
|
||||
let deviceTypeSlug: string;
|
||||
|
||||
@ -223,7 +223,7 @@ export default class OsConfigureCmd extends Command {
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
},
|
||||
})) as ApplicationWithDeviceType;
|
||||
})) as ApplicationWithDeviceTypeSlug;
|
||||
await checkDeviceTypeCompatibility(options, app);
|
||||
deviceTypeSlug =
|
||||
options['device-type'] || app.is_for__device_type[0].slug;
|
||||
|
@ -46,7 +46,7 @@ interface FlagsDef extends DockerConnectionCliFlags {
|
||||
commit?: string;
|
||||
'splash-image'?: string;
|
||||
'dont-check-arch': boolean;
|
||||
'pin-device-to-release': boolean;
|
||||
'pin-device-to-release'?: boolean;
|
||||
'additional-space'?: number;
|
||||
'add-certificate'?: string[];
|
||||
help: void;
|
||||
@ -122,7 +122,7 @@ https://github.com/balena-io-examples/staged-releases\
|
||||
'disable architecture compatibility check between image and fleet',
|
||||
}),
|
||||
'pin-device-to-release': flags.boolean({
|
||||
default: false,
|
||||
allowNo: true,
|
||||
description:
|
||||
'pin the preloaded device to the preloaded release on provision',
|
||||
char: 'p',
|
||||
@ -230,7 +230,7 @@ Can be repeated to add multiple certificates.\
|
||||
const splashImage = options['splash-image'];
|
||||
const additionalSpace = options['additional-space'];
|
||||
const dontCheckArch = options['dont-check-arch'] || false;
|
||||
const pinDevice = options['pin-device-to-release'] || false;
|
||||
const pinDevice = options['pin-device-to-release'];
|
||||
|
||||
if (dontCheckArch && !fleetSlug) {
|
||||
throw new ExpectedError(
|
||||
@ -257,7 +257,7 @@ Can be repeated to add multiple certificates.\
|
||||
splashImage,
|
||||
undefined, // TODO: Currently always undefined, investigate approach in ssh command.
|
||||
dontCheckArch,
|
||||
pinDevice,
|
||||
pinDevice ?? false,
|
||||
certificates,
|
||||
additionalSpace,
|
||||
);
|
||||
@ -450,14 +450,14 @@ Can be repeated to add multiple certificates.\
|
||||
async offerToDisableAutomaticUpdates(
|
||||
application: Pick<Application, 'id' | 'should_track_latest_release'>,
|
||||
commit: string,
|
||||
pinDevice: boolean,
|
||||
pinDevice: boolean | undefined,
|
||||
) {
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
if (
|
||||
this.isCurrentCommit(commit) ||
|
||||
!application.should_track_latest_release ||
|
||||
pinDevice
|
||||
pinDevice != null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@ -476,8 +476,9 @@ through the web dashboard or programatically through the balena API / SDK.
|
||||
Documentation about release policies and pinning can be found at:
|
||||
https://www.balena.io/docs/learn/deploy/release-strategy/release-policy/
|
||||
|
||||
Alternatively, the --pin-device-to-release flag may be used to pin only the
|
||||
preloaded device to the selected release.
|
||||
Alternatively, the --pin-device-to-release or --no-pin-device-to-release flags may be used
|
||||
to avoid this interactive confirmation and pin only the preloaded device to the selected release
|
||||
or keep it unpinned respectively.
|
||||
|
||||
Would you like to disable automatic updates for this fleet now?\
|
||||
`;
|
||||
@ -511,7 +512,7 @@ Would you like to disable automatic updates for this fleet now?\
|
||||
options: {
|
||||
slug?: string;
|
||||
commit?: string;
|
||||
pinDevice: boolean;
|
||||
pinDevice?: boolean;
|
||||
},
|
||||
) {
|
||||
await preloader.prepare();
|
||||
|
@ -152,9 +152,9 @@ export default class SshCmd extends Command {
|
||||
|
||||
const { which } = await import('../utils/which');
|
||||
|
||||
const [whichProxytunnel, username, proxyUrl] = await Promise.all([
|
||||
const [whichProxytunnel, { username }, proxyUrl] = await Promise.all([
|
||||
useProxy ? which('proxytunnel', false) : undefined,
|
||||
sdk.auth.whoami(),
|
||||
sdk.auth.getUserInfo(),
|
||||
// note that `proxyUrl` refers to the balenaCloud "resin-proxy"
|
||||
// service, currently "balena-devices.com", rather than some
|
||||
// local proxy server URL
|
||||
@ -208,7 +208,7 @@ export default class SshCmd extends Command {
|
||||
port: options.port || 'cloud',
|
||||
proxyCommand,
|
||||
service: params.service,
|
||||
username: username!,
|
||||
username,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -36,11 +36,11 @@ export default class WhoamiCmd extends Command {
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const [username, email, url] = await Promise.all([
|
||||
balena.auth.whoami(),
|
||||
balena.auth.getEmail(),
|
||||
const [{ username, email }, url] = await Promise.all([
|
||||
balena.auth.getUserInfo(),
|
||||
balena.settings.get('balenaUrl'),
|
||||
]);
|
||||
|
||||
console.log(
|
||||
getVisuals().table.vertical({ username, email, url }, [
|
||||
'$account information$',
|
||||
|
@ -15,6 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { BalenaSettingsStorage } from 'balena-settings-storage';
|
||||
|
||||
export interface ReleaseTimestampsByVersion {
|
||||
[version: string]: string; // e.g. { '12.0.0': '2021-06-16T12:54:52.000Z' }
|
||||
lastFetched: string; // ISO 8601 timestamp, e.g. '2021-06-27T16:46:10.000Z'
|
||||
@ -46,7 +48,7 @@ export class DeprecationChecker {
|
||||
readonly cacheFile = 'cachedReleaseTimestamps';
|
||||
readonly now = new Date().getTime();
|
||||
private initialized = false;
|
||||
storage: ReturnType<typeof import('balena-settings-storage')>;
|
||||
storage: BalenaSettingsStorage;
|
||||
cachedTimestamps: ReleaseTimestampsByVersion;
|
||||
nextMajorVersion: string; // semver without the 'v' prefix
|
||||
|
||||
@ -63,7 +65,7 @@ export class DeprecationChecker {
|
||||
this.initialized = true;
|
||||
|
||||
const settings = await import('balena-settings-client');
|
||||
const getStorage = await import('balena-settings-storage');
|
||||
const { getStorage } = await import('balena-settings-storage');
|
||||
const dataDirectory = settings.get<string>('dataDirectory');
|
||||
this.storage = getStorage({ dataDirectory });
|
||||
let stored: ReleaseTimestampsByVersion | undefined;
|
||||
|
@ -133,7 +133,6 @@ Please use "balena ${alternative}" instead.`);
|
||||
'local stop': [removed, stopAlternative, 'v11.0.0'],
|
||||
app: [replaced, 'fleet', 'v13.0.0'],
|
||||
apps: [replaced, 'fleets', 'v13.0.0'],
|
||||
'app create': [replaced, 'fleet create', 'v13.0.0'],
|
||||
'app purge': [replaced, 'fleet purge', 'v13.0.0'],
|
||||
'app rename': [replaced, 'fleet rename', 'v13.0.0'],
|
||||
'app restart': [replaced, 'fleet restart', 'v13.0.0'],
|
||||
|
58
lib/utils/application-create.ts
Normal file
58
lib/utils/application-create.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { ExpectedError } from '../errors';
|
||||
import { getBalenaSdk } from './lazy';
|
||||
|
||||
export interface FlagsDef {
|
||||
organization?: string;
|
||||
type?: string; // application device type
|
||||
help: void;
|
||||
}
|
||||
|
||||
export interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export async function applicationCreateBase(
|
||||
resource: 'fleet' | 'app' | 'block',
|
||||
options: FlagsDef,
|
||||
params: ArgsDef,
|
||||
) {
|
||||
// Ascertain device type
|
||||
const deviceType =
|
||||
options.type || (await (await import('./patterns')).selectDeviceType());
|
||||
|
||||
// Ascertain organization
|
||||
const organization =
|
||||
options.organization?.toLowerCase() ||
|
||||
(await (await import('./patterns')).getAndSelectOrganization());
|
||||
|
||||
// Create application
|
||||
try {
|
||||
const application = await getBalenaSdk().models.application.create({
|
||||
name: params.name,
|
||||
deviceType,
|
||||
organization,
|
||||
});
|
||||
|
||||
// Output
|
||||
const { capitalize } = await import('lodash');
|
||||
console.log(
|
||||
`${capitalize(resource)} created: slug "${
|
||||
application.slug
|
||||
}", device type "${deviceType}"`,
|
||||
);
|
||||
} catch (err) {
|
||||
if ((err.message || '').toLowerCase().includes('unique')) {
|
||||
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
|
||||
throw new ExpectedError(
|
||||
`Error: An app or block or fleet with the name "${params.name}" already exists in organization "${organization}".`,
|
||||
);
|
||||
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
|
||||
// BalenaRequestError: Request error: Unauthorized
|
||||
throw new ExpectedError(
|
||||
`Error: You are not authorized to create ${resource}s in organization "${organization}".`,
|
||||
);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
@ -138,7 +138,7 @@ export async function getCachedUsername(): Promise<CachedUsername | undefined> {
|
||||
if (cachedUsername) {
|
||||
return cachedUsername;
|
||||
}
|
||||
const [{ getBalenaSdk }, getStorage, settings] = await Promise.all([
|
||||
const [{ getBalenaSdk }, { getStorage }, settings] = await Promise.all([
|
||||
import('./lazy'),
|
||||
import('balena-settings-storage'),
|
||||
import('balena-settings-client'),
|
||||
@ -167,7 +167,7 @@ export async function getCachedUsername(): Promise<CachedUsername | undefined> {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
const username = await getBalenaSdk().auth.whoami();
|
||||
const { username } = await getBalenaSdk().auth.getUserInfo();
|
||||
if (username) {
|
||||
cachedUsername = { token, username };
|
||||
await storage.set('cachedUsername', cachedUsername);
|
||||
|
@ -588,7 +588,7 @@ async function assignDockerBuildOpts(
|
||||
pull: opts.pull,
|
||||
};
|
||||
if (task.external) {
|
||||
task.dockerOpts.authconfig = await getAuthConfigObj(
|
||||
task.dockerOpts.authconfig = getAuthConfigObj(
|
||||
task.imageName!,
|
||||
opts.registrySecrets,
|
||||
);
|
||||
|
@ -19,10 +19,10 @@ import type {
|
||||
BalenaSDK,
|
||||
Device,
|
||||
Organization,
|
||||
PineFilter,
|
||||
PineOptions,
|
||||
PineTypedResult,
|
||||
} from 'balena-sdk';
|
||||
import _ = require('lodash');
|
||||
|
||||
import { instanceOf, NotLoggedInError, ExpectedError } from '../errors';
|
||||
import { getBalenaSdk, getVisuals, stripIndent, getCliForm } from './lazy';
|
||||
@ -115,22 +115,22 @@ export function askLoginType() {
|
||||
});
|
||||
}
|
||||
|
||||
export function selectDeviceType() {
|
||||
return getBalenaSdk()
|
||||
.models.config.getDeviceTypes()
|
||||
.then((deviceTypes) => {
|
||||
deviceTypes = _.sortBy(deviceTypes, 'name').filter(
|
||||
(dt) => dt.state !== 'DISCONTINUED',
|
||||
);
|
||||
return getCliForm().ask({
|
||||
message: 'Device Type',
|
||||
type: 'list',
|
||||
choices: _.map(deviceTypes, ({ slug: value, name }) => ({
|
||||
name,
|
||||
value,
|
||||
})),
|
||||
});
|
||||
});
|
||||
export async function selectDeviceType() {
|
||||
const sdk = getBalenaSdk();
|
||||
let deviceTypes = await sdk.models.deviceType.getAllSupported();
|
||||
if (deviceTypes.length === 0) {
|
||||
// Without this open-balena users would get an empty list
|
||||
// until we add a hostApps import in open-balena.
|
||||
deviceTypes = await sdk.models.deviceType.getAll();
|
||||
}
|
||||
return getCliForm().ask({
|
||||
message: 'Device Type',
|
||||
type: 'list',
|
||||
choices: deviceTypes.map(({ slug: value, name }) => ({
|
||||
name,
|
||||
value,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -179,27 +179,32 @@ type SelectApplicationResult = PineTypedResult<
|
||||
>;
|
||||
|
||||
export async function selectApplication(
|
||||
filter?: (app: SelectApplicationResult) => boolean,
|
||||
filter?:
|
||||
| PineFilter<Application>
|
||||
| ((app: SelectApplicationResult) => boolean),
|
||||
errorOnEmptySelection = false,
|
||||
) {
|
||||
const balena = getBalenaSdk();
|
||||
const apps = (await balena.models.application.getAllDirectlyAccessible(
|
||||
selectApplicationPineOptions,
|
||||
)) as SelectApplicationResult[];
|
||||
let apps = (await balena.models.application.getAllDirectlyAccessible({
|
||||
...selectApplicationPineOptions,
|
||||
...(filter != null && typeof filter === 'object' && { $filter: filter }),
|
||||
})) as SelectApplicationResult[];
|
||||
|
||||
if (!apps.length) {
|
||||
throw new ExpectedError('No fleets found');
|
||||
}
|
||||
|
||||
const applications = filter ? apps.filter(filter) : apps;
|
||||
if (filter != null && typeof filter === 'function') {
|
||||
apps = apps.filter(filter);
|
||||
}
|
||||
|
||||
if (errorOnEmptySelection && applications.length === 0) {
|
||||
if (errorOnEmptySelection && apps.length === 0) {
|
||||
throw new ExpectedError('No suitable fleets found for selection');
|
||||
}
|
||||
return getCliForm().ask({
|
||||
message: 'Select an application',
|
||||
type: 'list',
|
||||
choices: _.map(applications, (application) => ({
|
||||
choices: apps.map((application) => ({
|
||||
name: `${application.app_name} (${application.slug}) [${application.is_for__device_type[0].slug}]`,
|
||||
value: application,
|
||||
})),
|
||||
@ -223,6 +228,24 @@ export async function selectOrganization(
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAndSelectOrganization() {
|
||||
const { getOwnOrganizations } = await import('./sdk');
|
||||
const organizations = await getOwnOrganizations(getBalenaSdk(), {
|
||||
$select: ['name', 'handle'],
|
||||
});
|
||||
|
||||
if (organizations.length === 0) {
|
||||
// User is not a member of any organizations (should not happen).
|
||||
throw new Error('This account is not a member of any organizations');
|
||||
} else if (organizations.length === 1) {
|
||||
// User is a member of only one organization - use this.
|
||||
return organizations[0].handle;
|
||||
} else {
|
||||
// User is a member of multiple organizations -
|
||||
return selectOrganization(organizations);
|
||||
}
|
||||
}
|
||||
|
||||
export async function awaitDeviceOsUpdate(
|
||||
uuid: string,
|
||||
targetOsVersion: string,
|
||||
@ -338,7 +361,7 @@ export async function getOnlineTargetDeviceUuid(
|
||||
const devices = application.owns__device;
|
||||
|
||||
// Throw if no devices online
|
||||
if (_.isEmpty(devices)) {
|
||||
if (!devices.length) {
|
||||
throw new ExpectedError(
|
||||
`Fleet ${application.slug} found, but has no devices online.`,
|
||||
);
|
||||
@ -349,7 +372,7 @@ export async function getOnlineTargetDeviceUuid(
|
||||
message: `Select a device on fleet ${application.slug}`,
|
||||
type: 'list',
|
||||
default: devices[0].uuid,
|
||||
choices: _.map(devices, (device) => ({
|
||||
choices: devices.map((device) => ({
|
||||
name: `${device.device_name || 'Untitled'} (${device.uuid.slice(0, 7)})`,
|
||||
value: device.uuid,
|
||||
})),
|
||||
@ -363,7 +386,7 @@ export function selectFromList<T>(
|
||||
return getCliForm().ask<T>({
|
||||
message,
|
||||
type: 'list',
|
||||
choices: _.map(choices, (s) => ({
|
||||
choices: choices.map((s) => ({
|
||||
name: s.name,
|
||||
value: s,
|
||||
})),
|
||||
|
@ -228,8 +228,8 @@ async function selectLocalDevice(): Promise<string> {
|
||||
}
|
||||
|
||||
async function selectAppFromList(
|
||||
applications: ApplicationWithDeviceType[],
|
||||
): Promise<ApplicationWithDeviceType> {
|
||||
applications: ApplicationWithDeviceTypeSlug[],
|
||||
): Promise<ApplicationWithDeviceTypeSlug> {
|
||||
const _ = await import('lodash');
|
||||
const { selectFromList } = await import('../utils/patterns');
|
||||
|
||||
@ -247,7 +247,7 @@ async function getOrSelectApplication(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
deviceTypeSlug: string,
|
||||
appName?: string,
|
||||
): Promise<ApplicationWithDeviceType> {
|
||||
): Promise<ApplicationWithDeviceTypeSlug> {
|
||||
const pineOptions = {
|
||||
$select: 'slug',
|
||||
$expand: {
|
||||
@ -256,51 +256,72 @@ async function getOrSelectApplication(
|
||||
},
|
||||
},
|
||||
} satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>;
|
||||
const [deviceType, allDeviceTypes] = await Promise.all([
|
||||
sdk.models.deviceType.get(deviceTypeSlug, pineOptions) as Promise<
|
||||
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
|
||||
>,
|
||||
sdk.models.deviceType.getAllSupported(pineOptions) as Promise<
|
||||
Array<BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>>
|
||||
>,
|
||||
]);
|
||||
const deviceType = (await sdk.models.deviceType.get(
|
||||
deviceTypeSlug,
|
||||
pineOptions,
|
||||
)) as BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>;
|
||||
const allCpuArches = await sdk.pine.get({
|
||||
resource: 'cpu_architecture',
|
||||
options: {
|
||||
$select: ['id', 'slug'],
|
||||
},
|
||||
});
|
||||
|
||||
const compatibleDeviceTypes = allDeviceTypes
|
||||
.filter((dt) =>
|
||||
const compatibleCpuArchIds = allCpuArches
|
||||
.filter((cpuArch) =>
|
||||
sdk.models.os.isArchitectureCompatibleWith(
|
||||
deviceType.is_of__cpu_architecture[0].slug,
|
||||
dt.is_of__cpu_architecture[0].slug,
|
||||
cpuArch.slug,
|
||||
),
|
||||
)
|
||||
.map((type) => type.slug);
|
||||
.map((cpu) => cpu.id);
|
||||
|
||||
if (!appName) {
|
||||
return createOrSelectApp(sdk, compatibleDeviceTypes, deviceTypeSlug);
|
||||
return createOrSelectApp(
|
||||
sdk,
|
||||
{
|
||||
is_for__device_type: {
|
||||
$any: {
|
||||
$alias: 'dt',
|
||||
$expr: {
|
||||
dt: {
|
||||
is_of__cpu_architecture: { $in: compatibleCpuArchIds },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
deviceTypeSlug,
|
||||
);
|
||||
}
|
||||
|
||||
const options: BalenaSdk.PineOptions<BalenaSdk.Application> = {
|
||||
const options = {
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
is_for__device_type: { $select: ['slug', 'is_of__cpu_architecture'] },
|
||||
},
|
||||
};
|
||||
} satisfies BalenaSdk.PineOptions<BalenaSdk.Application>;
|
||||
|
||||
// Check for a fleet slug of the form `user/fleet` and update the API query.
|
||||
let name: string;
|
||||
const match = appName.split('/');
|
||||
if (match.length > 1) {
|
||||
// These will match at most one fleet
|
||||
options.$filter = { slug: appName.toLowerCase() };
|
||||
(options as BalenaSdk.PineOptions<BalenaSdk.Application>).$filter = {
|
||||
slug: appName.toLowerCase(),
|
||||
};
|
||||
name = match[1];
|
||||
} else {
|
||||
// We're given an application; resolve it if it's ambiguous and also validate
|
||||
// it's of appropriate device type.
|
||||
options.$filter = { app_name: appName };
|
||||
(options as BalenaSdk.PineOptions<BalenaSdk.Application>).$filter = {
|
||||
app_name: appName,
|
||||
};
|
||||
name = appName;
|
||||
}
|
||||
|
||||
const applications = (await sdk.models.application.getAllDirectlyAccessible(
|
||||
options,
|
||||
)) as ApplicationWithDeviceType[];
|
||||
)) as Array<BalenaSdk.PineTypedResult<BalenaSdk.Application, typeof options>>;
|
||||
|
||||
if (applications.length === 0) {
|
||||
await confirm(
|
||||
@ -315,8 +336,11 @@ async function getOrSelectApplication(
|
||||
|
||||
// We've found at least one fleet with the given name.
|
||||
// Filter out fleets for non-matching device types and see what we're left with.
|
||||
const compatibleCpuArchIdsSet = new Set(compatibleCpuArchIds);
|
||||
const validApplications = applications.filter((app) =>
|
||||
compatibleDeviceTypes.includes(app.is_for__device_type[0].slug),
|
||||
compatibleCpuArchIdsSet.has(
|
||||
app.is_for__device_type[0].is_of__cpu_architecture.__id,
|
||||
),
|
||||
);
|
||||
|
||||
if (validApplications.length === 0) {
|
||||
@ -332,21 +356,14 @@ async function getOrSelectApplication(
|
||||
|
||||
async function createOrSelectApp(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
compatibleDeviceTypes: string[],
|
||||
compatibleDeviceTypesFilter: BalenaSdk.PineFilter<BalenaSdk.Application>,
|
||||
deviceType: string,
|
||||
): Promise<ApplicationWithDeviceType> {
|
||||
): Promise<ApplicationWithDeviceTypeSlug> {
|
||||
// No fleet specified, show a list to select one.
|
||||
const applications = (await sdk.models.application.getAllDirectlyAccessible({
|
||||
$expand: { is_for__device_type: { $select: 'slug' } },
|
||||
$filter: {
|
||||
is_for__device_type: {
|
||||
$any: {
|
||||
$alias: 'dt',
|
||||
$expr: { dt: { slug: { $in: compatibleDeviceTypes } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
})) as ApplicationWithDeviceType[];
|
||||
$filter: compatibleDeviceTypesFilter,
|
||||
})) as ApplicationWithDeviceTypeSlug[];
|
||||
|
||||
if (applications.length === 0) {
|
||||
await confirm(
|
||||
@ -366,11 +383,14 @@ async function createApplication(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
deviceType: string,
|
||||
name?: string,
|
||||
): Promise<ApplicationWithDeviceType> {
|
||||
): Promise<ApplicationWithDeviceTypeSlug> {
|
||||
const validation = await import('./validation');
|
||||
|
||||
const username = await sdk.auth.whoami();
|
||||
if (!username) {
|
||||
let username: string;
|
||||
try {
|
||||
const userInfo = await sdk.auth.getUserInfo();
|
||||
username = userInfo.username;
|
||||
} catch (err) {
|
||||
throw new sdk.errors.BalenaNotLoggedIn();
|
||||
}
|
||||
|
||||
@ -387,7 +407,7 @@ async function createApplication(
|
||||
try {
|
||||
await sdk.models.application.getDirectlyAccessible(appName, {
|
||||
$filter: {
|
||||
slug: { $startswith: `${username!.toLowerCase()}/` },
|
||||
slug: { $startswith: `${username.toLowerCase()}/` },
|
||||
},
|
||||
});
|
||||
// TODO: This is the only example in the codebase where `printErrorMessage()`
|
||||
@ -414,12 +434,12 @@ async function createApplication(
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
},
|
||||
})) as ApplicationWithDeviceType;
|
||||
})) as ApplicationWithDeviceTypeSlug;
|
||||
}
|
||||
|
||||
async function generateApplicationConfig(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
app: ApplicationWithDeviceType,
|
||||
app: ApplicationWithDeviceTypeSlug,
|
||||
options: {
|
||||
version: string;
|
||||
appUpdatePollInterval?: number;
|
||||
|
@ -105,7 +105,7 @@ export async function getOwnOrganizations(
|
||||
$alias: 'orm',
|
||||
$expr: {
|
||||
orm: {
|
||||
user: await sdk.auth.getUserId(),
|
||||
user: (await sdk.auth.getUserInfo()).id,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -51,7 +51,7 @@ export const tunnelConnectionToDevice = (
|
||||
sdk.auth.getToken(),
|
||||
]).then(([tunnelUrl, whoami, token]) => {
|
||||
const auth = {
|
||||
user: whoami || 'root',
|
||||
user: whoami?.actorType === 'user' ? whoami.username : 'root',
|
||||
password: token,
|
||||
};
|
||||
|
||||
|
908
npm-shrinkwrap.json
generated
908
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "balena-cli",
|
||||
"version": "16.6.3",
|
||||
"version": "16.7.9",
|
||||
"description": "The official balena Command Line Interface",
|
||||
"main": "./build/app.js",
|
||||
"homepage": "https://github.com/balena-io/balena-cli",
|
||||
@ -193,7 +193,7 @@
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@balena/compose": "^2.2.1",
|
||||
"@balena/compose": "^3.0.2",
|
||||
"@balena/dockerignore": "^1.0.2",
|
||||
"@balena/es-version": "^1.0.1",
|
||||
"@oclif/command": "^1.8.16",
|
||||
@ -205,12 +205,12 @@
|
||||
"balena-device-init": "^6.0.0",
|
||||
"balena-errors": "^4.7.3",
|
||||
"balena-image-fs": "^7.0.6",
|
||||
"balena-image-manager": "^9.0.0",
|
||||
"balena-preload": "^14.0.0",
|
||||
"balena-sdk": "^17.0.0",
|
||||
"balena-image-manager": "^9.0.2",
|
||||
"balena-preload": "^14.0.2",
|
||||
"balena-sdk": "^18.0.0",
|
||||
"balena-semver": "^2.3.0",
|
||||
"balena-settings-client": "^5.0.2",
|
||||
"balena-settings-storage": "^7.0.0",
|
||||
"balena-settings-storage": "^8.1.0",
|
||||
"bluebird": "^3.7.2",
|
||||
"body-parser": "^1.19.1",
|
||||
"chalk": "^3.0.0",
|
||||
@ -284,6 +284,6 @@
|
||||
"windosu": "^0.3.0"
|
||||
},
|
||||
"versionist": {
|
||||
"publishedAt": "2023-06-30T17:07:33.152Z"
|
||||
"publishedAt": "2023-08-22T13:33:26.774Z"
|
||||
}
|
||||
}
|
||||
|
10
tests/commands/env/envs.spec.ts
vendored
10
tests/commands/env/envs.spec.ts
vendored
@ -95,7 +95,7 @@ describe('balena envs', function () {
|
||||
|
||||
it('should successfully list service variables for a test fleet (-s flag)', async () => {
|
||||
const serviceName = 'service2';
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetServiceFromApp({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
@ -117,7 +117,7 @@ describe('balena envs', function () {
|
||||
|
||||
it('should successfully list env and service vars for a test fleet (-s flags)', async () => {
|
||||
const serviceName = 'service1';
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetServiceFromApp({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
@ -216,7 +216,7 @@ describe('balena envs', function () {
|
||||
|
||||
it('should successfully list service variables for a test device (-s flag)', async () => {
|
||||
const serviceName = 'service2';
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetServiceFromApp({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetDevice({ shortUUID, fullUUID });
|
||||
api.expectGetDevice({ fullUUID });
|
||||
@ -269,7 +269,7 @@ describe('balena envs', function () {
|
||||
|
||||
it('should successfully list env and service vars for a test device (-s flags)', async () => {
|
||||
const serviceName = 'service1';
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetServiceFromApp({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
@ -299,7 +299,7 @@ describe('balena envs', function () {
|
||||
|
||||
it('should successfully list env and service vars for a test device (-js flags)', async () => {
|
||||
const serviceName = 'service1';
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetServiceFromApp({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
|
@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import * as settings from 'balena-settings-client';
|
||||
import * as getStorage from 'balena-settings-storage';
|
||||
import { getStorage } from 'balena-settings-storage';
|
||||
import { expect } from 'chai';
|
||||
import mock = require('mock-require');
|
||||
import * as semver from 'semver';
|
||||
@ -78,7 +78,7 @@ describe('DeprecationChecker', function () {
|
||||
.stub(mockStorage, 'set')
|
||||
.withArgs(checker.cacheFile, sinon.match.any);
|
||||
|
||||
mock(storageModPath, () => mockStorage);
|
||||
mock(storageModPath, { getStorage: () => mockStorage });
|
||||
});
|
||||
|
||||
this.afterEach(() => {
|
||||
|
@ -386,6 +386,21 @@ export class BalenaAPIMock extends NockMock {
|
||||
});
|
||||
}
|
||||
|
||||
public expectGetServiceFromApp(opts: {
|
||||
optional?: boolean;
|
||||
persist?: boolean;
|
||||
serviceId?: number;
|
||||
serviceName: string;
|
||||
}) {
|
||||
const serviceId = opts.serviceId || 243768;
|
||||
this.optGet(/^\/v6\/application($|\?).*\$expand=service.*/, opts).reply(
|
||||
200,
|
||||
{
|
||||
d: [{ service: [{ id: serviceId, service_name: opts.serviceName }] }],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public expectPostService409(opts: ScopeOpts = {}) {
|
||||
this.optPost(/^\/v\d+\/service$/, opts).reply(
|
||||
409,
|
||||
@ -415,8 +430,10 @@ export class BalenaAPIMock extends NockMock {
|
||||
// User details are cached in the SDK
|
||||
// so often we don't know if we can expect the whoami request
|
||||
public expectGetWhoAmI(opts: ScopeOpts = { optional: true }) {
|
||||
this.optGet('/user/v1/whoami', opts).reply(200, {
|
||||
id: 99999,
|
||||
this.optGet('/actor/v1/whoami', opts).reply(200, {
|
||||
id: 1234,
|
||||
actorType: 'user',
|
||||
actorTypeId: 99999,
|
||||
username: 'gh_user',
|
||||
email: 'testuser@test.com',
|
||||
});
|
||||
|
7
typings/global.d.ts
vendored
7
typings/global.d.ts
vendored
@ -1,8 +1,11 @@
|
||||
import { Application, DeviceType, Device } from 'balena-sdk';
|
||||
|
||||
declare global {
|
||||
type ApplicationWithDeviceType = Application & {
|
||||
is_for__device_type: [DeviceType];
|
||||
type ApplicationWithDeviceTypeSlug = Omit<
|
||||
Application,
|
||||
'is_for__device_type'
|
||||
> & {
|
||||
is_for__device_type: [Pick<DeviceType, 'slug'>];
|
||||
};
|
||||
type DeviceWithDeviceType = Device & {
|
||||
is_of__device_type: [DeviceType];
|
||||
|
Reference in New Issue
Block a user