mirror of
https://github.com/redhat-developer/odo.git
synced 2025-10-19 03:06:19 +03:00
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:
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
12
pkg/alizer/mock.go
generated
@@ -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.
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
24
pkg/init/asker/mock.go
generated
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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{
|
||||
Name: "a-devfile-name",
|
||||
}, "1.0.0", api.Registry{
|
||||
Name: "a-registry",
|
||||
alizerClient.EXPECT().DetectFramework(gomock.Any(), gomock.Any()).Return(alizer.DetectedFramework{
|
||||
Type: model.DevFileType{
|
||||
Name: "a-devfile-name",
|
||||
},
|
||||
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{
|
||||
Name: "a-devfile-name",
|
||||
}, "1.0.0", api.Registry{
|
||||
Name: "a-registry",
|
||||
alizerClient.EXPECT().DetectFramework(gomock.Any(), gomock.Any()).Return(alizer.DetectedFramework{
|
||||
Type: model.DevFileType{
|
||||
Name: "a-devfile-name",
|
||||
},
|
||||
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{
|
||||
Name: "a-devfile-name",
|
||||
}, "1.0.0", api.Registry{
|
||||
Name: "a-registry",
|
||||
alizerClient.EXPECT().DetectFramework(gomock.Any(), gomock.Any()).Return(alizer.DetectedFramework{
|
||||
Type: model.DevFileType{
|
||||
Name: "a-devfile-name",
|
||||
},
|
||||
DefaultVersion: "1.0.0",
|
||||
Registry: api.Registry{
|
||||
Name: "a-registry",
|
||||
},
|
||||
}, nil)
|
||||
alizerClient.EXPECT().DetectPorts(gomock.Any()).Return([]int{1234, 5678}, nil)
|
||||
return alizerClient
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ func TestFlagsBackend_SelectDevfile(t *testing.T) {
|
||||
Devfile: "adevfile",
|
||||
DevfilePath: "apath",
|
||||
DevfileRegistry: "aregistry",
|
||||
Architectures: []string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -283,32 +283,37 @@ 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 != "" {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
return false
|
||||
}
|
||||
if !strings.Contains(devfile.Name, filterFlag) && !strings.Contains(devfile.Description, filterFlag) && !containsArch(filterFlag) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user