package requestservice import ( "context" "database/sql" "errors" "fmt" "slices" "github.com/hashicorp/go-version" "edge-infra.dev/pkg/lib/fog" "edge-infra.dev/pkg/sds/emergencyaccess/eaconst" "edge-infra.dev/pkg/sds/emergencyaccess/msgdata" "edge-infra.dev/pkg/sds/emergencyaccess/types" ) type versionsMap map[string]eaconst.MessageVersion var ( defaultEdgeOSToMessageVersion = versionsMap{ // 0.0 <= x < 1.16 "0.0": eaconst.MessageVersion1_0, // x >= 1.16 "1.16": eaconst.MessageVersion2_0, } ) type versionMapping struct { edgeOS *version.Version message *version.Version } func messageVersionMappings(versions versionsMap) (mappings []versionMapping, err error) { for k, v := range versions { edgeOSVersion, err := version.NewVersion(k) if err != nil { return nil, err } messageVersion, err := version.NewVersion(string(v)) if err != nil { return nil, err } mappings = append(mappings, versionMapping{ edgeOS: edgeOSVersion, message: messageVersion, }) } slices.SortFunc(mappings, func(a, b versionMapping) int { // Sort by descending order return b.edgeOS.Compare(a.edgeOS) }) return mappings, nil } type RequestService struct { db *sql.DB // A descending-order slice of edgeOS to message version mappings. // A message version is guaranteed to be compatible with every edgeOS version greater // than or equal to its mapped edgeOS version _until_ the next greatest edgeOS // version in the slice. Afterwards, a message version may or may not be compatible // with the next greatest edgeOS version in the slice. versions []versionMapping versionCache versionCache minimumVersion *version.Version } func New(db *sql.DB) (*RequestService, error) { versionMappings, err := messageVersionMappings(defaultEdgeOSToMessageVersion) if err != nil { return nil, fmt.Errorf("error failed to create message version mappings: %w", err) } minimumVersion, err := version.NewVersion(string(eaconst.MinimumSupportedMessageVersion)) if err != nil { return nil, fmt.Errorf("error failed to parse minimum supported message version: %w", err) } return &RequestService{ db: db, minimumVersion: minimumVersion, versions: versionMappings, versionCache: versionCache{ cache: make(map[types.Target]*version.Version), expiration: defaultExpiration, }, }, nil } type Config struct { Target types.Target } func (rs *RequestService) CreateRequest(ctx context.Context, payload string, config Config) (request msgdata.Request, err error) { messageVersion, err := rs.getMessageVersion(ctx, config.Target) if err != nil { return nil, fmt.Errorf("error getting compatible message version for target: %w", err) } return newRequest(payload, messageVersion) } func (rs *RequestService) getMessageVersion(ctx context.Context, target types.Target) (messageVersion *version.Version, err error) { log := fog.FromContext(ctx).WithValues( "projectID", target.Projectid, "bannerID", target.Bannerid, "storeID", target.Storeid, "terminalID", target.Terminalid, ) ctx = fog.IntoContext(ctx, log) messageVersion, ok := rs.versionCache.Get(target) if ok { return messageVersion, nil } rows := rs.db.QueryRowContext(ctx, getEdgeOSVersionQuery, target.StoreID(), target.TerminalID()) var edgeOSVersion string err = rows.Scan(&edgeOSVersion) // Older IENs may not query correctly and we should not error in this case. // Return a minimum supported message version to attempt a request with. if err == sql.ErrNoRows { log.Info("Could not find a target edgeOS version. Attempting a request with minimum supported message version", "messageVersion", rs.minimumVersion.Original()) messageVersion = rs.minimumVersion rs.versionCache.Insert(target, messageVersion) return messageVersion, nil } if err != nil { return nil, fmt.Errorf("error scanning edgeOS version results: %w", err) } log.Info("Target edgeOS version queried", "edgeOSVersion", edgeOSVersion) messageVersion, err = rs.pickMessageVersion(ctx, edgeOSVersion) if err != nil { return nil, fmt.Errorf("error picking message version: %w", err) } rs.versionCache.Insert(target, messageVersion) return messageVersion, nil } func (rs *RequestService) pickMessageVersion(ctx context.Context, edgeOSVersion string) (messageVersion *version.Version, err error) { log := fog.FromContext(ctx) targetVersion, err := version.NewVersion(edgeOSVersion) if err != nil { log.Error(err, "Target edgeOS version could not be parsed. Attempting a request with minimum supported message version", "edgeOSVersion", edgeOSVersion, "messageVersion", rs.minimumVersion.Original(), ) return rs.minimumVersion, nil } // Iterate through all keys and return the latest compatible message version. // A match is made with the first edgeOS version in this descending-ordered slice where // the target version is greater than or equal to it. "Greater than or equal" in this case means that // target major is greater *or* target major is equal and target minor is greater than or equal. for _, v := range rs.versions { if targetVersion.GreaterThanOrEqual(v.edgeOS) { return v.message, nil } } return nil, errors.New("failed to find matching message version for target edge os version") } func newRequest(payload string, messageVersion *version.Version) (req msgdata.Request, err error) { switch messageVersion.Original() { case string(eaconst.MessageVersion1_0): req, err = msgdata.NewV1_0Request(payload) case string(eaconst.MessageVersion2_0): req, err = msgdata.NewV2_0Request(payload) default: return nil, fmt.Errorf("unsupported message version %s", messageVersion.Original()) } if err != nil { return nil, fmt.Errorf("failed to create v%s request: %w", messageVersion.Original(), err) } return req, nil }