Files
Marc Schier 7fb986bd0d Add recycling operator console and sample container images (#4810)
# Description

Add a local browser-based operator console for the synthetic recycling
line, using direct, encrypted OPC UA connections and the existing typed
coordination interface.

- Extract a sample-local, non-packable `Recycling.Client` library shared
by the server and console. Preserve the model XML bytes, namespace/type
identifiers, resource identity, wire format and journal schema; the
server retains its .NET 8/9/10 targets.
- Add a .NET 10 ASP.NET Core workstation host with bounded SSE
state/trend delivery, native companion inspection, paged
material/genealogy reads, and role-limited coordinator commands. Exact
command envelopes are retained for explicit retries; uncertain outcomes
are not automatically reissued.
- Add the operator interface using local HTML/SVG, CSS and JavaScript
modules: ten process groups, substep inspection, faults/constraints,
persistent quality holds, trends, material tracing and simulator
administration. Show raw quality/provenance and handle stale data, run
changes, browser expiry and large identifiers explicitly. No
browser-generated plant telemetry or frontend framework/npm build is
introduced.
- Keep application/user credentials in external certificate stores. Use
strict X.509 trust, signed/encrypted UA sessions, local browser
bootstrap sessions, same-origin/CSRF checks, and a conservative
configured control ceiling. This is a local synthetic sample, not a
physical emergency-stop or shared remote-user system.
- Register `recyclingserver` and `recyclingclient` in the existing
Images CI matrix, using source/prebuilt Dockerfile stages and the
existing portable-IL/multiarchitecture packaging flow. Add an optional
loopback-published Compose console service. No new schedules or NuGet
publication entries are added.
- Add managed, real-UA, Chromium, NativeAOT and image-contract
qualification, with Microsoft.Playwright as a test-only centrally
managed dependency, and update usage/deployment documentation.

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-10-17 16:16:29 +02:00
..

Generic recycling deployment examples

These examples run the sample as one coordinator and ten process views. They use the same synthetic configuration and typed coordination model as the console host. They do not add a WoT connector, cloud ingestion, hardware control, or certification. Check the sample README's current limitations before using the data.

The server Dockerfile publishes and runs the managed .NET 10 target; all eleven server services use that one image. Existing Images CI includes recyclingserver and the separate recyclingclient browser host, with its existing GHCR naming/tag policy and no new schedule. Published tags require a successful publishing run; the examples below build locally. All private scenarios, certificate stores, user keys, and runtime state stay outside the repository and the container build context.

Shared simulation configuration

All eleven roles must use the same effective simulation configuration and embedded sample model. The checked-in Compose and Kubernetes commands omit --scenario for every role, so all use healthy; the shared image also supplies the same compiled sample model.

To select another named scenario, add the same --scenario <name> to the coordinator and all ten process-view commands/argument lists. The option is local to each executable; configuring only the coordinator does not propagate it to the views.

For an external scenario JSON file, supply identical effective contents to every role and add --scenario <container-path> to each command. Mount the file read-only into each Compose service and each Kubernetes workload using your deployment's configuration provisioning. The existing identity-mapping mounts do not automatically supply a scenario file. Keep scenario files out of the image context and keep existing data mounts and persistent claims intact.

Views verify the complete configuration/model hash before membership commands or projection. A mismatch or missing identity produces BadConfigurationError, leaves the endpoint available for diagnosis, and stops automatic reconnect. Correct the view's scenario/build and restart it; do not delete journals, reset the run, or replace retained volumes to bypass the refusal. Matching scenario names alone is insufficient: residence, tick duration, leases, limits and all other effective simulation settings must also match. Role-specific certificates/endpoints and wall-clock pacing may remain independent.

Provision first

Use the stack's certificate manager and certificate-store guidance. Do not enable automatic acceptance of unknown certificates.

Provision a separate data directory for each role:

external-recycling-data
  identities.json
  coordinator
    pki
    state
  collection
    pki
    users
    state
  sorting
    pki
    users
    state
  ...
  bagging
    pki
    users
    state

pki is the application's normal certificate-store root. users is a directory certificate store containing only that process's user identity and private key. state is writable runtime storage; only the coordinator has a production journal. Create these directories through the appropriate storage/certificate APIs and allow the container's non-root UID/GID 1654 to access them. Protect private keys and encrypt persistent storage and backups as appropriate.

The sample's application URI is stable: urn:localhost:OPCFoundation:Recycling-line-1-<role>. Application certificates must include the configured endpoint DNS name, such as recycling-sorting, in their permitted names. The process user subject selected by these examples is CN=Recycling-line-1-<role>; provision that subject in its own users store. Application identity and user identity are separate certificates and trust checks.

Populate identities.json using the sample README's schema. Give each of the ten process identities its own ProcessView binding and process selection. Add separately provisioned observer/operator/administrator user identities as needed. Never place operator or administrator private keys in a process-view container. The mapping contains public certificate fingerprints, not private keys; public fingerprints alone do not authenticate a caller.

Exchange the required application public certificates into peer trust and the required user public certificates into user trust. Preserve the stack's actual directory-store layout and filenames; do not manufacture private-key filenames or store keys in JSON.

Docker Compose

From the repository root:

docker build -f samples\Recycling\RecyclingServer\Dockerfile -t recycling-server:local .
$env:RECYCLING_DATA_DIR = 'D:\recycling-data'
docker compose -f samples\Recycling\RecyclingServer\compose.yaml config --quiet
docker compose -f samples\Recycling\RecyclingServer\compose.yaml up -d

Use an absolute, provisioned RECYCLING_DATA_DIR outside the repository; the path above is an example, not a folder the sample creates. Each container mounts only its own role directory and the shared read-only fingerprint mapping. Its application filesystem is read-only, with a temporary filesystem for runtime scratch work. Change RECYCLING_IMAGE to use a different locally built image.

Coordinator port 62570 and view ports 62571 through 62580 are published on the host's loopback interface only. Inside the Compose network every role listens on port 62570 and advertises its own recycling-<role> DNS endpoint. An external client must be able to resolve the advertised DNS names and trust matching certificates; publishing a port does not rewrite OPC UA discovery URLs or certificate names. Configure deployment-appropriate endpoints rather than disabling domain validation.

Views can start before the coordinator is usable and retry the connection. Their material state continues to come from the coordinator. The new run starts only after all ten roles authenticate, register, and initialize. Inspect the typed Coordinator.State and view availability with an authenticated OPC UA client; an existing container or an open TCP port does not prove the material flow is running.

docker compose -f samples\Recycling\RecyclingServer\compose.yaml logs --tail 100
docker compose -f samples\Recycling\RecyclingServer\compose.yaml down

Stopping containers does not delete the externally mounted state. Do not delete journals or reset the run as a substitute for recovery.

Optional operator console

The separate operator console is an optional local browser host, not another process participant. Closing it cannot reset/stop the run or renew the process servers' leases.

Provision a separate operator-console directory under RECYCLING_DATA_DIR:

operator-console
  pki
    own
    trusted
    issuer
    rejected
  secrets
    bootstrap-key
    users
      certs
      private
    user-password

Use the certificate manager to provision the application's certificate/private key in own, its peer/issuer trust, and a dedicated UA user certificate in the users directory store. The example selects subject CN=Recycling-line-1-observer; user-password contains the user store's private-key password. bootstrap-key contains a random 32-byte hexadecimal browser launch key. These secrets are external, read-only mounts; do not add their values to Compose environment variables or the image. Allow UID/GID 1654 to access these stores. The client never mounts coordinator state, the shared identity mapping or any process user key.

Trust the client's application certificate on each server it accesses. Exchange server application certificates into the client's peer trust, and bind the client user certificate in the coordinator's user trust and external identity mapping with the intended role. Compose defaults to the conservative Observer mode. Changing the configured role does not elevate a UA identity: server authorization remains authoritative.

docker build -f samples\Recycling\RecyclingOperatorConsole\Dockerfile -t recycling-client:local .
docker compose -f samples\Recycling\RecyclingServer\compose.yaml --profile operator-console config --quiet
docker compose -f samples\Recycling\RecyclingServer\compose.yaml --profile operator-console up -d

Browse locally to http://localhost:8080/bootstrap?token=<external-bootstrap-key>, protecting the credential URL from logs and shared browser history. The profile explicitly binds the container on port 8080 and publishes only 127.0.0.1:8080; localhost:8080 is the allowed browser host. Do not use a remote workstation URL or expose this sample on all host interfaces. RECYCLING_CLIENT_IMAGE selects another built/published client image without changing the server's RECYCLING_IMAGE.

The ten native inspector mappings and coordinator endpoint use the Compose DNS names and container port 62570, not the host-published ports. The servers' application certificates must match those names; port publishing does not rewrite discovery or bypass hostname checks. Read the sample guide for session/CSRF protection, strict UA trust, command confirmation, unknown outcomes and version-one limits. The Kubernetes example remains server-only.

Kubernetes

recycling.yaml contains one namespace, eleven headless Services, and eleven single-replica StatefulSets. Each role has its own persistent volume claim; the coordinator is not an HA replica set. The workload has no Kubernetes API permissions or mounted service-account token. Pods run without root privileges, drop capabilities, and mount a read-only application filesystem.

Choose an explicit disposable/test context. Load the locally built image into that cluster or change the image reference to a registry you control; recycling-server:local is not a published registry image.

kubectl kustomize samples\Recycling\RecyclingServer\deploy
kubectl --context <test-context> create namespace recycling-sample
kubectl --context <test-context> -n recycling-sample create configmap recycling-identities `
    --from-file=identities.json=<external-identities-file>

Provision each role's claim through your cluster's approved storage/certificate tooling before starting it. Claim names follow data-recycling-<role>-0; their root contains the same pki, users (views), and state directories as the console/Compose layout. The manifests use the cluster's default storage class and request 1 GiB per role. Adjust the storage class, encryption, capacity, resources, and network policy to your environment. For staged provisioning, pre-create and populate the claims with those exact names before enabling the roles. A StatefulSet with zero replicas does not itself create its claims.

kubectl --context <test-context> apply --dry-run=server `
    -k samples\Recycling\RecyclingServer\deploy
kubectl --context <test-context> apply -k samples\Recycling\RecyclingServer\deploy
kubectl --context <test-context> -n recycling-sample get pods,pvc,services

Do not run these commands against an implicit current context. Do not force-delete a coordinator pod or scale it above one while its prior writer may still own the volume. ReadWriteOnce alone is not a distributed writer lock, and a StatefulSet is not a consensus protocol. The coordinator also enforces exclusive writer ownership in its store.

TCP startup/readiness/liveness probes check transport availability only. They deliberately do not wait for all participants, which would hide the endpoints needed to satisfy that barrier. Use the typed run state, leases, snapshot revisions, and unavailable statuses for production readiness and recovery checks. A paused run is not a liveness failure.

Operational checks

With provisioned test identities, verify a new run's participant barrier, material progress, hold/disposition behavior, one view's loss/reconnection, and a coordinator restart with its volume preserved. Confirm the run ID, committed counters, source contributions, and command outcomes are recovered without duplicate transfers. Leave paused runs paused after recovery.

Do not interpret static manifest parsing, a listening socket, or successful process startup as evidence of full companion conformance or source-data parity. Native/container startup and cluster checks require the relevant runtime and an explicitly provisioned test environment.