...

Source file src/edge-infra.dev/pkg/sds/emergencyaccess/requestservice/requestservice.go

Documentation: edge-infra.dev/pkg/sds/emergencyaccess/requestservice

     1  package requestservice
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"errors"
     7  	"fmt"
     8  	"slices"
     9  
    10  	"github.com/hashicorp/go-version"
    11  
    12  	"edge-infra.dev/pkg/lib/fog"
    13  	"edge-infra.dev/pkg/sds/emergencyaccess/eaconst"
    14  	"edge-infra.dev/pkg/sds/emergencyaccess/msgdata"
    15  	"edge-infra.dev/pkg/sds/emergencyaccess/types"
    16  )
    17  
    18  type versionsMap map[string]eaconst.MessageVersion
    19  
    20  var (
    21  	defaultEdgeOSToMessageVersion = versionsMap{
    22  		// 0.0 <= x < 1.16
    23  		"0.0": eaconst.MessageVersion1_0,
    24  		// x >= 1.16
    25  		"1.16": eaconst.MessageVersion2_0,
    26  	}
    27  )
    28  
    29  type versionMapping struct {
    30  	edgeOS  *version.Version
    31  	message *version.Version
    32  }
    33  
    34  func messageVersionMappings(versions versionsMap) (mappings []versionMapping, err error) {
    35  	for k, v := range versions {
    36  		edgeOSVersion, err := version.NewVersion(k)
    37  		if err != nil {
    38  			return nil, err
    39  		}
    40  		messageVersion, err := version.NewVersion(string(v))
    41  		if err != nil {
    42  			return nil, err
    43  		}
    44  		mappings = append(mappings, versionMapping{
    45  			edgeOS:  edgeOSVersion,
    46  			message: messageVersion,
    47  		})
    48  	}
    49  	slices.SortFunc(mappings, func(a, b versionMapping) int {
    50  		// Sort by descending order
    51  		return b.edgeOS.Compare(a.edgeOS)
    52  	})
    53  	return mappings, nil
    54  }
    55  
    56  type RequestService struct {
    57  	db *sql.DB
    58  	// A descending-order slice of edgeOS to message version mappings.
    59  	// A message version is guaranteed to be compatible with every edgeOS version greater
    60  	// than  or equal to its mapped edgeOS version _until_ the next greatest edgeOS
    61  	// version in the slice. Afterwards, a message version may or may not be compatible
    62  	// with the next greatest edgeOS version in the slice.
    63  	versions       []versionMapping
    64  	versionCache   versionCache
    65  	minimumVersion *version.Version
    66  }
    67  
    68  func New(db *sql.DB) (*RequestService, error) {
    69  	versionMappings, err := messageVersionMappings(defaultEdgeOSToMessageVersion)
    70  	if err != nil {
    71  		return nil, fmt.Errorf("error failed to create message version mappings: %w", err)
    72  	}
    73  	minimumVersion, err := version.NewVersion(string(eaconst.MinimumSupportedMessageVersion))
    74  	if err != nil {
    75  		return nil, fmt.Errorf("error failed to parse minimum supported message version: %w", err)
    76  	}
    77  	return &RequestService{
    78  		db:             db,
    79  		minimumVersion: minimumVersion,
    80  		versions:       versionMappings,
    81  		versionCache: versionCache{
    82  			cache:      make(map[types.Target]*version.Version),
    83  			expiration: defaultExpiration,
    84  		},
    85  	}, nil
    86  }
    87  
    88  type Config struct {
    89  	Target types.Target
    90  }
    91  
    92  func (rs *RequestService) CreateRequest(ctx context.Context, payload string, config Config) (request msgdata.Request, err error) {
    93  	messageVersion, err := rs.getMessageVersion(ctx, config.Target)
    94  	if err != nil {
    95  		return nil, fmt.Errorf("error getting compatible message version for target: %w", err)
    96  	}
    97  	return newRequest(payload, messageVersion)
    98  }
    99  
   100  func (rs *RequestService) getMessageVersion(ctx context.Context, target types.Target) (messageVersion *version.Version, err error) {
   101  	log := fog.FromContext(ctx).WithValues(
   102  		"projectID", target.Projectid,
   103  		"bannerID", target.Bannerid,
   104  		"storeID", target.Storeid,
   105  		"terminalID", target.Terminalid,
   106  	)
   107  	ctx = fog.IntoContext(ctx, log)
   108  
   109  	messageVersion, ok := rs.versionCache.Get(target)
   110  	if ok {
   111  		return messageVersion, nil
   112  	}
   113  	rows := rs.db.QueryRowContext(ctx, getEdgeOSVersionQuery, target.StoreID(), target.TerminalID())
   114  	var edgeOSVersion string
   115  	err = rows.Scan(&edgeOSVersion)
   116  	// Older IENs may not query correctly and we should not error in this case.
   117  	// Return a minimum supported message version to attempt a request with.
   118  	if err == sql.ErrNoRows {
   119  		log.Info("Could not find a target edgeOS version. Attempting a request with minimum supported message version", "messageVersion", rs.minimumVersion.Original())
   120  		messageVersion = rs.minimumVersion
   121  		rs.versionCache.Insert(target, messageVersion)
   122  		return messageVersion, nil
   123  	}
   124  	if err != nil {
   125  		return nil, fmt.Errorf("error scanning edgeOS version results: %w", err)
   126  	}
   127  	log.Info("Target edgeOS version queried", "edgeOSVersion", edgeOSVersion)
   128  	messageVersion, err = rs.pickMessageVersion(ctx, edgeOSVersion)
   129  	if err != nil {
   130  		return nil, fmt.Errorf("error picking message version: %w", err)
   131  	}
   132  	rs.versionCache.Insert(target, messageVersion)
   133  	return messageVersion, nil
   134  }
   135  
   136  func (rs *RequestService) pickMessageVersion(ctx context.Context, edgeOSVersion string) (messageVersion *version.Version, err error) {
   137  	log := fog.FromContext(ctx)
   138  
   139  	targetVersion, err := version.NewVersion(edgeOSVersion)
   140  	if err != nil {
   141  		log.Error(err,
   142  			"Target edgeOS version could not be parsed. Attempting a request with minimum supported message version",
   143  			"edgeOSVersion", edgeOSVersion,
   144  			"messageVersion", rs.minimumVersion.Original(),
   145  		)
   146  		return rs.minimumVersion, nil
   147  	}
   148  
   149  	// Iterate through all keys and return the latest compatible message version.
   150  	// A match is made with the first edgeOS version in this descending-ordered slice where
   151  	// the target version is greater than or equal to it. "Greater than or equal" in this case means that
   152  	// target major is greater *or* target major is equal and target minor is greater than or equal.
   153  	for _, v := range rs.versions {
   154  		if targetVersion.GreaterThanOrEqual(v.edgeOS) {
   155  			return v.message, nil
   156  		}
   157  	}
   158  	return nil, errors.New("failed to find matching message version for target edge os version")
   159  }
   160  
   161  func newRequest(payload string, messageVersion *version.Version) (req msgdata.Request, err error) {
   162  	switch messageVersion.Original() {
   163  	case string(eaconst.MessageVersion1_0):
   164  		req, err = msgdata.NewV1_0Request(payload)
   165  	case string(eaconst.MessageVersion2_0):
   166  		req, err = msgdata.NewV2_0Request(payload)
   167  	default:
   168  		return nil, fmt.Errorf("unsupported message version %s", messageVersion.Original())
   169  	}
   170  	if err != nil {
   171  		return nil, fmt.Errorf("failed to create v%s request: %w", messageVersion.Original(), err)
   172  	}
   173  	return req, nil
   174  }
   175  

View as plain text