mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-04-19 16:40:47 +00:00
Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
01585c688e | ||
|
eeac56efc3 | ||
|
d475b1d830 | ||
|
49b18b4a37 | ||
|
623a1638c1 | ||
|
caed4dcca0 | ||
|
7efdeea0f7 | ||
|
2d1871e16d | ||
|
b596c77ce2 |
@ -1,3 +1,75 @@
|
||||
- commits:
|
||||
- subject: Fix search for app leftover locks
|
||||
hash: d475b1d8301c83b932ce272d3496bf4aac0ef1ad
|
||||
body: |
|
||||
The leftover locks search was creating an array rather than an object
|
||||
keyed by the appId. This could affect the lock cleanup and make leftover
|
||||
locks from one app affect the install of the app in local mode.
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Felipe Lalanne
|
||||
nested: []
|
||||
version: 17.0.2
|
||||
title: ""
|
||||
date: 2025-04-02T20:16:09.754Z
|
||||
- commits:
|
||||
- subject: Clarify firewall docs on behavior with host network containers
|
||||
hash: caed4dcca0043f848f6dd5a3d1a2f82a2466e8d6
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
Signed-off-by: Christina Ying Wang <christina@balena.io>
|
||||
signed-off-by: Christina Ying Wang <christina@balena.io>
|
||||
author: Christina Ying Wang
|
||||
nested: []
|
||||
version: 17.0.1
|
||||
title: ""
|
||||
date: 2025-03-25T20:41:20.141Z
|
||||
- commits:
|
||||
- subject: Add Docker network label if custom ipam config
|
||||
hash: b596c77ce2d229e79082cbb1f0022f93806f09ae
|
||||
body: >
|
||||
In a target release where the only change is the addition or removal
|
||||
|
||||
of a custom ipam config, the Supervisor does not recreate the network
|
||||
|
||||
due to ignoring ipam config differences when comparing current and
|
||||
target
|
||||
|
||||
network (in network.isEqualConfig). This commit implements the addition
|
||||
of
|
||||
|
||||
a network label if the target compose object includes a network with
|
||||
custom
|
||||
|
||||
ipam. With the label, the Supervisor will detect a difference between a
|
||||
|
||||
network with a custom ipam and a network without, without needing to
|
||||
compare
|
||||
|
||||
the ipam configs themselves.
|
||||
|
||||
|
||||
This is a major change, as devices running networks with custom ipam
|
||||
configs
|
||||
|
||||
will have their networks recreated to add the network label.
|
||||
footer:
|
||||
Closes: "#2251"
|
||||
closes: "#2251"
|
||||
Change-type: major
|
||||
change-type: major
|
||||
See: https://balena.fibery.io/Work/Project/Fix-Supervisor-not-recreating-network-when-passed-custom-ipam-config-1127
|
||||
see: https://balena.fibery.io/Work/Project/Fix-Supervisor-not-recreating-network-when-passed-custom-ipam-config-1127
|
||||
Signed-off-by: Christina Ying Wang <christina@balena.io>
|
||||
signed-off-by: Christina Ying Wang <christina@balena.io>
|
||||
author: Christina Ying Wang
|
||||
nested: []
|
||||
version: 17.0.0
|
||||
title: ""
|
||||
date: 2025-03-24T22:18:08.753Z
|
||||
- commits:
|
||||
- subject: Start a dependent if all dependencies are started
|
||||
hash: 7764f98c9d357a1942628e57951266767555f67b
|
||||
|
15
CHANGELOG.md
15
CHANGELOG.md
@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file
|
||||
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
# v17.0.2
|
||||
## (2025-04-02)
|
||||
|
||||
* Fix search for app leftover locks [Felipe Lalanne]
|
||||
|
||||
# v17.0.1
|
||||
## (2025-03-25)
|
||||
|
||||
* Clarify firewall docs on behavior with host network containers [Christina Ying Wang]
|
||||
|
||||
# v17.0.0
|
||||
## (2025-03-24)
|
||||
|
||||
* Add Docker network label if custom ipam config [Christina Ying Wang]
|
||||
|
||||
# v16.12.9
|
||||
## (2025-03-20)
|
||||
|
||||
|
@ -2,6 +2,6 @@ name: balena-supervisor
|
||||
description: 'Balena Supervisor: balena''s agent on devices.'
|
||||
joinable: false
|
||||
type: sw.application
|
||||
version: 16.12.9
|
||||
version: 17.0.2
|
||||
provides:
|
||||
- slug: sw.compose.long-volume-syntax
|
||||
|
@ -8,10 +8,10 @@ To switch between firewall modes, the `HOST_FIREWALL_MODE` (with `BALENA_` or le
|
||||
|
||||
> [!NOTE] Configuration variables defined in the dashboard will not apply to devices in local mode.
|
||||
|
||||
| Mode | Description |
|
||||
| ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| on | Only traffic for core services provided by balena and containers on the host network are allowed. |
|
||||
| off | All network traffic is allowed. |
|
||||
| Mode | Description |
|
||||
| ---- | ----------- |
|
||||
| on | Only traffic for core services provided by balena are allowed. Any other ports, including those used by containers with host networking, are blocked unless explicitly configured. |
|
||||
| off | All network traffic is allowed. |
|
||||
| auto | If there _are_ host network services, behaves as if `FIREWALL_MODE` = `on`. If there _aren't_ host network services, behaves as if `FIREWALL_MODE` = `off`. |
|
||||
|
||||
## Issues
|
||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "balena-supervisor",
|
||||
"version": "16.12.9",
|
||||
"version": "17.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "balena-supervisor",
|
||||
"version": "16.12.9",
|
||||
"version": "17.0.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@balena/systemd": "^0.5.0",
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "balena-supervisor",
|
||||
"description": "This is balena's Supervisor, a program that runs on IoT devices and has the task of running user Apps (which are Docker containers), and updating them as the balena API informs it to.",
|
||||
"version": "16.12.9",
|
||||
"version": "17.0.2",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -137,6 +137,6 @@
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"versionist": {
|
||||
"publishedAt": "2025-03-20T18:43:06.641Z"
|
||||
"publishedAt": "2025-04-02T20:16:10.284Z"
|
||||
}
|
||||
}
|
||||
|
@ -187,8 +187,12 @@ export async function inferNextSteps(
|
||||
const currentAppIds = Object.keys(currentApps).map((i) => parseInt(i, 10));
|
||||
const targetAppIds = Object.keys(targetApps).map((i) => parseInt(i, 10));
|
||||
|
||||
const withLeftoverLocks = await Promise.all(
|
||||
currentAppIds.map((id) => hasLeftoverLocks(id)),
|
||||
const withLeftoverLocks = Object.fromEntries(
|
||||
await Promise.all(
|
||||
currentAppIds.map(
|
||||
async (id) => [id, await hasLeftoverLocks(id)] as [number, boolean],
|
||||
),
|
||||
),
|
||||
);
|
||||
const bootTime = getBootTime();
|
||||
|
||||
|
@ -160,6 +160,15 @@ class NetworkImpl implements Network {
|
||||
configOnly: network.config_only || false,
|
||||
};
|
||||
|
||||
// Add label if there's non-default ipam config
|
||||
// e.g. explicitly defined subnet or gateway.
|
||||
// When updating between a release where the ipam config
|
||||
// changes, this label informs the Supervisor that
|
||||
// there's an ipam diff that requires recreating the network.
|
||||
if (net.config.ipam.config.length > 0) {
|
||||
net.config.labels['io.balena.private.ipam.config'] = 'true';
|
||||
}
|
||||
|
||||
return net;
|
||||
}
|
||||
|
||||
|
@ -67,6 +67,8 @@ describe('compose/network: integration tests', () => {
|
||||
Labels: {
|
||||
'io.balena.supervised': 'true',
|
||||
'io.balena.app-id': '12345',
|
||||
// This label should be present as we've defined a custom ipam config
|
||||
'io.balena.private.ipam.config': 'true',
|
||||
},
|
||||
Options: {},
|
||||
ConfigOnly: false,
|
||||
|
@ -260,7 +260,33 @@ describe('state engine', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('updates an app with two services with a network change', async () => {
|
||||
it('updates an app with two services with a network change where the only change is a custom ipam config addition', async () => {
|
||||
const services = {
|
||||
'1': {
|
||||
image: 'alpine:latest',
|
||||
imageId: 11,
|
||||
serviceName: 'one',
|
||||
restart: 'unless-stopped',
|
||||
running: true,
|
||||
command: 'sleep infinity',
|
||||
stop_signal: 'SIGKILL',
|
||||
networks: ['default'],
|
||||
labels: {},
|
||||
environment: {},
|
||||
},
|
||||
'2': {
|
||||
image: 'alpine:latest',
|
||||
imageId: 12,
|
||||
serviceName: 'two',
|
||||
restart: 'unless-stopped',
|
||||
running: true,
|
||||
command: 'sleep infinity',
|
||||
stop_signal: 'SIGKILL',
|
||||
networks: ['default'],
|
||||
labels: {},
|
||||
environment: {},
|
||||
},
|
||||
};
|
||||
await setTargetState({
|
||||
config: {},
|
||||
apps: {
|
||||
@ -268,30 +294,10 @@ describe('state engine', () => {
|
||||
name: 'test-app',
|
||||
commit: 'deadbeef',
|
||||
releaseId: 1,
|
||||
services: {
|
||||
'1': {
|
||||
image: 'alpine:latest',
|
||||
imageId: 11,
|
||||
serviceName: 'one',
|
||||
restart: 'unless-stopped',
|
||||
running: true,
|
||||
command: 'sleep infinity',
|
||||
stop_signal: 'SIGKILL',
|
||||
labels: {},
|
||||
environment: {},
|
||||
},
|
||||
'2': {
|
||||
image: 'alpine:latest',
|
||||
imageId: 12,
|
||||
serviceName: 'two',
|
||||
restart: 'unless-stopped',
|
||||
running: true,
|
||||
command: 'sleep infinity',
|
||||
labels: {},
|
||||
environment: {},
|
||||
},
|
||||
services,
|
||||
networks: {
|
||||
default: {},
|
||||
},
|
||||
networks: {},
|
||||
volumes: {},
|
||||
},
|
||||
},
|
||||
@ -311,6 +317,21 @@ describe('state engine', () => {
|
||||
]);
|
||||
const containerIds = containers.map(({ Id }) => Id);
|
||||
|
||||
// Network should not have custom ipam config
|
||||
const defaultNet = await docker.getNetwork('123_default').inspect();
|
||||
expect(defaultNet)
|
||||
.to.have.property('IPAM')
|
||||
.to.not.deep.equal({
|
||||
Config: [{ Gateway: '192.168.91.1', Subnet: '192.168.91.0/24' }],
|
||||
Driver: 'default',
|
||||
Options: {},
|
||||
});
|
||||
|
||||
// Network should not have custom ipam label
|
||||
expect(defaultNet)
|
||||
.to.have.property('Labels')
|
||||
.to.not.have.property('io.balena.private.ipam.config');
|
||||
|
||||
await setTargetState({
|
||||
config: {},
|
||||
apps: {
|
||||
@ -318,32 +339,7 @@ describe('state engine', () => {
|
||||
name: 'test-app',
|
||||
commit: 'deadca1f',
|
||||
releaseId: 2,
|
||||
services: {
|
||||
'1': {
|
||||
image: 'alpine:latest',
|
||||
imageId: 21,
|
||||
serviceName: 'one',
|
||||
restart: 'unless-stopped',
|
||||
running: true,
|
||||
command: 'sleep infinity',
|
||||
stop_signal: 'SIGKILL',
|
||||
networks: ['default'],
|
||||
labels: {},
|
||||
environment: {},
|
||||
},
|
||||
'2': {
|
||||
image: 'alpine:latest',
|
||||
imageId: 22,
|
||||
serviceName: 'two',
|
||||
restart: 'unless-stopped',
|
||||
running: true,
|
||||
command: 'sh -c "echo two && sleep infinity"',
|
||||
stop_signal: 'SIGKILL',
|
||||
networks: ['default'],
|
||||
labels: {},
|
||||
environment: {},
|
||||
},
|
||||
},
|
||||
services,
|
||||
networks: {
|
||||
default: {
|
||||
driver: 'bridge',
|
||||
@ -364,8 +360,8 @@ describe('state engine', () => {
|
||||
expect(
|
||||
updatedContainers.map(({ Names, State }) => ({ Name: Names[0], State })),
|
||||
).to.have.deep.members([
|
||||
{ Name: '/one_21_2_deadca1f', State: 'running' },
|
||||
{ Name: '/two_22_2_deadca1f', State: 'running' },
|
||||
{ Name: '/one_11_2_deadca1f', State: 'running' },
|
||||
{ Name: '/two_12_2_deadca1f', State: 'running' },
|
||||
]);
|
||||
|
||||
// Container ids must have changed
|
||||
@ -373,13 +369,145 @@ describe('state engine', () => {
|
||||
containerIds,
|
||||
);
|
||||
|
||||
expect(await docker.getNetwork('123_default').inspect())
|
||||
// Network should have custom ipam config
|
||||
const customNet = await docker.getNetwork('123_default').inspect();
|
||||
expect(customNet)
|
||||
.to.have.property('IPAM')
|
||||
.to.deep.equal({
|
||||
Config: [{ Gateway: '192.168.91.1', Subnet: '192.168.91.0/24' }],
|
||||
Driver: 'default',
|
||||
Options: {},
|
||||
});
|
||||
|
||||
// Network should have custom ipam label
|
||||
expect(customNet)
|
||||
.to.have.property('Labels')
|
||||
.to.have.property('io.balena.private.ipam.config');
|
||||
});
|
||||
|
||||
it('updates an app with two services with a network change where the only change is a custom ipam config removal', async () => {
|
||||
const services = {
|
||||
'1': {
|
||||
image: 'alpine:latest',
|
||||
imageId: 11,
|
||||
serviceName: 'one',
|
||||
restart: 'unless-stopped',
|
||||
running: true,
|
||||
command: 'sleep infinity',
|
||||
stop_signal: 'SIGKILL',
|
||||
networks: ['default'],
|
||||
labels: {},
|
||||
environment: {},
|
||||
},
|
||||
'2': {
|
||||
image: 'alpine:latest',
|
||||
imageId: 12,
|
||||
serviceName: 'two',
|
||||
restart: 'unless-stopped',
|
||||
running: true,
|
||||
command: 'sleep infinity',
|
||||
stop_signal: 'SIGKILL',
|
||||
networks: ['default'],
|
||||
labels: {},
|
||||
environment: {},
|
||||
},
|
||||
};
|
||||
await setTargetState({
|
||||
config: {},
|
||||
apps: {
|
||||
'123': {
|
||||
name: 'test-app',
|
||||
commit: 'deadbeef',
|
||||
releaseId: 1,
|
||||
services,
|
||||
networks: {
|
||||
default: {
|
||||
driver: 'bridge',
|
||||
ipam: {
|
||||
config: [
|
||||
{ gateway: '192.168.91.1', subnet: '192.168.91.0/24' },
|
||||
],
|
||||
driver: 'default',
|
||||
},
|
||||
},
|
||||
},
|
||||
volumes: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const state = await getCurrentState();
|
||||
expect(
|
||||
state.apps['123'].services.map((s: any) => s.serviceName),
|
||||
).to.deep.equal(['one', 'two']);
|
||||
|
||||
// Network should have custom ipam config
|
||||
const customNet = await docker.getNetwork('123_default').inspect();
|
||||
expect(customNet)
|
||||
.to.have.property('IPAM')
|
||||
.to.deep.equal({
|
||||
Config: [{ Gateway: '192.168.91.1', Subnet: '192.168.91.0/24' }],
|
||||
Driver: 'default',
|
||||
Options: {},
|
||||
});
|
||||
|
||||
// Network should have custom ipam label
|
||||
expect(customNet)
|
||||
.to.have.property('Labels')
|
||||
.to.have.property('io.balena.private.ipam.config');
|
||||
|
||||
const containers = await docker.listContainers();
|
||||
expect(
|
||||
containers.map(({ Names, State }) => ({ Name: Names[0], State })),
|
||||
).to.have.deep.members([
|
||||
{ Name: '/one_11_1_deadbeef', State: 'running' },
|
||||
{ Name: '/two_12_1_deadbeef', State: 'running' },
|
||||
]);
|
||||
const containerIds = containers.map(({ Id }) => Id);
|
||||
|
||||
await setTargetState({
|
||||
config: {},
|
||||
apps: {
|
||||
'123': {
|
||||
name: 'test-app',
|
||||
commit: 'deadca1f',
|
||||
releaseId: 2,
|
||||
services,
|
||||
networks: {
|
||||
default: {},
|
||||
},
|
||||
volumes: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const updatedContainers = await docker.listContainers();
|
||||
expect(
|
||||
updatedContainers.map(({ Names, State }) => ({ Name: Names[0], State })),
|
||||
).to.have.deep.members([
|
||||
{ Name: '/one_11_2_deadca1f', State: 'running' },
|
||||
{ Name: '/two_12_2_deadca1f', State: 'running' },
|
||||
]);
|
||||
|
||||
// Container ids must have changed
|
||||
expect(updatedContainers.map(({ Id }) => Id)).to.not.have.members(
|
||||
containerIds,
|
||||
);
|
||||
|
||||
// Network should not have custom ipam config
|
||||
const defaultNet = await docker.getNetwork('123_default').inspect();
|
||||
expect(defaultNet)
|
||||
.to.have.property('IPAM')
|
||||
.to.not.deep.equal({
|
||||
Config: [{ Gateway: '192.168.91.1', Subnet: '192.168.91.0/24' }],
|
||||
Driver: 'default',
|
||||
Options: {},
|
||||
});
|
||||
|
||||
// Network should not have custom ipam label
|
||||
expect(defaultNet)
|
||||
.to.have.property('Labels')
|
||||
.to.not.have.property('io.balena.private.ipam.config');
|
||||
});
|
||||
|
||||
it('updates an app with two services with a network removal', async () => {
|
||||
|
@ -183,6 +183,8 @@ describe('compose/network', () => {
|
||||
'io.balena.supervised': 'true',
|
||||
'io.balena.app-id': '12345',
|
||||
'com.docker.some-label': 'yes',
|
||||
// This label should be present as we've defined a custom ipam config
|
||||
'io.balena.private.ipam.config': 'true',
|
||||
});
|
||||
|
||||
expect(dockerConfig.Options).to.deep.equal({
|
||||
@ -344,12 +346,14 @@ describe('compose/network', () => {
|
||||
'io.resin.features.something': '123',
|
||||
'io.balena.features.dummy': 'abc',
|
||||
'io.balena.supervised': 'true',
|
||||
'io.balena.private.ipam.config': 'true',
|
||||
} as NetworkInspectInfo['Labels'],
|
||||
} as NetworkInspectInfo);
|
||||
|
||||
expect(network.config.labels).to.deep.equal({
|
||||
'io.balena.features.something': '123',
|
||||
'io.balena.features.dummy': 'abc',
|
||||
'io.balena.private.ipam.config': 'true',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -425,34 +429,32 @@ describe('compose/network', () => {
|
||||
});
|
||||
|
||||
describe('comparing network configurations', () => {
|
||||
it('ignores IPAM configuration', () => {
|
||||
const network = Network.fromComposeObject('default', 12345, 'deadbeef', {
|
||||
ipam: {
|
||||
driver: 'default',
|
||||
config: [
|
||||
{
|
||||
subnet: '172.20.0.0/16',
|
||||
ip_range: '172.20.10.0/24',
|
||||
gateway: '172.20.0.1',
|
||||
},
|
||||
],
|
||||
options: {},
|
||||
it('distinguishes a network with custom ipam config from a network without', () => {
|
||||
const customIpam = Network.fromComposeObject(
|
||||
'default',
|
||||
12345,
|
||||
'deadbeef',
|
||||
{
|
||||
ipam: {
|
||||
driver: 'default',
|
||||
config: [
|
||||
{
|
||||
subnet: '172.20.0.0/16',
|
||||
gateway: '172.20.0.1',
|
||||
},
|
||||
],
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(
|
||||
network.isEqualConfig(
|
||||
Network.fromComposeObject('default', 12345, 'deadbeef', {}),
|
||||
),
|
||||
).to.be.true;
|
||||
);
|
||||
const noCustomIpam = Network.fromComposeObject(
|
||||
'default',
|
||||
12345,
|
||||
'deadbeef',
|
||||
{},
|
||||
);
|
||||
|
||||
// Only ignores ipam.config, not other ipam elements
|
||||
expect(
|
||||
network.isEqualConfig(
|
||||
Network.fromComposeObject('default', 12345, 'deadbeef', {
|
||||
ipam: { driver: 'aaa' },
|
||||
}),
|
||||
),
|
||||
).to.be.false;
|
||||
expect(customIpam.isEqualConfig(noCustomIpam)).to.be.false;
|
||||
});
|
||||
|
||||
it('compares configurations recursively', () => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user