Skip to content
Draft
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
3 changes: 3 additions & 0 deletions changelogs/unreleased/add-references-tab.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
description: Add a "References" tab to the resource details page, showing each reference with an expandable view of its arguments.
change-type: minor
destination-branches: [master, iso9]
25 changes: 25 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
services:
vite:
image: "node:22"
pull_policy: always
entrypoint: ["/entrypoint.sh"]
volumes:
- ./entrypoint.sh:/entrypoint.sh
- ./:/home/node/web-console
environment:
VITE_API_BASEURL: http://172.25.162.3:8888/
JOINTJS_NPM_TOKEN: ${JOINTJS_NPM_TOKEN}
user: "root:root"
networks:
mgmt:
ipv4_address: 172.20.0.3

volumes:
web-console-data: {}

networks:
mgmt:
ipam:
config:
- subnet: 172.20.0.0/28
gateway: 172.20.0.1
38 changes: 38 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Alternative entrypoint for the node container.
# It starts a code-server to allow easy access to the web console state directory.

set -x
set -e
set -o allexport

## SETUP ##
# Make sure that any environment variable prefixed with NODE which is available
# to the container, will also be available to the node user when login in
NODE_USER_HOME_DIR=$(getent passwd node | cut -d: -f6)
NODE_ENV_FILE="${NODE_USER_HOME_DIR}/.node_env"
NODE_PROFILE="${NODE_USER_HOME_DIR}/.profile"
LOAD_ENV_CMD=". $NODE_ENV_FILE"

# Overwrite environment variables in dedicated file
if export | grep VITE; then
export | grep VITE >> $NODE_ENV_FILE
fi
if export | grep NPM; then
export | grep NPM >> $NODE_ENV_FILE
fi

# Make sure to load environment variables when login in
touch $NODE_PROFILE
grep -e "$LOAD_ENV_CMD" "$NODE_PROFILE" || echo "$LOAD_ENV_CMD" >> "$NODE_PROFILE"

# Install sudo
apt-get update -y
apt-get install -y sudo vim tini

# Install the web-console dependencies
exec /usr/bin/tini -- sudo -i -u node bash <<EOF
cd web-console
yarn install
exec yarn start
EOF
181 changes: 181 additions & 0 deletions resource.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
{
"data": {
"resource_id": "future::lsm::ServiceAttributeValue[lsm,uri=3ef00f70-ef21-3821-969f-1c3193f69fd4:compliance]",
"resource_type": "future::lsm::ServiceAttributeValue",
"agent": "lsm",
"id_attribute": "uri",
"id_attribute_value": "3ef00f70-ef21-3821-969f-1c3193f69fd4:compliance",
"attributes": {
"uri": "3ef00f70-ef21-3821-969f-1c3193f69fd4:compliance",
"value": null,
"purged": false,
"mutators": [
{
"args": [
{
"id": "future::lsm::ServiceAttributeValue[lsm,uri=3ef00f70-ef21-3821-969f-1c3193f69fd4:compliance]",
"name": "resource",
"type": "resource"
},
{ "id": "b5d776d4-f3c4-358e-a102-2d1b2fc28a18", "name": "value", "type": "reference" },
{ "name": "destination", "type": "literal", "value": "value" }
],
"type": "core::Replace"
}
],
"requires": [
"future::std::ResourceCompliance[std,name=3ef00f70-ef21-3821-969f-1c3193f69fd4]"
],
"references": [
{
"id": "b5d776d4-f3c4-358e-a102-2d1b2fc28a18",
"args": [
{
"name": "resources_compliance_info",
"type": "mjson",
"value": [null, null, null],
"references": {
"$[0]": {
"id": "39a2aaf5-70df-3363-b7a0-1f09bc3a313a",
"name": "$[0]",
"type": "reference"
},
"$[1]": {
"id": "0bca1fb2-d88a-335e-a834-17324dde83bf",
"name": "$[1]",
"type": "reference"
},
"$[2]": {
"id": "0432e70b-cb53-36e5-aa07-d3292b726249",
"name": "$[2]",
"type": "reference"
}
}
},
{
"name": "environment",
"type": "literal",
"value": "1c337bad-1701-49c9-90fe-028d88deb7ee"
},
{
"name": "template",
"type": "literal",
"value": "# Compliance report\n\nThe compliance report below summarizes the compliance of a part of the current desired state. This report\nis updated every time a resource is redeployed, whether the resource fails or not.\n\n{{ dict(name=\"\", value=\"**Value**\", description=\"**Description**\") | md_table(stats_table) }}\n\n## Warning resources\n\nHere are all the resources that should be looked at because their compliance (or non-compliance) is unexpected.\n\n{%- for info in warning_resources_info %}\n{%- set diff = (info.diff | md_diff_block()) if info.diff else (info.error | md_code_block()) %}\n{%- set link = \":link: \" + (\"Resource details\" | md_link(info.resource_url)) %}\n{%- set message = \":bulb: \" + info.message %}\n{%- set content = link + \"\\n\" + message + \"\\n\" + diff %}\n\n{{ info.resource_id | md_details(content) }}\n\n{%- endfor %}\n\n## Non compliant resources\n\nHere is the list of all resources which currently don't match the desired state. This shows the changes the orchestrator\nwould make if these resources were deployed successfully. Depending on the context in which this desired state is emitted,\nthese differences may or may not be expected/desired. They are shown here below not because they need to be looked into per\nse, but rather for visibility on the orchestrator knowledge of the current network compliance.\n\n{%- for info in non_compliant_resources_info %}\n{%- set diff = (info.diff | md_diff_block()) if info.diff else (info.error | md_code_block()) %}\n{%- set link = \":link: \" + (\"Resource details\" | md_link(info.resource_url)) %}\n{%- set content = link + \"\\n\" + diff %}\n\n{{ info.resource_id | md_details(content) }}\n\n{%- endfor %}\n"
}
],
"type": "future::std::ComplianceReport"
},
{
"id": "39a2aaf5-70df-3363-b7a0-1f09bc3a313a",
"args": [
{
"id": "01297085-f2d8-3097-9ba0-4b4c418bf180",
"name": "compliance_status",
"type": "reference"
}
],
"type": "future::std::CompliantResourceCompliance"
},
{
"id": "01297085-f2d8-3097-9ba0-4b4c418bf180",
"args": [
{
"name": "compliance_resource",
"type": "literal",
"value": "future::std::ResourceCompliance[std,name=3ef00f70-ef21-3821-969f-1c3193f69fd4]"
},
{
"name": "resource_id",
"type": "literal",
"value": "future::lsm::ServiceAttributeValue[lsm,uri=3ef00f70-ef21-3821-969f-1c3193f69fd4:documentation]"
},
{
"name": "environment",
"type": "literal",
"value": "1c337bad-1701-49c9-90fe-028d88deb7ee"
}
],
"type": "future::std::ResourceComplianceStatus"
},
{
"id": "0bca1fb2-d88a-335e-a834-17324dde83bf",
"args": [
{
"id": "79b1c346-09cd-33cb-afe3-344cec8a4192",
"name": "compliance_status",
"type": "reference"
}
],
"type": "future::std::CompliantResourceCompliance"
},
{
"id": "79b1c346-09cd-33cb-afe3-344cec8a4192",
"args": [
{
"name": "compliance_resource",
"type": "literal",
"value": "future::std::ResourceCompliance[std,name=3ef00f70-ef21-3821-969f-1c3193f69fd4]"
},
{
"name": "resource_id",
"type": "literal",
"value": "netbox::resources::Interface[http://172.25.162.59:8080,uri=cpe-1:ethernet-1/2]"
},
{
"name": "environment",
"type": "literal",
"value": "1c337bad-1701-49c9-90fe-028d88deb7ee"
}
],
"type": "future::std::ResourceComplianceStatus"
},
{
"id": "0432e70b-cb53-36e5-aa07-d3292b726249",
"args": [
{
"id": "8e713d57-5c6c-33a7-9732-fd40c32324fc",
"name": "compliance_status",
"type": "reference"
}
],
"type": "future::std::CompliantResourceCompliance"
},
{
"id": "8e713d57-5c6c-33a7-9732-fd40c32324fc",
"args": [
{
"name": "compliance_resource",
"type": "literal",
"value": "future::std::ResourceCompliance[std,name=3ef00f70-ef21-3821-969f-1c3193f69fd4]"
},
{
"name": "resource_id",
"type": "literal",
"value": "fs::JsonFile[telegraf,uri=/config/port-info-service-tags.json:cpe-1:ethernet-1/2]"
},
{
"name": "environment",
"type": "literal",
"value": "1c337bad-1701-49c9-90fe-028d88deb7ee"
}
],
"type": "future::std::ResourceComplianceStatus"
}
],
"send_event": true,
"instance_id": "3ef00f70-ef21-3821-969f-1c3193f69fd4",
"report_only": false,
"attribute_path": "compliance",
"receive_events": true,
"service_entity": "UserNetworkInterface",
"purge_on_delete": false,
"desired_state_version": 3
},
"last_deploy": "2026-05-18T12:13:22.879226+00:00",
"first_generated_time": "2026-05-15T11:53:38.851136+00:00",
"status": "deployed",
"requires_status": {
"future::std::ResourceCompliance[std,name=3ef00f70-ef21-3821-969f-1c3193f69fd4]": "deployed"
}
}
}
72 changes: 72 additions & 0 deletions src/Slices/ResourceDetails/Core/Mutator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { ReferenceArg } from "./Reference";

/**
* Mutator carried on a resource's desired state (`attributes.mutators`). A
* mutator describes a deferred mutation that the orchestrator will apply at
* deploy time. Today the orchestrator only ships `core::Replace`, which
* substitutes the value at a given `destination` (a JSONPath / dictpath
* expression on the resource) with the resolution of a `value` reference.
*
* Source of truth for the contract:
* `inmanta_core.references` — `Mutator`, `MutatorModel`, `ReplaceValue`.
*/
export interface Mutator {
type: string;
args: ReferenceArg[];
}

export const extractMutators = (attributes: Record<string, unknown>): Mutator[] => {
const value = attributes.mutators;

return Array.isArray(value) ? (value as Mutator[]) : [];
};

interface ReplaceMutation {
destination: string;
referenceId: string;
}

/**
* Best-effort interpretation of a single mutator. Returns null for mutators
* we do not (yet) know how to surface in the UI.
*/
const interpretReplace = (mutator: Mutator): ReplaceMutation | null => {
if (mutator.type !== "core::Replace") {
return null;
}
let destination: string | null = null;
let referenceId: string | null = null;

for (const arg of mutator.args) {
if (arg.name === "destination" && arg.type === "literal" && typeof arg.value === "string") {
destination = arg.value;
} else if (arg.name === "value" && arg.type === "reference") {
referenceId = arg.id;
}
}

if (destination === null || referenceId === null) {
return null;
}

return { destination, referenceId };
};

/**
* Returns a `path → referenceId` map ready to feed `substituteReferences`.
* The map's keys are JSONPath expressions rooted at the resource's
* `attributes` object.
*/
export const buildMutatorSubstitutions = (mutators: Mutator[]): Record<string, string> => {
const out: Record<string, string> = {};

for (const mutator of mutators) {
const replace = interpretReplace(mutator);

if (replace) {
out[replace.destination] = replace.referenceId;
}
}

return out;
};
59 changes: 59 additions & 0 deletions src/Slices/ResourceDetails/Core/Reference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Domain types for the `references` array carried on a resource's desired state
* (`details.attributes.references`). Scoped to the ResourceDetails slice — the
* canonical `attributes` field on `Resource.Details` stays typed as
* `Record<string, unknown>` because not every consumer expects this shape.
*/

export type ReferenceArg =
| LiteralArg
| ReferenceRefArg
| ResourceRefArg
| MjsonArg;

export interface LiteralArg {
name: string;
type: "literal";
value: unknown;
}

export interface ReferenceRefArg {
name: string;
type: "reference";
id: string;
}

export interface ResourceRefArg {
name: string;
type: "resource";
id: string;
}

export interface MjsonArg {
name: string;
type: "mjson";
value: unknown;
references?: Record<string, { id: string; name: string; type: string }>;
}

export interface Reference {
id: string;
type: string;
args: ReferenceArg[];
}

export const isReferenceArg = (arg: ReferenceArg): arg is ReferenceRefArg =>
arg.type === "reference";

export const isResourceArg = (arg: ReferenceArg): arg is ResourceRefArg =>
arg.type === "resource";

/**
* Pulls the `references` array off the raw attributes blob. Returns an empty
* array when the field is missing or not an array.
*/
export const extractReferences = (attributes: Record<string, unknown>): Reference[] => {
const value = attributes.references;

return Array.isArray(value) ? (value as Reference[]) : [];
};
Loading