Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 115 additions & 1 deletion app/agent/AgentClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const mockCreateSbomStorage = vi.hoisted(() => vi.fn(() => ({ storage: 'controll
const mockRegistryState = vi.hoisted(() => ({
trigger: {},
watcher: {} as Record<string, { watchContainer?: ReturnType<typeof vi.fn> }>,
registry: {},
registry: {} as Record<string, unknown>,
authentication: {},
agent: {},
}));
Expand Down Expand Up @@ -87,6 +87,7 @@ vi.mock('../registry/index.js', () => ({

import * as event from '../event/index.js';
import * as gateWatch from '../maturity/gate-watch.js';
import Hub from '../registries/providers/hub/Hub.js';
import * as registry from '../registry/index.js';
import * as storeContainer from '../store/container.js';
import * as updateOperationStore from '../store/update-operation.js';
Expand All @@ -105,6 +106,9 @@ describe('AgentClient', () => {
for (const watcherId of Object.keys(mockRegistryState.watcher)) {
delete mockRegistryState.watcher[watcherId];
}
for (const registryId of Object.keys(mockRegistryState.registry)) {
delete mockRegistryState.registry[registryId];
}
vi.useFakeTimers();
client = new AgentClient('test-agent', {
host: 'localhost',
Expand Down Expand Up @@ -8061,6 +8065,12 @@ describe('AgentClient', () => {
});

describe('Portwing Docker API transport', () => {
async function registerAnonymousHub() {
const hub = new Hub();
await hub.register('registry', 'hub', 'public', {});
mockRegistryState.registry[hub.getId()] = hub;
}

test('reports whether a watcher uses controller Docker transport', async () => {
await client.handleComponentSync(
[
Expand Down Expand Up @@ -8193,6 +8203,55 @@ describe('AgentClient', () => {
);
});

test('marker-mode inventory normalizes a raw Portwing Docker Hub image through the configured provider', async () => {
await registerAnonymousHub();
vi.mocked(storeContainer.getContainer).mockReturnValue(undefined);
vi.mocked(storeContainer.insertContainer).mockImplementation((value) => value);
await client.handleComponentSync(
[
{
type: 'docker',
name: 'docker',
configuration: {
transport: 'docker-api',
execution: 'controller',
events: 'portwing',
},
},
],
[],
);

await client.handleContainerSync([
{
id: 'c1',
name: 'busybox',
watcher: 'docker',
image: {
id: 'sha256:current',
registry: { name: 'unknown', url: 'docker.io' },
name: 'busybox',
tag: { value: '1.36.0', semver: false },
digest: { watch: false },
architecture: 'arm64',
os: 'linux',
},
} as never,
]);

expect(storeContainer.insertContainer).toHaveBeenCalledWith(
expect.objectContaining({
image: expect.objectContaining({
name: 'library/busybox',
registry: {
name: 'hub.public',
url: 'https://registry-1.docker.io/v2',
},
}),
}),
);
});

test('marker-mode incremental events cannot clear controller-owned update enrichment', async () => {
const existing = {
id: 'c1',
Expand Down Expand Up @@ -8284,6 +8343,61 @@ describe('AgentClient', () => {
);
});

test('marker-mode Portwing events give the native watcher a configured registry identity', async () => {
await registerAnonymousHub();
const refreshContainer = vi.fn().mockResolvedValue({
container: { id: 'c1', watcher: 'docker', updateAvailable: false },
changed: false,
});
await client.handleComponentSync(
[
{
type: 'docker',
name: 'docker',
configuration: {
transport: 'docker-api',
execution: 'controller',
events: 'portwing',
},
},
],
[],
);
mockRegistryState.watcher['test-agent.docker.docker'] = {
watchContainer: refreshContainer,
};
vi.mocked(storeContainer.getContainer).mockReturnValue(undefined);
vi.mocked(storeContainer.insertContainer).mockImplementation((value) => value);

await client.handleEvent('dd:container-updated', {
id: 'c1',
name: 'busybox',
watcher: 'docker',
image: {
id: 'sha256:current',
registry: { name: 'unknown', url: 'docker.io' },
name: 'busybox',
tag: { value: '1.36.0', semver: false },
digest: { watch: false },
architecture: 'arm64',
os: 'linux',
},
});

expect(refreshContainer).toHaveBeenCalledWith(
expect.objectContaining({
image: expect.objectContaining({
name: 'library/busybox',
registry: {
name: 'hub.public',
url: 'https://registry-1.docker.io/v2',
},
}),
}),
{ emitBatchEvent: true },
);
});

test('standard mode rolls back a registered watcher when synthetic trigger registration fails', async () => {
axios.get
.mockResolvedValueOnce({
Expand Down
11 changes: 9 additions & 2 deletions app/agent/AgentClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import * as updateOperationStore from '../store/update-operation.js';
import { getRequestedOperationId } from '../triggers/providers/docker/update-runtime-context.js';
import { getErrorMessage } from '../util/error.js';
import { uuidv7 } from '../util/uuid.js';
import { normalizeContainer } from '../watchers/providers/docker/image-comparison.js';
import type { AgentAuthMode } from './components/Agent.js';
import { usesControllerDockerTransport } from './controller-docker-transport.js';
import type { EdgeAgentAdapter } from './EdgeAgentAdapter.js';
Expand Down Expand Up @@ -788,9 +789,15 @@ export class AgentClient {
};
}
container.agent = this.name;
if (
this.controllerDockerTransportWatchers.has(container.watcher) &&
container.image?.registry?.url
) {
container = normalizeContainer(container);
}
container = this.preserveControllerDockerEnrichment(container);
// The container coming from Agent should already be normalized and have results
// We rely on the Agent to perform Registry checks if configured
// Traditional agents own registry normalization and results. Controller-owned
// Docker transport instead applies the controller's configured registry above.

// Strip redaction metadata (e.g. `sensitive`) that the agent's event
// emitter may attach — the controller's Joi schema does not allow it.
Expand Down
Loading