feat(#23): elevate connection-binding completeness to a deploy-gating Error (M2.8)
Pre-deployment validation only WARNED when a data-sourced attribute had no
connection binding, so an instance with unresolved bindings still passed IsValid
and could deploy. There was also no check that a binding resolves to a connection
that actually exists at the target site.
- ValidationService.Validate gains an opt-in `enforceConnectionBindings` flag
(default false) plus a `siteConnectionNames` set. Default-false keeps the
template DESIGN-TIME path (ManagementActor.HandleValidateTemplate) non-blocking,
since bindings are legitimately set later at instance/deploy time. The DEPLOY
path (FlatteningPipeline) opts in (true) so:
* a data-sourced attribute with no binding is now a deploy-gating Error;
* a binding to a connection that does not exist on the target site is an Error.
Static (non-data-sourced) attributes are never flagged.
- FlatteningPipeline computes the site-connection-names set from the loaded site
data connections (mirroring M2.1's alarmCapableConnectionNames) and threads it in.
- Tests: TemplateEngine.Tests covers design-time warning / deploy-time error /
static-ok / exists-at-site / non-existent-connection. New
FlatteningPipelineConnectionBindingTests proves the deploy path enforces it.
Mark M2.7 + M2.8 completed in the plan task tracker.
This commit is contained in:
@@ -14,7 +14,10 @@ namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;
|
||||
/// 4. Alarm trigger references exist (referenced attributes must be in the flattened config)
|
||||
/// 5. Script trigger references exist (referenced attributes must be in the flattened config)
|
||||
/// 6. Expression triggers — blank check, syntax check, and attribute-reference scan
|
||||
/// 7. Connection binding completeness (all data-sourced attributes must have a binding)
|
||||
/// 7. Connection binding completeness — every data-sourced attribute must have a binding,
|
||||
/// and (on the deploy path) the bound connection must exist on the target site.
|
||||
/// Severity is context-dependent: a non-blocking Warning at template design time
|
||||
/// (bindings are set later) and a deploy-gating Error when enforced (M2.8 / #23).
|
||||
/// 8. Does NOT verify tag path resolution on devices
|
||||
/// </summary>
|
||||
public class ValidationService
|
||||
@@ -52,11 +55,37 @@ public class ValidationService
|
||||
/// the semantic validator gates every native-alarm-source binding against it.
|
||||
/// <c>null</c> skips the capability check (its absence makes the check inert).
|
||||
/// </param>
|
||||
/// <param name="enforceConnectionBindings">
|
||||
/// M2.8 (#23): controls the severity of the connection-binding-completeness check.
|
||||
/// <para>
|
||||
/// <c>false</c> (default) — template DESIGN-TIME: a data-sourced attribute that is
|
||||
/// not yet bound produces only a non-blocking <c>Warning</c>. Bindings are set later,
|
||||
/// at instance/deploy time, so an unbound data-sourced template attribute is legitimate
|
||||
/// here (see <see cref="ManagementService"/>'s ValidateTemplate path, which builds a
|
||||
/// config straight from raw template members with no bindings).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <c>true</c> — DEPLOY path (<see cref="DeploymentManager"/>'s FlatteningPipeline):
|
||||
/// an unbound data-sourced attribute becomes a deploy-gating <c>Error</c> (IsValid false),
|
||||
/// and — when <paramref name="siteConnectionNames"/> is supplied — a binding pointing at a
|
||||
/// connection that does not exist on the target site is also an <c>Error</c>.
|
||||
/// </para>
|
||||
/// </param>
|
||||
/// <param name="siteConnectionNames">
|
||||
/// M2.8 (#23): optional set of the data-connection names that actually exist on the
|
||||
/// target site (computed by the deploy pipeline from the site's loaded connections,
|
||||
/// mirroring <paramref name="alarmCapableConnectionNames"/>). When supplied (and
|
||||
/// <paramref name="enforceConnectionBindings"/> is <c>true</c>), every bound
|
||||
/// connection is checked against this set so a binding to a phantom/stale connection
|
||||
/// is caught. <c>null</c> skips the "exists at site" half (it stays inert).
|
||||
/// </param>
|
||||
/// <returns>A merged <see cref="ValidationResult"/> aggregating all pipeline stage outcomes.</returns>
|
||||
public ValidationResult Validate(
|
||||
FlattenedConfiguration configuration,
|
||||
IReadOnlyList<ResolvedScript>? sharedScripts = null,
|
||||
IReadOnlySet<string>? alarmCapableConnectionNames = null)
|
||||
IReadOnlySet<string>? alarmCapableConnectionNames = null,
|
||||
bool enforceConnectionBindings = false,
|
||||
IReadOnlySet<string>? siteConnectionNames = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
@@ -68,7 +97,7 @@ public class ValidationService
|
||||
ValidateAlarmTriggerReferences(configuration),
|
||||
ValidateScriptTriggerReferences(configuration),
|
||||
ValidateExpressionTriggers(configuration),
|
||||
ValidateConnectionBindingCompleteness(configuration),
|
||||
ValidateConnectionBindingCompleteness(configuration, enforceConnectionBindings, siteConnectionNames),
|
||||
_semanticValidator.Validate(configuration, sharedScripts, alarmCapableConnectionNames)
|
||||
};
|
||||
|
||||
@@ -507,21 +536,76 @@ public class ValidationService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that all data-sourced attributes have connection bindings.
|
||||
/// Validates connection bindings on data-sourced attributes. Only DATA-SOURCED
|
||||
/// attributes (<see cref="ResolvedAttribute.DataSourceReference"/> != <c>null</c>)
|
||||
/// require a binding; static attributes are never flagged.
|
||||
///
|
||||
/// M2.8 (#23): the severity is context-dependent (see <paramref name="enforce"/>).
|
||||
/// At template design time (<c>enforce == false</c>) an unbound data-sourced
|
||||
/// attribute is legitimate (bindings are set later) so it is only a non-blocking
|
||||
/// <c>Warning</c>. On the deploy path (<c>enforce == true</c>) an unbound
|
||||
/// data-sourced attribute is a deploy-gating <c>Error</c>, and — when
|
||||
/// <paramref name="siteConnectionNames"/> is supplied — a binding to a connection
|
||||
/// that does not exist on the target site is also an <c>Error</c>.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The flattened configuration to validate.</param>
|
||||
/// <returns>A <see cref="ValidationResult"/> with warnings for each data-sourced attribute that lacks a connection binding.</returns>
|
||||
public static ValidationResult ValidateConnectionBindingCompleteness(FlattenedConfiguration configuration)
|
||||
/// <param name="enforce">
|
||||
/// <c>true</c> on the deploy path (unbound → Error + "exists at site" check);
|
||||
/// <c>false</c> at design time (unbound → Warning only). Defaults to <c>false</c>
|
||||
/// so design-time validation stays non-blocking.
|
||||
/// </param>
|
||||
/// <param name="siteConnectionNames">
|
||||
/// Optional set of data-connection names that actually exist on the target site.
|
||||
/// When non-<c>null</c> and <paramref name="enforce"/> is <c>true</c>, every bound
|
||||
/// connection name is checked against this set. <c>null</c> skips the "exists at
|
||||
/// site" check.
|
||||
/// </param>
|
||||
/// <returns>A <see cref="ValidationResult"/> with the binding findings at the appropriate severity.</returns>
|
||||
public static ValidationResult ValidateConnectionBindingCompleteness(
|
||||
FlattenedConfiguration configuration,
|
||||
bool enforce = false,
|
||||
IReadOnlySet<string>? siteConnectionNames = null)
|
||||
{
|
||||
var errors = new List<ValidationEntry>();
|
||||
var warnings = new List<ValidationEntry>();
|
||||
|
||||
foreach (var attr in configuration.Attributes)
|
||||
{
|
||||
if (attr.DataSourceReference != null && attr.BoundDataConnectionId == null)
|
||||
// Only data-sourced attributes participate in binding validation.
|
||||
if (attr.DataSourceReference == null)
|
||||
continue;
|
||||
|
||||
if (attr.BoundDataConnectionId == null)
|
||||
{
|
||||
warnings.Add(ValidationEntry.Warning(ValidationCategory.ConnectionBinding,
|
||||
$"Attribute '{attr.CanonicalName}' has a data source reference but no connection binding.",
|
||||
// Unbound data-sourced attribute. At deploy time this gates the
|
||||
// deployment; at design time the binding is set later, so it is
|
||||
// only advisory.
|
||||
if (enforce)
|
||||
{
|
||||
errors.Add(ValidationEntry.Error(ValidationCategory.ConnectionBinding,
|
||||
$"Attribute '{attr.CanonicalName}' has a data source reference but no connection binding.",
|
||||
attr.CanonicalName));
|
||||
}
|
||||
else
|
||||
{
|
||||
warnings.Add(ValidationEntry.Warning(ValidationCategory.ConnectionBinding,
|
||||
$"Attribute '{attr.CanonicalName}' has a data source reference but no connection binding.",
|
||||
attr.CanonicalName));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// The attribute IS bound. On the deploy path, verify the bound connection
|
||||
// actually exists on the target site (resolve against the site's connection
|
||||
// set, not just name presence in the config). A binding pointing at a
|
||||
// non-existent/stale site connection is a deploy-gating Error.
|
||||
if (enforce && siteConnectionNames != null &&
|
||||
attr.BoundDataConnectionName != null &&
|
||||
!siteConnectionNames.Contains(attr.BoundDataConnectionName))
|
||||
{
|
||||
errors.Add(ValidationEntry.Error(ValidationCategory.ConnectionBinding,
|
||||
$"Attribute '{attr.CanonicalName}' is bound to data connection '{attr.BoundDataConnectionName}' " +
|
||||
"which does not exist on the target site.",
|
||||
attr.CanonicalName));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user