mirror of
https://github.com/redhat-developer/odo.git
synced 2025-10-19 03:06:19 +03:00
* Add --architecture flag * Ask architecture during interactive mode * Display architectures of detected Devfile * Fix integration tests * Fix automated doc * Fix e2e tests * Ignore empty lines on doc automation tests * Update pkg/odo/cli/registry/registry.go Co-authored-by: Armel Soro <armel@rm3l.org> * Fix Architectures field in API * Change "select architectures" prompt --------- Co-authored-by: Armel Soro <armel@rm3l.org>
480 lines
18 KiB
Go
480 lines
18 KiB
Go
package registry
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/fs"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/blang/semver"
|
|
devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
|
|
apidevfile "github.com/devfile/api/v2/pkg/devfile"
|
|
dfutil "github.com/devfile/library/v2/pkg/util"
|
|
indexSchema "github.com/devfile/registry-support/index/generator/schema"
|
|
"github.com/devfile/registry-support/registry-library/library"
|
|
"k8s.io/klog"
|
|
|
|
"github.com/redhat-developer/odo/pkg/api"
|
|
"github.com/redhat-developer/odo/pkg/devfile"
|
|
"github.com/redhat-developer/odo/pkg/devfile/location"
|
|
"github.com/redhat-developer/odo/pkg/kclient"
|
|
"github.com/redhat-developer/odo/pkg/log"
|
|
"github.com/redhat-developer/odo/pkg/preference"
|
|
"github.com/redhat-developer/odo/pkg/segment"
|
|
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
|
|
"github.com/redhat-developer/odo/pkg/util"
|
|
)
|
|
|
|
type RegistryClient struct {
|
|
fsys filesystem.Filesystem
|
|
preferenceClient preference.Client
|
|
kubeClient kclient.ClientInterface
|
|
}
|
|
|
|
var _ Client = (*RegistryClient)(nil)
|
|
|
|
const (
|
|
CONFLICT_DIR_NAME = "CONFLICT_STARTER_PROJECT"
|
|
)
|
|
|
|
func NewRegistryClient(fsys filesystem.Filesystem, preferenceClient preference.Client, kubeClient kclient.ClientInterface) RegistryClient {
|
|
return RegistryClient{
|
|
fsys: fsys,
|
|
preferenceClient: preferenceClient,
|
|
kubeClient: kubeClient,
|
|
}
|
|
}
|
|
|
|
// PullStackFromRegistry pulls stack from registry with all stack resources (all media types) to the destination directory
|
|
func (o RegistryClient) PullStackFromRegistry(registry string, stack string, destDir string, options library.RegistryOptions) error {
|
|
klog.V(3).Infof("sending telemetry data: %#v", options.Telemetry)
|
|
return library.PullStackFromRegistry(registry, stack, destDir, options)
|
|
}
|
|
|
|
// DownloadFileInMemory uses the url to download the file and return bytes
|
|
func (o RegistryClient) DownloadFileInMemory(params dfutil.HTTPRequestParams) ([]byte, error) {
|
|
return util.DownloadFileInMemory(params)
|
|
}
|
|
|
|
// DownloadStarterProject downloads a starter project referenced in devfile
|
|
// There are 3 cases to consider here:
|
|
// Case 1: If there is devfile in the starterproject, replace all the contents of contextDir with that of the starterproject; warn about this
|
|
// Case 2: If there is no devfile, and there is no conflict between the contents of contextDir and starterproject, then copy the contents of the starterproject into contextDir.
|
|
// Case 3: If there is no devfile, and there is conflict between the contents of contextDir and starterproject, copy contents of starterproject into a dir named CONFLICT_STARTER_PROJECT; warn about this
|
|
func (o RegistryClient) DownloadStarterProject(starterProject *devfilev1.StarterProject, decryptedToken string, contextDir string, verbose bool) (containsDevfile bool, err error) {
|
|
// Let the project be downloaded in a temp directory
|
|
starterProjectTmpDir, err := o.fsys.TempDir("", "odostarterproject")
|
|
if err != nil {
|
|
return containsDevfile, err
|
|
}
|
|
defer func() {
|
|
err = o.fsys.RemoveAll(starterProjectTmpDir)
|
|
if err != nil {
|
|
klog.V(2).Infof("failed to delete temporary starter project dir %s; cause: %s", starterProjectTmpDir, err.Error())
|
|
}
|
|
}()
|
|
err = DownloadStarterProject(o.fsys, starterProject, decryptedToken, starterProjectTmpDir, verbose)
|
|
if err != nil {
|
|
return containsDevfile, err
|
|
}
|
|
|
|
// Case 1: If there is devfile in the starterproject, replace all the contents of contextDir with that of the starterproject; warn about this
|
|
if containsDevfile, err = location.DirectoryContainsDevfile(o.fsys, starterProjectTmpDir); err != nil {
|
|
return containsDevfile, err
|
|
}
|
|
if containsDevfile {
|
|
fmt.Println()
|
|
log.Warning("A Devfile is present inside the starter project; replacing the entire content of the current directory with the starter project")
|
|
err = removeDirectoryContents(contextDir, o.fsys)
|
|
if err != nil {
|
|
return containsDevfile, fmt.Errorf("failed to delete contents of the current directory; cause %w", err)
|
|
}
|
|
return containsDevfile, util.CopyDirWithFS(starterProjectTmpDir, contextDir, o.fsys)
|
|
}
|
|
|
|
// Case 2: If there is no devfile, and there is no conflict between the contents of contextDir and starterproject, then copy the contents of the starterproject into contextDir.
|
|
// Case 3: If there is no devfile, and there is conflict between the contents of contextDir and starterproject, copy contents of starterproject into a dir named CONFLICT_STARTER_PROJECT; warn about this
|
|
var conflictingFiles []string
|
|
conflictingFiles, err = getConflictingFiles(starterProjectTmpDir, contextDir, o.fsys)
|
|
if err != nil {
|
|
return containsDevfile, err
|
|
}
|
|
|
|
// Case 2
|
|
if len(conflictingFiles) == 0 {
|
|
return containsDevfile, util.CopyDirWithFS(starterProjectTmpDir, contextDir, o.fsys)
|
|
}
|
|
|
|
// Case 3
|
|
conflictingDirPath := filepath.Join(contextDir, CONFLICT_DIR_NAME)
|
|
err = o.fsys.MkdirAll(conflictingDirPath, 0750)
|
|
if err != nil {
|
|
return containsDevfile, err
|
|
}
|
|
|
|
err = util.CopyDirWithFS(starterProjectTmpDir, conflictingDirPath, o.fsys)
|
|
if err != nil {
|
|
return containsDevfile, err
|
|
}
|
|
fmt.Println()
|
|
log.Warningf("There are conflicting files (%s) between starter project and the current directory, hence the starter project has been copied to %s", strings.Join(conflictingFiles, ", "), conflictingDirPath)
|
|
|
|
return containsDevfile, nil
|
|
}
|
|
|
|
// removeDirectoryContents attempts to remove dir contents without deleting the directory itself, unlike os.RemoveAll() method
|
|
func removeDirectoryContents(path string, fsys filesystem.Filesystem) error {
|
|
dir, err := fsys.ReadDir(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, f := range dir {
|
|
// a bit of cheating by using absolute file name to make sure this works with a fake filesystem, especially a memory map which is used by our unit tests
|
|
// memorymap's Name() method trims the full path and returns just the file name, which then becomes impossible to find by the RemoveAll method that looks for prefix
|
|
// See: https://github.com/redhat-developer/odo/blob/d717421494f746a5cb12da135f561d12750935f3/vendor/github.com/spf13/afero/memmap.go#L282
|
|
absFileName := filepath.Join(path, f.Name())
|
|
err = fsys.RemoveAll(absFileName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to remove %s; cause: %w", absFileName, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getConflictingFiles fetches the contents of the two directories in question and compares them to check for conflicting files.
|
|
// it returns a list of conflicting files (if any) along with an error (if any).
|
|
func getConflictingFiles(spDir, contextDir string, fsys filesystem.Filesystem) (conflictingFiles []string, err error) {
|
|
var (
|
|
contextDirMap = map[string]struct{}{}
|
|
)
|
|
// walk through the contextDir, trim the file path from the file name and append it to a map
|
|
err = fsys.Walk(contextDir, func(path string, info fs.FileInfo, err error) error {
|
|
if err != nil {
|
|
return fmt.Errorf("failed to fetch contents of dir %s; cause: %w", contextDirMap, err)
|
|
}
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
path = strings.TrimPrefix(path, contextDir)
|
|
contextDirMap[path] = struct{}{}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to walk %s dir; cause: %w", contextDir, err)
|
|
}
|
|
|
|
// walk through the starterproject dir, trim the file path from file name, and check if it exists in the contextDir map;
|
|
// if it does, it is a conflicting file, hence append it to the conflictingFiles list.
|
|
err = fsys.Walk(spDir, func(path string, info fs.FileInfo, err error) error {
|
|
if err != nil {
|
|
return fmt.Errorf("failed to fetch contents of dir %s; cause: %w", spDir, err)
|
|
}
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
path = strings.TrimPrefix(path, spDir)
|
|
if _, ok := contextDirMap[path]; ok {
|
|
conflictingFiles = append(conflictingFiles, path)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to walk %s dir; cause: %w", spDir, err)
|
|
}
|
|
|
|
return conflictingFiles, nil
|
|
}
|
|
|
|
// GetDevfileRegistries gets devfile registries from preference file,
|
|
// if registry name is specified return the specific registry, otherwise return all registries
|
|
func (o RegistryClient) GetDevfileRegistries(registryName string) ([]api.Registry, error) {
|
|
var allRegistries []api.Registry
|
|
|
|
if o.kubeClient != nil {
|
|
clusterRegistries, err := o.kubeClient.GetRegistryList()
|
|
if err != nil {
|
|
// #6636 : errors should not be blocking
|
|
klog.V(3).Infof("failed to get Devfile registries from the cluster: %v", err)
|
|
} else {
|
|
allRegistries = append(allRegistries, clusterRegistries...)
|
|
}
|
|
}
|
|
allRegistries = append(allRegistries, o.preferenceClient.RegistryList()...)
|
|
|
|
hasName := registryName != ""
|
|
var result []api.Registry
|
|
for _, registry := range allRegistries {
|
|
if hasName {
|
|
if registryName == registry.Name {
|
|
reg := api.Registry{
|
|
Name: registry.Name,
|
|
URL: registry.URL,
|
|
Secure: registry.Secure,
|
|
}
|
|
result = append(result, reg)
|
|
return result, nil
|
|
}
|
|
continue
|
|
}
|
|
reg := api.Registry{
|
|
Name: registry.Name,
|
|
URL: registry.URL,
|
|
Secure: registry.Secure,
|
|
}
|
|
result = append(result, reg)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// ListDevfileStacks lists all the available devfile stacks in devfile registry
|
|
// When `withDevfileContent` and `detailsFlag` are both true, another HTTP call is executed to download the Devfile
|
|
func (o RegistryClient) ListDevfileStacks(ctx context.Context, registryName, devfileFlag, filterFlag string, detailsFlag bool, withDevfileContent bool) (DevfileStackList, error) {
|
|
catalogDevfileList := &DevfileStackList{}
|
|
var err error
|
|
|
|
// TODO: consider caching registry information for better performance since it should be fairly stable over time
|
|
// Get devfile registries
|
|
catalogDevfileList.DevfileRegistries, err = o.GetDevfileRegistries(registryName)
|
|
if err != nil {
|
|
return *catalogDevfileList, err
|
|
}
|
|
if catalogDevfileList.DevfileRegistries == nil {
|
|
return *catalogDevfileList, nil
|
|
}
|
|
|
|
// first retrieve the indices for each registry, concurrently
|
|
devfileIndicesMutex := &sync.Mutex{}
|
|
retrieveRegistryIndices := util.NewConcurrentTasks(len(catalogDevfileList.DevfileRegistries))
|
|
|
|
// The 2D slice index is the priority of the registry (highest priority has highest index)
|
|
// and the element is the devfile slice that belongs to the registry
|
|
registrySlice := make([][]api.DevfileStack, len(catalogDevfileList.DevfileRegistries))
|
|
for regPriority, reg := range catalogDevfileList.DevfileRegistries {
|
|
// Load the devfile registry index.json
|
|
registry := reg // Needed to prevent the lambda from capturing the value
|
|
registryPriority := regPriority // Needed to prevent the lambda from capturing the value
|
|
retrieveRegistryIndices.Add(util.ConcurrentTask{ToRun: func(errChannel chan error) {
|
|
registryDevfiles, err := getRegistryStacks(ctx, registry)
|
|
if err != nil {
|
|
log.Warningf("Registry %s is not set up properly with error: %v, please check the registry URL, and credential and remove add the registry again (refer to `odo preference add registry --help`)\n", registry.Name, err)
|
|
return
|
|
}
|
|
|
|
devfileIndicesMutex.Lock()
|
|
registrySlice[registryPriority] = registryDevfiles
|
|
devfileIndicesMutex.Unlock()
|
|
}})
|
|
}
|
|
if err := retrieveRegistryIndices.Run(); err != nil {
|
|
return *catalogDevfileList, err
|
|
}
|
|
|
|
// Go through all the devfiles and filter based on:
|
|
// What's in the name or description
|
|
// The exact name of the devfile
|
|
for priorityNumber, registryDevfiles := range registrySlice {
|
|
|
|
devfiles := []api.DevfileStack{}
|
|
|
|
devfileLoop:
|
|
for _, devfile := range registryDevfiles {
|
|
|
|
// Add the "priority" of the registry to the devfile
|
|
devfile.Registry.Priority = priorityNumber
|
|
|
|
if filterFlag != "" {
|
|
filters := strings.Split(filterFlag, ",")
|
|
for _, filter := range filters {
|
|
filter = strings.TrimSpace(filter)
|
|
archs := append(make([]string, 0, len(devfile.Architectures)), devfile.Architectures...)
|
|
if len(archs) == 0 {
|
|
// Devfiles with no architectures are compatible with all architectures.
|
|
archs = append(archs,
|
|
string(apidevfile.AMD64),
|
|
string(apidevfile.ARM64),
|
|
string(apidevfile.PPC64LE),
|
|
string(apidevfile.S390X),
|
|
)
|
|
}
|
|
containsArch := func(s string) bool {
|
|
for _, arch := range archs {
|
|
if strings.Contains(arch, s) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
if !strings.Contains(devfile.Name, filter) && !strings.Contains(devfile.Description, filter) && !containsArch(filter) {
|
|
continue devfileLoop
|
|
}
|
|
}
|
|
}
|
|
|
|
if devfileFlag != "" {
|
|
if devfileFlag != devfile.Name {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// We are fetching the Devfile content only when `--details` and `-o json` flags are used
|
|
if detailsFlag && withDevfileContent {
|
|
devfileData, err := o.retrieveDevfileDataFromRegistry(ctx, devfile.Registry.Name, devfile.Name)
|
|
if err != nil {
|
|
return *catalogDevfileList, err
|
|
}
|
|
devfile.DevfileData = &devfileData
|
|
}
|
|
|
|
devfiles = append(devfiles, devfile)
|
|
}
|
|
|
|
catalogDevfileList.Items = append(catalogDevfileList.Items, devfiles...)
|
|
}
|
|
|
|
// Sort catalogDevfileList.Items by:
|
|
// 1. Priority of the registry (highest priority has highest index)
|
|
// 2. Name of the devfile
|
|
sort.Slice(catalogDevfileList.Items[:], func(i, j int) bool {
|
|
if catalogDevfileList.Items[i].Name == catalogDevfileList.Items[j].Name {
|
|
return catalogDevfileList.Items[i].Registry.Priority < catalogDevfileList.Items[j].Registry.Priority
|
|
}
|
|
return catalogDevfileList.Items[i].Name < catalogDevfileList.Items[j].Name
|
|
})
|
|
|
|
return *catalogDevfileList, nil
|
|
}
|
|
|
|
// getRegistryStacks retrieves the registry's index devfile stack entries
|
|
func getRegistryStacks(ctx context.Context, registry api.Registry) ([]api.DevfileStack, error) {
|
|
isGithubregistry, err := IsGithubBasedRegistry(registry.URL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if isGithubregistry {
|
|
return nil, &ErrGithubRegistryNotSupported{}
|
|
}
|
|
// OCI-based registry
|
|
options := segment.GetRegistryOptions(ctx)
|
|
options.NewIndexSchema = true
|
|
devfileIndex, err := library.GetRegistryIndex(registry.URL, options, indexSchema.StackDevfileType)
|
|
if err != nil {
|
|
// Fallback to the "old" index
|
|
klog.V(3).Infof("error while accessing the v2index endpoint for registry %s (%s) => falling back to the old index endpoint: %v",
|
|
registry.Name, registry.URL, err)
|
|
options.NewIndexSchema = false
|
|
devfileIndex, err = library.GetRegistryIndex(registry.URL, options, indexSchema.StackDevfileType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return createRegistryDevfiles(registry, devfileIndex)
|
|
}
|
|
|
|
func createRegistryDevfiles(registry api.Registry, devfileIndex []indexSchema.Schema) ([]api.DevfileStack, error) {
|
|
registryDevfiles := make([]api.DevfileStack, 0, len(devfileIndex))
|
|
for _, devfileIndexEntry := range devfileIndex {
|
|
stackDevfile := api.DevfileStack{
|
|
Name: devfileIndexEntry.Name,
|
|
DisplayName: devfileIndexEntry.DisplayName,
|
|
Description: devfileIndexEntry.Description,
|
|
Registry: registry,
|
|
Language: devfileIndexEntry.Language,
|
|
Tags: devfileIndexEntry.Tags,
|
|
ProjectType: devfileIndexEntry.ProjectType,
|
|
DefaultStarterProjects: devfileIndexEntry.StarterProjects,
|
|
DefaultVersion: devfileIndexEntry.Version,
|
|
Architectures: devfileIndexEntry.Architectures,
|
|
}
|
|
for _, v := range devfileIndexEntry.Versions {
|
|
if v.Default {
|
|
// There should be only 1 default version. But if there is more than one, the last one will be used.
|
|
stackDevfile.DefaultVersion = v.Version
|
|
stackDevfile.DefaultStarterProjects = v.StarterProjects
|
|
}
|
|
stackDevfile.Versions = append(stackDevfile.Versions, api.DevfileStackVersion{
|
|
IsDefault: v.Default,
|
|
Version: v.Version,
|
|
SchemaVersion: v.SchemaVersion,
|
|
StarterProjects: v.StarterProjects,
|
|
CommandGroups: v.CommandGroups,
|
|
})
|
|
}
|
|
sort.Slice(stackDevfile.Versions, func(i, j int) bool {
|
|
vi, err := semver.Make(stackDevfile.Versions[i].Version)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
vj, err := semver.Make(stackDevfile.Versions[j].Version)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return vi.LT(vj)
|
|
})
|
|
|
|
registryDevfiles = append(registryDevfiles, stackDevfile)
|
|
}
|
|
|
|
return registryDevfiles, nil
|
|
}
|
|
|
|
func (o RegistryClient) retrieveDevfileDataFromRegistry(ctx context.Context, registryName string, devfileName string) (api.DevfileData, error) {
|
|
|
|
// Create random temporary file
|
|
tmpFile, err := o.fsys.TempDir("", "odo")
|
|
if err != nil {
|
|
return api.DevfileData{}, err
|
|
}
|
|
defer func() {
|
|
err = o.fsys.RemoveAll(tmpFile)
|
|
if err != nil {
|
|
klog.V(2).Infof("failed to delete temporary starter project dir %s; cause: %s", tmpFile, err.Error())
|
|
}
|
|
}()
|
|
|
|
registries, err := o.GetDevfileRegistries(registryName)
|
|
if err != nil {
|
|
return api.DevfileData{}, err
|
|
}
|
|
registryOptions := segment.GetRegistryOptions(ctx)
|
|
registryOptions.NewIndexSchema = true
|
|
// Get the file and save it to the temporary file
|
|
// Why do we do that?
|
|
// 1. We need to get the file from the registry
|
|
// 2. The devfile api library does not support saving in memory
|
|
// 3. We need to get the file from the registry and save it to the temporary file
|
|
// 4. We need to read the file from the temporary file, unmarshal it and then return the devfile data
|
|
for _, reg := range registries {
|
|
if reg.Name == registryName {
|
|
err = o.PullStackFromRegistry(reg.URL, devfileName, tmpFile, registryOptions)
|
|
if err != nil {
|
|
return api.DevfileData{}, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get the devfile yaml file from the directory
|
|
devfileYamlFile := location.DevfileFilenamesProvider(o.fsys, tmpFile)
|
|
|
|
// Parse and validate the file and return the devfile data
|
|
devfileObj, err := devfile.ParseAndValidateFromFile(path.Join(tmpFile, devfileYamlFile), "", true)
|
|
if err != nil {
|
|
return api.DevfileData{}, err
|
|
}
|
|
|
|
// Convert DevfileObj to DevfileData
|
|
// use api.GetDevfileData to get supported features
|
|
devfileData, err := api.GetDevfileData(devfileObj)
|
|
if err != nil {
|
|
return api.DevfileData{}, err
|
|
}
|
|
|
|
return *devfileData, nil
|
|
}
|