1 package registry
2
3 import (
4 "context"
5 "fmt"
6 "io/fs"
7 "path"
8 "path/filepath"
9 "sort"
10 "strings"
11 "sync"
12
13 "github.com/blang/semver"
14 devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
15 apidevfile "github.com/devfile/api/v2/pkg/devfile"
16 dfutil "github.com/devfile/library/v2/pkg/util"
17 indexSchema "github.com/devfile/registry-support/index/generator/schema"
18 "github.com/devfile/registry-support/registry-library/library"
19 "k8s.io/klog"
20
21 "github.com/redhat-developer/odo/pkg/api"
22 "github.com/redhat-developer/odo/pkg/devfile"
23 "github.com/redhat-developer/odo/pkg/devfile/location"
24 "github.com/redhat-developer/odo/pkg/kclient"
25 "github.com/redhat-developer/odo/pkg/log"
26 "github.com/redhat-developer/odo/pkg/preference"
27 "github.com/redhat-developer/odo/pkg/segment"
28 "github.com/redhat-developer/odo/pkg/testingutil/filesystem"
29 "github.com/redhat-developer/odo/pkg/util"
30 )
31
32 type RegistryClient struct {
33 fsys filesystem.Filesystem
34 preferenceClient preference.Client
35 kubeClient kclient.ClientInterface
36 }
37
38 var _ Client = (*RegistryClient)(nil)
39
40 const (
41 CONFLICT_DIR_NAME = "CONFLICT_STARTER_PROJECT"
42 )
43
44 func NewRegistryClient(fsys filesystem.Filesystem, preferenceClient preference.Client, kubeClient kclient.ClientInterface) RegistryClient {
45 return RegistryClient{
46 fsys: fsys,
47 preferenceClient: preferenceClient,
48 kubeClient: kubeClient,
49 }
50 }
51
52
53 func (o RegistryClient) PullStackFromRegistry(registry string, stack string, destDir string, options library.RegistryOptions) error {
54 klog.V(3).Infof("sending telemetry data: %#v", options.Telemetry)
55 return library.PullStackFromRegistry(registry, stack, destDir, options)
56 }
57
58
59 func (o RegistryClient) DownloadFileInMemory(params dfutil.HTTPRequestParams) ([]byte, error) {
60 return util.DownloadFileInMemory(params)
61 }
62
63
64
65
66
67
68 func (o RegistryClient) DownloadStarterProject(starterProject *devfilev1.StarterProject, decryptedToken string, contextDir string, verbose bool) (containsDevfile bool, err error) {
69
70 starterProjectTmpDir, err := o.fsys.TempDir("", "odostarterproject")
71 if err != nil {
72 return containsDevfile, err
73 }
74 defer func() {
75 err = o.fsys.RemoveAll(starterProjectTmpDir)
76 if err != nil {
77 klog.V(2).Infof("failed to delete temporary starter project dir %s; cause: %s", starterProjectTmpDir, err.Error())
78 }
79 }()
80 err = DownloadStarterProject(o.fsys, starterProject, decryptedToken, starterProjectTmpDir, verbose)
81 if err != nil {
82 return containsDevfile, err
83 }
84
85
86 if containsDevfile, err = location.DirectoryContainsDevfile(o.fsys, starterProjectTmpDir); err != nil {
87 return containsDevfile, err
88 }
89 if containsDevfile {
90 fmt.Println()
91 log.Warning("A Devfile is present inside the starter project; replacing the entire content of the current directory with the starter project")
92 err = removeDirectoryContents(contextDir, o.fsys)
93 if err != nil {
94 return containsDevfile, fmt.Errorf("failed to delete contents of the current directory; cause %w", err)
95 }
96 return containsDevfile, util.CopyDirWithFS(starterProjectTmpDir, contextDir, o.fsys)
97 }
98
99
100
101 var conflictingFiles []string
102 conflictingFiles, err = getConflictingFiles(starterProjectTmpDir, contextDir, o.fsys)
103 if err != nil {
104 return containsDevfile, err
105 }
106
107
108 if len(conflictingFiles) == 0 {
109 return containsDevfile, util.CopyDirWithFS(starterProjectTmpDir, contextDir, o.fsys)
110 }
111
112
113 conflictingDirPath := filepath.Join(contextDir, CONFLICT_DIR_NAME)
114 err = o.fsys.MkdirAll(conflictingDirPath, 0750)
115 if err != nil {
116 return containsDevfile, err
117 }
118
119 err = util.CopyDirWithFS(starterProjectTmpDir, conflictingDirPath, o.fsys)
120 if err != nil {
121 return containsDevfile, err
122 }
123 fmt.Println()
124 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)
125
126 return containsDevfile, nil
127 }
128
129
130 func removeDirectoryContents(path string, fsys filesystem.Filesystem) error {
131 dir, err := fsys.ReadDir(path)
132 if err != nil {
133 return err
134 }
135 for _, f := range dir {
136
137
138
139 absFileName := filepath.Join(path, f.Name())
140 err = fsys.RemoveAll(absFileName)
141 if err != nil {
142 return fmt.Errorf("failed to remove %s; cause: %w", absFileName, err)
143 }
144 }
145
146 return nil
147 }
148
149
150
151 func getConflictingFiles(spDir, contextDir string, fsys filesystem.Filesystem) (conflictingFiles []string, err error) {
152 var (
153 contextDirMap = map[string]struct{}{}
154 )
155
156 err = fsys.Walk(contextDir, func(path string, info fs.FileInfo, err error) error {
157 if err != nil {
158 return fmt.Errorf("failed to fetch contents of dir %s; cause: %w", contextDirMap, err)
159 }
160 if info.IsDir() {
161 return nil
162 }
163 path = strings.TrimPrefix(path, contextDir)
164 contextDirMap[path] = struct{}{}
165
166 return nil
167 })
168 if err != nil {
169 return nil, fmt.Errorf("failed to walk %s dir; cause: %w", contextDir, err)
170 }
171
172
173
174 err = fsys.Walk(spDir, func(path string, info fs.FileInfo, err error) error {
175 if err != nil {
176 return fmt.Errorf("failed to fetch contents of dir %s; cause: %w", spDir, err)
177 }
178 if info.IsDir() {
179 return nil
180 }
181 path = strings.TrimPrefix(path, spDir)
182 if _, ok := contextDirMap[path]; ok {
183 conflictingFiles = append(conflictingFiles, path)
184 }
185 return nil
186 })
187 if err != nil {
188 return nil, fmt.Errorf("failed to walk %s dir; cause: %w", spDir, err)
189 }
190
191 return conflictingFiles, nil
192 }
193
194
195
196 func (o RegistryClient) GetDevfileRegistries(registryName string) ([]api.Registry, error) {
197 var allRegistries []api.Registry
198
199 if o.kubeClient != nil {
200 clusterRegistries, err := o.kubeClient.GetRegistryList()
201 if err != nil {
202
203 klog.V(3).Infof("failed to get Devfile registries from the cluster: %v", err)
204 } else {
205 allRegistries = append(allRegistries, clusterRegistries...)
206 }
207 }
208 allRegistries = append(allRegistries, o.preferenceClient.RegistryList()...)
209
210 hasName := registryName != ""
211 var result []api.Registry
212 for _, registry := range allRegistries {
213 if hasName {
214 if registryName == registry.Name {
215 reg := api.Registry{
216 Name: registry.Name,
217 URL: registry.URL,
218 Secure: registry.Secure,
219 }
220 result = append(result, reg)
221 return result, nil
222 }
223 continue
224 }
225 reg := api.Registry{
226 Name: registry.Name,
227 URL: registry.URL,
228 Secure: registry.Secure,
229 }
230 result = append(result, reg)
231 }
232
233 return result, nil
234 }
235
236
237
238 func (o RegistryClient) ListDevfileStacks(ctx context.Context, registryName, devfileFlag, filterFlag string, detailsFlag bool, withDevfileContent bool) (DevfileStackList, error) {
239 catalogDevfileList := &DevfileStackList{}
240 var err error
241
242
243
244 catalogDevfileList.DevfileRegistries, err = o.GetDevfileRegistries(registryName)
245 if err != nil {
246 return *catalogDevfileList, err
247 }
248 if catalogDevfileList.DevfileRegistries == nil {
249 return *catalogDevfileList, nil
250 }
251
252
253 devfileIndicesMutex := &sync.Mutex{}
254 retrieveRegistryIndices := util.NewConcurrentTasks(len(catalogDevfileList.DevfileRegistries))
255
256
257
258 registrySlice := make([][]api.DevfileStack, len(catalogDevfileList.DevfileRegistries))
259 for regPriority, reg := range catalogDevfileList.DevfileRegistries {
260
261 registry := reg
262 registryPriority := regPriority
263 retrieveRegistryIndices.Add(util.ConcurrentTask{ToRun: func(errChannel chan error) {
264 registryDevfiles, err := getRegistryStacks(ctx, registry)
265 if err != nil {
266 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)
267 return
268 }
269
270 devfileIndicesMutex.Lock()
271 registrySlice[registryPriority] = registryDevfiles
272 devfileIndicesMutex.Unlock()
273 }})
274 }
275 if err := retrieveRegistryIndices.Run(); err != nil {
276 return *catalogDevfileList, err
277 }
278
279
280
281
282 for priorityNumber, registryDevfiles := range registrySlice {
283
284 devfiles := []api.DevfileStack{}
285
286 devfileLoop:
287 for _, devfile := range registryDevfiles {
288
289
290 devfile.Registry.Priority = priorityNumber
291
292 if filterFlag != "" {
293 filters := strings.Split(filterFlag, ",")
294 for _, filter := range filters {
295 filter = strings.TrimSpace(filter)
296 archs := append(make([]string, 0, len(devfile.Architectures)), devfile.Architectures...)
297 if len(archs) == 0 {
298
299 archs = append(archs,
300 string(apidevfile.AMD64),
301 string(apidevfile.ARM64),
302 string(apidevfile.PPC64LE),
303 string(apidevfile.S390X),
304 )
305 }
306 containsArch := func(s string) bool {
307 for _, arch := range archs {
308 if strings.Contains(arch, s) {
309 return true
310 }
311 }
312 return false
313 }
314 if !strings.Contains(devfile.Name, filter) && !strings.Contains(devfile.Description, filter) && !containsArch(filter) {
315 continue devfileLoop
316 }
317 }
318 }
319
320 if devfileFlag != "" {
321 if devfileFlag != devfile.Name {
322 continue
323 }
324 }
325
326
327 if detailsFlag && withDevfileContent {
328 devfileData, err := o.retrieveDevfileDataFromRegistry(ctx, devfile.Registry.Name, devfile.Name)
329 if err != nil {
330 return *catalogDevfileList, err
331 }
332 devfile.DevfileData = &devfileData
333 }
334
335 devfiles = append(devfiles, devfile)
336 }
337
338 catalogDevfileList.Items = append(catalogDevfileList.Items, devfiles...)
339 }
340
341
342
343
344 sort.Slice(catalogDevfileList.Items[:], func(i, j int) bool {
345 if catalogDevfileList.Items[i].Name == catalogDevfileList.Items[j].Name {
346 return catalogDevfileList.Items[i].Registry.Priority < catalogDevfileList.Items[j].Registry.Priority
347 }
348 return catalogDevfileList.Items[i].Name < catalogDevfileList.Items[j].Name
349 })
350
351 return *catalogDevfileList, nil
352 }
353
354
355 func getRegistryStacks(ctx context.Context, registry api.Registry) ([]api.DevfileStack, error) {
356 isGithubregistry, err := IsGithubBasedRegistry(registry.URL)
357 if err != nil {
358 return nil, err
359 }
360 if isGithubregistry {
361 return nil, &ErrGithubRegistryNotSupported{}
362 }
363
364 options := segment.GetRegistryOptions(ctx)
365 options.NewIndexSchema = true
366 devfileIndex, err := library.GetRegistryIndex(registry.URL, options, indexSchema.StackDevfileType)
367 if err != nil {
368
369 klog.V(3).Infof("error while accessing the v2index endpoint for registry %s (%s) => falling back to the old index endpoint: %v",
370 registry.Name, registry.URL, err)
371 options.NewIndexSchema = false
372 devfileIndex, err = library.GetRegistryIndex(registry.URL, options, indexSchema.StackDevfileType)
373 if err != nil {
374 return nil, err
375 }
376 }
377 return createRegistryDevfiles(registry, devfileIndex)
378 }
379
380 func createRegistryDevfiles(registry api.Registry, devfileIndex []indexSchema.Schema) ([]api.DevfileStack, error) {
381 registryDevfiles := make([]api.DevfileStack, 0, len(devfileIndex))
382 for _, devfileIndexEntry := range devfileIndex {
383 stackDevfile := api.DevfileStack{
384 Name: devfileIndexEntry.Name,
385 DisplayName: devfileIndexEntry.DisplayName,
386 Description: devfileIndexEntry.Description,
387 Registry: registry,
388 Language: devfileIndexEntry.Language,
389 Tags: devfileIndexEntry.Tags,
390 ProjectType: devfileIndexEntry.ProjectType,
391 DefaultStarterProjects: devfileIndexEntry.StarterProjects,
392 DefaultVersion: devfileIndexEntry.Version,
393 Architectures: devfileIndexEntry.Architectures,
394 }
395 for _, v := range devfileIndexEntry.Versions {
396 if v.Default {
397
398 stackDevfile.DefaultVersion = v.Version
399 stackDevfile.DefaultStarterProjects = v.StarterProjects
400 }
401 stackDevfile.Versions = append(stackDevfile.Versions, api.DevfileStackVersion{
402 IsDefault: v.Default,
403 Version: v.Version,
404 SchemaVersion: v.SchemaVersion,
405 StarterProjects: v.StarterProjects,
406 CommandGroups: v.CommandGroups,
407 })
408 }
409 sort.Slice(stackDevfile.Versions, func(i, j int) bool {
410 vi, err := semver.Make(stackDevfile.Versions[i].Version)
411 if err != nil {
412 return false
413 }
414 vj, err := semver.Make(stackDevfile.Versions[j].Version)
415 if err != nil {
416 return false
417 }
418 return vi.LT(vj)
419 })
420
421 registryDevfiles = append(registryDevfiles, stackDevfile)
422 }
423
424 return registryDevfiles, nil
425 }
426
427 func (o RegistryClient) retrieveDevfileDataFromRegistry(ctx context.Context, registryName string, devfileName string) (api.DevfileData, error) {
428
429
430 tmpFile, err := o.fsys.TempDir("", "odo")
431 if err != nil {
432 return api.DevfileData{}, err
433 }
434 defer func() {
435 err = o.fsys.RemoveAll(tmpFile)
436 if err != nil {
437 klog.V(2).Infof("failed to delete temporary starter project dir %s; cause: %s", tmpFile, err.Error())
438 }
439 }()
440
441 registries, err := o.GetDevfileRegistries(registryName)
442 if err != nil {
443 return api.DevfileData{}, err
444 }
445 registryOptions := segment.GetRegistryOptions(ctx)
446 registryOptions.NewIndexSchema = true
447
448
449
450
451
452
453 for _, reg := range registries {
454 if reg.Name == registryName {
455 err = o.PullStackFromRegistry(reg.URL, devfileName, tmpFile, registryOptions)
456 if err != nil {
457 return api.DevfileData{}, err
458 }
459 }
460 }
461
462
463 devfileYamlFile := location.DevfileFilenamesProvider(o.fsys, tmpFile)
464
465
466 devfileObj, err := devfile.ParseAndValidateFromFile(path.Join(tmpFile, devfileYamlFile), "", true)
467 if err != nil {
468 return api.DevfileData{}, err
469 }
470
471
472
473 devfileData, err := api.GetDevfileData(devfileObj)
474 if err != nil {
475 return api.DevfileData{}, err
476 }
477
478 return *devfileData, nil
479 }
480
View as plain text