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  }