Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions pkg/scd/actions/constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package actions

import (
"context"
"time"

"github.com/golang/geo/s2"
restapi "github.com/interuss/dss/pkg/api/scdv1"
Expand All @@ -10,6 +11,7 @@ import (
scdmodels "github.com/interuss/dss/pkg/scd/models"
"github.com/interuss/dss/pkg/scd/repos"
dssstore "github.com/interuss/dss/pkg/store"
"github.com/interuss/dss/pkg/timestamp"
"github.com/interuss/stacktrace"
"github.com/jackc/pgx/v5"
)
Expand All @@ -26,6 +28,16 @@ func init() {
Execute: ExecuteGetConstraint,
IsReadOnly: true,
}
Registry[restapi.CreateConstraintReferenceOperationID] = dssstore.OperationHandler[repos.Repository]{
Encode: dssstore.EncodeJSON,
Decode: dssstore.DecodeJSON[*restapi.CreateConstraintReferenceRequest],
Execute: ExecutePutConstraint,
}
Registry[restapi.UpdateConstraintReferenceOperationID] = dssstore.OperationHandler[repos.Repository]{
Encode: dssstore.EncodeJSON,
Decode: dssstore.DecodeJSON[*restapi.UpdateConstraintReferenceRequest],
Execute: ExecutePutConstraint,
}
Registry[restapi.QueryConstraintReferencesOperationID] = dssstore.OperationHandler[repos.Repository]{
Encode: dssstore.EncodeJSON,
Decode: dssstore.DecodeJSON[*restapi.QueryConstraintReferencesRequest],
Expand Down Expand Up @@ -63,6 +75,161 @@ func ExecuteGetConstraint(ctx context.Context, repo repos.Repository, request ds
}, nil
}

// ExecutePutConstraint inserts or updates a Constraint.
// If ovn is empty (""), it will attempt to create a new Constraint.
func ExecutePutConstraint(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) {
var (
manager string
entityid restapi.EntityID
ovn restapi.EntityOVN
params *restapi.PutConstraintReferenceParameters
)

switch req := request.(type) {
case *restapi.CreateConstraintReferenceRequest:
manager, entityid, params = *req.Auth.ClientID, req.Entityid, req.Body
case *restapi.UpdateConstraintReferenceRequest:
manager, entityid, ovn, params = *req.Auth.ClientID, req.Entityid, req.Ovn, req.Body
default:
return nil, stacktrace.NewError("unexpected request type %T for operation %q", request, restapi.CreateConstraintReferenceOperationID)
}

validParams, err := ValidateAndReturnConstraintUpsertParams(timestamp.MustGetRequestTimestamp(ctx), entityid, params)
if err != nil {
return nil, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Failed to validate Constraint upsert parameters")
}

version := scdmodels.VersionNumber(1)

// Get existing Constraint, if any, and validate request
old, err := repo.GetConstraint(ctx, validParams.id)
switch {
case err == pgx.ErrNoRows:
// No existing Constraint; verify that creation was requested
if ovn != "" {
return nil, stacktrace.NewErrorWithCode(dsserr.VersionMismatch, "Old version %s does not exist", ovn)
}
case err != nil:
return nil, stacktrace.Propagate(err, "Could not get Constraint from repo")
}
if old != nil {
if old.Manager != dssmodels.Manager(manager) {
return nil, stacktrace.NewErrorWithCode(dsserr.PermissionDenied,
"Constraint owned by %s, but %s attempted to modify", old.Manager, manager)
}
if old.OVN != scdmodels.OVN(ovn) {
return nil, stacktrace.NewErrorWithCode(dsserr.VersionMismatch,
"Current version is %s but client specified version %s", old.OVN, ovn)
}
version = old.Version + 1
}

// Compute total affected Volume4D for notification purposes
var notifyVol4 *dssmodels.Volume4D
if old == nil {
notifyVol4 = validParams.uExtent
} else {
oldVol4 := &dssmodels.Volume4D{
StartTime: old.StartTime,
EndTime: old.EndTime,
SpatialVolume: &dssmodels.Volume3D{
AltitudeHi: old.AltitudeUpper,
AltitudeLo: old.AltitudeLower,
Footprint: dssmodels.GeometryFunc(func() (s2.CellUnion, error) {
return old.Cells, nil
}),
}}
notifyVol4, err = dssmodels.UnionVolumes4D(validParams.uExtent, oldVol4)
if err != nil {
return nil, stacktrace.Propagate(err, "Error constructing 4D volumes union")
}
}

// Construct the new Constraint
constraint := validParams.toConstraint(dssmodels.Manager(manager), version)

// Upsert the Constraint
constraint, err = repo.UpsertConstraint(ctx, constraint)
if err != nil {
return nil, err
}

// Find the Subscriptions interested in Constraints and increment their
// notification indices.
subs, err := repo.IncrementNotificationIndicesForConstraints(ctx, notifyVol4)
if err != nil {
return nil, err
}

// Return response to client
return &restapi.ChangeConstraintReferenceResponse{
ConstraintReference: *constraint.ToRest(),
Subscribers: makeSubscribersToNotify(subs),
}, nil
}

type validConstraintParams struct {
id dssmodels.ID
uExtent *dssmodels.Volume4D
cells s2.CellUnion
ussBaseURL string
}

func (vp *validConstraintParams) toConstraint(manager dssmodels.Manager, version scdmodels.VersionNumber) *scdmodels.Constraint {
return &scdmodels.Constraint{
ID: vp.id,
Manager: manager,
Version: version,

StartTime: vp.uExtent.StartTime,
EndTime: vp.uExtent.EndTime,
AltitudeLower: vp.uExtent.SpatialVolume.AltitudeLo,
AltitudeUpper: vp.uExtent.SpatialVolume.AltitudeHi,

USSBaseURL: vp.ussBaseURL,
Cells: vp.cells,
}
}

// ValidateAndReturnConstraintUpsertParams performs validation of Constraint upsert requests and returns a validConstraintParams struct if successful.
// Note that this does NOT check for anything related to access controls: any error returned should be labeled as a dsserr.BadRequest.
func ValidateAndReturnConstraintUpsertParams(
now time.Time,
entityid restapi.EntityID,
params *restapi.PutConstraintReferenceParameters,
) (*validConstraintParams, error) {
var err error
valid := &validConstraintParams{}
valid.id, err = dssmodels.IDFromString(string(entityid))
if err != nil {
return nil, stacktrace.NewError("Invalid ID format: `%s`", entityid)
}

if len(params.UssBaseUrl) == 0 {
return nil, stacktrace.NewError("Missing required UssBaseUrl")
}

valid.ussBaseURL = string(params.UssBaseUrl)

// Start and end times are required for each volume
// The end time may not be in the past
valid.uExtent, err = scdmodels.UnionVolumes4DFromSCDRest(
params.Extents,
scdmodels.WithRequireTimeBounds(),
scdmodels.WithRequireEndTimeAfter(now),
)
if err != nil {
return nil, stacktrace.Propagate(err, "Invalid extents")
}

valid.cells, err = valid.uExtent.CalculateSpatialCovering()
if err != nil {
return nil, stacktrace.Propagate(err, "Invalid area")
}

return valid, nil
}

func ExecuteQueryConstraintReferences(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) {
req, ok := request.(*restapi.QueryConstraintReferencesRequest)
if !ok {
Expand Down
Loading
Loading