Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e13152f340 | |||
| deba5ed115 | |||
| 4bf71a0b2c | |||
| b4a7bac4c0 | |||
| 6df373ae4c | |||
| fe44e3c18a | |||
| 523f944f3e | |||
| c33f1e6047 |
@@ -0,0 +1,21 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<!-- Shared package metadata for clients/dotnet/. Individual projects opt in via <IsPackable>true</IsPackable>. -->
|
||||||
|
<Authors>Joseph Doherty</Authors>
|
||||||
|
<Company>ZB MOM WW</Company>
|
||||||
|
<Copyright>Copyright (c) ZB MOM WW. All rights reserved.</Copyright>
|
||||||
|
<Product>MxAccessGateway Client</Product>
|
||||||
|
<RepositoryUrl>https://gitea.dohertylan.com/dohertj2/mxaccessgw</RepositoryUrl>
|
||||||
|
<RepositoryType>git</RepositoryType>
|
||||||
|
<PackageProjectUrl>https://gitea.dohertylan.com/dohertj2/mxaccessgw</PackageProjectUrl>
|
||||||
|
<PackageTags>mxaccess;mxgateway;grpc;client;archestra</PackageTags>
|
||||||
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
|
<!-- Versioning: bump per release. Symbols ship as snupkg. -->
|
||||||
|
<Version>0.1.0</Version>
|
||||||
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
|
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<!-- Default: do NOT pack. Each project opts in. -->
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -299,6 +299,29 @@ $env:MXGATEWAY_TEST_ITEM = 'Area001.Pump001.Speed'
|
|||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- smoke --endpoint $env:MXGATEWAY_ENDPOINT --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- smoke --endpoint $env:MXGATEWAY_ENDPOINT --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Installing as a NuGet Package
|
||||||
|
|
||||||
|
The client publishes to the internal Gitea NuGet feed at
|
||||||
|
`https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json`.
|
||||||
|
|
||||||
|
Add the feed once:
|
||||||
|
|
||||||
|
````bash
|
||||||
|
dotnet nuget add source https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json \
|
||||||
|
--name dohertj2-gitea \
|
||||||
|
--username <gitea-username> \
|
||||||
|
--password <gitea-token-or-password> \
|
||||||
|
--store-password-in-clear-text
|
||||||
|
````
|
||||||
|
|
||||||
|
Then add the package to your project:
|
||||||
|
|
||||||
|
````bash
|
||||||
|
dotnet add package ZB.MOM.WW.MxGateway.Client --version 0.1.0
|
||||||
|
````
|
||||||
|
|
||||||
|
The `ZB.MOM.WW.MxGateway.Contracts` package is pulled in transitively.
|
||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Client Packaging](../../docs/ClientPackaging.md)
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ namespace ZB.MOM.WW.MxGateway.Client;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class MxGatewayClientContractInfo
|
public static class MxGatewayClientContractInfo
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc cref="GatewayContractInfo.GatewayProtocolVersion"/>
|
||||||
public const uint GatewayProtocolVersion =
|
public const uint GatewayProtocolVersion =
|
||||||
GatewayContractInfo.GatewayProtocolVersion;
|
GatewayContractInfo.GatewayProtocolVersion;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="GatewayContractInfo.WorkerProtocolVersion"/>
|
||||||
public const uint WorkerProtocolVersion =
|
public const uint WorkerProtocolVersion =
|
||||||
GatewayContractInfo.WorkerProtocolVersion;
|
GatewayContractInfo.WorkerProtocolVersion;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,4 +16,15 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<IsPackable>true</IsPackable>
|
||||||
|
<PackageId>ZB.MOM.WW.MxGateway.Client</PackageId>
|
||||||
|
<Description>.NET 10 gRPC client for the MxAccessGateway service. Provides typed wrappers, retry, and a lazy-browse walker over the Galaxy Repository hierarchy.</Description>
|
||||||
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -275,6 +275,38 @@ $env:MXGATEWAY_TEST_ITEM = 'Area001.Tag.Value'
|
|||||||
go run ./cmd/mxgw-go smoke -endpoint $env:MXGATEWAY_ENDPOINT -plaintext -api-key-env MXGATEWAY_API_KEY -item $env:MXGATEWAY_TEST_ITEM -json
|
go run ./cmd/mxgw-go smoke -endpoint $env:MXGATEWAY_ENDPOINT -plaintext -api-key-env MXGATEWAY_API_KEY -item $env:MXGATEWAY_TEST_ITEM -json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Installing the Go client
|
||||||
|
|
||||||
|
The module is resolved directly from the git repo — no package registry:
|
||||||
|
|
||||||
|
````bash
|
||||||
|
go get gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go@v0.1.0
|
||||||
|
````
|
||||||
|
|
||||||
|
Then import:
|
||||||
|
|
||||||
|
````go
|
||||||
|
import "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/mxgateway"
|
||||||
|
````
|
||||||
|
|
||||||
|
If your build environment cannot reach `gitea.dohertylan.com` directly,
|
||||||
|
configure `GOPROXY` to point at an internal proxy that fronts the Gitea
|
||||||
|
repo, or use `GONOSUMCHECK` + `GOPRIVATE` to bypass the checksum database
|
||||||
|
for the internal module path.
|
||||||
|
|
||||||
|
## Releasing a new version
|
||||||
|
|
||||||
|
Go modules in monorepo subdirectories use prefixed tags. To tag a release
|
||||||
|
from this repo:
|
||||||
|
|
||||||
|
````bash
|
||||||
|
pwsh scripts/tag-go-module.ps1 -Version v0.1.1 -Push
|
||||||
|
````
|
||||||
|
|
||||||
|
The script validates semver, refuses to tag with uncommitted tracked
|
||||||
|
changes, creates an annotated tag `clients/go/v0.1.1`, and (with `-Push`)
|
||||||
|
pushes it to origin.
|
||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Client Packaging](../../docs/ClientPackaging.md)
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||||
|
|||||||
@@ -282,6 +282,37 @@ $env:MXGATEWAY_TEST_ITEM = 'TestObject.TestInt'
|
|||||||
gradle :zb-mom-ww-mxgateway-cli:run --args="smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json"
|
gradle :zb-mom-ww-mxgateway-cli:run --args="smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Installing from the Gitea Maven repository
|
||||||
|
|
||||||
|
The client publishes to the internal Gitea Maven repository at
|
||||||
|
`https://gitea.dohertylan.com/api/packages/dohertj2/maven`.
|
||||||
|
|
||||||
|
In your consumer project's `build.gradle`:
|
||||||
|
|
||||||
|
````groovy
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
url 'https://gitea.dohertylan.com/api/packages/dohertj2/maven'
|
||||||
|
credentials {
|
||||||
|
username = System.getenv('GITEA_USERNAME')
|
||||||
|
password = System.getenv('GITEA_TOKEN')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'com.zb.mom.ww.mxgateway:zb-mom-ww-mxgateway-client:0.1.0'
|
||||||
|
}
|
||||||
|
````
|
||||||
|
|
||||||
|
To publish a new version from this repo:
|
||||||
|
|
||||||
|
````bash
|
||||||
|
export GITEA_USERNAME=dohertj2
|
||||||
|
export GITEA_TOKEN=<your-gitea-token>
|
||||||
|
gradle :zb-mom-ww-mxgateway-client:publish
|
||||||
|
````
|
||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Client Packaging](../../docs/ClientPackaging.md)
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||||
|
|||||||
@@ -37,4 +37,44 @@ subprojects {
|
|||||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pluginManager.withPlugin('maven-publish') {
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
maven(MavenPublication) {
|
||||||
|
from components.java
|
||||||
|
pom {
|
||||||
|
url = 'https://gitea.dohertylan.com/dohertj2/mxaccessgw'
|
||||||
|
description = 'MxAccessGateway Java client'
|
||||||
|
scm {
|
||||||
|
url = 'https://gitea.dohertylan.com/dohertj2/mxaccessgw'
|
||||||
|
connection = 'scm:git:https://gitea.dohertylan.com/dohertj2/mxaccessgw.git'
|
||||||
|
}
|
||||||
|
developers {
|
||||||
|
developer {
|
||||||
|
id = 'dohertj2'
|
||||||
|
name = 'Joseph Doherty'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
licenses {
|
||||||
|
license {
|
||||||
|
name = 'Proprietary'
|
||||||
|
distribution = 'repo'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
name = 'GiteaPackages'
|
||||||
|
url = 'https://gitea.dohertylan.com/api/packages/dohertj2/maven'
|
||||||
|
credentials {
|
||||||
|
username = System.getenv('GITEA_USERNAME') ?: ''
|
||||||
|
password = System.getenv('GITEA_TOKEN') ?: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'java-library'
|
id 'java-library'
|
||||||
id 'com.google.protobuf'
|
id 'com.google.protobuf'
|
||||||
|
id 'maven-publish'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -30,6 +31,11 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
withSourcesJar()
|
||||||
|
withJavadocJar()
|
||||||
|
}
|
||||||
|
|
||||||
protobuf {
|
protobuf {
|
||||||
protoc {
|
protoc {
|
||||||
artifact = "com.google.protobuf:protoc:${protobufVersion}"
|
artifact = "com.google.protobuf:protoc:${protobufVersion}"
|
||||||
|
|||||||
@@ -268,6 +268,19 @@ $env:MXGATEWAY_TEST_ITEM = 'Object.Attribute'
|
|||||||
mxgw-py smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
|
mxgw-py smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Installing from the Gitea PyPI Feed
|
||||||
|
|
||||||
|
The client publishes to the internal Gitea PyPI feed:
|
||||||
|
|
||||||
|
````bash
|
||||||
|
pip install \
|
||||||
|
--index-url https://gitea.dohertylan.com/api/packages/dohertj2/pypi/simple/ \
|
||||||
|
zb-mom-ww-mxaccess-gateway-client
|
||||||
|
````
|
||||||
|
|
||||||
|
If you need authentication (private feed), use `--extra-index-url` and either
|
||||||
|
a `~/.netrc` entry or `PIP_INDEX_URL=https://<user>:<token>@gitea.dohertylan.com/...`.
|
||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Client Packaging](../../docs/ClientPackaging.md)
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||||
|
|||||||
@@ -13,12 +13,35 @@ dependencies = [
|
|||||||
"grpcio>=1.80,<2",
|
"grpcio>=1.80,<2",
|
||||||
"protobuf>=6.33,<7",
|
"protobuf>=6.33,<7",
|
||||||
]
|
]
|
||||||
|
authors = [
|
||||||
|
{ name = "Joseph Doherty" },
|
||||||
|
]
|
||||||
|
license = { text = "Proprietary" }
|
||||||
|
keywords = ["mxaccess", "mxgateway", "grpc", "client", "archestra"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 3 - Alpha",
|
||||||
|
"License :: Other/Proprietary License",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Topic :: System :: Distributed Computing",
|
||||||
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://gitea.dohertylan.com/dohertj2/mxaccessgw"
|
||||||
|
Repository = "https://gitea.dohertylan.com/dohertj2/mxaccessgw"
|
||||||
|
Issues = "https://gitea.dohertylan.com/dohertj2/mxaccessgw/issues"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"grpcio-tools>=1.80,<2",
|
"grpcio-tools>=1.80,<2",
|
||||||
"pytest>=9,<10",
|
"pytest>=9,<10",
|
||||||
"pytest-asyncio>=1.3,<2",
|
"pytest-asyncio>=1.3,<2",
|
||||||
|
"build>=1.2,<2",
|
||||||
|
"twine>=5,<6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -17,3 +17,6 @@
|
|||||||
# args through the GNU linker and reject `/STACK:`, are unaffected.
|
# args through the GNU linker and reject `/STACK:`, are unaffected.
|
||||||
[target.'cfg(all(windows, target_env = "msvc"))']
|
[target.'cfg(all(windows, target_env = "msvc"))']
|
||||||
rustflags = ["-C", "link-arg=/STACK:8388608"]
|
rustflags = ["-C", "link-arg=/STACK:8388608"]
|
||||||
|
|
||||||
|
[registries.dohertj2-gitea]
|
||||||
|
index = "sparse+https://gitea.dohertylan.com/api/packages/dohertj2/cargo/"
|
||||||
|
|||||||
+14
-2
@@ -2,7 +2,16 @@
|
|||||||
name = "zb-mom-ww-mxgateway-client"
|
name = "zb-mom-ww-mxgateway-client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
authors = ["Joseph Doherty"]
|
||||||
|
description = "Async Rust client for the MxAccessGateway gRPC service, including a lazy-browse walker over the Galaxy Repository hierarchy."
|
||||||
|
license = "Proprietary"
|
||||||
|
repository = "https://gitea.dohertylan.com/dohertj2/mxaccessgw"
|
||||||
|
homepage = "https://gitea.dohertylan.com/dohertj2/mxaccessgw"
|
||||||
|
documentation = "https://gitea.dohertylan.com/dohertj2/mxaccessgw"
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = ["mxaccess", "mxgateway", "grpc", "client", "archestra"]
|
||||||
|
categories = ["api-bindings", "asynchronous"]
|
||||||
|
publish = ["dohertj2-gitea"]
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
@@ -12,7 +21,10 @@ resolver = "2"
|
|||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
publish = false
|
authors = ["Joseph Doherty"]
|
||||||
|
license = "Proprietary"
|
||||||
|
repository = "https://gitea.dohertylan.com/dohertj2/mxaccessgw"
|
||||||
|
publish = ["dohertj2-gitea"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
clap = { version = "4.5.53", features = ["derive"] }
|
clap = { version = "4.5.53", features = ["derive"] }
|
||||||
|
|||||||
@@ -236,3 +236,27 @@ cargo run -p mxgw-cli -- smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --
|
|||||||
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
||||||
- [Rust Client Detailed Design](./RustClientDesign.md)
|
- [Rust Client Detailed Design](./RustClientDesign.md)
|
||||||
- [Rust Style Guide](../../docs/style-guides/RustStyleGuide.md)
|
- [Rust Style Guide](../../docs/style-guides/RustStyleGuide.md)
|
||||||
|
|
||||||
|
## Installing from the Gitea Cargo registry
|
||||||
|
|
||||||
|
The crate publishes to the internal Gitea Cargo registry. Register the
|
||||||
|
registry once in your global `~/.cargo/config.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[registries.dohertj2-gitea]
|
||||||
|
index = "sparse+https://gitea.dohertylan.com/api/packages/dohertj2/cargo/"
|
||||||
|
```
|
||||||
|
|
||||||
|
Authentication: cargo reads credentials from `~/.cargo/credentials.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[registries.dohertj2-gitea]
|
||||||
|
token = "Bearer <your-gitea-token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add the dependency:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
zb-mom-ww-mxgateway-client = { version = "0.1.0", registry = "dohertj2-gitea" }
|
||||||
|
```
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name = "mxgw-cli"
|
name = "mxgw-cli"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
publish.workspace = true
|
publish = false
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "mxgw"
|
name = "mxgw"
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
#Requires -Version 7
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Packs all MxAccessGateway clients into a single dist/ directory.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Runs each language client's native packaging command:
|
||||||
|
.NET -> dotnet pack (NuGet)
|
||||||
|
Python -> python -m build (sdist + wheel)
|
||||||
|
Rust -> cargo package (.crate)
|
||||||
|
Java -> gradle assemble + jars (jar + sources + javadoc + pom)
|
||||||
|
Go -> skipped; use scripts/tag-go-module.ps1
|
||||||
|
|
||||||
|
All artifacts land in -OutputDir (default: dist/).
|
||||||
|
|
||||||
|
With -Publish, each language pushes its package to the internal Gitea
|
||||||
|
feed. Requires GITEA_USERNAME and GITEA_TOKEN env vars.
|
||||||
|
|
||||||
|
.PARAMETER OutputDir
|
||||||
|
Where to drop the packed artifacts. Default: ./dist
|
||||||
|
|
||||||
|
.PARAMETER Languages
|
||||||
|
Subset of languages to pack. Default: all five.
|
||||||
|
Values: dotnet, python, rust, java, go
|
||||||
|
|
||||||
|
.PARAMETER Publish
|
||||||
|
After packing, upload to Gitea feeds. Requires:
|
||||||
|
GITEA_USERNAME
|
||||||
|
GITEA_TOKEN
|
||||||
|
Will refuse to publish if either is missing.
|
||||||
|
|
||||||
|
.PARAMETER SkipTests
|
||||||
|
Skip per-language regression tests before packing. Default: false.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
pwsh scripts/pack-clients.ps1
|
||||||
|
pwsh scripts/pack-clients.ps1 -Languages dotnet,python
|
||||||
|
pwsh scripts/pack-clients.ps1 -Publish
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$OutputDir = (Join-Path $PSScriptRoot '..' 'dist'),
|
||||||
|
[string[]]$Languages = @('dotnet', 'python', 'rust', 'java', 'go'),
|
||||||
|
[switch]$Publish,
|
||||||
|
[switch]$SkipTests
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Normalize comma-separated strings that shells may pass as a single element.
|
||||||
|
$validLanguages = @('dotnet', 'python', 'rust', 'java', 'go')
|
||||||
|
$Languages = @($Languages | ForEach-Object { $_ -split ',' } | ForEach-Object {
|
||||||
|
$_.Trim().ToLowerInvariant()
|
||||||
|
} | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||||
|
|
||||||
|
foreach ($lang in $Languages) {
|
||||||
|
if ($validLanguages -notcontains $lang) {
|
||||||
|
throw "Unsupported language '$lang'. Supported values: $($validLanguages -join ', ')."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Languages.Count -eq 0) {
|
||||||
|
throw "At least one language is required. Supported values: $($validLanguages -join ', ')."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve absolute output dir
|
||||||
|
$OutputDir = [System.IO.Path]::GetFullPath($OutputDir)
|
||||||
|
$RepoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||||
|
|
||||||
|
if (-not (Test-Path $OutputDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $OutputDir | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Publish) {
|
||||||
|
if ([string]::IsNullOrEmpty($env:GITEA_USERNAME)) {
|
||||||
|
throw 'Publish requires GITEA_USERNAME env var.'
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrEmpty($env:GITEA_TOKEN)) {
|
||||||
|
throw 'Publish requires GITEA_TOKEN env var.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$GiteaNugetFeed = 'https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json'
|
||||||
|
$GiteaPypiFeed = 'https://gitea.dohertylan.com/api/packages/dohertj2/pypi'
|
||||||
|
$JavaHome = '/Users/dohertj2/.local/jdks/jdk-21.0.11+10/Contents/Home'
|
||||||
|
|
||||||
|
function Write-Header {
|
||||||
|
param([string]$Text)
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host '=== ' -NoNewline -ForegroundColor Cyan
|
||||||
|
Write-Host $Text -ForegroundColor Cyan
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------- .NET --------
|
||||||
|
|
||||||
|
function Invoke-PackDotnet {
|
||||||
|
Write-Header '.NET'
|
||||||
|
|
||||||
|
if (-not $SkipTests) {
|
||||||
|
Write-Host 'Running .NET client tests...'
|
||||||
|
$testProject = Join-Path $RepoRoot 'clients/dotnet/ZB.MOM.WW.MxGateway.Client.Tests/ZB.MOM.WW.MxGateway.Client.Tests.csproj'
|
||||||
|
& dotnet test $testProject --no-restore
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw '.NET tests failed.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host 'Packing ZB.MOM.WW.MxGateway.Contracts...'
|
||||||
|
& dotnet pack (Join-Path $RepoRoot 'src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj') `
|
||||||
|
-c Release -o $OutputDir
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw '.NET Contracts pack failed.' }
|
||||||
|
|
||||||
|
Write-Host 'Packing ZB.MOM.WW.MxGateway.Client...'
|
||||||
|
& dotnet pack (Join-Path $RepoRoot 'clients/dotnet/ZB.MOM.WW.MxGateway.Client/ZB.MOM.WW.MxGateway.Client.csproj') `
|
||||||
|
-c Release -o $OutputDir
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw '.NET Client pack failed.' }
|
||||||
|
|
||||||
|
Write-Host "Packed .NET artifacts -> $OutputDir" -ForegroundColor Green
|
||||||
|
|
||||||
|
if ($Publish) {
|
||||||
|
Write-Host 'Publishing .NET packages to Gitea...' -ForegroundColor Yellow
|
||||||
|
Get-ChildItem $OutputDir -Filter 'ZB.MOM.WW.MxGateway.*.nupkg' | ForEach-Object {
|
||||||
|
& dotnet nuget push $_.FullName --source $GiteaNugetFeed --api-key $env:GITEA_TOKEN
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "dotnet nuget push failed for '$($_.Name)'." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------- Python --------
|
||||||
|
|
||||||
|
function Invoke-PackPython {
|
||||||
|
Write-Header 'Python'
|
||||||
|
|
||||||
|
# Use a persistent venv in /tmp so repeated runs skip reinstall.
|
||||||
|
$Venv = '/tmp/mxgw-py'
|
||||||
|
if (-not (Test-Path "$Venv/bin/python")) {
|
||||||
|
Write-Host "Creating Python venv at $Venv..."
|
||||||
|
& python3 -m venv $Venv
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'python3 -m venv failed.' }
|
||||||
|
& "$Venv/bin/pip" install --quiet --upgrade pip
|
||||||
|
& "$Venv/bin/pip" install --quiet build twine
|
||||||
|
& "$Venv/bin/pip" install --quiet -e (Join-Path $RepoRoot 'clients/python[dev]')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $SkipTests) {
|
||||||
|
Write-Host 'Running Python tests...'
|
||||||
|
Push-Location (Join-Path $RepoRoot 'clients/python')
|
||||||
|
try {
|
||||||
|
& "$Venv/bin/python" -m pytest -q
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Python tests failed.' }
|
||||||
|
} finally { Pop-Location }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host 'Building Python sdist + wheel...'
|
||||||
|
& "$Venv/bin/python" -m build (Join-Path $RepoRoot 'clients/python') --outdir $OutputDir
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Python build failed.' }
|
||||||
|
|
||||||
|
Write-Host "Packed Python artifacts -> $OutputDir" -ForegroundColor Green
|
||||||
|
|
||||||
|
if ($Publish) {
|
||||||
|
Write-Host 'Publishing Python distribution to Gitea...' -ForegroundColor Yellow
|
||||||
|
$wheels = @(Get-ChildItem $OutputDir -Filter 'zb_mom_ww_mxaccess_gateway_client-*.whl')
|
||||||
|
$sdists = @(Get-ChildItem $OutputDir -Filter 'zb_mom_ww_mxaccess_gateway_client-*.tar.gz')
|
||||||
|
$files = ($wheels + $sdists) | ForEach-Object { $_.FullName }
|
||||||
|
& "$Venv/bin/python" -m twine upload `
|
||||||
|
--repository-url $GiteaPypiFeed `
|
||||||
|
-u $env:GITEA_USERNAME `
|
||||||
|
-p $env:GITEA_TOKEN `
|
||||||
|
@files
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'twine upload failed.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------- Rust --------
|
||||||
|
|
||||||
|
function Invoke-PackRust {
|
||||||
|
Write-Header 'Rust'
|
||||||
|
|
||||||
|
$rustDir = Join-Path $RepoRoot 'clients/rust'
|
||||||
|
Push-Location $rustDir
|
||||||
|
try {
|
||||||
|
if (-not $SkipTests) {
|
||||||
|
Write-Host 'Running Rust tests...'
|
||||||
|
& cargo test --workspace
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Rust tests failed.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host 'Running cargo package...'
|
||||||
|
& cargo package --no-verify
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'cargo package failed.' }
|
||||||
|
|
||||||
|
$packageDir = Join-Path $rustDir 'target/package'
|
||||||
|
$crates = @(Get-ChildItem $packageDir -Filter '*.crate')
|
||||||
|
if ($crates.Count -eq 0) {
|
||||||
|
throw 'cargo package produced no .crate files.'
|
||||||
|
}
|
||||||
|
foreach ($crate in $crates) {
|
||||||
|
Copy-Item $crate.FullName -Destination $OutputDir -Force
|
||||||
|
Write-Host " Copied $($crate.Name)"
|
||||||
|
}
|
||||||
|
} finally { Pop-Location }
|
||||||
|
|
||||||
|
Write-Host "Packed Rust artifacts -> $OutputDir" -ForegroundColor Green
|
||||||
|
|
||||||
|
if ($Publish) {
|
||||||
|
Write-Host 'Publishing Rust crate to Gitea...' -ForegroundColor Yellow
|
||||||
|
Push-Location (Join-Path $RepoRoot 'clients/rust')
|
||||||
|
try {
|
||||||
|
& cargo publish --no-verify --registry dohertj2-gitea
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'cargo publish failed.' }
|
||||||
|
} finally { Pop-Location }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------- Java --------
|
||||||
|
|
||||||
|
function Invoke-PackJava {
|
||||||
|
Write-Header 'Java'
|
||||||
|
|
||||||
|
$env:JAVA_HOME = $JavaHome
|
||||||
|
$javaDir = Join-Path $RepoRoot 'clients/java'
|
||||||
|
Push-Location $javaDir
|
||||||
|
try {
|
||||||
|
if (-not $SkipTests) {
|
||||||
|
Write-Host 'Running Java tests...'
|
||||||
|
& gradle ':zb-mom-ww-mxgateway-client:test' --no-daemon
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Java tests failed.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host 'Assembling Java jars + pom...'
|
||||||
|
& gradle `
|
||||||
|
':zb-mom-ww-mxgateway-client:assemble' `
|
||||||
|
':zb-mom-ww-mxgateway-client:sourcesJar' `
|
||||||
|
':zb-mom-ww-mxgateway-client:javadocJar' `
|
||||||
|
':zb-mom-ww-mxgateway-client:generatePomFileForMavenPublication' `
|
||||||
|
--no-daemon
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Java assemble failed.' }
|
||||||
|
|
||||||
|
$libsDir = Join-Path $javaDir 'zb-mom-ww-mxgateway-client/build/libs'
|
||||||
|
$jars = @(Get-ChildItem $libsDir -Filter 'zb-mom-ww-mxgateway-client-*.jar')
|
||||||
|
if ($jars.Count -eq 0) {
|
||||||
|
throw "No jars found under '$libsDir'."
|
||||||
|
}
|
||||||
|
foreach ($jar in $jars) {
|
||||||
|
Copy-Item $jar.FullName -Destination $OutputDir -Force
|
||||||
|
Write-Host " Copied $($jar.Name)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$pomSrc = Join-Path $javaDir 'zb-mom-ww-mxgateway-client/build/publications/maven/pom-default.xml'
|
||||||
|
if (Test-Path $pomSrc) {
|
||||||
|
# Derive the version from the jar filename (e.g. zb-mom-ww-mxgateway-client-0.1.0.jar).
|
||||||
|
$versionJar = $jars | Where-Object { $_.Name -notmatch '-(sources|javadoc)\.jar$' } | Select-Object -First 1
|
||||||
|
$version = if ($versionJar) {
|
||||||
|
[System.IO.Path]::GetFileNameWithoutExtension($versionJar.Name) -replace '^zb-mom-ww-mxgateway-client-', ''
|
||||||
|
} else {
|
||||||
|
'0.1.0'
|
||||||
|
}
|
||||||
|
$pomDest = Join-Path $OutputDir "zb-mom-ww-mxgateway-client-$version.pom"
|
||||||
|
Copy-Item $pomSrc -Destination $pomDest -Force
|
||||||
|
Write-Host " Copied pom -> $([System.IO.Path]::GetFileName($pomDest))"
|
||||||
|
} else {
|
||||||
|
Write-Warning "POM not found at '$pomSrc'; skipping."
|
||||||
|
}
|
||||||
|
} finally { Pop-Location }
|
||||||
|
|
||||||
|
Write-Host "Packed Java artifacts -> $OutputDir" -ForegroundColor Green
|
||||||
|
|
||||||
|
if ($Publish) {
|
||||||
|
Write-Host 'Publishing Java artifacts to Gitea Maven feed...' -ForegroundColor Yellow
|
||||||
|
Push-Location $javaDir
|
||||||
|
try {
|
||||||
|
& gradle ':zb-mom-ww-mxgateway-client:publish' --no-daemon
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'gradle publish failed.' }
|
||||||
|
} finally { Pop-Location }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------- Go --------
|
||||||
|
|
||||||
|
function Invoke-PackGo {
|
||||||
|
Write-Header 'Go'
|
||||||
|
Write-Host 'Go modules are released by git-tagging — no artifact to pack.' -ForegroundColor Yellow
|
||||||
|
Write-Host 'To publish a Go release, run:' -ForegroundColor Yellow
|
||||||
|
Write-Host ' pwsh scripts/tag-go-module.ps1 -Version v0.1.0 -Push' -ForegroundColor Yellow
|
||||||
|
Write-Host '(skipping)' -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------- Dispatch --------
|
||||||
|
|
||||||
|
$wanted = @{}
|
||||||
|
foreach ($lang in $Languages) { $wanted[$lang.ToLower()] = $true }
|
||||||
|
|
||||||
|
if ($wanted.ContainsKey('dotnet')) { Invoke-PackDotnet }
|
||||||
|
if ($wanted.ContainsKey('python')) { Invoke-PackPython }
|
||||||
|
if ($wanted.ContainsKey('rust')) { Invoke-PackRust }
|
||||||
|
if ($wanted.ContainsKey('java')) { Invoke-PackJava }
|
||||||
|
if ($wanted.ContainsKey('go')) { Invoke-PackGo }
|
||||||
|
|
||||||
|
# -------- Summary --------
|
||||||
|
|
||||||
|
Write-Header 'Summary'
|
||||||
|
$artifacts = @(Get-ChildItem $OutputDir)
|
||||||
|
if ($artifacts.Count -eq 0) {
|
||||||
|
Write-Host ' (no artifacts)' -ForegroundColor DarkGray
|
||||||
|
} else {
|
||||||
|
foreach ($a in $artifacts) {
|
||||||
|
Write-Host (' {0,10} {1}' -f $a.Length, $a.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host "All artifacts in: $OutputDir" -ForegroundColor Green
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#Requires -Version 7
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Tags a release of the Go MxAccessGateway client module.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Go modules in monorepo subdirectories use prefixed tags
|
||||||
|
("clients/go/v0.1.0") so `go get <module>@v0.1.0` resolves correctly.
|
||||||
|
This script validates the version, creates the prefixed tag at HEAD,
|
||||||
|
and (optionally) pushes it.
|
||||||
|
|
||||||
|
.PARAMETER Version
|
||||||
|
Semver tag without the prefix, e.g. "v0.1.0".
|
||||||
|
|
||||||
|
.PARAMETER Push
|
||||||
|
When set, pushes the tag to origin after creation.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
pwsh scripts/tag-go-module.ps1 -Version v0.1.0
|
||||||
|
pwsh scripts/tag-go-module.ps1 -Version v0.1.1 -Push
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Version,
|
||||||
|
|
||||||
|
[switch]$Push
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
if ($Version -notmatch '^v\d+\.\d+\.\d+(-[A-Za-z0-9.-]+)?$') {
|
||||||
|
throw "Version '$Version' must match semver vX.Y.Z (optionally with -prerelease suffix)."
|
||||||
|
}
|
||||||
|
|
||||||
|
$tag = "clients/go/$Version"
|
||||||
|
Write-Host "Creating Go-module tag: $tag" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Verify we're on a clean checkout — refuse to tag with uncommitted changes.
|
||||||
|
$status = (git status --porcelain) -join "`n"
|
||||||
|
if ($status -and -not ($status -match '^\?\?')) {
|
||||||
|
throw "Working tree has tracked changes. Commit or stash before tagging."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify the tag doesn't already exist.
|
||||||
|
$existing = git tag --list $tag
|
||||||
|
if ($existing) {
|
||||||
|
throw "Tag '$tag' already exists. Use a new version."
|
||||||
|
}
|
||||||
|
|
||||||
|
git tag -a $tag -m "Go client release $Version"
|
||||||
|
Write-Host "Created tag: $tag" -ForegroundColor Green
|
||||||
|
|
||||||
|
if ($Push) {
|
||||||
|
git push origin $tag
|
||||||
|
Write-Host "Pushed tag to origin." -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Tag not pushed. To publish, run: git push origin $tag" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
@@ -8,10 +8,13 @@ namespace ZB.MOM.WW.MxGateway.Contracts;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class GatewayContractInfo
|
public static class GatewayContractInfo
|
||||||
{
|
{
|
||||||
|
/// <summary>Protocol version advertised to clients in <c>OpenSessionReply</c>.</summary>
|
||||||
public const uint GatewayProtocolVersion = 3;
|
public const uint GatewayProtocolVersion = 3;
|
||||||
|
|
||||||
|
/// <summary>Protocol version used to validate <c>WorkerEnvelope</c> framing on the gateway-worker pipe.</summary>
|
||||||
public const uint WorkerProtocolVersion = 1;
|
public const uint WorkerProtocolVersion = 1;
|
||||||
|
|
||||||
|
/// <summary>Default backend name identifying the MXAccess worker process type.</summary>
|
||||||
public const string DefaultBackendName = "mxaccess-worker";
|
public const string DefaultBackendName = "mxaccess-worker";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -4,6 +4,24 @@
|
|||||||
<TargetFrameworks>net10.0;net48</TargetFrameworks>
|
<TargetFrameworks>net10.0;net48</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<IsPackable>true</IsPackable>
|
||||||
|
<PackageId>ZB.MOM.WW.MxGateway.Contracts</PackageId>
|
||||||
|
<Version>0.1.0</Version>
|
||||||
|
<Authors>Joseph Doherty</Authors>
|
||||||
|
<Company>ZB MOM WW</Company>
|
||||||
|
<Copyright>Copyright (c) ZB MOM WW. All rights reserved.</Copyright>
|
||||||
|
<Description>Protobuf contracts and gRPC stubs for the MxAccessGateway service. Multi-targets net10.0 and net48.</Description>
|
||||||
|
<RepositoryUrl>https://gitea.dohertylan.com/dohertj2/mxaccessgw</RepositoryUrl>
|
||||||
|
<RepositoryType>git</RepositoryType>
|
||||||
|
<PackageProjectUrl>https://gitea.dohertylan.com/dohertj2/mxaccessgw</PackageProjectUrl>
|
||||||
|
<PackageTags>mxaccess;mxgateway;grpc;contracts;protobuf</PackageTags>
|
||||||
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
|
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Remove="Generated\**\*.cs" />
|
<Compile Remove="Generated\**\*.cs" />
|
||||||
<Protobuf Include="Protos\mxaccess_gateway.proto" ProtoRoot="Protos" OutputDir="Generated" GrpcOutputDir="Generated" GrpcServices="Both" />
|
<Protobuf Include="Protos\mxaccess_gateway.proto" ProtoRoot="Protos" OutputDir="Generated" GrpcOutputDir="Generated" GrpcServices="Both" />
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using ZB.MOM.WW.Telemetry.Serilog;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.MxGateway.Server.Diagnostics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bridges the gateway's <see cref="GatewayLogRedactor"/> policy onto the shared
|
||||||
|
/// <see cref="ILogRedactor"/> seam consumed by <c>ZB.MOM.WW.Telemetry.Serilog</c>'s redaction
|
||||||
|
/// enricher. Applied to every Serilog log event before it reaches a sink, it masks the same
|
||||||
|
/// secrets the original MEL-scope path masked: API-key bearer tokens / client identities
|
||||||
|
/// (<c>mxgw_*</c>) and command values for credential-bearing MXAccess commands. All masking
|
||||||
|
/// decisions delegate to <see cref="GatewayLogRedactor"/> — this type adds no new policy.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GatewayLogRedactorAdapter : ILogRedactor
|
||||||
|
{
|
||||||
|
/// <summary>Property name carrying a client identity / authorization header value.</summary>
|
||||||
|
private const string ClientIdentityProperty = "ClientIdentity";
|
||||||
|
|
||||||
|
/// <summary>Property name carrying a raw authorization header value.</summary>
|
||||||
|
private const string AuthorizationProperty = "Authorization";
|
||||||
|
|
||||||
|
/// <summary>Property name carrying the MXAccess command method, used to gate value redaction.</summary>
|
||||||
|
private const string CommandMethodProperty = "CommandMethod";
|
||||||
|
|
||||||
|
/// <summary>Property name carrying a command payload value that may bear credentials.</summary>
|
||||||
|
private const string CommandValueProperty = "CommandValue";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Masks any sensitive values in <paramref name="properties"/> in place using the shared
|
||||||
|
/// <see cref="GatewayLogRedactor"/> policy. Identity/authorization properties have their API-key
|
||||||
|
/// secret stripped; a command value is redacted when its associated command method bears
|
||||||
|
/// credentials.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="properties">The mutable log-event property dictionary for the current event.</param>
|
||||||
|
public void Redact(IDictionary<string, object?> properties)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(properties);
|
||||||
|
|
||||||
|
RedactIdentity(properties, ClientIdentityProperty);
|
||||||
|
RedactIdentity(properties, AuthorizationProperty);
|
||||||
|
RedactCommandValue(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RedactIdentity(IDictionary<string, object?> properties, string propertyName)
|
||||||
|
{
|
||||||
|
if (properties.TryGetValue(propertyName, out object? value) && value is string identity)
|
||||||
|
{
|
||||||
|
properties[propertyName] = GatewayLogRedactor.RedactClientIdentity(identity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RedactCommandValue(IDictionary<string, object?> properties)
|
||||||
|
{
|
||||||
|
if (!properties.TryGetValue(CommandValueProperty, out object? value) || value is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? commandMethod = properties.TryGetValue(CommandMethodProperty, out object? method)
|
||||||
|
? method as string
|
||||||
|
: null;
|
||||||
|
|
||||||
|
properties[CommandValueProperty] = GatewayLogRedactor.RedactCommandValue(commandMethod, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Server.Diagnostics;
|
|
||||||
|
|
||||||
public static class GatewayLoggerExtensions
|
|
||||||
{
|
|
||||||
/// <summary>Begins a gateway log scope with the specified scope properties.</summary>
|
|
||||||
/// <param name="logger">Logger used for diagnostic output.</param>
|
|
||||||
/// <param name="scope">Scope properties to apply.</param>
|
|
||||||
/// <returns>A disposable that ends the scope when disposed.</returns>
|
|
||||||
public static IDisposable? BeginGatewayScope(
|
|
||||||
this ILogger logger,
|
|
||||||
GatewayLogScope scope)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(logger);
|
|
||||||
ArgumentNullException.ThrowIfNull(scope);
|
|
||||||
|
|
||||||
return logger.BeginScope(scope.ToDictionary());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+48
-7
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Serilog.Context;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Server.Diagnostics;
|
namespace ZB.MOM.WW.MxGateway.Server.Diagnostics;
|
||||||
|
|
||||||
@@ -17,7 +18,12 @@ public static class GatewayRequestLoggingMiddlewareExtensions
|
|||||||
/// <summary>Header name for the command method name.</summary>
|
/// <summary>Header name for the command method name.</summary>
|
||||||
public const string CommandMethodHeaderName = "x-command-method";
|
public const string CommandMethodHeaderName = "x-command-method";
|
||||||
|
|
||||||
/// <summary>Adds gateway request logging scope middleware that reads correlation headers and redacts sensitive data.</summary>
|
/// <summary>
|
||||||
|
/// Adds gateway request logging middleware that reads the correlation headers and pushes them
|
||||||
|
/// as Serilog <see cref="LogContext"/> properties for the duration of the request. The pushed
|
||||||
|
/// properties (SessionId / WorkerProcessId / CorrelationId / CommandMethod / ClientIdentity)
|
||||||
|
/// are disposed when the request completes; the shared redaction enricher masks any secrets.
|
||||||
|
/// </summary>
|
||||||
/// <param name="app">Application builder.</param>
|
/// <param name="app">Application builder.</param>
|
||||||
public static IApplicationBuilder UseGatewayRequestLoggingScope(this IApplicationBuilder app)
|
public static IApplicationBuilder UseGatewayRequestLoggingScope(this IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
@@ -25,21 +31,56 @@ public static class GatewayRequestLoggingMiddlewareExtensions
|
|||||||
|
|
||||||
return app.Use(async (context, next) =>
|
return app.Use(async (context, next) =>
|
||||||
{
|
{
|
||||||
ILogger logger = context.RequestServices
|
GatewayLogScope scope = new(
|
||||||
.GetRequiredService<ILoggerFactory>()
|
|
||||||
.CreateLogger("MxGateway.Request");
|
|
||||||
|
|
||||||
using IDisposable? scope = logger.BeginGatewayScope(new GatewayLogScope(
|
|
||||||
SessionId: ReadHeader(context, SessionIdHeaderName),
|
SessionId: ReadHeader(context, SessionIdHeaderName),
|
||||||
WorkerProcessId: ReadInt32Header(context, WorkerProcessIdHeaderName),
|
WorkerProcessId: ReadInt32Header(context, WorkerProcessIdHeaderName),
|
||||||
CorrelationId: ReadUInt64Header(context, CorrelationIdHeaderName),
|
CorrelationId: ReadUInt64Header(context, CorrelationIdHeaderName),
|
||||||
CommandMethod: ReadHeader(context, CommandMethodHeaderName),
|
CommandMethod: ReadHeader(context, CommandMethodHeaderName),
|
||||||
ClientIdentity: ReadHeader(context, "authorization")));
|
ClientIdentity: ReadHeader(context, "authorization"));
|
||||||
|
|
||||||
|
using IDisposable correlationScope = PushCorrelationProperties(scope);
|
||||||
|
|
||||||
await next(context);
|
await next(context);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pushes the populated <paramref name="scope"/> properties onto the Serilog
|
||||||
|
/// <see cref="LogContext"/>, returning a single disposable that pops them all when the request
|
||||||
|
/// completes. Only the properties present in <see cref="GatewayLogScope.ToDictionary"/> (which
|
||||||
|
/// already applies the client-identity redaction policy) are pushed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="scope">The correlation properties for the current request.</param>
|
||||||
|
/// <returns>A disposable that removes the pushed properties on disposal.</returns>
|
||||||
|
private static IDisposable PushCorrelationProperties(GatewayLogScope scope)
|
||||||
|
{
|
||||||
|
Stack<IDisposable> pushed = new();
|
||||||
|
|
||||||
|
foreach (KeyValuePair<string, object?> property in scope.ToDictionary())
|
||||||
|
{
|
||||||
|
pushed.Push(LogContext.PushProperty(property.Key, property.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CorrelationPropertyScope(pushed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disposes the pushed <see cref="LogContext"/> property bindings in reverse order, restoring
|
||||||
|
/// the ambient context to its pre-request state.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class CorrelationPropertyScope(Stack<IDisposable> bindings) : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Stack<IDisposable> _bindings = bindings;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
while (_bindings.Count > 0)
|
||||||
|
{
|
||||||
|
_bindings.Pop().Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string? ReadHeader(HttpContext context, string headerName)
|
private static string? ReadHeader(HttpContext context, string headerName)
|
||||||
{
|
{
|
||||||
return context.Request.Headers.TryGetValue(headerName, out StringValues values)
|
return context.Request.Headers.TryGetValue(headerName, out StringValues values)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
|
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
|
||||||
|
using Serilog;
|
||||||
using ZB.MOM.WW.MxGateway.Contracts;
|
using ZB.MOM.WW.MxGateway.Contracts;
|
||||||
using ZB.MOM.WW.MxGateway.Server.Alarms;
|
using ZB.MOM.WW.MxGateway.Server.Alarms;
|
||||||
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||||
@@ -11,6 +12,7 @@ using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
|
|||||||
using ZB.MOM.WW.MxGateway.Server.Security.Authorization;
|
using ZB.MOM.WW.MxGateway.Server.Security.Authorization;
|
||||||
using ZB.MOM.WW.MxGateway.Server.Sessions;
|
using ZB.MOM.WW.MxGateway.Server.Sessions;
|
||||||
using ZB.MOM.WW.MxGateway.Server.Workers;
|
using ZB.MOM.WW.MxGateway.Server.Workers;
|
||||||
|
using ZB.MOM.WW.Telemetry.Serilog;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Server;
|
namespace ZB.MOM.WW.MxGateway.Server;
|
||||||
|
|
||||||
@@ -31,7 +33,10 @@ public static class GatewayApplication
|
|||||||
WebApplicationBuilder builder = CreateBuilder(args);
|
WebApplicationBuilder builder = CreateBuilder(args);
|
||||||
WebApplication app = builder.Build();
|
WebApplication app = builder.Build();
|
||||||
|
|
||||||
|
// Push the per-request correlation properties (via Serilog LogContext) before the
|
||||||
|
// request-logging middleware emits its completion event, so those properties appear on it.
|
||||||
app.UseGatewayRequestLoggingScope();
|
app.UseGatewayRequestLoggingScope();
|
||||||
|
app.UseSerilogRequestLogging();
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
@@ -55,6 +60,8 @@ public static class GatewayApplication
|
|||||||
});
|
});
|
||||||
StaticWebAssetsLoader.UseStaticWebAssets(builder.Environment, builder.Configuration);
|
StaticWebAssetsLoader.UseStaticWebAssets(builder.Environment, builder.Configuration);
|
||||||
|
|
||||||
|
ConfigureSerilog(builder);
|
||||||
|
|
||||||
builder.Services.AddGatewayConfiguration();
|
builder.Services.AddGatewayConfiguration();
|
||||||
builder.Services.AddSqliteAuthStore();
|
builder.Services.AddSqliteAuthStore();
|
||||||
builder.Services.AddGatewayGrpcAuthorization();
|
builder.Services.AddGatewayGrpcAuthorization();
|
||||||
@@ -72,6 +79,30 @@ public static class GatewayApplication
|
|||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replaces the default Microsoft.Extensions.Logging provider with the shared
|
||||||
|
/// <c>ZB.MOM.WW.Telemetry.Serilog</c> bootstrap (<see cref="ZbSerilogExtensions.AddZbSerilog"/>).
|
||||||
|
/// Sinks and minimum level come from the <c>Serilog</c> configuration section; identity
|
||||||
|
/// (<c>SiteId</c>/<c>NodeRole</c>) is read from <c>MxGateway:Telemetry</c> when present.
|
||||||
|
/// Also registers the project's <see cref="ILogRedactor"/> adapter so the shared redaction
|
||||||
|
/// enricher masks gateway secrets on every event.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The web application builder being configured.</param>
|
||||||
|
private static void ConfigureSerilog(WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
string? siteId = builder.Configuration["MxGateway:Telemetry:SiteId"];
|
||||||
|
string? nodeRole = builder.Configuration["MxGateway:Telemetry:NodeRole"];
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<ILogRedactor, GatewayLogRedactorAdapter>();
|
||||||
|
|
||||||
|
builder.AddZbSerilog(options =>
|
||||||
|
{
|
||||||
|
options.ServiceName = "mxgateway";
|
||||||
|
options.SiteId = string.IsNullOrWhiteSpace(siteId) ? null : siteId;
|
||||||
|
options.NodeRole = string.IsNullOrWhiteSpace(nodeRole) ? null : nodeRole;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static string ResolveContentRootPath()
|
private static string ResolveContentRootPath()
|
||||||
{
|
{
|
||||||
string? configuredContentRootPath = Environment.GetEnvironmentVariable("ASPNETCORE_CONTENTROOT");
|
string? configuredContentRootPath = Environment.GetEnvironmentVariable("ASPNETCORE_CONTENTROOT");
|
||||||
|
|||||||
@@ -15,6 +15,13 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ZB.MOM.WW.MxGateway.Contracts\ZB.MOM.WW.MxGateway.Contracts.csproj" />
|
<ProjectReference Include="..\ZB.MOM.WW.MxGateway.Contracts\ZB.MOM.WW.MxGateway.Contracts.csproj" />
|
||||||
|
<!--
|
||||||
|
Shared structured-logging bootstrap (ZB.MOM.WW.Telemetry.Serilog) lives in the sibling
|
||||||
|
scadaproj workspace. Cross-repo ProjectReference: the referenced project resolves its own
|
||||||
|
Directory.Build.props / Directory.Packages.props from its own tree, so it does not perturb
|
||||||
|
this repo's build settings. It transitively brings the ZB.MOM.WW.Telemetry core package.
|
||||||
|
-->
|
||||||
|
<ProjectReference Include="..\..\..\scadaproj\ZB.MOM.WW.Telemetry\src\ZB.MOM.WW.Telemetry.Serilog\ZB.MOM.WW.Telemetry.Serilog.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Serilog": {
|
||||||
"LogLevel": {
|
"MinimumLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Override": {
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,30 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Serilog": {
|
||||||
"LogLevel": {
|
"Using": [
|
||||||
|
"Serilog.Sinks.Console",
|
||||||
|
"Serilog.Sinks.File"
|
||||||
|
],
|
||||||
|
"MinimumLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Override": {
|
||||||
}
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"WriteTo": [
|
||||||
|
{
|
||||||
|
"Name": "Console",
|
||||||
|
"Args": {
|
||||||
|
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] [{NodeRole}/{NodeHostname}] {Message:lj} {Properties:j}{NewLine}{Exception}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "File",
|
||||||
|
"Args": {
|
||||||
|
"path": "logs/mxgateway-.log",
|
||||||
|
"rollingInterval": "Day"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"MxGateway": {
|
"MxGateway": {
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using ZB.MOM.WW.MxGateway.Server.Diagnostics;
|
||||||
|
using ZB.MOM.WW.Telemetry.Serilog;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.MxGateway.Tests.Diagnostics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pins that <see cref="GatewayLogRedactorAdapter"/> applies the gateway's redaction policy through
|
||||||
|
/// the shared <see cref="ILogRedactor"/> seam — the same secrets the former MEL-scope path masked
|
||||||
|
/// must still be masked once events flow through the Serilog redaction enricher.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GatewayLogRedactorAdapterTests
|
||||||
|
{
|
||||||
|
private readonly ILogRedactor _redactor = new GatewayLogRedactorAdapter();
|
||||||
|
|
||||||
|
/// <summary>Verifies the client identity property has its API-key secret stripped in place.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Redact_StripsApiKeySecretFromClientIdentity()
|
||||||
|
{
|
||||||
|
Dictionary<string, object?> properties = new()
|
||||||
|
{
|
||||||
|
["ClientIdentity"] = "Bearer mxgw_operator01_super-secret",
|
||||||
|
};
|
||||||
|
|
||||||
|
_redactor.Redact(properties);
|
||||||
|
|
||||||
|
Assert.Equal("Bearer mxgw_operator01_[redacted]", properties["ClientIdentity"]);
|
||||||
|
Assert.DoesNotContain("super-secret", (string?)properties["ClientIdentity"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies a raw authorization header property is redacted too.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Redact_StripsApiKeySecretFromAuthorizationProperty()
|
||||||
|
{
|
||||||
|
Dictionary<string, object?> properties = new()
|
||||||
|
{
|
||||||
|
["Authorization"] = "Bearer mxgw_admin_top-secret",
|
||||||
|
};
|
||||||
|
|
||||||
|
_redactor.Redact(properties);
|
||||||
|
|
||||||
|
Assert.Equal("Bearer mxgw_admin_[redacted]", properties["Authorization"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies a command value is redacted for a credential-bearing command method.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Redact_RedactsCommandValueForCredentialBearingCommand()
|
||||||
|
{
|
||||||
|
Dictionary<string, object?> properties = new()
|
||||||
|
{
|
||||||
|
["CommandMethod"] = "WriteSecured",
|
||||||
|
["CommandValue"] = "credential-bearing-value",
|
||||||
|
};
|
||||||
|
|
||||||
|
_redactor.Redact(properties);
|
||||||
|
|
||||||
|
Assert.Equal(GatewayLogRedactor.RedactedValue, properties["CommandValue"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies a command value is redacted by default (value logging disabled) for any command.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Redact_RedactsCommandValueByDefault()
|
||||||
|
{
|
||||||
|
Dictionary<string, object?> properties = new()
|
||||||
|
{
|
||||||
|
["CommandMethod"] = "Write",
|
||||||
|
["CommandValue"] = "plaintext-tag-value",
|
||||||
|
};
|
||||||
|
|
||||||
|
_redactor.Redact(properties);
|
||||||
|
|
||||||
|
Assert.Equal(GatewayLogRedactor.RedactedValue, properties["CommandValue"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies non-sensitive properties are left untouched.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Redact_LeavesNonSensitivePropertiesUnchanged()
|
||||||
|
{
|
||||||
|
Dictionary<string, object?> properties = new()
|
||||||
|
{
|
||||||
|
["SessionId"] = "session-1",
|
||||||
|
["CorrelationId"] = (ulong)99,
|
||||||
|
["ClientIdentity"] = "Bearer plain-token-no-marker",
|
||||||
|
};
|
||||||
|
|
||||||
|
_redactor.Redact(properties);
|
||||||
|
|
||||||
|
Assert.Equal("session-1", properties["SessionId"]);
|
||||||
|
Assert.Equal((ulong)99, properties["CorrelationId"]);
|
||||||
|
// No mxgw_ marker — identity passes through unchanged.
|
||||||
|
Assert.Equal("Bearer plain-token-no-marker", properties["ClientIdentity"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user