mirror of
https://github.com/redhat-developer/odo.git
synced 2025-10-19 03:06:19 +03:00
Show Devfile Stack versions in "odo registry" output (#6397)
* Make the registry client collect and return all stack versions * Call the '/v2index' endpoint first and fallback to the index endpoint when listing Stack versions * Display the Stack versions in the 'odo registry' human-readable output * For convenience, sort stack versions based on the semantic version This allows seeing them in a meaningful order in both human-readable and JSON outputs. * Enrich unit tests for 'getRegistryStacks' * Fill the "starterProjects" field for backward compatibility Similar to the "version" field, this represents the list of starter projects of the default Stack version. * Update integration tests * Update documentation
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/blang/semver"
|
||||
devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
|
||||
dfutil "github.com/devfile/library/pkg/util"
|
||||
indexSchema "github.com/devfile/registry-support/index/generator/schema"
|
||||
@@ -197,9 +198,18 @@ func getRegistryStacks(ctx context.Context, preferenceClient preference.Client,
|
||||
return nil, &ErrGithubRegistryNotSupported{}
|
||||
}
|
||||
// OCI-based registry
|
||||
devfileIndex, err := library.GetRegistryIndex(registry.URL, segment.GetRegistryOptions(ctx), indexSchema.StackDevfileType)
|
||||
options := segment.GetRegistryOptions(ctx)
|
||||
options.NewIndexSchema = true
|
||||
devfileIndex, err := library.GetRegistryIndex(registry.URL, options, indexSchema.StackDevfileType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// 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)
|
||||
}
|
||||
@@ -208,16 +218,41 @@ func createRegistryDevfiles(registry api.Registry, devfileIndex []indexSchema.Sc
|
||||
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,
|
||||
StarterProjects: devfileIndexEntry.StarterProjects,
|
||||
Version: devfileIndexEntry.Version,
|
||||
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,
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -276,59 +276,246 @@ func TestListDevfileStacks(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetRegistryDevfiles(t *testing.T) {
|
||||
// Start a local HTTP server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
// Send response to be tested
|
||||
_, err := rw.Write([]byte(
|
||||
`
|
||||
[
|
||||
{
|
||||
"name": "nodejs",
|
||||
"displayName": "NodeJS Angular Web Application",
|
||||
"description": "Stack for developing NodeJS Angular Web Application",
|
||||
"tags": [
|
||||
"NodeJS",
|
||||
"Angular",
|
||||
"Alpine"
|
||||
],
|
||||
"language": "nodejs",
|
||||
"icon": "/images/angular.svg",
|
||||
"globalMemoryLimit": "2686Mi",
|
||||
"links": {
|
||||
"self": "/devfiles/angular/devfile.yaml"
|
||||
}
|
||||
}
|
||||
]
|
||||
`,
|
||||
))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}))
|
||||
// Close the server when test finishes
|
||||
defer server.Close()
|
||||
|
||||
const registryName = "some registry"
|
||||
tests := []struct {
|
||||
name string
|
||||
registry api.Registry
|
||||
want []api.DevfileStack
|
||||
}{
|
||||
const (
|
||||
v1IndexResponse = `
|
||||
[
|
||||
{
|
||||
"name": "nodejs",
|
||||
"displayName": "NodeJS Angular Web Application",
|
||||
"description": "Stack for developing NodeJS Angular Web Application",
|
||||
"version": "1.2.3",
|
||||
"tags": [
|
||||
"NodeJS",
|
||||
"Angular",
|
||||
"Alpine"
|
||||
],
|
||||
"language": "nodejs",
|
||||
"icon": "/images/angular.svg",
|
||||
"globalMemoryLimit": "2686Mi",
|
||||
"links": {
|
||||
"self": "/devfiles/angular/devfile.yaml"
|
||||
}
|
||||
}
|
||||
]
|
||||
`
|
||||
v2IndexResponse = `
|
||||
[
|
||||
{
|
||||
"name": "go",
|
||||
"displayName": "Go Runtime",
|
||||
"description": "Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.",
|
||||
"type": "stack",
|
||||
"tags": [
|
||||
"Go"
|
||||
],
|
||||
"icon": "https://raw.githubusercontent.com/devfile-samples/devfile-stack-icons/main/golang.svg",
|
||||
"projectType": "Go",
|
||||
"language": "Go",
|
||||
"provider": "Red Hat",
|
||||
"versions": [
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"schemaVersion": "2.2.0",
|
||||
"description": "Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.",
|
||||
"tags": [
|
||||
"Go"
|
||||
],
|
||||
"icon": "https://raw.githubusercontent.com/devfile-samples/devfile-stack-icons/main/golang.svg",
|
||||
"links": {
|
||||
"self": "devfile-catalog/go:2.0.0"
|
||||
},
|
||||
"resources": [
|
||||
"devfile.yaml"
|
||||
],
|
||||
"starterProjects": [
|
||||
"go-starter"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.0.2",
|
||||
"schemaVersion": "2.1.0",
|
||||
"default": true,
|
||||
"description": "Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.",
|
||||
"tags": [
|
||||
"Go"
|
||||
],
|
||||
"icon": "https://raw.githubusercontent.com/devfile-samples/devfile-stack-icons/main/golang.svg",
|
||||
"links": {
|
||||
"self": "devfile-catalog/go:1.0.2"
|
||||
},
|
||||
"resources": [
|
||||
"devfile.yaml"
|
||||
],
|
||||
"starterProjects": [
|
||||
"go-starter"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
`
|
||||
)
|
||||
|
||||
type test struct {
|
||||
name string
|
||||
registryServerProvider func(t *testing.T) (*httptest.Server, string)
|
||||
wantErr bool
|
||||
wantProvider func(registryUrl string) []api.DevfileStack
|
||||
}
|
||||
tests := []test{
|
||||
{
|
||||
name: "Test NodeJS devfile index",
|
||||
registry: api.Registry{Name: registryName, URL: server.URL},
|
||||
want: []api.DevfileStack{
|
||||
{
|
||||
Name: "nodejs",
|
||||
DisplayName: "NodeJS Angular Web Application",
|
||||
Description: "Stack for developing NodeJS Angular Web Application",
|
||||
Registry: api.Registry{
|
||||
Name: registryName,
|
||||
URL: server.URL,
|
||||
name: "GitHub-based registry: github.com",
|
||||
registryServerProvider: func(t *testing.T) (*httptest.Server, string) {
|
||||
return nil, "https://github.com/redhat-developer/odo"
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "GitHub-based registry: raw.githubusercontent.com",
|
||||
registryServerProvider: func(t *testing.T) (*httptest.Server, string) {
|
||||
return nil, "https://raw.githubusercontent.com/redhat-developer/odo"
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "GitHub-based registry: *.github.com",
|
||||
registryServerProvider: func(t *testing.T) (*httptest.Server, string) {
|
||||
return nil, "https://redhat-developer.github.com/odo"
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "GitHub-based registry: *.raw.githubusercontent.com",
|
||||
registryServerProvider: func(t *testing.T) (*httptest.Server, string) {
|
||||
return nil, "https://redhat-developer.raw.githubusercontent.com/odo"
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Devfile registry server: client error (4xx)",
|
||||
registryServerProvider: func(t *testing.T) (*httptest.Server, string) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
return server, server.URL
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Devfile registry server: server error (5xx)",
|
||||
registryServerProvider: func(t *testing.T) (*httptest.Server, string) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
return server, server.URL
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Devfile registry: only /index",
|
||||
registryServerProvider: func(t *testing.T) (*httptest.Server, string) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.URL.Path != "/index" {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
_, err := rw.Write([]byte(v1IndexResponse))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}))
|
||||
return server, server.URL
|
||||
},
|
||||
wantProvider: func(registryUrl string) []api.DevfileStack {
|
||||
return []api.DevfileStack{
|
||||
{
|
||||
Name: "nodejs",
|
||||
DisplayName: "NodeJS Angular Web Application",
|
||||
Description: "Stack for developing NodeJS Angular Web Application",
|
||||
Registry: api.Registry{Name: registryName, URL: registryUrl},
|
||||
Language: "nodejs",
|
||||
Tags: []string{"NodeJS", "Angular", "Alpine"},
|
||||
DefaultVersion: "1.2.3",
|
||||
},
|
||||
Language: "nodejs",
|
||||
Tags: []string{"NodeJS", "Angular", "Alpine"},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Devfile registry: only /v2index",
|
||||
registryServerProvider: func(t *testing.T) (*httptest.Server, string) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.URL.Path != "/v2index" {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
_, err := rw.Write([]byte(v2IndexResponse))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}))
|
||||
return server, server.URL
|
||||
},
|
||||
wantProvider: func(registryUrl string) []api.DevfileStack {
|
||||
return []api.DevfileStack{
|
||||
{
|
||||
Name: "go",
|
||||
DisplayName: "Go Runtime",
|
||||
Description: "Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.",
|
||||
Registry: api.Registry{Name: registryName, URL: registryUrl},
|
||||
Language: "Go",
|
||||
ProjectType: "Go",
|
||||
Tags: []string{"Go"},
|
||||
DefaultVersion: "1.0.2",
|
||||
DefaultStarterProjects: []string{"go-starter"},
|
||||
Versions: []api.DevfileStackVersion{
|
||||
{Version: "1.0.2", IsDefault: true, SchemaVersion: "2.1.0", StarterProjects: []string{"go-starter"}},
|
||||
{Version: "2.0.0", IsDefault: false, SchemaVersion: "2.2.0", StarterProjects: []string{"go-starter"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Devfile registry: both /index and /v2index => v2index has precedence",
|
||||
registryServerProvider: func(t *testing.T) (*httptest.Server, string) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
var resp string
|
||||
switch req.URL.Path {
|
||||
case "/index":
|
||||
resp = v1IndexResponse
|
||||
case "/v2index":
|
||||
resp = v2IndexResponse
|
||||
}
|
||||
if resp == "" {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
_, err := rw.Write([]byte(resp))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}))
|
||||
return server, server.URL
|
||||
},
|
||||
wantProvider: func(registryUrl string) []api.DevfileStack {
|
||||
return []api.DevfileStack{
|
||||
{
|
||||
Name: "go",
|
||||
DisplayName: "Go Runtime",
|
||||
Description: "Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.",
|
||||
Registry: api.Registry{Name: registryName, URL: registryUrl},
|
||||
Language: "Go",
|
||||
ProjectType: "Go",
|
||||
Tags: []string{"Go"},
|
||||
DefaultVersion: "1.0.2",
|
||||
DefaultStarterProjects: []string{"go-starter"},
|
||||
Versions: []api.DevfileStackVersion{
|
||||
{Version: "1.0.2", IsDefault: true, SchemaVersion: "2.1.0", StarterProjects: []string{"go-starter"}},
|
||||
{Version: "2.0.0", IsDefault: false, SchemaVersion: "2.2.0", StarterProjects: []string{"go-starter"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -337,13 +524,24 @@ func TestGetRegistryDevfiles(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
prefClient := preference.NewMockClient(ctrl)
|
||||
ctx := context.Background()
|
||||
ctx = envcontext.WithEnvConfig(ctx, config.Configuration{})
|
||||
got, err := getRegistryStacks(ctx, prefClient, tt.registry)
|
||||
ctx := envcontext.WithEnvConfig(context.Background(), config.Configuration{})
|
||||
server, url := tt.registryServerProvider(t)
|
||||
if server != nil {
|
||||
defer server.Close()
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
t.Errorf("getRegistryStacks() mismatch (-want +got):\n%s", diff)
|
||||
t.Logf("Error message is: %v", err)
|
||||
got, err := getRegistryStacks(ctx, prefClient, api.Registry{Name: registryName, URL: url})
|
||||
|
||||
if tt.wantErr != (err != nil) {
|
||||
t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if tt.wantProvider != nil {
|
||||
want := tt.wantProvider(url)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("getRegistryStacks() mismatch (-want +got):\n%s", diff)
|
||||
t.Logf("Error message is: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user