github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/platform/model/inventory.go (about) 1 package model 2 3 import ( 4 "fmt" 5 "regexp" 6 "runtime" 7 "sort" 8 "strconv" 9 "strings" 10 "time" 11 12 "github.com/ActiveState/cli/internal/logging" 13 "github.com/ActiveState/cli/internal/rtutils/ptr" 14 "github.com/go-openapi/strfmt" 15 16 "github.com/ActiveState/cli/internal/constants" 17 "github.com/ActiveState/cli/internal/errs" 18 "github.com/ActiveState/cli/internal/locale" 19 configMediator "github.com/ActiveState/cli/internal/mediators/config" 20 "github.com/ActiveState/cli/pkg/platform/api" 21 hsInventory "github.com/ActiveState/cli/pkg/platform/api/hasura_inventory" 22 hsInventoryModel "github.com/ActiveState/cli/pkg/platform/api/hasura_inventory/model" 23 hsInventoryRequest "github.com/ActiveState/cli/pkg/platform/api/hasura_inventory/request" 24 "github.com/ActiveState/cli/pkg/platform/api/inventory" 25 "github.com/ActiveState/cli/pkg/platform/api/inventory/inventory_client/inventory_operations" 26 "github.com/ActiveState/cli/pkg/platform/api/inventory/inventory_models" 27 "github.com/ActiveState/cli/pkg/platform/authentication" 28 "github.com/ActiveState/cli/pkg/sysinfo" 29 ) 30 31 func init() { 32 configMediator.RegisterOption(constants.PreferredGlibcVersionConfig, configMediator.String, "") 33 } 34 35 type Configurable interface { 36 GetString(key string) string 37 } 38 39 type ErrNoMatchingPlatform struct { 40 HostPlatform string 41 HostArch string 42 LibcVersion string 43 } 44 45 func (e ErrNoMatchingPlatform) Error() string { 46 return "no matching platform" 47 } 48 49 type ErrSearch404 struct{ *locale.LocalizedError } 50 51 // IngredientAndVersion is a sane version of whatever the hell it is go-swagger thinks it's doing 52 type IngredientAndVersion struct { 53 *inventory_models.SearchIngredientsResponseItem 54 Version string 55 } 56 57 // Platform is a sane version of whatever the hell it is go-swagger thinks it's doing 58 type Platform = inventory_models.Platform 59 60 // Authors is a collection of inventory Author data. 61 type Authors []*inventory_models.Author 62 63 var platformCache []*Platform 64 65 func GetIngredientByNameAndVersion(namespace string, name string, version string, ts *time.Time, auth *authentication.Auth) (*inventory_models.FullIngredientVersion, error) { 66 client := inventory.Get(auth) 67 68 params := inventory_operations.NewGetNamespaceIngredientVersionParams() 69 params.SetNamespace(namespace) 70 params.SetName(name) 71 params.SetVersion(version) 72 73 if ts != nil { 74 params.SetStateAt(ptr.To(strfmt.DateTime(*ts))) 75 } 76 params.SetHTTPClient(api.NewHTTPClient()) 77 78 response, err := client.GetNamespaceIngredientVersion(params, auth.ClientAuth()) 79 if err != nil { 80 return nil, errs.Wrap(err, "GetNamespaceIngredientVersion failed") 81 } 82 83 return response.Payload, nil 84 } 85 86 // SearchIngredients will return all ingredients+ingredientVersions that fuzzily 87 // match the ingredient name. 88 func SearchIngredients(namespace string, name string, includeVersions bool, ts *time.Time, auth *authentication.Auth) ([]*IngredientAndVersion, error) { 89 return searchIngredientsNamespace(namespace, name, includeVersions, false, ts, auth) 90 } 91 92 // SearchIngredientsStrict will return all ingredients+ingredientVersions that 93 // strictly match the ingredient name. 94 func SearchIngredientsStrict(namespace string, name string, caseSensitive bool, includeVersions bool, ts *time.Time, auth *authentication.Auth) ([]*IngredientAndVersion, error) { 95 results, err := searchIngredientsNamespace(namespace, name, includeVersions, true, ts, auth) 96 if err != nil { 97 return nil, err 98 } 99 100 if !caseSensitive { 101 name = strings.ToLower(name) 102 } 103 104 ingredients := results[:0] 105 for _, ing := range results { 106 var ingName string 107 if ing.Ingredient.Name != nil { 108 ingName = *ing.Ingredient.Name 109 } 110 if !caseSensitive { 111 ingName = strings.ToLower(ingName) 112 } 113 if ingName == name { 114 ingredients = append(ingredients, ing) 115 } 116 } 117 118 return ingredients, nil 119 } 120 121 // SearchIngredientsLatest will return all ingredients+ingredientVersions that 122 // fuzzily match the ingredient name, but only the latest version of each 123 // ingredient. 124 func SearchIngredientsLatest(namespace string, name string, includeVersions bool, ts *time.Time, auth *authentication.Auth) ([]*IngredientAndVersion, error) { 125 results, err := searchIngredientsNamespace(namespace, name, includeVersions, false, ts, auth) 126 if err != nil { 127 return nil, err 128 } 129 130 return processLatestIngredients(results), nil 131 } 132 133 // SearchIngredientsLatestStrict will return all ingredients+ingredientVersions that 134 // strictly match the ingredient name, but only the latest version of each 135 // ingredient. 136 func SearchIngredientsLatestStrict(namespace string, name string, caseSensitive bool, includeVersions bool, ts *time.Time, auth *authentication.Auth) ([]*IngredientAndVersion, error) { 137 results, err := SearchIngredientsStrict(namespace, name, caseSensitive, includeVersions, ts, auth) 138 if err != nil { 139 return nil, err 140 } 141 142 return processLatestIngredients(results), nil 143 } 144 145 func processLatestIngredients(ingredients []*IngredientAndVersion) []*IngredientAndVersion { 146 seen := make(map[string]bool) 147 var processedIngredients []*IngredientAndVersion 148 for _, ing := range ingredients { 149 if ing.Ingredient.Name == nil { 150 continue 151 } 152 if seen[*ing.Ingredient.Name] { 153 continue 154 } 155 processedIngredients = append(processedIngredients, ing) 156 seen[*ing.Ingredient.Name] = true 157 } 158 return processedIngredients 159 } 160 161 // FetchAuthors obtains author info for an ingredient at a particular version. 162 func FetchAuthors(ingredID, ingredVersionID *strfmt.UUID, auth *authentication.Auth) (Authors, error) { 163 if ingredID == nil { 164 return nil, errs.New("nil ingredient id provided") 165 } 166 if ingredVersionID == nil { 167 return nil, errs.New("nil ingredient version id provided") 168 } 169 170 lim := int64(32) 171 client := inventory.Get(auth) 172 173 params := inventory_operations.NewGetIngredientVersionAuthorsParams() 174 params.SetIngredientID(*ingredID) 175 params.SetIngredientVersionID(*ingredVersionID) 176 params.SetLimit(&lim) 177 params.SetHTTPClient(api.NewHTTPClient()) 178 179 results, err := client.GetIngredientVersionAuthors(params, auth.ClientAuth()) 180 if err != nil { 181 return nil, errs.Wrap(err, "GetIngredientVersionAuthors failed") 182 } 183 184 return results.Payload.Authors, nil 185 } 186 187 type ErrTooManyMatches struct { 188 *locale.LocalizedError 189 Query string 190 } 191 192 func searchIngredientsNamespace(ns string, name string, includeVersions bool, exactOnly bool, ts *time.Time, auth *authentication.Auth) ([]*IngredientAndVersion, error) { 193 limit := int64(100) 194 offset := int64(0) 195 196 client := inventory.Get(auth) 197 198 params := inventory_operations.NewSearchIngredientsParams() 199 params.SetQ(&name) 200 if exactOnly { 201 params.SetExactOnly(&exactOnly) 202 } 203 if ns != "" { 204 params.SetNamespaces(&ns) 205 } 206 params.SetLimit(&limit) 207 params.SetHTTPClient(api.NewHTTPClient()) 208 209 if ts != nil { 210 dt := strfmt.DateTime(*ts) 211 params.SetStateAt(&dt) 212 } 213 214 var ingredients []*IngredientAndVersion 215 var entries []*inventory_models.SearchIngredientsResponseItem 216 for offset == 0 || len(entries) == int(limit) { 217 if offset > (limit * 10) { // at most we will get 10 pages of ingredients (that's ONE THOUSAND ingredients) 218 // Guard against queries that match TOO MANY ingredients 219 return nil, &ErrTooManyMatches{locale.NewInputError("err_searchingredient_toomany", "", name), name} 220 } 221 222 params.SetOffset(&offset) 223 results, err := client.SearchIngredients(params, auth.ClientAuth()) 224 if err != nil { 225 if sidErr, ok := err.(*inventory_operations.SearchIngredientsDefault); ok { 226 errv := locale.NewError(*sidErr.Payload.Message) 227 if sidErr.Code() == 404 { 228 return nil, &ErrSearch404{errv} 229 } 230 return nil, errv 231 } 232 return nil, errs.Wrap(err, "SearchIngredients failed") 233 } 234 entries = results.Payload.Ingredients 235 236 for _, res := range entries { 237 if res.Ingredient.PrimaryNamespace == nil { 238 continue // Shouldn't ever happen, but this at least guards around nil pointer panics 239 } 240 if includeVersions { 241 for _, v := range res.Versions { 242 ingredients = append(ingredients, &IngredientAndVersion{res, v.Version}) 243 } 244 } else { 245 ingredients = append(ingredients, &IngredientAndVersion{res, ""}) 246 } 247 } 248 249 offset += limit 250 } 251 252 return ingredients, nil 253 } 254 255 func FetchPlatforms() ([]*Platform, error) { 256 if platformCache == nil { 257 client := inventory.Get(nil) 258 259 params := inventory_operations.NewGetPlatformsParams() 260 limit := int64(99999) 261 params.SetLimit(&limit) 262 params.SetHTTPClient(api.NewHTTPClient()) 263 264 response, err := client.GetPlatforms(params) 265 if err != nil { 266 return nil, errs.Wrap(err, "GetPlatforms failed") 267 } 268 269 // remove unwanted platforms 270 var platforms []*Platform 271 for _, p := range response.Payload.Platforms { 272 if p.KernelVersion == nil || p.KernelVersion.Version == nil { 273 continue 274 } 275 version := *p.KernelVersion.Version 276 if version == "" || version == "0" { 277 continue 278 } 279 platforms = append(platforms, p) 280 } 281 282 platformCache = platforms 283 } 284 285 return platformCache, nil 286 } 287 288 func FetchPlatformsMap() (map[strfmt.UUID]*Platform, error) { 289 platforms, err := FetchPlatforms() 290 if err != nil { 291 return nil, err 292 } 293 294 platformMap := make(map[strfmt.UUID]*Platform) 295 for _, p := range platforms { 296 platformMap[*p.PlatformID] = p 297 } 298 return platformMap, nil 299 } 300 301 func FetchPlatformsForCommit(commitID strfmt.UUID, auth *authentication.Auth) ([]*Platform, error) { 302 checkpt, _, err := FetchCheckpointForCommit(commitID, auth) 303 if err != nil { 304 return nil, err 305 } 306 307 platformIDs := CheckpointToPlatforms(checkpt) 308 309 var platforms []*Platform 310 for _, pID := range platformIDs { 311 platform, err := FetchPlatformByUID(pID) 312 if err != nil { 313 return nil, err 314 } 315 316 platforms = append(platforms, platform) 317 } 318 319 return platforms, nil 320 } 321 322 func FilterPlatformIDs(hostPlatform, hostArch string, platformIDs []strfmt.UUID, cfg Configurable) ([]strfmt.UUID, error) { 323 runtimePlatforms, err := FetchPlatforms() 324 if err != nil { 325 return nil, err 326 } 327 328 libcVersion, err := fetchLibcVersion(cfg) 329 if err != nil { 330 return nil, errs.Wrap(err, "failed to fetch libc version") 331 } 332 333 var pids []strfmt.UUID 334 var fallback []strfmt.UUID 335 libcMap := make(map[strfmt.UUID]float64) 336 for _, platformID := range platformIDs { 337 for _, rtPf := range runtimePlatforms { 338 if rtPf.PlatformID == nil || platformID != *rtPf.PlatformID { 339 continue 340 } 341 if rtPf.Kernel == nil || rtPf.Kernel.Name == nil { 342 continue 343 } 344 if rtPf.CPUArchitecture == nil || rtPf.CPUArchitecture.Name == nil { 345 continue 346 } 347 if *rtPf.Kernel.Name != HostPlatformToKernelName(hostPlatform) { 348 continue 349 } 350 351 if rtPf.LibcVersion != nil && rtPf.LibcVersion.Version != nil { 352 if libcVersion != "" && libcVersion != *rtPf.LibcVersion.Version { 353 continue 354 } 355 // Convert the libc version to a major-minor float and map it to the platform ID for 356 // subsequent comparisons. 357 regex := regexp.MustCompile(`^\d+\D\d+`) 358 versionString := regex.FindString(*rtPf.LibcVersion.Version) 359 if versionString == "" { 360 return nil, errs.New("Unable to parse libc string '%s'", *rtPf.LibcVersion.Version) 361 } 362 version, err := strconv.ParseFloat(versionString, 32) 363 if err != nil { 364 return nil, errs.Wrap(err, "libc version is not a number: %s", versionString) 365 } 366 libcMap[platformID] = version 367 } 368 369 platformArch := platformArchToHostArch( 370 *rtPf.CPUArchitecture.Name, 371 *rtPf.CPUArchitecture.BitWidth, 372 ) 373 if fallbackArch(hostPlatform, hostArch) == platformArch { 374 fallback = append(fallback, platformID) 375 } 376 if hostArch != platformArch { 377 continue 378 } 379 380 pids = append(pids, platformID) 381 break 382 } 383 } 384 385 if len(pids) == 0 && len(fallback) == 0 { 386 return nil, &ErrNoMatchingPlatform{hostPlatform, hostArch, libcVersion} 387 } else if len(pids) == 0 { 388 pids = fallback 389 } 390 391 if runtime.GOOS == "linux" { 392 // Sort platforms by closest matching libc version. 393 // Note: for macOS, the Platform gives a libc version based on libSystem, while sysinfo.Libc() 394 // returns the clang version, which is something different altogether. At this time, the pid 395 // list to return contains only one Platform, so sorting is not an issue and unnecessary. 396 // When it does become necessary, DX-2780 will address this. 397 // Note: the Platform does not specify libc on Windows, so this sorting is not applicable on 398 // Windows. 399 libc, err := sysinfo.Libc() 400 if err != nil { 401 return nil, errs.Wrap(err, "Unable to get system libc") 402 } 403 localLibc, err := strconv.ParseFloat(libc.Version(), 32) 404 if err != nil { 405 return nil, errs.Wrap(err, "Libc version is not a number: %s", libc.Version()) 406 } 407 sort.SliceStable(pids, func(i, j int) bool { 408 libcI, existsI := libcMap[pids[i]] 409 libcJ, existsJ := libcMap[pids[j]] 410 less := false 411 switch { 412 case !existsI || !existsJ: 413 break 414 case localLibc >= libcI && localLibc >= libcJ: 415 // If both platform libc versions are less than to the local libc version, prefer the 416 // greater of the two. 417 less = libcI > libcJ 418 case localLibc < libcI && localLibc < libcJ: 419 // If both platform libc versions are greater than the local libc version, prefer the lesser 420 // of the two. 421 less = libcI < libcJ 422 case localLibc >= libcI && localLibc < libcJ: 423 // If only one of the platform libc versions is greater than local libc version, prefer the 424 // other one. 425 less = true 426 case localLibc < libcI && localLibc >= libcJ: 427 // If only one of the platform libc versions is greater than local libc version, prefer the 428 // other one. 429 less = false 430 } 431 return less 432 }) 433 } 434 435 return pids, nil 436 } 437 438 func fetchLibcVersion(cfg Configurable) (string, error) { 439 if runtime.GOOS != "linux" { 440 return "", nil 441 } 442 443 return cfg.GetString(constants.PreferredGlibcVersionConfig), nil 444 } 445 446 func FetchPlatformByUID(uid strfmt.UUID) (*Platform, error) { 447 platforms, err := FetchPlatforms() 448 if err != nil { 449 return nil, err 450 } 451 452 for _, platform := range platforms { 453 if platform.PlatformID != nil && *platform.PlatformID == uid { 454 return platform, nil 455 } 456 } 457 458 return nil, nil 459 } 460 461 func FetchPlatformByDetails(name, version string, word int, auth *authentication.Auth) (*Platform, error) { 462 runtimePlatforms, err := FetchPlatforms() 463 if err != nil { 464 return nil, err 465 } 466 467 lower := strings.ToLower 468 469 for _, rtPf := range runtimePlatforms { 470 if rtPf.Kernel == nil || rtPf.Kernel.Name == nil { 471 continue 472 } 473 if lower(*rtPf.Kernel.Name) != lower(name) { 474 continue 475 } 476 477 if rtPf.KernelVersion == nil || rtPf.KernelVersion.Version == nil { 478 continue 479 } 480 if lower(*rtPf.KernelVersion.Version) != lower(version) { 481 continue 482 } 483 484 if rtPf.CPUArchitecture == nil { 485 continue 486 } 487 if rtPf.CPUArchitecture.BitWidth == nil || *rtPf.CPUArchitecture.BitWidth != strconv.Itoa(word) { 488 continue 489 } 490 491 return rtPf, nil 492 } 493 494 details := fmt.Sprintf("%s %d %s", name, word, version) 495 496 return nil, locale.NewExternalError("err_unsupported_platform", "", details) 497 } 498 499 func FetchLanguageForCommit(commitID strfmt.UUID, auth *authentication.Auth) (*Language, error) { 500 langs, err := FetchLanguagesForCommit(commitID, auth) 501 if err != nil { 502 return nil, locale.WrapError(err, "err_detect_language") 503 } 504 if len(langs) == 0 { 505 return nil, locale.NewError("err_detect_language") 506 } 507 return &langs[0], nil 508 } 509 510 func FetchLanguageByDetails(name, version string, auth *authentication.Auth) (*Language, error) { 511 languages, err := FetchLanguages(auth) 512 if err != nil { 513 return nil, err 514 } 515 516 for _, language := range languages { 517 if language.Name == name && language.Version == version { 518 return &language, nil 519 } 520 } 521 522 return nil, locale.NewInputError("err_language_not_found", "", name, version) 523 } 524 525 func FetchLanguageVersions(name string, auth *authentication.Auth) ([]string, error) { 526 languages, err := FetchLanguages(auth) 527 if err != nil { 528 return nil, err 529 } 530 531 var versions []string 532 for _, lang := range languages { 533 if lang.Name == name { 534 versions = append(versions, lang.Version) 535 } 536 } 537 538 return versions, nil 539 } 540 541 func FetchLanguages(auth *authentication.Auth) ([]Language, error) { 542 client := inventory.Get(auth) 543 544 params := inventory_operations.NewGetNamespaceIngredientsParams() 545 params.SetNamespace("language") 546 limit := int64(10000) 547 params.SetLimit(&limit) 548 params.SetHTTPClient(api.NewHTTPClient()) 549 550 res, err := client.GetNamespaceIngredients(params, auth.ClientAuth()) 551 if err != nil { 552 return nil, errs.Wrap(err, "GetNamespaceIngredients failed") 553 } 554 555 var languages []Language 556 for _, ting := range res.Payload.IngredientsAndVersions { 557 languages = append(languages, Language{ 558 Name: *ting.Ingredient.Name, 559 Version: *ting.Version.Version, 560 }) 561 } 562 563 return languages, nil 564 } 565 566 func FetchIngredient(ingredientID *strfmt.UUID, auth *authentication.Auth) (*inventory_models.Ingredient, error) { 567 client := inventory.Get(auth) 568 569 params := inventory_operations.NewGetIngredientParams() 570 params.SetIngredientID(*ingredientID) 571 params.SetHTTPClient(api.NewHTTPClient()) 572 573 res, err := client.GetIngredient(params, auth.ClientAuth()) 574 if err != nil { 575 return nil, errs.Wrap(err, "GetIngredient failed") 576 } 577 578 return res.Payload, nil 579 } 580 581 func FetchIngredientVersion(ingredientID *strfmt.UUID, versionID *strfmt.UUID, allowUnstable bool, atTime *strfmt.DateTime, auth *authentication.Auth) (*inventory_models.FullIngredientVersion, error) { 582 client := inventory.Get(auth) 583 584 params := inventory_operations.NewGetIngredientVersionParams() 585 params.SetIngredientID(*ingredientID) 586 params.SetIngredientVersionID(*versionID) 587 params.SetAllowUnstable(&allowUnstable) 588 params.SetStateAt(atTime) 589 params.SetHTTPClient(api.NewHTTPClient()) 590 591 res, err := client.GetIngredientVersion(params, auth.ClientAuth()) 592 if err != nil { 593 return nil, errs.Wrap(err, "GetIngredientVersion failed") 594 } 595 596 return res.Payload, nil 597 } 598 599 func FetchIngredientVersions(ingredientID *strfmt.UUID, auth *authentication.Auth) ([]*inventory_models.IngredientVersion, error) { 600 client := inventory.Get(auth) 601 602 params := inventory_operations.NewGetIngredientVersionsParams() 603 params.SetIngredientID(*ingredientID) 604 limit := int64(10000) 605 params.SetLimit(&limit) 606 params.SetHTTPClient(api.NewHTTPClient()) 607 608 res, err := client.GetIngredientVersions(params, auth.ClientAuth()) 609 if err != nil { 610 return nil, errs.Wrap(err, "GetIngredientVersions failed") 611 } 612 613 return res.Payload.IngredientVersions, nil 614 } 615 616 // FetchLatestTimeStamp fetches the latest timestamp from the inventory service. 617 // This is not the same as FetchLatestRevisionTimeStamp. 618 func FetchLatestTimeStamp(auth *authentication.Auth) (time.Time, error) { 619 client := inventory.Get(auth) 620 result, err := client.GetLatestTimestamp(inventory_operations.NewGetLatestTimestampParams()) 621 if err != nil { 622 return time.Now(), errs.Wrap(err, "GetLatestTimestamp failed") 623 } 624 625 return time.Time(*result.Payload.Timestamp), nil 626 } 627 628 // FetchLatestRevisionTimeStamp fetches the time of the last inventory change from the Hasura 629 // inventory service. 630 // This is not the same as FetchLatestTimeStamp. 631 func FetchLatestRevisionTimeStamp(auth *authentication.Auth) (time.Time, error) { 632 client := hsInventory.New(auth) 633 request := hsInventoryRequest.NewLatestRevision() 634 response := hsInventoryModel.LatestRevisionResponse{} 635 err := client.Run(request, &response) 636 if err != nil { 637 return time.Now(), errs.Wrap(err, "Failed to get latest change time") 638 } 639 640 // Increment time by 1 second to work around API precision issue where same second comparisons can fall on either side 641 t := time.Time(response.RevisionTimes[0].RevisionTime) 642 t = t.Add(time.Second) 643 644 return t, nil 645 } 646 647 func FetchNormalizedName(namespace Namespace, name string, auth *authentication.Auth) (string, error) { 648 client := inventory.Get(auth) 649 params := inventory_operations.NewNormalizeNamesParams() 650 params.SetNamespace(namespace.String()) 651 params.SetNames(&inventory_models.UnnormalizedNames{Names: []string{name}}) 652 params.SetHTTPClient(api.NewHTTPClient()) 653 res, err := client.NormalizeNames(params, auth.ClientAuth()) 654 if err != nil { 655 return "", errs.Wrap(err, "NormalizeName failed") 656 } 657 if len(res.Payload.NormalizedNames) == 0 { 658 return "", errs.New("Normalized name for %s not found", name) 659 } 660 return *res.Payload.NormalizedNames[0].Normalized, nil 661 } 662 663 func FilterCurrentPlatform(hostPlatform string, platforms []strfmt.UUID, cfg Configurable) (strfmt.UUID, error) { 664 platformIDs, err := FilterPlatformIDs(hostPlatform, runtime.GOARCH, platforms, cfg) 665 if err != nil { 666 return "", errs.Wrap(err, "filterPlatformIDs failed") 667 } 668 669 if len(platformIDs) == 0 { 670 return "", locale.NewInputError("err_recipe_no_platform") 671 } else if len(platformIDs) > 1 { 672 logging.Debug("Received multiple platform IDs. Picking the first one: %s", platformIDs[0]) 673 } 674 675 return platformIDs[0], nil 676 }