Files

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
}