Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance improvement for odo catalog list components when using devfiles #3112

Merged
merged 20 commits into from
May 17, 2020
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 97 additions & 45 deletions pkg/catalog/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ package catalog
import (
"encoding/json"
"fmt"
"github.com/openshift/odo/pkg/preference"
"sort"
"strings"
"sync"

imagev1 "github.com/openshift/api/image/v1"
"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/occlient"
"github.com/openshift/odo/pkg/preference"
"github.com/openshift/odo/pkg/util"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
Expand All @@ -19,23 +20,30 @@ import (

// GetDevfileRegistries gets devfile registries from preference file,
// if registry name is specified return the specific registry, otherwise return all registries
func GetDevfileRegistries(registryName string) (map[string]string, error) {
devfileRegistries := make(map[string]string)
func GetDevfileRegistries(registryName string) (map[string]Registry, error) {
devfileRegistries := make(map[string]Registry)

cfg, err := preference.New()
if err != nil {
return nil, err
}

hasName := len(registryName) != 0
if cfg.OdoSettings.RegistryList != nil {
for _, registry := range *cfg.OdoSettings.RegistryList {
if len(registryName) != 0 {
if hasName {
if registryName == registry.Name {
devfileRegistries[registry.Name] = registry.URL
devfileRegistries[registry.Name] = Registry{
Name: registry.Name,
URL: registry.URL,
}
return devfileRegistries, nil
}
} else {
devfileRegistries[registry.Name] = registry.URL
devfileRegistries[registry.Name] = Registry{
Name: registry.Name,
URL: registry.URL,
}
}
}
} else {
Expand All @@ -45,18 +53,42 @@ func GetDevfileRegistries(registryName string) (map[string]string, error) {
return devfileRegistries, nil
}

const indexPath = "/devfiles/index.json"

// GetDevfileIndex loads the devfile registry index.json
func GetDevfileIndex(devfileIndexLink string) ([]DevfileIndexEntry, error) {
var devfileIndex []DevfileIndexEntry
func GetDevfileIndex(registryName string) ([]DevfileIndexEntry, error) {
registries, err2 := GetDevfileRegistries(registryName)
if err2 != nil {
return nil, err2
}

registry := registries[registryName]
return getDevfileIndexEntries(registry)
}

func getDevfileIndexEntriesFrom(registryName, registryURL string) ([]DevfileIndexEntry, error) {
registry := Registry{
Name: registryName,
URL: registryURL,
}
return getDevfileIndexEntries(registry)
}

jsonBytes, err := util.HTTPGetRequest(devfileIndexLink)
func getDevfileIndexEntries(registry Registry) ([]DevfileIndexEntry, error) {
var devfileIndex []DevfileIndexEntry
indexLink := registry.URL + indexPath
jsonBytes, err := util.HTTPGetRequest(indexLink)
if err != nil {
return nil, errors.Wrapf(err, "Unable to download the devfile index.json from %s", devfileIndexLink)
return nil, errors.Wrapf(err, "Unable to download the devfile index.json from %s", indexLink)
}

err = json.Unmarshal(jsonBytes, &devfileIndex)
if err != nil {
return nil, errors.Wrapf(err, "Unable to unmarshal the devfile index.json from %s", devfileIndexLink)
return nil, errors.Wrapf(err, "Unable to unmarshal the devfile index.json from %s", indexLink)
}

for i := range devfileIndex {
devfileIndex[i].Registry = registry
}

return devfileIndex, nil
Expand Down Expand Up @@ -128,63 +160,83 @@ func IsDevfileComponentSupported(devfile Devfile) bool {

// ListDevfileComponents lists all the available devfile components
func ListDevfileComponents(registryName string) (DevfileComponentTypeList, error) {
var catalogDevfileList DevfileComponentTypeList
catalogDevfileList := &DevfileComponentTypeList{}
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 = GetDevfileRegistries(registryName)
if err != nil {
return catalogDevfileList, err
return *catalogDevfileList, err
}
if catalogDevfileList.DevfileRegistries == nil {
return catalogDevfileList, nil
return *catalogDevfileList, nil
}

for registryName, registryURL := range catalogDevfileList.DevfileRegistries {
// first retrieve the indices for each registry, concurrently
registryIndices := make([]DevfileIndexEntry, 0, 20)
devfileIndicesMutex := &sync.Mutex{}
retrieveRegistryIndices := util.NewConcurrentTasks(len(catalogDevfileList.DevfileRegistries))
for _, reg := range catalogDevfileList.DevfileRegistries {
// Load the devfile registry index.json
devfileIndexLink := registryURL + "/devfiles/index.json"
devfileIndex, err := GetDevfileIndex(devfileIndexLink)
if err != nil {
log.Warningf("Registry %s is not set up properly with error: %v", registryName, err)
break
}
registry := reg // needed to prevent the lambda from capturing the value
retrieveRegistryIndices.Add(util.ConcurrentTask{ToRun: func(errChannel chan error) {
indexEntries, e := getDevfileIndexEntries(registry)
if e != nil {
log.Warningf("Registry %s is not set up properly with error: %v", registryName, err)
errChannel <- e
return
}

// 1. Load devfiles that indexed in devfile registry index.json
// 2. Populate devfile components with devfile data
// 3. Form devfile component list
for _, devfileIndexEntry := range devfileIndex {
devfileIndexEntryLink := devfileIndexEntry.Links.Link

// Load the devfile
devfileLink := registryURL + devfileIndexEntryLink
// TODO: We send http get resquest in this function multiple times
// since devfile registry uses different links to host different devfiles,
// this can reduce the performance especially when we load devfiles from
// big registry. We may need to rethink and optimize this in the future
devfile, err := GetDevfile(devfileLink)
devfileIndicesMutex.Lock()
registryIndices = append(registryIndices, indexEntries...)
devfileIndicesMutex.Unlock()
}})
}
if err := retrieveRegistryIndices.Run(); err != nil {
return *catalogDevfileList, err
}

// 1. Load each devfile concurrently from the previously retrieved devfile index entries
// 2. Populate devfile components with devfile data
// 3. Add devfile component types to the catalog devfile list
retrieveDevfiles := util.NewConcurrentTasks(len(registryIndices))
devfileMutex := &sync.Mutex{}
for _, index := range registryIndices {
// Load the devfile
devfileIndex := index // needed to prevent the lambda from capturing the value
link := devfileIndex.Registry.URL + devfileIndex.Links.Link
retrieveDevfiles.Add(util.ConcurrentTask{ToRun: func(errChannel chan error) {

// Note that this issues an HTTP get per devfile entry in the catalog, while doing it concurrently instead of
// sequentially improves the performance, caching that information would improve the performance even more
devfile, err := GetDevfile(link)
if err != nil {
log.Warningf("Registry %s is not set up properly with error: %v", registryName, err)
break
log.Warningf("Registry %s is not set up properly with error: %v", devfileIndex.Registry.Name, err)
errChannel <- err
return
}

// Populate devfile component with devfile data and form devfile component list
catalogDevfile := DevfileComponentType{
Name: strings.TrimSuffix(devfile.MetaData.GenerateName, "-"),
DisplayName: devfileIndexEntry.DisplayName,
Description: devfileIndexEntry.Description,
Link: devfileIndexEntryLink,
DisplayName: devfileIndex.DisplayName,
Description: devfileIndex.Description,
Link: devfileIndex.Links.Link,
Support: IsDevfileComponentSupported(devfile),
Registry: Registry{
Name: registryName,
URL: registryURL,
},
Registry: devfileIndex.Registry,
}

devfileMutex.Lock()
catalogDevfileList.Items = append(catalogDevfileList.Items, catalogDevfile)
}
devfileMutex.Unlock()
}})
}
if err := retrieveDevfiles.Run(); err != nil {
return *catalogDevfileList, err
}

return catalogDevfileList, nil
return *catalogDevfileList, nil
}

// ListComponents lists all the available component types
Expand Down
40 changes: 28 additions & 12 deletions pkg/catalog/catalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,21 +199,30 @@ OdoSettings:
tests := []struct {
name string
registryName string
want map[string]string
want map[string]Registry
}{
{
name: "Case 1: Test get all devfile registries",
registryName: "",
want: map[string]string{
"CheDevfileRegistry": "https://che-devfile-registry.openshift.io/",
"DefaultDevfileRegistry": "https://raw.githubusercontent.com/elsony/devfile-registry/master",
want: map[string]Registry{
"CheDevfileRegistry": {
Name: "CheDevfileRegistry",
URL: "https://che-devfile-registry.openshift.io/",
},
"DefaultDevfileRegistry": {
Name: "DefaultDevfileRegistry",
URL: "https://raw.githubusercontent.com/elsony/devfile-registry/master",
},
},
},
{
name: "Case 2: Test get specific devfile registry",
registryName: "CheDevfileRegistry",
want: map[string]string{
"CheDevfileRegistry": "https://che-devfile-registry.openshift.io/",
want: map[string]Registry{
"CheDevfileRegistry": {
Name: "CheDevfileRegistry",
URL: "https://che-devfile-registry.openshift.io/",
},
},
},
}
Expand Down Expand Up @@ -263,14 +272,17 @@ func TestGetDevfileIndex(t *testing.T) {
// Close the server when test finishes
defer server.Close()

const registryName = "some registry"
tests := []struct {
name string
devfileIndexLink string
want []DevfileIndexEntry
name string
registryURL string
registryName string
want []DevfileIndexEntry
}{
{
name: "Test NodeJS devfile index",
devfileIndexLink: server.URL,
name: "Test NodeJS devfile index",
registryURL: server.URL,
registryName: registryName,
want: []DevfileIndexEntry{
{
DisplayName: "NodeJS Angular Web Application",
Expand All @@ -282,6 +294,10 @@ func TestGetDevfileIndex(t *testing.T) {
},
Icon: "/images/angular.svg",
GlobalMemoryLimit: "2686Mi",
Registry: Registry{
Name: registryName,
URL: server.URL,
},
Links: struct {
Link string `json:"self"`
}{
Expand All @@ -294,7 +310,7 @@ func TestGetDevfileIndex(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetDevfileIndex(tt.devfileIndexLink)
got, err := getDevfileIndexEntriesFrom(tt.registryName, tt.registryURL)

if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Got: %v, want: %v", got, tt.want)
Expand Down
3 changes: 2 additions & 1 deletion pkg/catalog/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type DevfileIndexEntry struct {
Tags []string `json:"tags"`
Icon string `json:"icon"`
GlobalMemoryLimit string `json:"globalMemoryLimit"`
Registry Registry `json:"registry"`
Links struct {
Link string `json:"self"`
} `json:"links"`
Expand Down Expand Up @@ -72,7 +73,7 @@ type ComponentTypeList struct {

// DevfileComponentTypeList lists all the DevfileComponentType's
type DevfileComponentTypeList struct {
DevfileRegistries map[string]string
DevfileRegistries map[string]Registry
Items []DevfileComponentType
}

Expand Down
51 changes: 29 additions & 22 deletions pkg/odo/cli/catalog/list/components.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ package list

import (
"fmt"
"io"
"os"
"strings"
"text/tabwriter"

"github.com/openshift/odo/pkg/catalog"
"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/machineoutput"
"github.com/openshift/odo/pkg/odo/cli/catalog/util"
"github.com/openshift/odo/pkg/odo/genericclioptions"
"github.com/openshift/odo/pkg/odo/util/experimental"
"github.com/openshift/odo/pkg/odo/util/pushtarget"
util2 "github.com/openshift/odo/pkg/util"
"github.com/spf13/cobra"
"io"
"k8s.io/klog"
"os"
"strings"
"text/tabwriter"
)

const componentsRecommendedCommandName = "components"
Expand All @@ -42,32 +42,39 @@ func NewListComponentsOptions() *ListComponentsOptions {

// Complete completes ListComponentsOptions after they've been created
func (o *ListComponentsOptions) Complete(name string, cmd *cobra.Command, args []string) (err error) {

tasks := util2.NewConcurrentTasks(2)

if !pushtarget.IsPushTargetDocker() {
o.Context = genericclioptions.NewContext(cmd)
o.catalogList, err = catalog.ListComponents(o.Client)
if err != nil {
if experimental.IsExperimentalModeEnabled() {
klog.V(4).Info("Please log in to an OpenShift cluster to list OpenShift/s2i components")

tasks.Add(util2.ConcurrentTask{ToRun: func(errChannel chan error) {
o.catalogList, err = catalog.ListComponents(o.Client)
if err != nil {
if experimental.IsExperimentalModeEnabled() {
klog.V(4).Info("Please log in to an OpenShift cluster to list OpenShift/s2i components")
} else {
errChannel <- err
}
} else {
return err
o.catalogList.Items = util.FilterHiddenComponents(o.catalogList.Items)
}
}

o.catalogList.Items = util.FilterHiddenComponents(o.catalogList.Items)
}})
}

if experimental.IsExperimentalModeEnabled() {
o.catalogDevfileList, err = catalog.ListDevfileComponents("")
if err != nil {
return err
}

if o.catalogDevfileList.DevfileRegistries == nil {
log.Warning("Please run 'odo registry add <registry name> <registry URL>' to add registry for listing devfile components\n")
}
tasks.Add(util2.ConcurrentTask{ToRun: func(errChannel chan error) {
o.catalogDevfileList, err = catalog.ListDevfileComponents("")
if o.catalogDevfileList.DevfileRegistries == nil {
log.Warning("Please run 'odo registry add <registry name> <registry URL>' to add registry for listing devfile components\n")
}
if err != nil {
errChannel <- err
}
}})
}

return
return tasks.Run()
}

// Validate validates the ListComponentsOptions based on completed values
Expand Down
Loading