mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
fix(drift): stop flagging declared external networks as drift (#1402)
The spatial drift engine reported a service attached to a declared
external network (top-level networks: { foo: { external: true } }) as
"attached to a network not declared in compose", marking the stack
permanently drifted. runtimeResourceName project-prefixed every declared
network to <project>_<key>, but Docker never prefixes an external
network: it references a pre-existing network by its real name. The
phantom <project>_<key> never matched the runtime name, so the
attachment fell through to a foreign-network finding.
Resolve external networks to their real name (the key, or a name:
override) without the project prefix, so the raw declared adapter agrees
with the rendered model the Network Inspector already used. Add unit,
adapter, and engine-level regression tests, and extend the adapter
equivalence test to cover an external network with no name override.
This commit is contained in:
@@ -41,6 +41,9 @@ describe('normalized-model adapters', () => {
|
||||
backend: { name: 'myapp_backend', external: false, internal: false },
|
||||
shared: { name: 'shared_net', external: true, internal: false },
|
||||
custom: { name: 'custom_name', external: false, internal: false },
|
||||
// External with no name override: the rendered model resolves the runtime
|
||||
// name to the key verbatim, so the raw adapter must agree (not myapp_extnet).
|
||||
extnet: { name: 'extnet', external: true, internal: false },
|
||||
},
|
||||
volumes: {},
|
||||
};
|
||||
@@ -50,6 +53,7 @@ describe('normalized-model adapters', () => {
|
||||
backend: { external: false },
|
||||
shared: { external: true, name: 'shared_net' },
|
||||
custom: { external: false, name: 'custom_name' },
|
||||
extnet: { external: true },
|
||||
},
|
||||
volumes: {},
|
||||
};
|
||||
@@ -126,6 +130,16 @@ describe('runtimeResourceName', () => {
|
||||
expect(runtimeResourceName('myapp', 'backend', 'backend')).toBe('myapp_backend'); // name == key is not an override
|
||||
expect(runtimeResourceName('myapp', 'shared', 'shared_net')).toBe('shared_net');
|
||||
});
|
||||
|
||||
it('never project-prefixes an external resource (runtime name is the key, or a name override)', () => {
|
||||
// Compose references an external network/volume by its real name and never
|
||||
// prefixes the project, so an external resource with no name override keeps
|
||||
// its key verbatim. Prefixing it invents a phantom `<project>_<key>` that no
|
||||
// runtime resource matches, which then reads as foreign-network drift.
|
||||
expect(runtimeResourceName('myapp', 'arr-net', undefined, true)).toBe('arr-net');
|
||||
expect(runtimeResourceName('myapp', 'arr-net', 'arr-net', true)).toBe('arr-net');
|
||||
expect(runtimeResourceName('myapp', 'shared', 'shared-prod', true)).toBe('shared-prod');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseAccessUrlPorts', () => {
|
||||
@@ -207,6 +221,24 @@ describe('compareStackNetworks', () => {
|
||||
expect(compareStackNetworks(declaredFromCompose, snap, 'myapp').missingFromRuntime).toEqual(['edge_override']);
|
||||
});
|
||||
|
||||
it('does not flag an external network declared without a name override through the raw adapter', () => {
|
||||
// The plex/arr-net case: an external network declared `external: true` with no
|
||||
// name override. Its real runtime name is the key (arr-net), unprefixed, so an
|
||||
// attachment to it reads as in-sync, not as a foreign network owned elsewhere.
|
||||
const declaredFromCompose = fromDeclaredCompose({
|
||||
services: [{ name: 'plex', dependsOn: [], networks: ['arr-net'], volumes: [], ports: [] }],
|
||||
networks: { 'arr-net': { external: true } },
|
||||
volumes: {},
|
||||
}, 'plex');
|
||||
const snap = snapshot(
|
||||
[container({ name: 'plex', service: 'plex', composeProject: 'plex', stack: 'plex', networks: [{ name: 'arr-net', id: 'a', ip: '' }] })],
|
||||
[depNet({ name: 'arr-net', composeProject: null, stack: null })],
|
||||
);
|
||||
const drift = compareStackNetworks(declaredFromCompose, snap, 'plex');
|
||||
expect(drift.foreignNetworkAttachments).toEqual([]);
|
||||
expect(drift.runtimeOnlyAttachments).toEqual([]);
|
||||
});
|
||||
|
||||
it('ignores system networks, the default network, and external networks', () => {
|
||||
const snap = snapshot(
|
||||
[container({ networks: [
|
||||
|
||||
@@ -349,6 +349,21 @@ describe('assembleStackDrift - network drift', () => {
|
||||
});
|
||||
expect(report.findings.filter(f => f.kind.startsWith('network-'))).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not flag an attachment to a declared external network (regression: shared arr-net)', () => {
|
||||
// arr-net is declared `external: true` with no name override, so Docker attaches
|
||||
// the container to the pre-existing network named "arr-net" (no project prefix).
|
||||
// The runtime matches the compose, so this must read as in-sync rather than a
|
||||
// foreign/undeclared-network finding.
|
||||
const report = assembleStackDrift({
|
||||
stack: 'plex',
|
||||
declared: { services: [service({ name: 'plex', networks: ['arr-net'] })], networks: { 'arr-net': { external: true } }, volumes: {} },
|
||||
containers: [container({ id: 'plex', service: 'plex', stack: 'plex', networks: [{ name: 'arr-net', id: 'a', ip: '' }] })],
|
||||
networks: [depNet('arr-net', { composeProject: null, stack: null })],
|
||||
});
|
||||
expect(report.findings.filter(f => f.kind.startsWith('network-'))).toEqual([]);
|
||||
expect(report.status).toBe('in-sync');
|
||||
});
|
||||
});
|
||||
|
||||
// ── normalizeImageRef ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -24,9 +24,13 @@ export function isLoopback(ip: string): boolean {
|
||||
}
|
||||
|
||||
/** Resolved runtime name of a top-level network/volume: a `name:` override wins,
|
||||
* otherwise compose prefixes the project (`<project>_<key>`). */
|
||||
export function runtimeResourceName(projectName: string, key: string, declaredName: string | undefined): string {
|
||||
return declaredName && declaredName !== key ? declaredName : `${projectName}_${key}`;
|
||||
* otherwise compose prefixes the project (`<project>_<key>`). An external
|
||||
* resource is never project-prefixed: it references a pre-existing network/volume
|
||||
* by its real name (the key, or a `name:` override), so prefixing it would invent
|
||||
* a `<project>_<key>` that no runtime resource matches and read as foreign drift. */
|
||||
export function runtimeResourceName(projectName: string, key: string, declaredName: string | undefined, external = false): string {
|
||||
if (declaredName && declaredName !== key) return declaredName;
|
||||
return external ? key : `${projectName}_${key}`;
|
||||
}
|
||||
|
||||
/** Extract host port numbers referenced by free-text access URLs, for the
|
||||
@@ -67,7 +71,7 @@ export function fromEffectiveModel(m: EffectiveModel): NormalizedNetworkModel {
|
||||
export function fromDeclaredCompose(m: DeclaredCompose, projectName: string): NormalizedNetworkModel {
|
||||
const networks: NormalizedNetworkModel['networks'] = {};
|
||||
for (const [key, res] of Object.entries(m.networks)) {
|
||||
networks[key] = { runtimeName: runtimeResourceName(projectName, key, res.name), external: res.external };
|
||||
networks[key] = { runtimeName: runtimeResourceName(projectName, key, res.name, res.external), external: res.external };
|
||||
}
|
||||
return {
|
||||
projectName,
|
||||
|
||||
Reference in New Issue
Block a user