From 9bf3883aeb4fc3e73325aa0228224b93ee6d464f Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Thu, 24 Nov 2022 18:03:36 +0100 Subject: [PATCH] wip --- pkg/alizer/alizer.go | 30 ++++- pkg/alizer/interface.go | 1 + pkg/alizer/mock.go | 15 +++ pkg/api/{devfile-location.go => analyze.go} | 7 +- pkg/init/backend/alizer.go | 27 +++- pkg/init/backend/alizer_test.go | 4 +- pkg/init/backend/applicationports.go | 65 +++++++++ pkg/init/backend/applicationports_test.go | 138 ++++++++++++++++++++ pkg/init/backend/flags.go | 9 +- pkg/init/backend/flags_test.go | 4 +- pkg/init/backend/interactive.go | 8 +- pkg/init/backend/interactive_test.go | 6 +- pkg/init/backend/interface.go | 8 +- pkg/init/backend/mock.go | 19 ++- pkg/init/init.go | 32 ++++- pkg/init/init_test.go | 2 +- pkg/init/interface.go | 9 +- pkg/init/mock.go | 25 +++- pkg/odo/cli/alizer/alizer.go | 10 +- pkg/odo/cli/init/init.go | 11 +- pkg/testingutil/devfile.go | 12 +- tests/integration/interactive_init_test.go | 53 ++++++++ 22 files changed, 445 insertions(+), 50 deletions(-) rename pkg/api/{devfile-location.go => analyze.go} (78%) create mode 100644 pkg/init/backend/applicationports.go create mode 100644 pkg/init/backend/applicationports_test.go diff --git a/pkg/alizer/alizer.go b/pkg/alizer/alizer.go index a64e7562303..a53407bd413 100644 --- a/pkg/alizer/alizer.go +++ b/pkg/alizer/alizer.go @@ -50,7 +50,7 @@ func (o *Alizer) DetectFramework(ctx context.Context, path string) (model.DevFil return types[typ], components.Items[typ].Registry, nil } -// DetectName retrieves the name of the project (if available) +// DetectName retrieves the name of the project (if available). // If source code is detected: // 1. Detect the name (pom.xml for java, package.json for nodejs, etc.) // 2. If unable to detect the name, use the directory name @@ -59,11 +59,11 @@ func (o *Alizer) DetectFramework(ctx context.Context, path string) (model.DevFil // 1. Use the directory name // // Last step. Sanitize the name so it's valid for a component name - +// // Use: // import "github.com/redhat-developer/alizer/pkg/apis/recognizer" // components, err := recognizer.DetectComponents("./") - +// // In order to detect the name, the name will first try to find out the name based on the program (pom.xml, etc.) but then if not, it will use the dir name. func (o *Alizer) DetectName(path string) (string, error) { if path == "" { @@ -117,9 +117,25 @@ func (o *Alizer) DetectName(path string) (string, error) { return name, nil } -func GetDevfileLocationFromDetection(typ model.DevFileType, registry api.Registry) *api.DevfileLocation { - return &api.DevfileLocation{ - Devfile: typ.Name, - DevfileRegistry: registry.Name, +func (o *Alizer) DetectPorts(path string) ([]int, error) { + //TODO(rm3l): Find a better way not to call recognizer.DetectComponents multiple times (in DetectFramework, DetectName and DetectPorts) + components, err := recognizer.DetectComponents(path) + if err != nil { + return nil, err + } + + if len(components) == 0 { + klog.V(4).Infof("no components found at path %q", path) + return nil, nil + } + + return components[0].Ports, nil +} + +func NewDetectionResult(typ model.DevFileType, registry api.Registry, appPorts []int) *api.DetectionResult { + return &api.DetectionResult{ + Devfile: typ.Name, + DevfileRegistry: registry.Name, + ApplicationPorts: appPorts, } } diff --git a/pkg/alizer/interface.go b/pkg/alizer/interface.go index 3c55b5abe6f..bfdc99d8b95 100644 --- a/pkg/alizer/interface.go +++ b/pkg/alizer/interface.go @@ -10,4 +10,5 @@ import ( type Client interface { DetectFramework(ctx context.Context, path string) (model.DevFileType, api.Registry, error) DetectName(path string) (string, error) + DetectPorts(path string) ([]int, error) } diff --git a/pkg/alizer/mock.go b/pkg/alizer/mock.go index 4f73ee587a1..df77ec28e72 100644 --- a/pkg/alizer/mock.go +++ b/pkg/alizer/mock.go @@ -66,3 +66,18 @@ func (mr *MockClientMockRecorder) DetectName(path interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DetectName", reflect.TypeOf((*MockClient)(nil).DetectName), path) } + +// DetectPorts mocks base method. +func (m *MockClient) DetectPorts(path string) ([]int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DetectPorts", path) + ret0, _ := ret[0].([]int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DetectPorts indicates an expected call of DetectPorts. +func (mr *MockClientMockRecorder) DetectPorts(path interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DetectPorts", reflect.TypeOf((*MockClient)(nil).DetectPorts), path) +} diff --git a/pkg/api/devfile-location.go b/pkg/api/analyze.go similarity index 78% rename from pkg/api/devfile-location.go rename to pkg/api/analyze.go index 47afdd4f927..b6b1f696044 100644 --- a/pkg/api/devfile-location.go +++ b/pkg/api/analyze.go @@ -1,7 +1,7 @@ package api -// DevfileLocation indicates the location of a devfile, either in a devfile registry or using a path or an URI -type DevfileLocation struct { +// DetectionResult indicates the location of a devfile, either in a devfile registry or using a path or an URI +type DetectionResult struct { // name of the Devfile in Devfile registry (required if DevfilePath is not defined) Devfile string `json:"devfile,omitempty"` @@ -10,4 +10,7 @@ type DevfileLocation struct { // path to a devfile. This is alternative to using devfile from Devfile registry. It can be local filesystem path or http(s) URL (required if Devfile is not defined) DevfilePath string `json:"devfilePath,omitempty"` + + // list of ports detected f + ApplicationPorts []int `json:"ports,omitempty"` } diff --git a/pkg/init/backend/alizer.go b/pkg/init/backend/alizer.go index 96514400345..017240e1b1d 100644 --- a/pkg/init/backend/alizer.go +++ b/pkg/init/backend/alizer.go @@ -3,9 +3,12 @@ package backend import ( "context" "fmt" + "strconv" + "strings" "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/devfile/library/pkg/devfile/parser" + "github.com/redhat-developer/odo/pkg/alizer" "github.com/redhat-developer/odo/pkg/api" "github.com/redhat-developer/odo/pkg/init/asker" @@ -31,13 +34,27 @@ func (o *AlizerBackend) Validate(flags map[string]string, fs filesystem.Filesyst } // SelectDevfile calls thz 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) (location *api.DevfileLocation, err error) { +func (o *AlizerBackend) SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (location *api.DetectionResult, err error) { selected, registry, err := o.alizerClient.DetectFramework(ctx, dir) if err != nil { return nil, err } - fmt.Printf("Based on the files in the current directory odo detected\nLanguage: %s\nProject type: %s\n", selected.Language, selected.ProjectType) + msg := fmt.Sprintf("Based on the files in the current directory odo detected\nLanguage: %s\nProject type: %s", selected.Language, selected.ProjectType) + + appPorts, err := o.alizerClient.DetectPorts(dir) + if err != nil { + return nil, err + } + appPortsAsString := make([]string, 0, len(appPorts)) + for _, p := range appPorts { + appPortsAsString = append(appPortsAsString, strconv.Itoa(p)) + } + if len(appPorts) > 0 { + msg += fmt.Sprintf("\nApplication ports: %s", strings.Join(appPortsAsString, ", ")) + } + + fmt.Println(msg) fmt.Printf("The devfile %q from the registry %q will be downloaded.\n", selected.Name, registry.Name) confirm, err := o.askerClient.AskCorrect() if err != nil { @@ -46,7 +63,7 @@ func (o *AlizerBackend) SelectDevfile(ctx context.Context, flags map[string]stri if !confirm { return nil, nil } - return alizer.GetDevfileLocationFromDetection(selected, registry), nil + return alizer.NewDetectionResult(selected, registry, appPorts), nil } func (o *AlizerBackend) SelectStarterProject(devfile parser.DevfileObj, flags map[string]string) (starter *v1alpha2.StarterProject, err error) { @@ -65,3 +82,7 @@ func (o *AlizerBackend) PersonalizeName(devfile parser.DevfileObj, flags map[str func (o *AlizerBackend) PersonalizeDevfileConfig(devfile parser.DevfileObj) (parser.DevfileObj, error) { return devfile, nil } + +func (o *AlizerBackend) HandleApplicationPorts(devfileobj parser.DevfileObj, ports []int, flags map[string]string) (parser.DevfileObj, error) { + return devfileobj, nil +} diff --git a/pkg/init/backend/alizer_test.go b/pkg/init/backend/alizer_test.go index 78d8584e0da..8d2cdcb09e9 100644 --- a/pkg/init/backend/alizer_test.go +++ b/pkg/init/backend/alizer_test.go @@ -38,7 +38,7 @@ func TestAlizerBackend_SelectDevfile(t *testing.T) { name string fields fields args args - wantLocation *api.DevfileLocation + wantLocation *api.DetectionResult wantErr bool }{ { @@ -63,7 +63,7 @@ func TestAlizerBackend_SelectDevfile(t *testing.T) { fs: filesystem.DefaultFs{}, dir: GetTestProjectPath("nodejs"), }, - wantLocation: &api.DevfileLocation{ + wantLocation: &api.DetectionResult{ Devfile: "a-devfile-name", DevfileRegistry: "a-registry", }, diff --git a/pkg/init/backend/applicationports.go b/pkg/init/backend/applicationports.go new file mode 100644 index 00000000000..411d715a3fb --- /dev/null +++ b/pkg/init/backend/applicationports.go @@ -0,0 +1,65 @@ +package backend + +import ( + "fmt" + "io" + "strconv" + "strings" + + "github.com/devfile/library/pkg/devfile/parser" + parsercommon "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + "k8s.io/klog" +) + +// handleApplicationPorts updates the ports in the Devfile as needed. +// If there are multiple container components in the Devfile, nothing is done. This will be handled in https://github.com/redhat-developer/odo/issues/6264. +// Otherwise, all the container component endpoints/ports (other than Debug) are updated with the specified ports. +func handleApplicationPorts(w io.Writer, devfileobj parser.DevfileObj, ports []int) (parser.DevfileObj, error) { + if len(ports) == 0 { + return devfileobj, nil + } + + components, err := devfileobj.Data.GetDevfileContainerComponents(parsercommon.DevfileOptions{}) + if err != nil { + return parser.DevfileObj{}, err + } + nbContainerComponents := len(components) + klog.V(3).Infof("Found %d container components in Devfile at path %q", nbContainerComponents, devfileobj.Ctx.GetAbsPath()) + if nbContainerComponents == 0 { + // no container components => nothing to do + return devfileobj, nil + } + if nbContainerComponents > 1 { + klog.V(3).Infof("found more than 1 container components in Devfile at path %q => cannot find out which component needs to be updated."+ + "This case will be handled in https://github.com/redhat-developer/odo/issues/6264", devfileobj.Ctx.GetAbsPath()) + fmt.Fprintln(w, "\nApplication ports detected but the current Devfile contains multiple container components. Could not determine which component to update. "+ + "Please feel free to customize the Devfile configuration below.") + return devfileobj, nil + } + + component := components[0] + + //Remove all but Debug endpoints + var portsToRemove []string + for _, ep := range component.Container.Endpoints { + if ep.Name == "debug" || strings.HasPrefix(ep.Name, "debug-") { + continue + } + portsToRemove = append(portsToRemove, strconv.Itoa(ep.TargetPort)) + } + err = devfileobj.Data.RemovePorts(map[string][]string{component.Name: portsToRemove}) + if err != nil { + return parser.DevfileObj{}, err + } + + portsToSet := make([]string, 0, len(ports)) + for _, p := range ports { + portsToSet = append(portsToSet, strconv.Itoa(p)) + } + err = devfileobj.Data.SetPorts(map[string][]string{component.Name: portsToSet}) + if err != nil { + return parser.DevfileObj{}, err + } + + return devfileobj, err +} diff --git a/pkg/init/backend/applicationports_test.go b/pkg/init/backend/applicationports_test.go new file mode 100644 index 00000000000..b863c2584ee --- /dev/null +++ b/pkg/init/backend/applicationports_test.go @@ -0,0 +1,138 @@ +package backend + +import ( + "bytes" + "testing" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + devfilepkg "github.com/devfile/api/v2/pkg/devfile" + "github.com/devfile/library/pkg/devfile/parser" + devfileCtx "github.com/devfile/library/pkg/devfile/parser/context" + "github.com/devfile/library/pkg/devfile/parser/data" + devfilefs "github.com/devfile/library/pkg/testingutil/filesystem" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/redhat-developer/odo/pkg/testingutil" +) + +var fs = devfilefs.NewFakeFs() + +func buildDevfileObjWithComponents(components ...v1.Component) parser.DevfileObj { + devfileData, _ := data.NewDevfileData(string(data.APISchemaVersion220)) + devfileData.SetMetadata(devfilepkg.DevfileMetadata{Name: "my-nodejs-app"}) + _ = devfileData.AddComponents(components) + return parser.DevfileObj{ + Ctx: devfileCtx.FakeContext(fs, parser.OutputDevfileYamlPath), + Data: devfileData, + } +} + +func Test_handleApplicationPorts(t *testing.T) { + type devfileProvider func() parser.DevfileObj + type args struct { + devfileObjProvider devfileProvider + ports []int + } + + tests := []struct { + name string + args args + wantErr bool + wantProvider devfileProvider + }{ + { + name: "no component, no ports to set", + args: args{ + devfileObjProvider: func() parser.DevfileObj { return buildDevfileObjWithComponents() }, + }, + wantProvider: func() parser.DevfileObj { return buildDevfileObjWithComponents() }, + }, + { + name: "multiple container components, no ports to set", + args: args{ + devfileObjProvider: func() parser.DevfileObj { + return buildDevfileObjWithComponents( + testingutil.GetFakeContainerComponent("cont1", 8080, 8081, 8082), + testingutil.GetFakeContainerComponent("cont2", 9080, 9081, 9082)) + }, + }, + wantProvider: func() parser.DevfileObj { + return buildDevfileObjWithComponents( + testingutil.GetFakeContainerComponent("cont1", 8080, 8081, 8082), + testingutil.GetFakeContainerComponent("cont2", 9080, 9081, 9082)) + }, + }, + { + name: "no container components", + args: args{ + devfileObjProvider: func() parser.DevfileObj { + return buildDevfileObjWithComponents(testingutil.GetFakeVolumeComponent("vol1", "1Gi")) + }, + ports: []int{8888, 8889, 8890}, + }, + wantProvider: func() parser.DevfileObj { + return buildDevfileObjWithComponents(testingutil.GetFakeVolumeComponent("vol1", "1Gi")) + }, + }, + { + name: "more than one container components", + args: args{ + devfileObjProvider: func() parser.DevfileObj { + return buildDevfileObjWithComponents( + testingutil.GetFakeContainerComponent("cont1", 8080, 8081, 8082), + testingutil.GetFakeContainerComponent("cont2", 9080, 9081, 9082), + testingutil.GetFakeVolumeComponent("vol1", "1Gi")) + }, + ports: []int{8888, 8889, 8890}, + }, + wantProvider: func() parser.DevfileObj { + return buildDevfileObjWithComponents( + testingutil.GetFakeContainerComponent("cont1", 8080, 8081, 8082), + testingutil.GetFakeContainerComponent("cont2", 9080, 9081, 9082), + testingutil.GetFakeVolumeComponent("vol1", "1Gi")) + }, + }, + { + name: "single container component with both application and debug ports", + args: args{ + devfileObjProvider: func() parser.DevfileObj { + contWithDebug := testingutil.GetFakeContainerComponent("cont1", 18080, 18081, 18082) + contWithDebug.ComponentUnion.Container.Endpoints = append(contWithDebug.ComponentUnion.Container.Endpoints, + v1.Endpoint{Name: "debug", TargetPort: 5005}, + v1.Endpoint{Name: "debug-another", TargetPort: 5858}) + return buildDevfileObjWithComponents( + contWithDebug, + testingutil.GetFakeVolumeComponent("vol1", "1Gi")) + }, + ports: []int{3000, 9000}, + }, + wantProvider: func() parser.DevfileObj { + newCont := testingutil.GetFakeContainerComponent("cont1") + newCont.ComponentUnion.Container.Endpoints = append(newCont.ComponentUnion.Container.Endpoints, + v1.Endpoint{Name: "debug", TargetPort: 5005}, + v1.Endpoint{Name: "debug-another", TargetPort: 5858}, + v1.Endpoint{Name: "port-3000-tcp", TargetPort: 3000, Protocol: v1.TCPEndpointProtocol}, + v1.Endpoint{Name: "port-9000-tcp", TargetPort: 9000, Protocol: v1.TCPEndpointProtocol}) + return buildDevfileObjWithComponents( + newCont, + testingutil.GetFakeVolumeComponent("vol1", "1Gi")) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var output bytes.Buffer + got, err := handleApplicationPorts(&output, tt.args.devfileObjProvider(), tt.args.ports) + if (err != nil) != tt.wantErr { + t.Errorf("handleApplicationPorts() error = %v, wantErr %v", err, tt.wantErr) + return + } + if diff := cmp.Diff(tt.wantProvider(), got, + cmp.AllowUnexported(devfileCtx.DevfileCtx{}), + cmpopts.IgnoreInterfaces(struct{ devfilefs.Filesystem }{})); diff != "" { + t.Errorf("handleApplicationPorts() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/init/backend/flags.go b/pkg/init/backend/flags.go index 4f6f8fd0fa9..74b14b1a597 100644 --- a/pkg/init/backend/flags.go +++ b/pkg/init/backend/flags.go @@ -86,8 +86,8 @@ func (o *FlagsBackend) Validate(flags map[string]string, fs filesystem.Filesyste return nil } -func (o *FlagsBackend) SelectDevfile(ctx context.Context, flags map[string]string, _ filesystem.Filesystem, _ string) (*api.DevfileLocation, error) { - return &api.DevfileLocation{ +func (o *FlagsBackend) SelectDevfile(ctx context.Context, flags map[string]string, _ filesystem.Filesystem, _ string) (*api.DetectionResult, error) { + return &api.DetectionResult{ Devfile: flags[FLAG_DEVFILE], DevfileRegistry: flags[FLAG_DEVFILE_REGISTRY], DevfilePath: flags[FLAG_DEVFILE_PATH], @@ -123,3 +123,8 @@ func (o *FlagsBackend) PersonalizeName(_ parser.DevfileObj, flags map[string]str func (o FlagsBackend) PersonalizeDevfileConfig(devfileobj parser.DevfileObj) (parser.DevfileObj, error) { return devfileobj, nil } + +func (o FlagsBackend) HandleApplicationPorts(devfileobj parser.DevfileObj, ports []int, flags map[string]string) (parser.DevfileObj, error) { + // Currently not supported, but this will be done in a separate issue: https://github.com/redhat-developer/odo/issues/6211 + return devfileobj, nil +} diff --git a/pkg/init/backend/flags_test.go b/pkg/init/backend/flags_test.go index b2474c0deec..12f10267a78 100644 --- a/pkg/init/backend/flags_test.go +++ b/pkg/init/backend/flags_test.go @@ -25,7 +25,7 @@ func TestFlagsBackend_SelectDevfile(t *testing.T) { tests := []struct { name string fields fields - want *api.DevfileLocation + want *api.DetectionResult wantErr bool }{ { @@ -38,7 +38,7 @@ func TestFlagsBackend_SelectDevfile(t *testing.T) { }, }, wantErr: false, - want: &api.DevfileLocation{ + want: &api.DetectionResult{ Devfile: "adevfile", DevfilePath: "apath", DevfileRegistry: "aregistry", diff --git a/pkg/init/backend/interactive.go b/pkg/init/backend/interactive.go index 5bd7d7088f3..ada39bb50b5 100644 --- a/pkg/init/backend/interactive.go +++ b/pkg/init/backend/interactive.go @@ -48,8 +48,8 @@ func (o *InteractiveBackend) Validate(flags map[string]string, fs filesystem.Fil return nil } -func (o *InteractiveBackend) SelectDevfile(ctx context.Context, flags map[string]string, _ filesystem.Filesystem, _ string) (*api.DevfileLocation, error) { - result := &api.DevfileLocation{} +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) langs := devfileEntries.GetLanguages() @@ -260,6 +260,10 @@ func (o *InteractiveBackend) PersonalizeDevfileConfig(devfileobj parser.DevfileO return devfileobj, nil } +func (o *InteractiveBackend) HandleApplicationPorts(devfileobj parser.DevfileObj, ports []int, flags map[string]string) (parser.DevfileObj, error) { + return handleApplicationPorts(log.GetStdout(), devfileobj, ports) +} + func PrintConfiguration(config asker.DevfileConfiguration) { var keys []string diff --git a/pkg/init/backend/interactive_test.go b/pkg/init/backend/interactive_test.go index 67c27d77ce7..c7bf87e0623 100644 --- a/pkg/init/backend/interactive_test.go +++ b/pkg/init/backend/interactive_test.go @@ -28,7 +28,7 @@ func TestInteractiveBackend_SelectDevfile(t *testing.T) { tests := []struct { name string fields fields - want *api.DevfileLocation + want *api.DetectionResult wantErr bool }{ { @@ -51,7 +51,7 @@ func TestInteractiveBackend_SelectDevfile(t *testing.T) { return client }, }, - want: &api.DevfileLocation{ + want: &api.DetectionResult{ Devfile: "a-devfile-name", DevfileRegistry: "MyRegistry1", }, @@ -78,7 +78,7 @@ func TestInteractiveBackend_SelectDevfile(t *testing.T) { return client }, }, - want: &api.DevfileLocation{ + want: &api.DetectionResult{ Devfile: "a-devfile-name", DevfileRegistry: "MyRegistry1", }, diff --git a/pkg/init/backend/interface.go b/pkg/init/backend/interface.go index f770c77754c..ee756dce619 100644 --- a/pkg/init/backend/interface.go +++ b/pkg/init/backend/interface.go @@ -1,4 +1,4 @@ -// package backend provides different backends to initiate projects. +// Package backend provides different backends to initiate projects. // - `Flags` backend gets needed information from command line flags. // - `Interactive` backend interacts with the user to get needed information. package backend @@ -8,6 +8,7 @@ import ( "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/devfile/library/pkg/devfile/parser" + "github.com/redhat-developer/odo/pkg/api" "github.com/redhat-developer/odo/pkg/testingutil/filesystem" ) @@ -18,7 +19,7 @@ type InitBackend interface { Validate(flags map[string]string, fs filesystem.Filesystem, dir string) error // SelectDevfile selects a devfile and returns its location information, depending on the flags - SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (location *api.DevfileLocation, err error) + SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (location *api.DetectionResult, err error) // SelectStarterProject selects a starter project from the devfile and returns information about the starter project, // depending on the flags. If not starter project is selected, a nil starter is returned @@ -30,4 +31,7 @@ type InitBackend interface { // PersonalizeDevfileConfig updates the devfile config for ports and environment variables PersonalizeDevfileConfig(devfileobj parser.DevfileObj) (parser.DevfileObj, error) + + // HandleApplicationPorts updates the ports in the Devfile accordingly. + HandleApplicationPorts(devfileobj parser.DevfileObj, ports []int, flags map[string]string) (parser.DevfileObj, error) } diff --git a/pkg/init/backend/mock.go b/pkg/init/backend/mock.go index 898fbcf1103..017895f8074 100644 --- a/pkg/init/backend/mock.go +++ b/pkg/init/backend/mock.go @@ -38,6 +38,21 @@ func (m *MockInitBackend) EXPECT() *MockInitBackendMockRecorder { return m.recorder } +// HandleApplicationPorts mocks base method. +func (m *MockInitBackend) HandleApplicationPorts(devfileobj parser.DevfileObj, ports []int, flags map[string]string) (parser.DevfileObj, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HandleApplicationPorts", devfileobj, ports, flags) + ret0, _ := ret[0].(parser.DevfileObj) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HandleApplicationPorts indicates an expected call of HandleApplicationPorts. +func (mr *MockInitBackendMockRecorder) HandleApplicationPorts(devfileobj, ports, flags interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleApplicationPorts", reflect.TypeOf((*MockInitBackend)(nil).HandleApplicationPorts), devfileobj, ports, flags) +} + // PersonalizeDevfileConfig mocks base method. func (m *MockInitBackend) PersonalizeDevfileConfig(devfileobj parser.DevfileObj) (parser.DevfileObj, error) { m.ctrl.T.Helper() @@ -69,10 +84,10 @@ func (mr *MockInitBackendMockRecorder) PersonalizeName(devfile, flags interface{ } // SelectDevfile mocks base method. -func (m *MockInitBackend) SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (*api.DevfileLocation, error) { +func (m *MockInitBackend) SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (*api.DetectionResult, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SelectDevfile", ctx, flags, fs, dir) - ret0, _ := ret[0].(*api.DevfileLocation) + ret0, _ := ret[0].(*api.DetectionResult) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/pkg/init/init.go b/pkg/init/init.go index 2157ce0195b..6b1a420a3cb 100644 --- a/pkg/init/init.go +++ b/pkg/init/init.go @@ -77,7 +77,7 @@ func (o *InitClient) Validate(flags map[string]string, fs filesystem.Filesystem, } // SelectDevfile calls SelectDevfile methods of the adequate backend -func (o *InitClient) SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (*api.DevfileLocation, error) { +func (o *InitClient) SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (*api.DetectionResult, error) { var backend backend.InitBackend empty, err := location.DirIsEmpty(fs, dir) @@ -109,7 +109,7 @@ func (o *InitClient) SelectDevfile(ctx context.Context, flags map[string]string, return location, err } -func (o *InitClient) DownloadDevfile(ctx context.Context, devfileLocation *api.DevfileLocation, destDir string) (string, error) { +func (o *InitClient) DownloadDevfile(ctx context.Context, devfileLocation *api.DetectionResult, destDir string) (string, error) { destDevfile := filepath.Join(destDir, "devfile.yaml") if devfileLocation.DevfilePath != "" { return destDevfile, o.downloadDirect(devfileLocation.DevfilePath, destDevfile) @@ -238,7 +238,24 @@ func (o *InitClient) PersonalizeName(devfile parser.DevfileObj, flags map[string return backend.PersonalizeName(devfile, flags) } -func (o InitClient) PersonalizeDevfileConfig(devfileobj parser.DevfileObj, flags map[string]string, fs filesystem.Filesystem, dir string) (parser.DevfileObj, error) { +func (o *InitClient) HandleApplicationPorts(devfileobj parser.DevfileObj, ports []int, flags map[string]string, fs filesystem.Filesystem, dir string) (parser.DevfileObj, error) { + var backend backend.InitBackend + onlyDevfile, err := location.DirContainsOnlyDevfile(fs, dir) + if err != nil { + return parser.DevfileObj{}, err + } + + // Interactive mode since no flags are provided + if len(flags) == 0 && !onlyDevfile { + // Other files present in the directory; hence alizer is run + backend = o.interactiveBackend + } else { + backend = o.flagsBackend + } + return backend.HandleApplicationPorts(devfileobj, ports, flags) +} + +func (o *InitClient) PersonalizeDevfileConfig(devfileobj parser.DevfileObj, flags map[string]string, fs filesystem.Filesystem, dir string) (parser.DevfileObj, error) { var backend backend.InitBackend onlyDevfile, err := location.DirContainsOnlyDevfile(fs, dir) if err != nil { @@ -255,7 +272,7 @@ func (o InitClient) PersonalizeDevfileConfig(devfileobj parser.DevfileObj, flags return backend.PersonalizeDevfileConfig(devfileobj) } -func (o InitClient) SelectAndPersonalizeDevfile(ctx context.Context, flags map[string]string, contextDir string) (parser.DevfileObj, string, *api.DevfileLocation, error) { +func (o *InitClient) SelectAndPersonalizeDevfile(ctx context.Context, flags map[string]string, contextDir string) (parser.DevfileObj, string, *api.DetectionResult, error) { devfileLocation, err := o.SelectDevfile(ctx, flags, o.fsys, contextDir) if err != nil { return parser.DevfileObj{}, "", nil, err @@ -271,6 +288,11 @@ func (o InitClient) SelectAndPersonalizeDevfile(ctx context.Context, flags map[s return parser.DevfileObj{}, "", nil, fmt.Errorf("unable to parse devfile: %w", err) } + devfileObj, err = o.HandleApplicationPorts(devfileObj, devfileLocation.ApplicationPorts, flags, o.fsys, contextDir) + if err != nil { + return parser.DevfileObj{}, "", nil, fmt.Errorf("unable to set application ports in devfile: %w", err) + } + devfileObj, err = o.PersonalizeDevfileConfig(devfileObj, flags, o.fsys, contextDir) if err != nil { return parser.DevfileObj{}, "", nil, fmt.Errorf("failed to configure devfile: %w", err) @@ -278,7 +300,7 @@ func (o InitClient) SelectAndPersonalizeDevfile(ctx context.Context, flags map[s return devfileObj, devfilePath, devfileLocation, nil } -func (o InitClient) InitDevfile(ctx context.Context, flags map[string]string, contextDir string, +func (o *InitClient) InitDevfile(ctx context.Context, flags map[string]string, contextDir string, preInitHandlerFunc func(interactiveMode bool), newDevfileHandlerFunc func(newDevfileObj parser.DevfileObj) error) error { containsDevfile, err := location.DirectoryContainsDevfile(o.fsys, contextDir) diff --git a/pkg/init/init_test.go b/pkg/init/init_test.go index 883656a3f90..247130bd6af 100644 --- a/pkg/init/init_test.go +++ b/pkg/init/init_test.go @@ -179,7 +179,7 @@ func TestInitClient_downloadDirect(t *testing.T) { type fields struct { fsys func(fs filesystem.Filesystem) filesystem.Filesystem registryClient func(ctrl *gomock.Controller) registry.Client - InitParams api.DevfileLocation + InitParams api.DetectionResult } type args struct { URL string diff --git a/pkg/init/interface.go b/pkg/init/interface.go index 9b9e5ed1e62..6de301ac9e1 100644 --- a/pkg/init/interface.go +++ b/pkg/init/interface.go @@ -37,11 +37,11 @@ type Client interface { // SelectDevfile returns information about a devfile selected based on Alizer if the directory content, // or based on the flags if the directory is empty, or // interactively if flags is empty - SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (*api.DevfileLocation, error) + SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (*api.DetectionResult, error) // DownloadDevfile downloads a devfile given its location information and a destination directory // and returns the path of the downloaded file - DownloadDevfile(ctx context.Context, devfileLocation *api.DevfileLocation, destDir string) (string, error) + DownloadDevfile(ctx context.Context, devfileLocation *api.DetectionResult, destDir string) (string, error) // SelectStarterProject selects a starter project from the devfile and returns information about the starter project, // depending on the flags. If not starter project is selected, a nil starter is returned @@ -60,5 +60,8 @@ type Client interface { // SelectAndPersonalizeDevfile selects a devfile, then downloads, parse and personalize it // Returns the devfile object, its path and pointer to *api.devfileLocation - SelectAndPersonalizeDevfile(ctx context.Context, flags map[string]string, contextDir string) (parser.DevfileObj, string, *api.DevfileLocation, error) + SelectAndPersonalizeDevfile(ctx context.Context, flags map[string]string, contextDir string) (parser.DevfileObj, string, *api.DetectionResult, error) + + // HandleApplicationPorts updates the ports in the Devfile accordingly. + HandleApplicationPorts(devfileobj parser.DevfileObj, ports []int, flags map[string]string, fs filesystem.Filesystem, dir string) (parser.DevfileObj, error) } diff --git a/pkg/init/mock.go b/pkg/init/mock.go index 29362a5d588..7c04fa31d66 100644 --- a/pkg/init/mock.go +++ b/pkg/init/mock.go @@ -39,7 +39,7 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder { } // DownloadDevfile mocks base method. -func (m *MockClient) DownloadDevfile(ctx context.Context, devfileLocation *api.DevfileLocation, destDir string) (string, error) { +func (m *MockClient) DownloadDevfile(ctx context.Context, devfileLocation *api.DetectionResult, destDir string) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DownloadDevfile", ctx, devfileLocation, destDir) ret0, _ := ret[0].(string) @@ -81,6 +81,21 @@ func (mr *MockClientMockRecorder) GetFlags(flags interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFlags", reflect.TypeOf((*MockClient)(nil).GetFlags), flags) } +// HandleApplicationPorts mocks base method. +func (m *MockClient) HandleApplicationPorts(devfileobj parser.DevfileObj, ports []int, flags map[string]string, fs filesystem.Filesystem, dir string) (parser.DevfileObj, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HandleApplicationPorts", devfileobj, ports, flags, fs, dir) + ret0, _ := ret[0].(parser.DevfileObj) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HandleApplicationPorts indicates an expected call of HandleApplicationPorts. +func (mr *MockClientMockRecorder) HandleApplicationPorts(devfileobj, ports, flags, fs, dir interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleApplicationPorts", reflect.TypeOf((*MockClient)(nil).HandleApplicationPorts), devfileobj, ports, flags, fs, dir) +} + // InitDevfile mocks base method. func (m *MockClient) InitDevfile(ctx context.Context, flags map[string]string, contextDir string, preInitHandlerFunc func(bool), newDevfileHandlerFunc func(parser.DevfileObj) error) error { m.ctrl.T.Helper() @@ -126,12 +141,12 @@ func (mr *MockClientMockRecorder) PersonalizeName(devfile, flags interface{}) *g } // SelectAndPersonalizeDevfile mocks base method. -func (m *MockClient) SelectAndPersonalizeDevfile(ctx context.Context, flags map[string]string, contextDir string) (parser.DevfileObj, string, *api.DevfileLocation, error) { +func (m *MockClient) SelectAndPersonalizeDevfile(ctx context.Context, flags map[string]string, contextDir string) (parser.DevfileObj, string, *api.DetectionResult, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SelectAndPersonalizeDevfile", ctx, flags, contextDir) ret0, _ := ret[0].(parser.DevfileObj) ret1, _ := ret[1].(string) - ret2, _ := ret[2].(*api.DevfileLocation) + ret2, _ := ret[2].(*api.DetectionResult) ret3, _ := ret[3].(error) return ret0, ret1, ret2, ret3 } @@ -143,10 +158,10 @@ func (mr *MockClientMockRecorder) SelectAndPersonalizeDevfile(ctx, flags, contex } // SelectDevfile mocks base method. -func (m *MockClient) SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (*api.DevfileLocation, error) { +func (m *MockClient) SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (*api.DetectionResult, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SelectDevfile", ctx, flags, fs, dir) - ret0, _ := ret[0].(*api.DevfileLocation) + ret0, _ := ret[0].(*api.DetectionResult) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/pkg/odo/cli/alizer/alizer.go b/pkg/odo/cli/alizer/alizer.go index b69701a7cb6..f5849263ba2 100644 --- a/pkg/odo/cli/alizer/alizer.go +++ b/pkg/odo/cli/alizer/alizer.go @@ -46,15 +46,19 @@ func (o *AlizerOptions) Run(ctx context.Context) (err error) { return errors.New("this command can be run with json output only, please use the flag: -o json") } -// Run contains the logic for the odo command +// RunForJsonOutput contains the logic for the odo command func (o *AlizerOptions) RunForJsonOutput(ctx context.Context) (out interface{}, err error) { workingDir := odocontext.GetWorkingDirectory(ctx) df, reg, err := o.clientset.AlizerClient.DetectFramework(ctx, workingDir) if err != nil { return nil, err } - result := alizer.GetDevfileLocationFromDetection(df, reg) - return []api.DevfileLocation{*result}, nil + appPorts, err := o.clientset.AlizerClient.DetectPorts(workingDir) + if err != nil { + return nil, err + } + result := alizer.NewDetectionResult(df, reg, appPorts) + return []api.DetectionResult{*result}, nil } func NewCmdAlizer(name, fullName string) *cobra.Command { diff --git a/pkg/odo/cli/init/init.go b/pkg/odo/cli/init/init.go index b914b5a3d0c..1679cf8215c 100644 --- a/pkg/odo/cli/init/init.go +++ b/pkg/odo/cli/init/init.go @@ -128,11 +128,12 @@ To start editing your component, use 'odo dev' and open this folder in your favo Changes will be directly reflected on the cluster.`, devfileObj.Data.GetMetadata().Name) if len(o.flags) == 0 { - automateCommad := fmt.Sprintf("odo init --name %s --devfile %s --devfile-registry %s", name, devfileLocation.Devfile, devfileLocation.DevfileRegistry) + automateCommand := fmt.Sprintf("odo init --name %s --devfile %s --devfile-registry %s", name, devfileLocation.Devfile, devfileLocation.DevfileRegistry) if starterInfo != nil { - automateCommad = fmt.Sprintf("%s --starter %s", automateCommad, starterInfo.Name) + automateCommand = fmt.Sprintf("%s --starter %s", automateCommand, starterInfo.Name) } - log.Infof("\nPort configuration using flag is currently not supported \n\nYou can automate this command by executing:\n %s\n", automateCommad) + klog.V(2).Infof("Port configuration using flag is currently not supported") + log.Infof("\nYou can automate this command by executing:\n %s", automateCommand) } if libdevfile.HasDeployCommand(devfileObj.Data) { @@ -158,8 +159,8 @@ func (o *InitOptions) RunForJsonOutput(ctx context.Context) (out interface{}, er }, nil } -// run downloads the devfile and starter project and returns the content of the devfile, path of the devfile, name of the component, api.DevfileLocation object for DevfileRegistry info and StarterProject object -func (o *InitOptions) run(ctx context.Context) (devfileObj parser.DevfileObj, path string, name string, devfileLocation *api.DevfileLocation, starterInfo *v1alpha2.StarterProject, err error) { +// run downloads the devfile and starter project and returns the content of the devfile, path of the devfile, name of the component, api.DetectionResult object for DevfileRegistry info and StarterProject object +func (o *InitOptions) run(ctx context.Context) (devfileObj parser.DevfileObj, path string, name string, devfileLocation *api.DetectionResult, starterInfo *v1alpha2.StarterProject, err error) { var starterDownloaded bool workingDir := odocontext.GetWorkingDirectory(ctx) diff --git a/pkg/testingutil/devfile.go b/pkg/testingutil/devfile.go index 4b8bc91e490..8f57f5170f0 100644 --- a/pkg/testingutil/devfile.go +++ b/pkg/testingutil/devfile.go @@ -1,6 +1,7 @@ package testingutil import ( + "fmt" "path/filepath" "runtime" "strings" @@ -15,13 +16,21 @@ import ( ) // GetFakeContainerComponent returns a fake container component for testing -func GetFakeContainerComponent(name string) v1.Component { +func GetFakeContainerComponent(name string, ports ...int) v1.Component { image := "docker.io/maven:latest" memoryLimit := "128Mi" volumeName := "myvolume1" volumePath := "/my/volume/mount/path1" mountSources := true + var endpoints []v1.Endpoint + for _, p := range ports { + endpoints = append(endpoints, v1.Endpoint{ + Name: fmt.Sprintf("port-%d", p), + TargetPort: p, + }) + } + return v1.Component{ Name: name, ComponentUnion: v1.ComponentUnion{ @@ -36,6 +45,7 @@ func GetFakeContainerComponent(name string) v1.Component { }}, MountSources: &mountSources, }, + Endpoints: endpoints, }}} } diff --git a/tests/integration/interactive_init_test.go b/tests/integration/interactive_init_test.go index c13768a7393..cd380db628c 100644 --- a/tests/integration/interactive_init_test.go +++ b/tests/integration/interactive_init_test.go @@ -7,6 +7,11 @@ import ( "path/filepath" "strings" + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/library/pkg/devfile/parser" + "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + "k8s.io/utils/pointer" + "github.com/redhat-developer/odo/pkg/odo/cli/messages" "github.com/redhat-developer/odo/pkg/util" @@ -443,4 +448,52 @@ var _ = Describe("odo init interactive command tests", Label(helper.LabelNoClust Expect(helper.ListFilesInDir(commonVar.Context)).To(ContainElements("devfile.yaml")) }) + + Context("Automatic port detection via Alizer", func() { + + When("starting with an existing project", func() { + const appPort = 34567 + + BeforeEach(func() { + helper.CopyExample(filepath.Join("source", "nodejs"), commonVar.Context) + helper.ReplaceString(filepath.Join(commonVar.Context, "Dockerfile"), "EXPOSE 8080", fmt.Sprintf("EXPOSE %d", appPort)) + }) + + It("should display ports detected", func() { + _, err := helper.RunInteractive([]string{"odo", "init"}, nil, func(ctx helper.InteractiveContext) { + helper.ExpectString(ctx, fmt.Sprintf("Application ports: %d", appPort)) + + helper.SendLine(ctx, "Is this correct") + helper.SendLine(ctx, "") + + helper.ExpectString(ctx, "Select container for which you want to change configuration") + helper.SendLine(ctx, "") + + helper.ExpectString(ctx, "Enter component name") + helper.SendLine(ctx, "my-nodejs-app-with-port-detected") + + helper.ExpectString(ctx, "Your new component 'my-nodejs-app-with-port-detected' is ready in the current directory") + }) + Expect(err).ShouldNot(HaveOccurred()) + + // Now make sure the Devfile contains a single container component with the right endpoint + d, err := parser.ParseDevfile(parser.ParserArgs{Path: filepath.Join(commonVar.Context, "devfile.yaml"), FlattenedDevfile: pointer.BoolPtr(false)}) + Expect(err).ShouldNot(HaveOccurred()) + + containerComponents, err := d.Data.GetDevfileContainerComponents(common.DevfileOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + + allPortsExtracter := func(comps []v1alpha2.Component) []int { + var ports []int + for _, c := range comps { + for _, ep := range c.Container.Endpoints { + ports = append(ports, ep.TargetPort) + } + } + return ports + } + Expect(containerComponents).Should(WithTransform(allPortsExtracter, ContainElements(appPort))) + }) + }) + }) })