odo init filters devfile stacks by supported architectures (#7004)

* 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>
This commit is contained in:
Philippe Martin
2023-08-01 19:36:48 +02:00
committed by GitHub
parent 3cb1f5c66c
commit d41364e68e
34 changed files with 394 additions and 113 deletions

View File

@@ -52,14 +52,15 @@ func TestOdoAlizer(t *testing.T) {
alizerClient := alizer.NewMockClient(ctrl)
path := "/"
alizerClient.EXPECT().DetectFramework(gomock.Any(), path).
Return(
model.DevFileType{
Return(alizer.DetectedFramework{
Type: model.DevFileType{
Name: "framework-name",
},
"1.1.1",
api.Registry{
DefaultVersion: "1.1.1",
Registry: api.Registry{
Name: "TheRegistryName",
},
},
nil,
)
alizerClient.EXPECT().DetectPorts(path).Return([]int{8080, 3000}, nil)
@@ -92,14 +93,15 @@ func TestOdoAlizer(t *testing.T) {
alizerClient := alizer.NewMockClient(ctrl)
path := "/"
alizerClient.EXPECT().DetectFramework(gomock.Any(), path).
Return(
model.DevFileType{
Return(alizer.DetectedFramework{
Type: model.DevFileType{
Name: "framework-name",
},
"1.1.1",
api.Registry{
DefaultVersion: "1.1.1",
Registry: api.Registry{
Name: "TheRegistryName",
},
},
nil,
)
alizerClient.EXPECT().DetectPorts(path).Return([]int{8080, 3000}, nil)

View File

@@ -9,6 +9,7 @@ $ odo init
Interactive mode enabled, please answer the following questions:
✓ Determining a Devfile for the current directory [1s]
Based on the files in the current directory odo detected
Supported architectures: all
Language: JavaScript
Project type: Node.js
Application ports: 8080

View File

@@ -7,6 +7,12 @@ $ odo init
\__/
Interactive mode enabled, please answer the following questions:
? Select architectures to filter by: [Use arrows to move, space to select, <right> to all, <left> to none, type to filter]
> [x] amd64
[ ] arm64
[ ] ppc64le
[ ] s390x
? Select architectures to filter by: amd64
? Select language: Java
? Select project type: Maven Java
✓ Downloading devfile "java-maven" from registry "DefaultDevfileRegistry" [4s]

View File

@@ -9,10 +9,17 @@ $ odo init
Interactive mode enabled, please answer the following questions:
✓ Determining a Devfile for the current directory [1s]
Based on the files in the current directory odo detected
Supported architectures: all
Language: .NET
Project type: dotnet
The devfile "dotnet50:1.0.3" from the registry "DefaultDevfileRegistry" will be downloaded.
? Is this correct? No
? Select architectures to filter by: [Use arrows to move, space to select, <right> to all, <left> to none, type to filter]
> [x] amd64
[ ] arm64
[ ] ppc64le
[ ] s390x
? Select architectures to filter by: amd64
? Select language: .NET
? Select project type: .NET 6.0
✓ Downloading devfile "dotnet60" from registry "DefaultDevfileRegistry" [3s]

View File

@@ -9,6 +9,7 @@ $ odo init
Interactive mode enabled, please answer the following questions:
✓ Determining a Devfile for the current directory [1s]
Based on the files in the current directory odo detected
Supported architectures: all
Language: Go
Project type: Go
Application ports: 8080

View File

@@ -9,6 +9,7 @@ $ odo init
Interactive mode enabled, please answer the following questions:
✓ Determining a Devfile for the current directory [1s]
Based on the files in the current directory odo detected
Supported architectures: all
Language: Java
Project type: springboot
The devfile "java-springboot:1.2.0" from the registry "DefaultDevfileRegistry" will be downloaded.

View File

@@ -9,6 +9,7 @@ $ odo init
Interactive mode enabled, please answer the following questions:
✓ Determining a Devfile for the current directory [1s]
Based on the files in the current directory odo detected
Supported architectures: all
Language: JavaScript
Project type: Node.js
Application ports: 3000

View File

@@ -29,11 +29,11 @@ func NewAlizerClient(registryClient registry.Client) *Alizer {
// DetectFramework uses the alizer library in order to detect the devfile
// to use depending on the files in the path
func (o *Alizer) DetectFramework(ctx context.Context, path string) (_ model.DevFileType, defaultVersion string, _ api.Registry, _ error) {
func (o *Alizer) DetectFramework(ctx context.Context, path string) (DetectedFramework, error) {
types := []model.DevFileType{}
components, err := o.registryClient.ListDevfileStacks(ctx, "", "", "", false, false)
if err != nil {
return model.DevFileType{}, defaultVersion, api.Registry{}, err
return DetectedFramework{}, err
}
for _, component := range components.Items {
types = append(types, model.DevFileType{
@@ -45,15 +45,21 @@ func (o *Alizer) DetectFramework(ctx context.Context, path string) (_ model.DevF
}
typ, err := recognizer.SelectDevFileFromTypes(path, types)
if err != nil {
return model.DevFileType{}, defaultVersion, api.Registry{}, err
return DetectedFramework{}, err
}
// Get the default stack version that will be downloaded
var defaultVersion string
for _, version := range components.Items[typ].Versions {
if version.IsDefault {
defaultVersion = version.Version
}
}
return types[typ], defaultVersion, components.Items[typ].Registry, nil
return DetectedFramework{
Type: types[typ],
DefaultVersion: defaultVersion,
Registry: components.Items[typ].Registry,
Architectures: components.Items[typ].Architectures,
}, nil
}
// DetectName retrieves the name of the project (if available).

View File

@@ -119,18 +119,18 @@ func TestDetectFramework(t *testing.T) {
registryClient.EXPECT().ListDevfileStacks(ctx, "", "", "", false, false).Return(list, nil)
alizerClient := NewAlizerClient(registryClient)
// Run function DetectFramework
detected, _, registry, err := alizerClient.DetectFramework(ctx, tt.args.path)
detected, err := alizerClient.DetectFramework(ctx, tt.args.path)
if !tt.wantErr == (err != nil) {
t.Errorf("unexpected error %v, wantErr %v", err, tt.wantErr)
return
}
if detected.Name != tt.wantedDevfile {
if detected.Type.Name != tt.wantedDevfile {
t.Errorf("unexpected devfile %v, wantedDevfile %v", detected, tt.wantedDevfile)
}
if registry.Name != tt.wantedRegistry {
t.Errorf("unexpected registry %v, wantedRegistry %v", registry, tt.wantedRegistry)
if detected.Registry.Name != tt.wantedRegistry {
t.Errorf("unexpected registry %v, wantedRegistry %v", detected.Registry, tt.wantedRegistry)
}
})
}

View File

@@ -4,12 +4,18 @@ import (
"context"
"github.com/devfile/alizer/pkg/apis/model"
"github.com/redhat-developer/odo/pkg/api"
)
type DetectedFramework struct {
Type model.DevFileType
DefaultVersion string
Registry api.Registry
Architectures []string
}
type Client interface {
DetectFramework(ctx context.Context, path string) (_ model.DevFileType, defaultVersion string, _ api.Registry, _ error)
DetectFramework(ctx context.Context, path string) (DetectedFramework, error)
DetectName(path string) (string, error)
DetectPorts(path string) ([]int, error)
}

12
pkg/alizer/mock.go generated
View File

@@ -8,9 +8,7 @@ import (
context "context"
reflect "reflect"
model "github.com/devfile/alizer/pkg/apis/model"
gomock "github.com/golang/mock/gomock"
api "github.com/redhat-developer/odo/pkg/api"
)
// MockClient is a mock of Client interface.
@@ -37,14 +35,12 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder {
}
// DetectFramework mocks base method.
func (m *MockClient) DetectFramework(ctx context.Context, path string) (model.DevFileType, string, api.Registry, error) {
func (m *MockClient) DetectFramework(ctx context.Context, path string) (DetectedFramework, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DetectFramework", ctx, path)
ret0, _ := ret[0].(model.DevFileType)
ret1, _ := ret[1].(string)
ret2, _ := ret[2].(api.Registry)
ret3, _ := ret[3].(error)
return ret0, ret1, ret2, ret3
ret0, _ := ret[0].(DetectedFramework)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DetectFramework indicates an expected call of DetectFramework.

View File

@@ -19,4 +19,6 @@ type DetectionResult struct {
DevfileVersion string `json:"devfileVersion,omitempty"`
// Name represents the project/application name as detected by alizer
Name string `json:"name,omitempty"`
// Architectures represent the architectures with which the Devfile must be compatible with.
Architectures []string `json:"architectures,omitempty"`
}

View File

@@ -24,18 +24,36 @@ func NewSurveyAsker() *Survey {
return &Survey{}
}
func (o *Survey) AskLanguage(langs []string) (string, error) {
func (o *Survey) AskArchitectures(archs []string, selectedDefault []string) ([]string, error) {
question := &survey.MultiSelect{
Message: "Select architectures to filter by:",
Options: archs,
Default: selectedDefault,
}
var answer []string
err := survey.AskOne(question, &answer)
if err != nil {
return nil, err
}
return answer, nil
}
func (o *Survey) AskLanguage(langs []string) (bool, string, error) {
sort.Strings(langs)
langs = append(langs, GOBACK)
question := &survey.Select{
Message: "Select language:",
Options: langs,
}
var answer string
err := survey.AskOne(question, &answer)
var answerPos int
err := survey.AskOne(question, &answerPos)
if err != nil {
return "", err
return false, "", err
}
return answer, nil
if answerPos == len(langs)-1 {
return true, "", nil
}
return false, langs[answerPos], nil
}
func (o *Survey) AskType(types registry.TypesWithDetails) (back bool, _ api.DevfileStack, _ error) {

View File

@@ -9,8 +9,12 @@ import (
// Asker interactively asks for information to the user
type Asker interface {
// AskLanguage asks for a language, from a list of language names. The language name is returned
AskLanguage(langs []string) (string, error)
// AskArchitectures asks for a selection of architectures from a list of architecture names
AskArchitectures(archs []string, selectedDefault []string) ([]string, error)
// AskLanguage asks for a language, from a list of language names.
// back is returned as true if the user selected to go back, or the language name is returned
AskLanguage(langs []string) (back bool, result string, err error)
// AskType asks for a Devfile type, or to go back. back is returned as true if the user selected to go back,
// or the selected type is returned

24
pkg/init/asker/mock.go generated
View File

@@ -66,6 +66,21 @@ func (mr *MockAskerMockRecorder) AskAddPort() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AskAddPort", reflect.TypeOf((*MockAsker)(nil).AskAddPort))
}
// AskArchitectures mocks base method.
func (m *MockAsker) AskArchitectures(archs, selectedDefault []string) ([]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AskArchitectures", archs, selectedDefault)
ret0, _ := ret[0].([]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AskArchitectures indicates an expected call of AskArchitectures.
func (mr *MockAskerMockRecorder) AskArchitectures(archs, selectedDefault interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AskArchitectures", reflect.TypeOf((*MockAsker)(nil).AskArchitectures), archs, selectedDefault)
}
// AskContainerName mocks base method.
func (m *MockAsker) AskContainerName(containers []string) (string, error) {
m.ctrl.T.Helper()
@@ -97,12 +112,13 @@ func (mr *MockAskerMockRecorder) AskCorrect() *gomock.Call {
}
// AskLanguage mocks base method.
func (m *MockAsker) AskLanguage(langs []string) (string, error) {
func (m *MockAsker) AskLanguage(langs []string) (bool, string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AskLanguage", langs)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(string)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// AskLanguage indicates an expected call of AskLanguage.

View File

@@ -3,10 +3,11 @@ package backend
import (
"context"
"fmt"
"github.com/redhat-developer/odo/pkg/log"
"strconv"
"strings"
"github.com/redhat-developer/odo/pkg/log"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/v2/pkg/devfile/parser"
@@ -34,6 +35,13 @@ func (o *AlizerBackend) Validate(flags map[string]string, fs filesystem.Filesyst
return nil
}
func archList(archs []string) string {
if len(archs) == 0 {
return "all"
}
return strings.Join(archs, ", ")
}
// SelectDevfile calls the Alizer to detect the devfile and asks for confirmation to the user
func (o *AlizerBackend) SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (*api.DetectionResult, error) {
type result struct {
@@ -47,12 +55,13 @@ func (o *AlizerBackend) SelectDevfile(ctx context.Context, flags map[string]stri
location, err := func() (location *api.DetectionResult, err error) {
spinner := log.Spinnerf("Determining a Devfile for the current directory")
defer spinner.End(err == nil)
selected, defaultVersion, registry, err := o.alizerClient.DetectFramework(ctx, dir)
detected, err := o.alizerClient.DetectFramework(ctx, dir)
if err != nil {
return nil, err
}
msg := fmt.Sprintf("Based on the files in the current directory odo detected\nLanguage: %s\nProject type: %s", selected.Language, selected.ProjectType)
msg := fmt.Sprintf("Based on the files in the current directory odo detected\nSupported architectures: %s\nLanguage: %s\nProject type: %s",
archList(detected.Architectures), detected.Type.Language, detected.Type.ProjectType)
appPorts, err := o.alizerClient.DetectPorts(dir)
if err != nil {
@@ -68,7 +77,7 @@ func (o *AlizerBackend) SelectDevfile(ctx context.Context, flags map[string]stri
}
fmt.Println(msg)
fmt.Printf("The devfile \"%s:%s\" from the registry %q will be downloaded.\n", selected.Name, defaultVersion, registry.Name)
fmt.Printf("The devfile \"%s:%s\" from the registry %q will be downloaded.\n", detected.Type.Name, detected.DefaultVersion, detected.Registry.Name)
confirm, err := o.askerClient.AskCorrect()
if err != nil {
return nil, err
@@ -76,7 +85,7 @@ func (o *AlizerBackend) SelectDevfile(ctx context.Context, flags map[string]stri
if !confirm {
return nil, nil
}
return alizer.NewDetectionResult(selected, registry, appPorts, defaultVersion, ""), nil
return alizer.NewDetectionResult(detected.Type, detected.Registry, appPorts, detected.DefaultVersion, ""), nil
}()
resultChan <- result{
location: location,

View File

@@ -53,11 +53,7 @@ func TestAlizerBackend_SelectDevfile(t *testing.T) {
},
alizerClient: func(ctrl *gomock.Controller) alizer.Client {
alizerClient := alizer.NewMockClient(ctrl)
alizerClient.EXPECT().DetectFramework(gomock.Any(), gomock.Any()).Return(model.DevFileType{
Name: "a-devfile-name",
}, "1.0.0", api.Registry{
Name: "a-registry",
}, errors.New("unable to detect framework"))
alizerClient.EXPECT().DetectFramework(gomock.Any(), gomock.Any()).Return(alizer.DetectedFramework{}, errors.New("unable to detect framework"))
return alizerClient
},
},
@@ -77,11 +73,7 @@ func TestAlizerBackend_SelectDevfile(t *testing.T) {
},
alizerClient: func(ctrl *gomock.Controller) alizer.Client {
alizerClient := alizer.NewMockClient(ctrl)
alizerClient.EXPECT().DetectFramework(gomock.Any(), gomock.Any()).Return(model.DevFileType{
Name: "a-devfile-name",
}, "1.0.0", api.Registry{
Name: "a-registry",
}, nil)
alizerClient.EXPECT().DetectFramework(gomock.Any(), gomock.Any()).Return(alizer.DetectedFramework{}, nil)
alizerClient.EXPECT().DetectPorts(gomock.Any()).Return(nil, errors.New("unable to detect ports"))
return alizerClient
},
@@ -102,10 +94,14 @@ func TestAlizerBackend_SelectDevfile(t *testing.T) {
},
alizerClient: func(ctrl *gomock.Controller) alizer.Client {
alizerClient := alizer.NewMockClient(ctrl)
alizerClient.EXPECT().DetectFramework(gomock.Any(), gomock.Any()).Return(model.DevFileType{
alizerClient.EXPECT().DetectFramework(gomock.Any(), gomock.Any()).Return(alizer.DetectedFramework{
Type: model.DevFileType{
Name: "a-devfile-name",
}, "1.0.0", api.Registry{
},
DefaultVersion: "1.0.0",
Registry: api.Registry{
Name: "a-registry",
},
}, nil)
alizerClient.EXPECT().DetectPorts(gomock.Any()).Return(nil, nil)
return alizerClient
@@ -127,10 +123,14 @@ func TestAlizerBackend_SelectDevfile(t *testing.T) {
},
alizerClient: func(ctrl *gomock.Controller) alizer.Client {
alizerClient := alizer.NewMockClient(ctrl)
alizerClient.EXPECT().DetectFramework(gomock.Any(), gomock.Any()).Return(model.DevFileType{
alizerClient.EXPECT().DetectFramework(gomock.Any(), gomock.Any()).Return(alizer.DetectedFramework{
Type: model.DevFileType{
Name: "a-devfile-name",
}, "1.0.0", api.Registry{
},
DefaultVersion: "1.0.0",
Registry: api.Registry{
Name: "a-registry",
},
}, nil)
alizerClient.EXPECT().DetectPorts(gomock.Any()).Return(nil, nil)
return alizerClient
@@ -156,7 +156,7 @@ func TestAlizerBackend_SelectDevfile(t *testing.T) {
},
alizerClient: func(ctrl *gomock.Controller) alizer.Client {
alizerClient := alizer.NewMockClient(ctrl)
alizerClient.EXPECT().DetectFramework(gomock.Any(), gomock.Any()).Return(model.DevFileType{}, "", api.Registry{}, nil)
alizerClient.EXPECT().DetectFramework(gomock.Any(), gomock.Any()).Return(alizer.DetectedFramework{}, nil)
alizerClient.EXPECT().DetectPorts(gomock.Any()).Return(nil, nil)
return alizerClient
},
@@ -177,10 +177,14 @@ func TestAlizerBackend_SelectDevfile(t *testing.T) {
},
alizerClient: func(ctrl *gomock.Controller) alizer.Client {
alizerClient := alizer.NewMockClient(ctrl)
alizerClient.EXPECT().DetectFramework(gomock.Any(), gomock.Any()).Return(model.DevFileType{
alizerClient.EXPECT().DetectFramework(gomock.Any(), gomock.Any()).Return(alizer.DetectedFramework{
Type: model.DevFileType{
Name: "a-devfile-name",
}, "1.0.0", api.Registry{
},
DefaultVersion: "1.0.0",
Registry: api.Registry{
Name: "a-registry",
},
}, nil)
alizerClient.EXPECT().DetectPorts(gomock.Any()).Return([]int{1234, 5678}, nil)
return alizerClient

View File

@@ -13,6 +13,7 @@ import (
"github.com/redhat-developer/odo/pkg/registry"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/api/v2/pkg/devfile"
"github.com/devfile/library/v2/pkg/devfile/parser"
"github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common"
dfutil "github.com/devfile/library/v2/pkg/util"
@@ -30,6 +31,7 @@ const (
FLAG_DEVFILE_PATH = "devfile-path"
FLAG_DEVFILE_VERSION = "devfile-version"
FLAG_RUN_PORT = "run-port"
FLAG_ARCHITECTURE = "architecture"
)
// FlagsBackend is a backend that will extract all needed information from flags passed to the command
@@ -39,6 +41,13 @@ type FlagsBackend struct {
var _ InitBackend = (*FlagsBackend)(nil)
var knownArchitectures []string = []string{
string(devfile.AMD64),
string(devfile.ARM64),
string(devfile.PPC64LE),
string(devfile.S390X),
}
func NewFlagsBackend(registryClient registry.Client) *FlagsBackend {
return &FlagsBackend{
registryClient: registryClient,
@@ -97,15 +106,37 @@ Please use 'odo preference <add/remove> registry'' command to configure devfile
return errors.New("--starter parameter cannot be used when the directory is not empty")
}
archs, err := parseStringArrayFlagValue(flags[FLAG_ARCHITECTURE])
if err != nil {
return err
}
for _, arch := range archs {
if !isKnownArch(arch) {
return fmt.Errorf("value %q is not valid for flag --architecture. Possible values are: %s", arch, strings.Join(knownArchitectures, ", "))
}
}
return nil
}
func isKnownArch(arch string) bool {
for _, known := range knownArchitectures {
if known == arch {
return true
}
}
return false
}
func (o *FlagsBackend) SelectDevfile(ctx context.Context, flags map[string]string, _ filesystem.Filesystem, _ string) (*api.DetectionResult, error) {
// This has been validated before
archs, _ := parseStringArrayFlagValue(flags[FLAG_ARCHITECTURE])
return &api.DetectionResult{
Devfile: flags[FLAG_DEVFILE],
DevfileRegistry: flags[FLAG_DEVFILE_REGISTRY],
DevfilePath: flags[FLAG_DEVFILE_PATH],
DevfileVersion: flags[FLAG_DEVFILE_VERSION],
Architectures: archs,
}, nil
}
@@ -150,16 +181,16 @@ func (o FlagsBackend) HandleApplicationPorts(devfileobj parser.DevfileObj, _ []i
func setPortsForFlag(devfileobj parser.DevfileObj, flags map[string]string, flagName string) (parser.DevfileObj, error) {
flagVal := flags[flagName]
// Repeatable flags are formatted as "[val1,val2]"
if !(strings.HasPrefix(flagVal, "[") && strings.HasSuffix(flagVal, "]")) {
split, err := parseStringArrayFlagValue(flagVal)
if err != nil || len(split) == 0 {
return devfileobj, nil
}
portsStr := flagVal[1 : len(flagVal)-1]
var ports []int
split := strings.Split(portsStr, ",")
for _, s := range split {
p, err := strconv.Atoi(s)
var p int
p, err = strconv.Atoi(s)
if err != nil {
return parser.DevfileObj{}, fmt.Errorf("invalid value for %s (%q): %w", flagName, s, err)
}
@@ -216,3 +247,15 @@ func setPortsForFlag(devfileobj parser.DevfileObj, flags map[string]string, flag
}
return devfileobj, nil
}
func parseStringArrayFlagValue(flagVal string) ([]string, error) {
if flagVal == "" {
return []string{}, nil
}
// Repeatable flags are formatted as "[val1,val2]"
if !(strings.HasPrefix(flagVal, "[") && strings.HasSuffix(flagVal, "]")) {
return nil, fmt.Errorf("malformed value %q", flagVal)
}
portsStr := flagVal[1 : len(flagVal)-1]
return strings.Split(portsStr, ","), nil
}

View File

@@ -45,6 +45,7 @@ func TestFlagsBackend_SelectDevfile(t *testing.T) {
Devfile: "adevfile",
DevfilePath: "apath",
DevfileRegistry: "aregistry",
Architectures: []string{},
},
},
}

View File

@@ -6,6 +6,7 @@ import (
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/v2/pkg/devfile/parser"
@@ -22,7 +23,8 @@ import (
)
const (
STATE_ASK_LANG = iota
STATE_ASK_ARCHITECTURES = iota
STATE_ASK_LANG
STATE_ASK_TYPE
STATE_ASK_VERSION
STATE_END
@@ -51,22 +53,36 @@ func (o *InteractiveBackend) Validate(flags map[string]string, fs filesystem.Fil
func (o *InteractiveBackend) SelectDevfile(ctx context.Context, flags map[string]string, _ filesystem.Filesystem, _ string) (*api.DetectionResult, error) {
result := &api.DetectionResult{}
devfileEntries, _ := o.registryClient.ListDevfileStacks(ctx, "", "", "", false, false)
langs := devfileEntries.GetLanguages()
state := STATE_ASK_LANG
var devfileEntries registry.DevfileStackList
state := STATE_ASK_ARCHITECTURES
var lang string
archs := []string{"amd64"}
var err error
var details api.DevfileStack
loop:
for {
switch state {
case STATE_ASK_LANG:
lang, err = o.askerClient.AskLanguage(langs)
case STATE_ASK_ARCHITECTURES:
archs, err = o.askerClient.AskArchitectures(knownArchitectures, archs)
if err != nil {
return nil, err
}
state = STATE_ASK_LANG
case STATE_ASK_LANG:
filter := strings.Join(archs, ",")
devfileEntries, _ = o.registryClient.ListDevfileStacks(ctx, "", "", filter, false, false)
langs := devfileEntries.GetLanguages()
var back bool
back, lang, err = o.askerClient.AskLanguage(langs)
if err != nil {
return nil, err
}
if back {
state = STATE_ASK_ARCHITECTURES
continue loop
}
state = STATE_ASK_TYPE
case STATE_ASK_TYPE:

View File

@@ -36,7 +36,8 @@ func TestInteractiveBackend_SelectDevfile(t *testing.T) {
fields: fields{
buildAsker: func(ctrl *gomock.Controller) asker.Asker {
client := asker.NewMockAsker(ctrl)
client.EXPECT().AskLanguage(gomock.Any()).Return("java", nil)
client.EXPECT().AskArchitectures(knownArchitectures, []string{"amd64"}).Return([]string{"amd64"}, nil)
client.EXPECT().AskLanguage(gomock.Any()).Return(false, "java", nil)
client.EXPECT().AskType(gomock.Any()).Return(false, api.DevfileStack{
Name: "a-devfile-name",
Registry: api.Registry{
@@ -57,13 +58,14 @@ func TestInteractiveBackend_SelectDevfile(t *testing.T) {
},
},
{
name: "selection with back",
name: "selection with back on language selection",
fields: fields{
buildAsker: func(ctrl *gomock.Controller) asker.Asker {
client := asker.NewMockAsker(ctrl)
client.EXPECT().AskLanguage(gomock.Any()).Return("java", nil)
client.EXPECT().AskType(gomock.Any()).Return(true, api.DevfileStack{}, nil)
client.EXPECT().AskLanguage(gomock.Any()).Return("go", nil)
client.EXPECT().AskArchitectures(knownArchitectures, []string{"amd64"}).Return([]string{"amd64", "arm64"}, nil)
client.EXPECT().AskLanguage(gomock.Any()).Return(true, "", nil)
client.EXPECT().AskArchitectures(knownArchitectures, []string{"amd64", "arm64"}).Return([]string{"arm64"}, nil)
client.EXPECT().AskLanguage(gomock.Any()).Return(false, "go", nil)
client.EXPECT().AskType(gomock.Any()).Return(false, api.DevfileStack{
Name: "a-devfile-name",
Registry: api.Registry{
@@ -74,7 +76,35 @@ func TestInteractiveBackend_SelectDevfile(t *testing.T) {
},
buildCatalogClient: func(ctrl *gomock.Controller) registry.Client {
client := registry.NewMockClient(ctrl)
client.EXPECT().ListDevfileStacks(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
client.EXPECT().ListDevfileStacks(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(2)
return client
},
},
want: &api.DetectionResult{
Devfile: "a-devfile-name",
DevfileRegistry: "MyRegistry1",
},
},
{
name: "selection with back on type selection",
fields: fields{
buildAsker: func(ctrl *gomock.Controller) asker.Asker {
client := asker.NewMockAsker(ctrl)
client.EXPECT().AskArchitectures(knownArchitectures, []string{"amd64"}).Return([]string{"amd64"}, nil)
client.EXPECT().AskLanguage(gomock.Any()).Return(false, "java", nil)
client.EXPECT().AskType(gomock.Any()).Return(true, api.DevfileStack{}, nil)
client.EXPECT().AskLanguage(gomock.Any()).Return(false, "go", nil)
client.EXPECT().AskType(gomock.Any()).Return(false, api.DevfileStack{
Name: "a-devfile-name",
Registry: api.Registry{
Name: "MyRegistry1",
},
}, nil)
return client
},
buildCatalogClient: func(ctrl *gomock.Controller) registry.Client {
client := registry.NewMockClient(ctrl)
client.EXPECT().ListDevfileStacks(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(2)
return client
},
},

View File

@@ -50,6 +50,7 @@ var _initFlags = []string{
backend.FLAG_DEVFILE_PATH,
backend.FLAG_DEVFILE_VERSION,
backend.FLAG_RUN_PORT,
backend.FLAG_ARCHITECTURE,
}
func NewInitClient(fsys filesystem.Filesystem, preferenceClient preference.Client, registryClient registry.Client, alizerClient alizer.Client) *InitClient {
@@ -137,7 +138,7 @@ func (o *InitClient) DownloadDevfile(ctx context.Context, devfileLocation *api.D
if devfileLocation.DevfileVersion != "" {
devfile = fmt.Sprintf("%s:%s", devfileLocation.Devfile, devfileLocation.DevfileVersion)
}
return destDevfile, o.downloadFromRegistry(ctx, devfileLocation.DevfileRegistry, devfile, destDir)
return destDevfile, o.downloadFromRegistry(ctx, devfileLocation.DevfileRegistry, devfile, destDir, devfileLocation.Architectures)
}
}
@@ -185,10 +186,14 @@ func (o *InitClient) downloadDirect(URL string, dest string) error {
// downloadFromRegistry downloads a devfile from the provided registry and saves it in dest
// If registryName is empty, will try to download the devfile from the list of registries in preferences
func (o *InitClient) downloadFromRegistry(ctx context.Context, registryName string, devfile string, dest string) error {
// The architectures value indicates to download a Devfile compatible with all of these architectures
func (o *InitClient) downloadFromRegistry(ctx context.Context, registryName string, devfile string, dest string, architectures []string) error {
// setting NewIndexSchema ensures that the Devfile library pulls registry based on the stack version
registryOptions := segment.GetRegistryOptions(ctx)
registryOptions.NewIndexSchema = true
if len(architectures) > 0 {
registryOptions.Filter.Architectures = architectures
}
var downloadSpinner *log.Status
var forceRegistry bool

View File

@@ -6,6 +6,7 @@ import (
"testing"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/registry-support/registry-library/library"
"github.com/golang/mock/gomock"
"github.com/redhat-developer/odo/pkg/api"
@@ -25,6 +26,7 @@ func TestInitClient_downloadFromRegistry(t *testing.T) {
registryName string
devfile string
dest string
archs []string
}
tests := []struct {
name string
@@ -52,7 +54,12 @@ func TestInitClient_downloadFromRegistry(t *testing.T) {
},
}
client.EXPECT().GetDevfileRegistries(gomock.Eq("Registry1")).Return(registryList, nil).Times(1)
client.EXPECT().PullStackFromRegistry("http://registry1", "java", gomock.Any(), gomock.Any()).Return(nil).Times(1)
client.EXPECT().PullStackFromRegistry("http://registry1", "java", gomock.Any(), library.RegistryOptions{
Telemetry: library.TelemetryData{
Client: "odo",
},
NewIndexSchema: true,
}).Return(nil).Times(1)
return client
},
},
@@ -63,6 +70,46 @@ func TestInitClient_downloadFromRegistry(t *testing.T) {
},
wantErr: false,
},
{
name: "Download devfile from one specific Registry where devfile is present and arch is passed",
fields: fields{
preferenceClient: func(ctrl *gomock.Controller) preference.Client {
client := preference.NewMockClient(ctrl)
return client
},
registryClient: func(ctrl *gomock.Controller) registry.Client {
client := registry.NewMockClient(ctrl)
registryList := []api.Registry{
{
Name: "Registry0",
URL: "http://registry0",
},
{
Name: "Registry1",
URL: "http://registry1",
},
}
client.EXPECT().GetDevfileRegistries(gomock.Eq("Registry1")).Return(registryList, nil).Times(1)
client.EXPECT().PullStackFromRegistry("http://registry1", "java", gomock.Any(), library.RegistryOptions{
Telemetry: library.TelemetryData{
Client: "odo",
},
Filter: library.RegistryFilter{
Architectures: []string{"arm64"},
},
NewIndexSchema: true,
}).Return(nil).Times(1)
return client
},
},
args: args{
registryName: "Registry1",
devfile: "java",
dest: ".",
archs: []string{"arm64"},
},
wantErr: false,
},
{
name: "Fail to download devfile from one specific Registry where devfile is absent",
fields: fields{
@@ -167,7 +214,7 @@ func TestInitClient_downloadFromRegistry(t *testing.T) {
}
ctx := context.Background()
ctx = envcontext.WithEnvConfig(ctx, config.Configuration{})
if err := o.downloadFromRegistry(ctx, tt.args.registryName, tt.args.devfile, tt.args.dest); (err != nil) != tt.wantErr {
if err := o.downloadFromRegistry(ctx, tt.args.registryName, tt.args.devfile, tt.args.dest, tt.args.archs); (err != nil) != tt.wantErr {
t.Errorf("InitClient.downloadFromRegistry() error = %v, wantErr %v", err, tt.wantErr)
}
})

View File

@@ -50,7 +50,7 @@ func (o *AlizerOptions) Run(ctx context.Context) (err error) {
// RunForJsonOutput contains the logic for the odo command
func (o *AlizerOptions) RunForJsonOutput(ctx context.Context) (out interface{}, err error) {
workingDir := odocontext.GetWorkingDirectory(ctx)
df, defaultVersion, reg, err := o.clientset.AlizerClient.DetectFramework(ctx, workingDir)
detected, err := o.clientset.AlizerClient.DetectFramework(ctx, workingDir)
if err != nil {
return nil, err
}
@@ -62,7 +62,7 @@ func (o *AlizerOptions) RunForJsonOutput(ctx context.Context) (out interface{},
if err != nil {
return nil, err
}
result := alizer.NewDetectionResult(df, reg, appPorts, defaultVersion, name)
result := alizer.NewDetectionResult(detected.Type, detected.Registry, appPorts, detected.DefaultVersion, name)
return []api.DetectionResult{*result}, nil
}

View File

@@ -63,6 +63,9 @@ var initExample = templates.Examples(`
# Bootstrap a new component and download a starter project
%[1]s --name my-app --devfile nodejs --starter nodejs-starter
# Bootstrap a new component with a specific devfile from registry for a specific architecture
%[1]s --name my-app --devfile nodejs --architecture s390x
`)
type InitOptions struct {
@@ -285,6 +288,7 @@ func NewCmdInit(name, fullName string, testClientset clientset.Clientset) *cobra
initCmd.Flags().String(backend.FLAG_STARTER, "", "name of the starter project")
initCmd.Flags().String(backend.FLAG_DEVFILE_PATH, "", "path to a devfile. This is an alternative to using devfile from Devfile registry. It can be local filesystem path or http(s) URL")
initCmd.Flags().String(backend.FLAG_DEVFILE_VERSION, "", "version of the devfile stack; use \"latest\" to dowload the latest stack")
initCmd.Flags().StringArray(backend.FLAG_ARCHITECTURE, []string{}, "Architecture supported. Can be one or multiple values from amd64, arm64, ppc64le, s390x. Default is amd64.")
initCmd.Flags().StringArray(backend.FLAG_RUN_PORT, []string{}, "ports used by the application (via the 'run' command)")
commonflags.UseOutputFlag(initCmd)

View File

@@ -32,8 +32,8 @@ var Example = ` # Get all devfile components
# Filter by name and devfile registry
%[1]s --filter nodejs --devfile-registry DefaultDevfileRegistry
# Filter by architecture
%[1]s --filter amd64
# Show the Devfiles supporting both architectures
%[1]s --filter amd64,arm64
# Show more details from a specific devfile
%[1]s --details --devfile nodejs
@@ -120,7 +120,7 @@ func NewCmdRegistry(name, fullName string, testClientset clientset.Clientset) *c
clientset.Add(listCmd, clientset.REGISTRY)
// Flags
listCmd.Flags().StringVar(&o.filterFlag, "filter", "", "Filter based on the name or description or supported architecture of the component")
listCmd.Flags().StringVar(&o.filterFlag, "filter", "", "Comma-separated list of terms for filtering. Search is done using a logical AND against the name or description or supported architectures of the component.")
listCmd.Flags().StringVar(&o.devfileFlag, "devfile", "", "Only the specific Devfile component")
listCmd.Flags().StringVar(&o.registryFlag, "devfile-registry", "", "Only show components from the specific Devfile registry")
listCmd.Flags().BoolVar(&o.detailsFlag, "details", false, "Show details of a Devfile, to be used only with --devfile")

View File

@@ -283,12 +283,16 @@ func (o RegistryClient) ListDevfileStacks(ctx context.Context, registryName, dev
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.
@@ -307,8 +311,9 @@ func (o RegistryClient) ListDevfileStacks(ctx context.Context, registryName, dev
}
return false
}
if !strings.Contains(devfile.Name, filterFlag) && !strings.Contains(devfile.Description, filterFlag) && !containsArch(filterFlag) {
continue
if !strings.Contains(devfile.Name, filter) && !strings.Contains(devfile.Description, filter) && !containsArch(filter) {
continue devfileLoop
}
}
}

View File

@@ -32,6 +32,9 @@ var _ = Describe("doc command reference odo init", Label(helper.LabelNoCluster),
It("Empty directory", func() {
args := []string{"odo", "init"}
out, err := helper.RunInteractive(args, []string{"ODO_LOG_LEVEL=0"}, func(ctx helper.InteractiveContext) {
helper.ExpectString(ctx, "Select architectures")
helper.SendLine(ctx, "")
helper.ExpectString(ctx, "Select language")
helper.SendLine(ctx, "Java")

View File

@@ -166,6 +166,9 @@ var _ = Describe("User guides: Quickstart test", func() {
helper.ExpectString(ctx, "Is this correct?")
helper.SendLine(ctx, "No")
helper.ExpectString(ctx, "Select architectures")
helper.SendLine(ctx, "")
helper.ExpectString(ctx, "Select language")
helper.SendLine(ctx, ".")

View File

@@ -69,6 +69,9 @@ var _ = Describe("E2E Test", func() {
command := []string{"odo", "init"}
_, err := helper.RunInteractive(command, nil, func(ctx helper.InteractiveContext) {
helper.ExpectString(ctx, "Select architectures")
helper.SendLine(ctx, "")
helper.ExpectString(ctx, "Select language")
helper.SendLine(ctx, "JavaScript")

View File

@@ -35,6 +35,11 @@ func StripSpinner(docString string) (returnString string) {
line := sc.Text()
// trim any special character present in the line
line = strings.TrimFunc(line, unicode.IsSpace)
if len(line) == 0 {
continue
}
// This check is to avoid spinner statements in the cmd output
// currently it does so for init and dev
// e.g. " • Syncing file changes ..."
@@ -92,6 +97,9 @@ func GetMDXContent(filePath string) (mdxContent string) {
for fileScanner.Scan() {
line := fileScanner.Text()
line = strings.TrimFunc(line, unicode.IsSpace)
if len(line) == 0 {
continue
}
mdxContent += line + "\n"
}

View File

@@ -159,7 +159,7 @@ var _ = Describe("odo deploy interactive command tests", func() {
It("should not fail but fallback to the interactive mode", func() {
_, err := helper.RunInteractive([]string{"odo", "deploy"}, nil, func(ctx helper.InteractiveContext) {
helper.ExpectString(ctx, "Could not determine a Devfile based on the files in the current directory")
helper.ExpectString(ctx, "Select language")
helper.ExpectString(ctx, "Select architectures")
ctx.StopCommand()
})
Expect(err).Should(HaveOccurred())

View File

@@ -165,6 +165,9 @@ var _ = Describe("odo dev interactive command tests", func() {
output, _ := helper.RunInteractive([]string{"odo", "dev", "--random-ports"}, nil, func(ctx helper.InteractiveContext) {
helper.ExpectString(ctx, "Could not determine a Devfile based on the files in the current directory")
helper.ExpectString(ctx, "Select architectures")
helper.SendLine(ctx, "")
helper.ExpectString(ctx, "Select language")
helper.SendLine(ctx, "Python")

View File

@@ -56,6 +56,9 @@ var _ = Describe("odo init interactive command tests", func() {
helper.ExpectString(ctx, messages.InteractiveModeEnabled)
})
helper.ExpectString(ctx, "Select architectures")
helper.SendLine(ctx, "")
helper.ExpectString(ctx, "Select language")
helper.SendLine(ctx, "Go")
@@ -97,6 +100,9 @@ var _ = Describe("odo init interactive command tests", func() {
command := []string{"odo", "init"}
output, err := helper.RunInteractive(command, nil, func(ctx helper.InteractiveContext) {
helper.ExpectString(ctx, "Select architectures")
helper.SendLine(ctx, "")
helper.ExpectString(ctx, "Select language")
helper.SendLine(ctx, "Javascript")
@@ -148,6 +154,9 @@ var _ = Describe("odo init interactive command tests", func() {
command := []string{"odo", "init"}
_, err := helper.RunInteractive(command, nil, func(ctx helper.InteractiveContext) {
helper.ExpectString(ctx, "Select architectures")
helper.SendLine(ctx, "")
helper.ExpectString(ctx, "Select language")
helper.SendLine(ctx, "Go")
@@ -192,6 +201,9 @@ var _ = Describe("odo init interactive command tests", func() {
output, err := helper.RunInteractive(command, nil, func(ctx helper.InteractiveContext) {
helper.ExpectString(ctx, "Select architectures")
helper.SendLine(ctx, "")
helper.ExpectString(ctx, "Select language")
helper.SendLine(ctx, "Go")
@@ -230,6 +242,9 @@ var _ = Describe("odo init interactive command tests", func() {
helper.ExpectString(ctx, messages.InteractiveModeEnabled)
})
helper.ExpectString(ctx, "Select architectures")
helper.SendLine(ctx, "")
helper.ExpectString(ctx, "Select language")
helper.SendLine(ctx, "Go")
@@ -265,6 +280,9 @@ var _ = Describe("odo init interactive command tests", func() {
output, err := helper.RunInteractive(command, nil, func(ctx helper.InteractiveContext) {
helper.ExpectString(ctx, "Select architectures")
helper.SendLine(ctx, "")
helper.ExpectString(ctx, "Select language")
helper.SendLine(ctx, "Go")
@@ -301,6 +319,9 @@ var _ = Describe("odo init interactive command tests", func() {
helper.ExpectString(ctx, messages.InteractiveModeEnabled)
})
helper.ExpectString(ctx, "Select architectures")
helper.SendLine(ctx, "")
helper.ExpectString(ctx, "Select language")
helper.SendLine(ctx, "java")
@@ -395,6 +416,9 @@ var _ = Describe("odo init interactive command tests", func() {
welcomingMsgs := strings.Split(odolog.Stitle(messages.InitializingNewComponent, messages.NoSourceCodeDetected, "odo version: "+version.VERSION), "\n")
output, err := testRunner(language, welcomingMsgs, func(ctx helper.InteractiveContext) {
helper.ExpectString(ctx, "Select architectures")
helper.SendLine(ctx, "")
helper.ExpectString(ctx, "Select language")
helper.SendLine(ctx, language)
@@ -547,6 +571,9 @@ var _ = Describe("odo init interactive command tests", func() {
output, err := helper.RunInteractive([]string{"odo", "init"}, nil, func(ctx helper.InteractiveContext) {
helper.ExpectString(ctx, "Select architectures")
helper.SendLine(ctx, "")
helper.ExpectString(ctx, "Select language")
helper.SendLine(ctx, ".NET")
@@ -643,7 +670,7 @@ var _ = Describe("odo init interactive command tests", func() {
_, err := helper.RunInteractive([]string{"odo", "init"}, nil, func(ctx helper.InteractiveContext) {
helper.ExpectString(ctx, "Could not determine a Devfile based on the files in the current directory")
helper.ExpectString(ctx, "Select language")
helper.ExpectString(ctx, "Select architectures")
ctx.StopCommand()
})
Expect(err).Should(HaveOccurred())
@@ -689,6 +716,9 @@ spec:
}
output, err := helper.RunInteractive([]string{"odo", "init"}, nil, func(ctx helper.InteractiveContext) {
helper.ExpectString(ctx, "Select architectures")
helper.SendLine(ctx, "")
helper.ExpectString(ctx, "Select language")
helper.SendLine(ctx, "Java")