159 lines
6.9 KiB
Go
159 lines
6.9 KiB
Go
package contract
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// StaticCellArtifact is the management-approved, immutable Cell/SIP hand-off
|
|
// artifact. Its JSON shape is owned by static-cell-artifact.schema.json; this
|
|
// type only provides a typed boundary after schema validation.
|
|
type StaticCellArtifact struct {
|
|
ArtifactID string `json:"artifact_id"`
|
|
SourceRelease string `json:"source_release"`
|
|
SourceDigest string `json:"source_digest"`
|
|
ApprovalReference string `json:"approval_reference"`
|
|
CellID string `json:"cell_id"`
|
|
Revision uint64 `json:"revision"`
|
|
ConfigSHA256 string `json:"config_sha256"`
|
|
Mode string `json:"mode"`
|
|
AllowedTargets []string `json:"allowed_targets"`
|
|
Trunks []StaticTrunk `json:"trunks"`
|
|
ARI *StaticARI `json:"ari,omitempty"`
|
|
MediaProfiles map[string]StaticMediaProfile `json:"media_profiles,omitempty"`
|
|
Media *StaticMedia `json:"media,omitempty"`
|
|
Recording *StaticRecording `json:"recording,omitempty"`
|
|
LoadEvidence *StaticLoadEvidence `json:"load_evidence"`
|
|
}
|
|
|
|
type StaticTrunk struct {
|
|
TrunkID string `json:"trunk_id"`
|
|
ProviderID string `json:"provider_id"`
|
|
EgressPoolID string `json:"egress_pool_id"`
|
|
Codec string `json:"codec"`
|
|
CallerProfileIDs []string `json:"caller_profile_ids"`
|
|
DialPrefix string `json:"dial_prefix"`
|
|
Enabled bool `json:"enabled"`
|
|
SIPEndpointRef string `json:"sip_endpoint_ref"`
|
|
CredentialRef *string `json:"credential_ref"`
|
|
MediaProfileID string `json:"media_profile_id,omitempty"`
|
|
}
|
|
|
|
type StaticARI struct {
|
|
BaseURL string `json:"base_url"`
|
|
WebsocketURL string `json:"websocket_url"`
|
|
Application string `json:"application"`
|
|
CredentialRef string `json:"credential_ref"`
|
|
}
|
|
|
|
type StaticMedia struct {
|
|
BindAddress string `json:"bind_address"`
|
|
Port int `json:"port"`
|
|
Format string `json:"format"`
|
|
SampleRateHz int `json:"sample_rate_hz"`
|
|
Channels int `json:"channels"`
|
|
PayloadType int `json:"payload_type"`
|
|
}
|
|
|
|
type StaticMediaProfile struct {
|
|
Format string `json:"format"`
|
|
SampleRateHz int `json:"sample_rate_hz"`
|
|
Channels int `json:"channels"`
|
|
PayloadType int `json:"payload_type"`
|
|
}
|
|
|
|
type StaticRecording struct {
|
|
Enabled bool `json:"enabled"`
|
|
Format string `json:"format"`
|
|
Directory string `json:"directory"`
|
|
MaxBytes int64 `json:"max_bytes"`
|
|
}
|
|
|
|
type StaticLoadEvidence struct {
|
|
AsteriskConfigSHA256 string `json:"asterisk_config_sha256"`
|
|
LoadedAt string `json:"loaded_at"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// StaticArtifactExpectation contains deployment-local binding constraints.
|
|
// Empty string/slice values leave the corresponding optional check disabled;
|
|
// the source contract remains mandatory and is always validated first.
|
|
type StaticArtifactExpectation struct {
|
|
CellID string
|
|
Mode string
|
|
SourceRelease string
|
|
SourceDigest string
|
|
ConfigSHA256 string
|
|
MinimumRevision uint64
|
|
AllowedEgressPoolIDs []string
|
|
RequiredTrunkIDs []string
|
|
}
|
|
|
|
// ValidateStaticArtifact validates the imported artifact schema and then
|
|
// applies the local Cell hand-off bindings. It deliberately does not claim
|
|
// that Asterisk has loaded the artifact: load_evidence.status is explicitly
|
|
// "not-yet-loaded" in the contract until an independent load check exists.
|
|
func ValidateStaticArtifact(raw []byte, expected StaticArtifactExpectation) (StaticCellArtifact, error) {
|
|
if err := ValidateSourceSchema("static-cell-artifact.schema.json", raw); err != nil {
|
|
return StaticCellArtifact{}, err
|
|
}
|
|
|
|
var artifact StaticCellArtifact
|
|
if err := json.Unmarshal(raw, &artifact); err != nil {
|
|
return StaticCellArtifact{}, fmt.Errorf("decode static Cell artifact: %w", err)
|
|
}
|
|
if expected.CellID != "" && artifact.CellID != expected.CellID {
|
|
return StaticCellArtifact{}, fmt.Errorf("static artifact cell binding mismatch: got %q, want %q", artifact.CellID, expected.CellID)
|
|
}
|
|
if expected.Mode != "" && artifact.Mode != expected.Mode {
|
|
return StaticCellArtifact{}, fmt.Errorf("static artifact mode mismatch: got %q, want %q", artifact.Mode, expected.Mode)
|
|
}
|
|
if expected.SourceRelease != "" && artifact.SourceRelease != expected.SourceRelease {
|
|
return StaticCellArtifact{}, fmt.Errorf("static artifact source release mismatch: got %q, want %q", artifact.SourceRelease, expected.SourceRelease)
|
|
}
|
|
if expected.SourceDigest != "" && artifact.SourceDigest != expected.SourceDigest {
|
|
return StaticCellArtifact{}, fmt.Errorf("static artifact source digest mismatch: got %q, want %q", artifact.SourceDigest, expected.SourceDigest)
|
|
}
|
|
if expected.ConfigSHA256 != "" && artifact.ConfigSHA256 != expected.ConfigSHA256 {
|
|
return StaticCellArtifact{}, fmt.Errorf("static artifact config digest mismatch: got %q, want %q", artifact.ConfigSHA256, expected.ConfigSHA256)
|
|
}
|
|
if expected.MinimumRevision != 0 && artifact.Revision < expected.MinimumRevision {
|
|
return StaticCellArtifact{}, fmt.Errorf("static artifact revision %d is older than required %d", artifact.Revision, expected.MinimumRevision)
|
|
}
|
|
|
|
allowedEgress := make(map[string]struct{}, len(expected.AllowedEgressPoolIDs))
|
|
for _, egressPoolID := range expected.AllowedEgressPoolIDs {
|
|
allowedEgress[egressPoolID] = struct{}{}
|
|
}
|
|
requiredTrunks := make(map[string]struct{}, len(expected.RequiredTrunkIDs))
|
|
for _, trunkID := range expected.RequiredTrunkIDs {
|
|
requiredTrunks[trunkID] = struct{}{}
|
|
}
|
|
seenTrunks := make(map[string]struct{}, len(artifact.Trunks))
|
|
for _, trunk := range artifact.Trunks {
|
|
if _, duplicate := seenTrunks[trunk.TrunkID]; duplicate {
|
|
return StaticCellArtifact{}, fmt.Errorf("static artifact contains duplicate trunk_id %q", trunk.TrunkID)
|
|
}
|
|
seenTrunks[trunk.TrunkID] = struct{}{}
|
|
if len(allowedEgress) != 0 {
|
|
if _, ok := allowedEgress[trunk.EgressPoolID]; !ok {
|
|
return StaticCellArtifact{}, fmt.Errorf("static artifact trunk %q uses disallowed egress pool %q", trunk.TrunkID, trunk.EgressPoolID)
|
|
}
|
|
}
|
|
if _, required := requiredTrunks[trunk.TrunkID]; required && !trunk.Enabled {
|
|
return StaticCellArtifact{}, fmt.Errorf("required static artifact trunk %q is disabled", trunk.TrunkID)
|
|
}
|
|
if trunk.MediaProfileID != "" {
|
|
if _, ok := artifact.MediaProfiles[trunk.MediaProfileID]; !ok {
|
|
return StaticCellArtifact{}, fmt.Errorf("static artifact trunk %q references unknown media profile %q", trunk.TrunkID, trunk.MediaProfileID)
|
|
}
|
|
}
|
|
}
|
|
for trunkID := range requiredTrunks {
|
|
if _, present := seenTrunks[trunkID]; !present {
|
|
return StaticCellArtifact{}, fmt.Errorf("required static artifact trunk %q is missing", trunkID)
|
|
}
|
|
}
|
|
return artifact, nil
|
|
}
|