Use agent update target for stale notices

This commit is contained in:
rcourtman
2026-06-03 14:42:43 +01:00
parent 259476907a
commit 17e854e7b6
28 changed files with 264 additions and 60 deletions
@@ -180,14 +180,18 @@ cannot prove agent-token continuity.
Stale-agent platform notices and the Infrastructure `agentUpdates` workspace
route are part of that same lifecycle contract. Platform pages may link to the
central Infrastructure update dialog with a scoped list of affected agent
connection IDs, but command rendering stays lifecycle-owned in Settings. For
Unix-like agents that Pulse already sees, the copied stale-agent update command
must use `scripts/install.sh --update` and recover URL, token, identity, custom
CA, and insecure transport from installer-owned saved state instead of asking
the operator to mint a fresh install token or exposing agent IDs in the copied
command. Windows stale-agent update commands remain on the existing
token-gated install transport until the Windows installer owns an equivalent
saved-state update mode.
connection IDs, but command rendering stays lifecycle-owned in Settings.
`currentAgentTargetVersion()` is the canonical agent update target and must be
projected to browser clients as `/api/version.agentUpdateTargetVersion`;
platform pages and Settings must not compare agent binaries against the app
build `version` when the server would tell agents `dev`. For Unix-like agents
that Pulse already sees, the copied stale-agent update command must use
`scripts/install.sh --update` and recover URL, token, identity, custom CA, and
insecure transport from installer-owned saved state instead of asking the
operator to mint a fresh install token or exposing agent IDs in the copied
command. Windows stale-agent update commands remain on the existing token-gated
install transport until the Windows installer owns an equivalent saved-state
update mode.
Agent lifecycle and fleet-operation surfaces may consume
`POST /api/actions/plan` for resource capability planning, but the action plan
@@ -120,8 +120,12 @@ update commands opened from platform notices must not serialize API tokens,
agent IDs, or hostnames into Unix shell payloads. Those commands route through
the `agentUpdates` settings query for scoped row selection and then call the
installer-owned `scripts/install.sh --update` saved-state path on the target
host. Windows remains on the existing token-gated PowerShell payload until its
installer owns the same saved-state update contract.
host. Stale-agent UI must consume `/api/version`'s
`agentUpdateTargetVersion`, not the app build `version`, so development/source
builds that intentionally report `dev` to agents do not surface update nags
when no real agent update target exists. Windows remains on the existing
token-gated PowerShell payload until its installer owns the same saved-state
update contract.
Summary-chart response caching is a shared API boundary:
`internal/api/router.go` may serve a short cached JSON payload for repeated
@@ -127,6 +127,11 @@ TLS floor in the dynamic config.
## Shared Boundaries
1. `frontend-modern/src/api/updates.ts` shared with `api-contracts`: the updates frontend client is both a deployment-installability control surface and a canonical API payload contract boundary.
The version payload consumed by this client must preserve the distinction
between the running app build `version` and the deployable
`agentUpdateTargetVersion`; update/install surfaces may display the app
build, but agent update prompts must only use the agent target when the
backend exposes one.
2. `internal/api/updates.go` shared with `api-contracts`: update handlers are both a deployment-installability control surface and a canonical API payload contract boundary.
3. `internal/cloudcp/docker/labels.go` shared with `cloud-paid`: hosted tenant Docker labels are both a Pulse Cloud runtime contract boundary and a deployment-installability rollout boundary.
4. `internal/cloudcp/docker/manager.go` shared with `cloud-paid`: hosted tenant container management is both a Pulse Cloud runtime contract boundary and a deployment-installability rollout boundary.
@@ -233,9 +233,12 @@ surface a compact stale-agent cue when their row model carries Pulse agent
identity and version evidence, but the CTA must route to the canonical
Infrastructure settings update-command surface with scoped agent IDs instead of
duplicating installer command assembly, tokens, or lifecycle copy in each
platform page. Agentless API-only platforms such as vSphere must not grow this
notice unless a concrete guest or monitored system row actually carries a Pulse
agent identity.
platform page. These notices must compare against the API-owned
`agentUpdateTargetVersion` rather than the app build version, so development
builds can show their dirty server version without implying agents can or
should update toward that build. Agentless API-only platforms such as vSphere
must not grow this notice unless a concrete guest or monitored system row
actually carries a Pulse agent identity.
Kubernetes policy inventory follows that same primitive boundary while the
unified-resource owner supplies NetworkPolicy policy type and rule-count
columns, PodDisruptionBudget budget and observed health columns, ResourceQuota
@@ -141,7 +141,6 @@ regression protection.
15. `frontend-modern/src/routing/routePreload.ts` shared with `frontend-primitives`: the app-shell route preload registry is both a canonical frontend shell boundary and an authenticated hot-path performance boundary.
16. `frontend-modern/src/useAppRuntimeState.ts` shared with `cloud-paid`: the authenticated app runtime bootstrap is both a hosted commercial org-context boundary and a protected app-shell performance boundary.
17. `internal/api/slo.go` shared with `api-contracts`: the SLO endpoint is both an API contract surface and a protected performance hot-path boundary.
## Extension Points
1. Add performance budgets through SLO or contract tests
@@ -177,6 +176,10 @@ regression protection.
process version once, but they must not add per-request release lookups,
filesystem walks, or other heavy work just to compute whether an attached
agent is current.
The `/api/version.agentUpdateTargetVersion` projection follows that same
rule: it may mirror the cached deployable agent target exposed to agents,
but it must not inspect attached agents, download artifacts, or resolve
release metadata on each request.
Update-readiness planning follows the same bounded-router rule. The router
may wire cached config and monitor host snapshots into the updates handler,
but it must not add release lookups, broad persistence scans, or live agent
@@ -121,13 +121,17 @@ controls as normal product settings.
storing a brand setting never becomes a free branding bypass.
16. `internal/cloudcp/auth/magiclink.go` shared with `cloud-paid`: control-plane magic-link HMAC handling is both a Pulse Cloud account-access boundary and a security/privacy token-secrecy boundary.
17. `internal/cloudcp/auth/magiclink_store.go` shared with `cloud-paid`: control-plane magic-link persistence is both a Pulse Cloud account-access boundary and a security/privacy storage-hardening boundary.
## Extension Points
1. Change privacy disclosures, usage-data vocabulary, or outbound-data guarantees through `docs/PRIVACY.md` and `internal/telemetry/telemetry.go` together.
2. Change security policy, hardening guidance, or supported auth boundaries through `SECURITY.md`.
3. Change telemetry/privacy settings state handling through `frontend-modern/src/components/Settings/useSystemSettingsState.ts`.
4. Change security/auth/token transport behavior through the shared `frontend-modern/src/api/security.ts`, `frontend-modern/src/components/Settings/APITokenManager.tsx`, `frontend-modern/src/components/Settings/apiTokenManagerModel.ts`, `frontend-modern/src/components/Settings/useAPITokenManagerState.ts`, `internal/api/security.go`, `internal/api/security_tokens.go`, and `internal/api/system_settings.go` boundary.
Release metadata surfaced through `/api/version` remains outside token,
auth, and privacy state. Adding or changing `agentUpdateTargetVersion`
must stay limited to non-secret deployable release identity and must not
expose agent inventory, scoped update selections, or command authorization
state.
CSRF token-store behavior in `internal/api/csrf_store.go` is part of that
shared browser-auth trust boundary: parallel stale-token mutations may
receive distinct bounded replacement tokens for one session, but explicit
@@ -77,7 +77,9 @@ plumbing even though `ProxmoxPageSurface` is a storage/recovery canonical file.
Those notices may link an operator to scoped agent update commands for
agent-contributed node detail and command support, but they must not be
interpreted as backup visibility, recovery readiness, restore capability, or a
storage/recovery-owned command path.
storage/recovery-owned command path. Suppressing those notices when the API has
no deployable agent update target is likewise lifecycle/frontend behavior and
does not change Proxmox backup or recovery evidence.
Tenant report branding settings are adjacent tenant-local configuration, not a
storage or recovery product state. `reportBranding` persisted in a tenant
runtime's `system.json` should be preserved by the existing tenant data
@@ -488,6 +488,9 @@ AI-only summary payloads, or page-local heuristics.
route to the affected agents. That scoped lifecycle handoff must not become a
new resource-action authority, a page-local command runner, or a substitute
for the `/api/connections` fleet command-policy truth described above.
Resource consumers must also use the API-owned agent update target when
comparing resource-carried agent versions; the running app build version is
not a resource freshness contract.
`internal/unifiedresources/top_level_systems.go`
Explicit linked-host correlation is canonical here: when Kubernetes node
ingest has a resolved backing host agent, the registry must merge that node
@@ -15,6 +15,23 @@ describe('UpdatesAPI', () => {
apiFetchJSONMock.mockReset();
});
it('preserves the app version and agent update target from version payloads', async () => {
const response = {
version: '6.0.0-rc.6+git.174.g259476907.dirty',
build: 'development',
runtime: 'go',
isDocker: false,
isSourceBuild: false,
isDevelopment: true,
deploymentType: 'development',
agentUpdateTargetVersion: '6.0.0-rc.6',
};
apiFetchJSONMock.mockResolvedValueOnce(response as any);
await expect(UpdatesAPI.getVersion()).resolves.toEqual(response);
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/version');
});
it('encodes optional update-check channel safely', async () => {
apiFetchJSONMock.mockResolvedValueOnce({ available: false } as any);
await UpdatesAPI.checkForUpdates('rc');
+1
View File
@@ -30,6 +30,7 @@ export interface VersionInfo {
isSourceBuild: boolean;
isDevelopment: boolean;
deploymentType?: string;
agentUpdateTargetVersion?: string;
}
export type UpdateReadinessStatus = 'ready' | 'attention' | 'blocked';
@@ -195,7 +195,7 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
const agentUpdateTargets = createMemo(() =>
collectInfrastructureAgentUpdateTargets(
rows(),
updateStore.versionInfo()?.version,
updateStore.versionInfo()?.agentUpdateTargetVersion,
agentUpdateScope(),
),
);
@@ -435,6 +435,62 @@ describe('InfrastructureWorkspace', () => {
});
});
it('does not invent scoped update commands when the backend publishes no agent target', async () => {
routeState.search = '?agentUpdates=1&agents=agent%3Aagent-zeus';
const primaryConnection = connectionFixture();
const attachedAgent = connectionFixture({
id: 'agent:agent-zeus',
type: 'agent',
name: 'zeus-agent',
address: 'zeus.lab',
surfaces: ['host'],
scope: { host: true },
source: 'agent',
agentVersion: '5.1.34',
agentUpdateAvailable: false,
agentIdentity: {
hostname: 'zeus',
platform: 'linux',
},
capabilities: { supportsPause: false, supportsScope: false, supportsTest: false },
});
connectionState.connections = [primaryConnection, attachedAgent];
connectionState.rows = [
{
id: primaryConnection.id,
ownerType: 'pve',
name: 'homelab',
subtitle: 'via platform API and Pulse Agent',
source: 'both',
host: primaryConnection.address,
coverageLabels: ['VMs', 'Host telemetry'],
statusLabel: 'Active',
statusClassName: 'bg-green-100 text-green-800',
agentUpdateCount: 0,
lastActivityText: '1m ago',
...emptyFleetRow,
enabled: true,
canEdit: true,
canPause: true,
canRemove: true,
isAgent: false,
isCluster: false,
attachedConnections: [attachedAgent],
members: [],
connection: primaryConnection,
},
];
renderWorkspace();
const dialog = await screen.findByRole('dialog');
expect(
within(dialog).getByText('Pulse does not currently see any agents behind the target version.'),
).toBeInTheDocument();
expect(within(dialog).queryByText('zeus')).not.toBeInTheDocument();
expect(within(dialog).queryByText(/upgrade agent:agent-zeus/)).not.toBeInTheDocument();
});
it('keeps source groups in the catalog order instead of count order', async () => {
const pveConnection = connectionFixture({
id: 'pve:zeus',
@@ -124,7 +124,7 @@ describe('infrastructure agent update commands model', () => {
});
});
it('falls back to the app build version when the connections ledger has no target version', () => {
it('uses the explicit agent update target when the connection has no target version', () => {
const staleAgent = connection({
id: 'agent:agent-delly',
type: 'agent',
@@ -144,7 +144,7 @@ describe('infrastructure agent update commands model', () => {
attachedConnections: [staleAgent],
}),
],
'6.0.0-rc.6+git.172.g2c360f779.dirty',
'v6.0.0-rc.6',
);
expect(targets).toHaveLength(1);
@@ -155,6 +155,29 @@ describe('infrastructure agent update commands model', () => {
});
});
it('does not infer stale targets without an agent update target', () => {
const staleAgent = connection({
id: 'agent:agent-delly',
type: 'agent',
name: 'delly',
address: 'delly',
surfaces: ['host'],
scope: { host: true },
source: 'agent',
agentVersion: 'v6.0.0-rc.5',
agentUpdateAvailable: false,
capabilities: { supportsPause: false, supportsScope: false, supportsTest: false },
});
const targets = collectInfrastructureAgentUpdateTargets([
row({
attachedConnections: [staleAgent],
}),
]);
expect(targets).toEqual([]);
});
it('filters to scoped agent IDs when a platform notice links to specific hosts', () => {
const staleDelly = connection({
id: 'agent:agent-delly',
@@ -64,14 +64,17 @@ export function DockerPageSurface() {
const activeTab = createMemo<DockerPageTabId>(() =>
tabs().some((tab) => tab.id === requestedTab()) ? requestedTab() : 'overview',
);
const agentUpdateTargetVersion = createMemo(
() => updateStore.versionInfo()?.agentUpdateTargetVersion,
);
const outdatedAgentHosts = createMemo(() =>
collectOutdatedAgentHosts(model().hosts, updateStore.versionInfo()?.version),
collectOutdatedAgentHosts(model().hosts, agentUpdateTargetVersion()),
);
const outdatedAgentUpdatePath = createMemo(() =>
buildInfrastructureAgentUpdatesPath(outdatedAgentHosts().map((host) => host.agentId)),
);
const serverVersionDisplay = createMemo(() =>
formatAgentVersionDisplay(updateStore.versionInfo()?.version),
formatAgentVersionDisplay(agentUpdateTargetVersion()),
);
return (
@@ -69,7 +69,10 @@ vi.mock('@/hooks/useUnifiedResources', () => ({
vi.mock('@/stores/updates', () => ({
updateStore: {
versionInfo: () => ({ version: '6.0.0-rc.6' }),
versionInfo: () => ({
version: '6.0.0-rc.6',
agentUpdateTargetVersion: '6.0.0-rc.6',
}),
},
}));
@@ -69,14 +69,17 @@ export function KubernetesPageSurface() {
tabs().some((tab) => tab.id === requestedTab()) ? requestedTab() : 'overview',
);
const controllerResources = createMemo(() => getKubernetesControllerResources(model()));
const agentUpdateTargetVersion = createMemo(
() => updateStore.versionInfo()?.agentUpdateTargetVersion,
);
const outdatedAgentHosts = createMemo(() =>
collectOutdatedAgentHosts(model().nodes, updateStore.versionInfo()?.version),
collectOutdatedAgentHosts(model().nodes, agentUpdateTargetVersion()),
);
const outdatedAgentUpdatePath = createMemo(() =>
buildInfrastructureAgentUpdatesPath(outdatedAgentHosts().map((host) => host.agentId)),
);
const serverVersionDisplay = createMemo(() =>
formatAgentVersionDisplay(updateStore.versionInfo()?.version),
formatAgentVersionDisplay(agentUpdateTargetVersion()),
);
return (
@@ -215,7 +215,10 @@ describe('KubernetesPageSurface contract', () => {
});
it('surfaces stale agent-backed Kubernetes nodes', () => {
mockVersionInfo.mockReturnValue({ version: 'v6.0.0-rc.6' });
mockVersionInfo.mockReturnValue({
version: 'v6.0.0-rc.6',
agentUpdateTargetVersion: 'v6.0.0-rc.6',
});
setResources([
makeResource({
id: 'agent:k8s-node-1',
@@ -85,14 +85,17 @@ export function ProxmoxPageSurface() {
if (!segment || !VALID_TABS.has(segment)) return 'overview';
return visibleTabIds().has(segment) ? segment : 'overview';
});
const agentUpdateTargetVersion = createMemo(
() => updateStore.versionInfo()?.agentUpdateTargetVersion,
);
const outdatedAgentHosts = createMemo(() =>
collectOutdatedAgentHosts(model().pveNodes, updateStore.versionInfo()?.version),
collectOutdatedAgentHosts(model().pveNodes, agentUpdateTargetVersion()),
);
const outdatedAgentUpdatePath = createMemo(() =>
buildInfrastructureAgentUpdatesPath(outdatedAgentHosts().map((host) => host.agentId)),
);
const serverVersionDisplay = createMemo(() =>
formatAgentVersionDisplay(updateStore.versionInfo()?.version),
formatAgentVersionDisplay(agentUpdateTargetVersion()),
);
// The hosts table at the top and the embedded WorkloadsSurface below share
@@ -123,7 +123,10 @@ describe('ProxmoxPageSurface contract', () => {
});
it('surfaces stale agent-backed Proxmox nodes', () => {
mockVersionInfo.mockReturnValue({ version: 'v6.0.0-rc.6' });
mockVersionInfo.mockReturnValue({
version: 'v6.0.0-rc.6',
agentUpdateTargetVersion: 'v6.0.0-rc.6',
});
setResources([
makeResource({
id: 'agent:delly',
@@ -149,4 +152,25 @@ describe('ProxmoxPageSurface contract', () => {
'/settings/infrastructure?agentUpdates=1&agents=agent%3Aagent-delly',
);
});
it('does not surface stale-agent notices for development builds without an agent target', () => {
mockVersionInfo.mockReturnValue({
version: '6.0.0-rc.6+git.172.g2c360f779.dirty',
isDevelopment: true,
});
setResources([
makeResource({
id: 'agent:delly',
name: 'delly',
displayName: 'delly',
type: 'agent',
proxmox: { nodeName: 'delly', clusterName: 'homelab' },
agent: { agentId: 'agent-delly', agentVersion: 'v5.1.34' },
}),
]);
render(() => <ProxmoxPageSurface />);
expect(screen.queryByTestId('platform-outdated-agent-notice')).not.toBeInTheDocument();
});
});
@@ -201,6 +201,7 @@ describe('proxmoxBackupRecoveryModel', () => {
expect(model.coverageRows).toHaveLength(1);
expect(model.coverageRows[0].posture).toBe('uncovered');
expect(model.coverageSummary.uncovered).toBe(1);
expect(model.coverageRows[0].pbsCount).toBe(0);
expect(model.coverageRows[0].archiveCount).toBe(0);
expect(model.coverageRows[0].snapshotCount).toBe(0);
@@ -69,14 +69,17 @@ export function StandalonePageSurface() {
model().machines.length === 0 &&
model().availabilityChecks.length === 0,
);
const agentUpdateTargetVersion = createMemo(
() => updateStore.versionInfo()?.agentUpdateTargetVersion,
);
const outdatedAgentHosts = createMemo(() =>
collectOutdatedAgentHosts(model().machines, updateStore.versionInfo()?.version),
collectOutdatedAgentHosts(model().machines, agentUpdateTargetVersion()),
);
const outdatedAgentUpdatePath = createMemo(() =>
buildInfrastructureAgentUpdatesPath(outdatedAgentHosts().map((host) => host.agentId)),
);
const serverVersionDisplay = createMemo(() =>
formatAgentVersionDisplay(updateStore.versionInfo()?.version),
formatAgentVersionDisplay(agentUpdateTargetVersion()),
);
createEffect(() => {
@@ -200,7 +203,7 @@ export function StandalonePageSurface() {
emptyIcon={machineIcon()}
emptyTitle="No machines"
emptyDescription="Install Pulse Agent on Linux, macOS, Windows, or Unraid systems for full CPU, memory, disk, and network telemetry."
targetAgentVersion={updateStore.versionInfo()?.version}
targetAgentVersion={agentUpdateTargetVersion()}
/>
</Show>
</div>
@@ -137,7 +137,10 @@ describe('StandalonePageSurface', () => {
});
it('surfaces stale Pulse Agent binaries on the Machines page', () => {
mocks.versionInfo.mockReturnValue({ version: 'v6.0.0-rc.6' });
mocks.versionInfo.mockReturnValue({
version: 'v6.0.0-rc.6',
agentUpdateTargetVersion: 'v6.0.0-rc.6',
});
mocks.useUnifiedResources.mockReturnValue({
resources: () => [
resource({
@@ -77,14 +77,17 @@ export function TrueNASPageSurface() {
const activeTab = createMemo<TrueNASPageTabId>(() =>
tabs().some((tab) => tab.id === requestedTab()) ? requestedTab() : 'overview',
);
const agentUpdateTargetVersion = createMemo(
() => updateStore.versionInfo()?.agentUpdateTargetVersion,
);
const outdatedAgentHosts = createMemo(() =>
collectOutdatedAgentHosts(model().systems, updateStore.versionInfo()?.version),
collectOutdatedAgentHosts(model().systems, agentUpdateTargetVersion()),
);
const outdatedAgentUpdatePath = createMemo(() =>
buildInfrastructureAgentUpdatesPath(outdatedAgentHosts().map((host) => host.agentId)),
);
const serverVersionDisplay = createMemo(() =>
formatAgentVersionDisplay(updateStore.versionInfo()?.version),
formatAgentVersionDisplay(agentUpdateTargetVersion()),
);
return (
@@ -134,7 +134,10 @@ describe('TrueNASPageSurface contract', () => {
});
it('surfaces stale agent-backed TrueNAS systems', () => {
mockVersionInfo.mockReturnValue({ version: 'v6.0.0-rc.6' });
mockVersionInfo.mockReturnValue({
version: 'v6.0.0-rc.6',
agentUpdateTargetVersion: 'v6.0.0-rc.6',
});
setResources([
makeResource({
id: 'agent:truenas-scale',
+11 -10
View File
@@ -5275,16 +5275,17 @@ func (r *Router) handleVersion(w http.ResponseWriter, req *http.Request) {
// Convert to typed response
response := VersionResponse{
Version: versionInfo.Version,
BuildTime: versionInfo.Build,
Build: versionInfo.Build,
GoVersion: runtime.Version(),
Runtime: versionInfo.Runtime,
Channel: versionInfo.Channel,
IsDocker: versionInfo.IsDocker,
IsSourceBuild: versionInfo.IsSourceBuild,
IsDevelopment: versionInfo.IsDevelopment,
DeploymentType: versionInfo.DeploymentType,
Version: versionInfo.Version,
BuildTime: versionInfo.Build,
Build: versionInfo.Build,
GoVersion: runtime.Version(),
Runtime: versionInfo.Runtime,
Channel: versionInfo.Channel,
IsDocker: versionInfo.IsDocker,
IsSourceBuild: versionInfo.IsSourceBuild,
IsDevelopment: versionInfo.IsDevelopment,
DeploymentType: versionInfo.DeploymentType,
AgentUpdateTargetVersion: currentAgentTargetVersion(),
}
// Detect containerization (LXC/Docker)
@@ -87,6 +87,31 @@ func TestHandleVersion_Success(t *testing.T) {
}
}
func TestHandleVersion_IncludesReleaseAgentUpdateTarget(t *testing.T) {
oldBuildVersion := updates.BuildVersion
updates.BuildVersion = "6.0.0-rc.6"
t.Cleanup(func() {
updates.BuildVersion = oldBuildVersion
})
router := &Router{updateManager: updates.NewManager(&config.Config{})}
req := httptest.NewRequest(http.MethodGet, "/api/version", nil)
rec := httptest.NewRecorder()
router.handleVersion(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}
var payload VersionResponse
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
t.Fatalf("decode response: %v", err)
}
if payload.AgentUpdateTargetVersion != "6.0.0-rc.6" {
t.Fatalf("AgentUpdateTargetVersion = %q, want 6.0.0-rc.6", payload.AgentUpdateTargetVersion)
}
}
func TestHandleMetricsHistory_MethodNotAllowed(t *testing.T) {
router := &Router{}
req := httptest.NewRequest(http.MethodPost, "/api/metrics/history", nil)
+15 -14
View File
@@ -25,20 +25,21 @@ func (r HealthResponse) NormalizeCollections() HealthResponse {
// VersionResponse represents version information
type VersionResponse struct {
Version string `json:"version"`
BuildTime string `json:"buildTime,omitempty"`
Build string `json:"build,omitempty"`
GoVersion string `json:"goVersion,omitempty"`
Runtime string `json:"runtime,omitempty"`
Channel string `json:"channel,omitempty"`
IsDocker bool `json:"isDocker"`
IsSourceBuild bool `json:"isSourceBuild"`
IsDevelopment bool `json:"isDevelopment"`
DeploymentType string `json:"deploymentType,omitempty"`
UpdateAvailable bool `json:"updateAvailable"`
LatestVersion string `json:"latestVersion,omitempty"`
Containerized bool `json:"containerized"`
ContainerID string `json:"containerId,omitempty"`
Version string `json:"version"`
BuildTime string `json:"buildTime,omitempty"`
Build string `json:"build,omitempty"`
GoVersion string `json:"goVersion,omitempty"`
Runtime string `json:"runtime,omitempty"`
Channel string `json:"channel,omitempty"`
IsDocker bool `json:"isDocker"`
IsSourceBuild bool `json:"isSourceBuild"`
IsDevelopment bool `json:"isDevelopment"`
DeploymentType string `json:"deploymentType,omitempty"`
AgentUpdateTargetVersion string `json:"agentUpdateTargetVersion,omitempty"`
UpdateAvailable bool `json:"updateAvailable"`
LatestVersion string `json:"latestVersion,omitempty"`
Containerized bool `json:"containerized"`
ContainerID string `json:"containerId,omitempty"`
}
// ChartResponse represents chart data
@@ -2860,7 +2860,7 @@ class SubsystemLookupTest(unittest.TestCase):
{
"heading": "## Shared Boundaries",
"path": "internal/api/access_control_handlers.go",
"line": 360,
"line": 364,
"heading_line": 113,
}
],