package services import ( "context" "database/sql" "errors" "fmt" "slices" "strconv" "strings" sqlerr "edge-infra.dev/pkg/edge/api/apierror/sql" "edge-infra.dev/pkg/edge/api/graph/model" sqlquery "edge-infra.dev/pkg/edge/api/sql" ) var ( defaultBWC = 1 ) //go:generate mockgen -destination=../mocks/mock_compatibility_service.go -package=mocks edge-infra.dev/pkg/edge/api/services CompatibilityService type CompatibilityService interface { GetArtifactVersionCompatibility(ctx context.Context, artifact model.ArtifactVersion, compatibleArtifactName *string) (*model.ArtifactCompatibility, error) IsCompatible(ctx context.Context, primaryArtifact, secondaryArtifact model.ArtifactVersion) (bool, error) AddArtifactCompatibility(ctx context.Context, artifactCompatibility model.ArtifactCompatibilityPayload) (added bool, err error) DeleteArtifactCompatibility(ctx context.Context, artifactCompatibility model.ArtifactCompatibilityPayload) (deleted bool, err error) } type compatibilityService struct { SQLDB *sql.DB } func NewCompatibilityService(sqlDB *sql.DB) *compatibilityService { // nolint can't return the CompatibilityService interface type or mocks will break return &compatibilityService{ SQLDB: sqlDB, } } // GetArtifactVersionCompatibility returns the compatible artifacts and their versions for a particular artifact and version func (s *compatibilityService) GetArtifactVersionCompatibility(ctx context.Context, artifact model.ArtifactVersion, compatibleArtifactName *string) (*model.ArtifactCompatibility, error) { var rows *sql.Rows var err error artifactCompatibility := model.ArtifactCompatibility{Artifact: &artifact} if compatibleArtifactName != nil { rows, err = s.SQLDB.QueryContext(ctx, sqlquery.GetArtifactVersionsAndCompatibilityForArtifactName, artifact.Name, artifact.Version, compatibleArtifactName) if err != nil { return nil, err } } else { rows, err = s.SQLDB.QueryContext(ctx, sqlquery.GetArtifactVersionsAndCompatibility, artifact.Name, artifact.Version) if err != nil { return nil, err } } for rows.Next() { var aName, aVersion, compatibleArtifactName, compatibleArtifactVersion string var artifactNthIndex int if err := rows.Scan(&aName, &aVersion, &artifactNthIndex, &compatibleArtifactName, &compatibleArtifactVersion); err != nil { return nil, err } artifactCompatibility.CompatibleArtifacts = append(artifactCompatibility.CompatibleArtifacts, &model.ArtifactVersion{Name: compatibleArtifactName, Version: compatibleArtifactVersion}) if artifactCompatibility.NthIndex == 0 { artifactCompatibility.NthIndex = artifactNthIndex } } if err := rows.Err(); err != nil { return nil, sqlerr.Wrap(err) } return &artifactCompatibility, nil } // IsCompatible checks if an artifact is compatible with another or bwc func (s *compatibilityService) IsCompatible(ctx context.Context, primaryArtifact, secondaryArtifact model.ArtifactVersion) (bool, error) { compatibleArtifacts, err := s.GetArtifactVersionCompatibility(ctx, primaryArtifact, nil) if err != nil { return false, err } if compatibleArtifacts == nil { return false, nil } if primaryArtifact.Name == secondaryArtifact.Name { primaryArtifactParts := strings.Split(primaryArtifact.Version, ".") primaryArtifactMinorVersion, err := strconv.Atoi(primaryArtifactParts[1]) if err != nil { return false, err } secondaryArtifactParts := strings.Split(secondaryArtifact.Version, ".") secondaryArtifactMinorVersion, err := strconv.Atoi(secondaryArtifactParts[1]) if err != nil { return false, err } return primaryArtifactMinorVersion-secondaryArtifactMinorVersion <= compatibleArtifacts.NthIndex, nil } compatible := slices.ContainsFunc(compatibleArtifacts.CompatibleArtifacts, func(artifact *model.ArtifactVersion) bool { return artifact.Name == secondaryArtifact.Name && artifact.Version == secondaryArtifact.Version }) return compatible, nil } // AddArtifactCompatibility adds a compatible artifact and version and self backwards compabitibility for a given artifact and version func (s *compatibilityService) AddArtifactCompatibility(ctx context.Context, artifactCompatibility model.ArtifactCompatibilityPayload) (added bool, err error) { // first checks if artifact version exists exists, err := s.validateArtifactExists(ctx, artifactCompatibility.Artifact.Name, artifactCompatibility.Artifact.Version) if err != nil { return false, err } if !exists { return false, fmt.Errorf("artifact name or version does not exist") } // backwards compatibility assigned to default to if not explicitly set if artifactCompatibility.NthIndex == nil { artifactCompatibility.NthIndex = &defaultBWC } tx, err := s.SQLDB.BeginTx(ctx, nil) if err != nil { return false, err } defer func() { if err != nil { err = errors.Join(err, tx.Rollback()) } }() for i := range artifactCompatibility.CompatibleArtifacts { if _, err := tx.ExecContext(ctx, sqlquery.AddArtifactCompatibility, artifactCompatibility.Artifact.Name, artifactCompatibility.Artifact.Version, artifactCompatibility.NthIndex, artifactCompatibility.CompatibleArtifacts[i].Name, artifactCompatibility.CompatibleArtifacts[i].Version); err != nil { return false, err } } if err := tx.Commit(); err != nil { return false, fmt.Errorf("failed to commit transaction. err: %v", err) } return true, nil } // DeleteArtifactCompatibility deletes a compatible artifact and version from a given artifact func (s *compatibilityService) DeleteArtifactCompatibility(ctx context.Context, artifactCompatibility model.ArtifactCompatibilityPayload) (deleted bool, err error) { // first checks if artifact version exists exists, err := s.validateArtifactExists(ctx, artifactCompatibility.Artifact.Name, artifactCompatibility.Artifact.Version) if err != nil { return false, err } if !exists { return false, fmt.Errorf("artifact name or version does not exist") } tx, err := s.SQLDB.BeginTx(ctx, nil) if err != nil { return false, err } defer func() { if err != nil { err = errors.Join(err, tx.Rollback()) } }() for i := range artifactCompatibility.CompatibleArtifacts { if _, err := tx.ExecContext(ctx, sqlquery.DeleteArtifactCompatibility, artifactCompatibility.Artifact.Name, artifactCompatibility.Artifact.Version, artifactCompatibility.CompatibleArtifacts[i].Name, artifactCompatibility.CompatibleArtifacts[i].Version); err != nil { return false, err } } if err := tx.Commit(); err != nil { return false, fmt.Errorf("failed to commit transaction. err: %v", err) } return true, nil } func (s *compatibilityService) validateArtifactExists(ctx context.Context, artifactName, artifactVersion string) (bool, error) { // first checks if artifact version exists row := s.SQLDB.QueryRowContext(ctx, sqlquery.CheckArtifactVersionExists, artifactName, artifactVersion) var exists bool err := row.Scan(&exists) if err != nil { return false, err } if !exists { return false, nil } return true, nil }