Skip to content

Commit

Permalink
Autodetect project name via Alizer
Browse files Browse the repository at this point in the history
<!--
Thank you for opening a PR! Here are some things you need to know before submitting:

1. Please read our developer guideline: https://github.com/redhat-developer/odo/wiki/Dev:-odo-Dev-Guidelines
2. Label this PR accordingly with the '/kind' line
3. Ensure you have written and ran the appropriate tests: https://github.com/redhat-developer/odo/wiki/Dev:-Writing-and-running-tests
4. Read how we approve and LGTM each PR: https://github.com/redhat-developer/odo/wiki/Pull-Requests:-Review-guideline

Documentation:

If you are pushing a change to documentation, please read: https://github.com/redhat-developer/odo/wiki/Documentation:-Contributing
-->

**What type of PR is this:**

<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind tests
/kind documentation

Feel free to use other [labels](https://github.com/redhat-developer/odo/labels) as needed. However one of the above labels must be present or the PR will not be reviewed. This instruction is for reviewers as well.
-->
/kind feature

**What does this PR do / why we need it:**

This PR:
* Detects the project name via pom.xml (java), package.json (node.js),
  etc. with alizer.
* If the detection fails, default to using the directory name.

**Which issue(s) this PR fixes:**
<!--
Specifying the issue will automatically close it when this PR is merged
-->

Fixes redhat-developer#5847

**PR acceptance criteria:**

- [ ] Unit test

- [ ] Integration test

- [ ] Documentation

**How to test changes / Special notes to the reviewer:**

Signed-off-by: Charlie Drage <charlie@charliedrage.com>
  • Loading branch information
cdrage committed Aug 4, 2022
1 parent 0a89798 commit 9d83656
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 18 deletions.
66 changes: 66 additions & 0 deletions pkg/alizer/alizer.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package alizer

import (
"fmt"
"os"
"path/filepath"

"github.com/redhat-developer/alizer/go/pkg/apis/recognizer"
"github.com/redhat-developer/odo/pkg/api"
"github.com/redhat-developer/odo/pkg/registry"
"github.com/redhat-developer/odo/pkg/util"
"k8s.io/klog"
)

type Alizer struct {
Expand Down Expand Up @@ -47,3 +53,63 @@ func GetDevfileLocationFromDetection(typ recognizer.DevFileType, registry api.Re
DevfileRegistry: registry.Name,
}
}

// 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
//
// If no source is detected:
// 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 DetectName(path string) (string, error) {
if path == "" {
return "", fmt.Errorf("path is empty")
}

// Check if the path exists using os.Stat
_, err := os.Stat(path)
if err != nil {
return "", err
}

// Step 1.
// Get the name of the directory from the devfile absolute path
// Use that path with Alizer to get the name of the project,
// if unable to find the name, we will use the directory name
components, err := recognizer.DetectComponents(path)
if err != nil {
return "", err
}

// Take the first name that is found
var detectedName string
if len(components) > 0 {
detectedName = components[0].Name
}

// Step 2. If unable to detect the name, we will use the directory name.
// Alizer will not correctly default to the directory name when unable to detect it via pom.xml, package.json, etc.
// So we must do it ourselves
if detectedName == "" {
detectedName = filepath.Base(filepath.Dir(path))
}

// Step 3. Sanitize the name
// Make sure that detectedName conforms with Kubernetes naming rules
// If not, we will use the directory name
name := util.GetDNS1123Name(detectedName)
klog.V(4).Infof("Path: %s, Detected name: %s, Sanitized name: %s", path, detectedName, name)
if name == "" {
return "", fmt.Errorf("unable to sanitize name to DNS1123 format: %q", name)
}

return name, nil
}
15 changes: 15 additions & 0 deletions pkg/alizer/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions pkg/init/asker/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion pkg/init/backend/alizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,12 @@ func (o *AlizerBackend) SelectStarterProject(devfile parser.DevfileObj, flags ma
}

func (o *AlizerBackend) PersonalizeName(devfile parser.DevfileObj, flags map[string]string) (string, error) {
return devfile.GetMetadataName(), nil
// Get the absolute path to the directory from the Devfile context
path := devfile.Ctx.GetAbsPath()
if path == "" {
return "", fmt.Errorf("cannot determine the absolute path of the directory")
}
return alizer.DetectName(path)
}

func (o *AlizerBackend) PersonalizeDevfileConfig(devfile parser.DevfileObj) (parser.DevfileObj, error) {
Expand Down
22 changes: 22 additions & 0 deletions pkg/init/backend/interactive.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/devfile/library/pkg/devfile/parser"
parsercommon "github.com/devfile/library/pkg/devfile/parser/data/v2/common"

"github.com/redhat-developer/odo/pkg/alizer"
"github.com/redhat-developer/odo/pkg/api"
"github.com/redhat-developer/odo/pkg/init/asker"
"github.com/redhat-developer/odo/pkg/log"
Expand Down Expand Up @@ -104,7 +105,28 @@ func (o *InteractiveBackend) SelectStarterProject(devfile parser.DevfileObj, fla
}

func (o *InteractiveBackend) PersonalizeName(devfile parser.DevfileObj, flags map[string]string) (string, error) {

// We will retrieve the name using alizer and then suggest it as the default name.
// 1. Check the pom.xml / package.json / etc. for the project name
// 2. If not, use the directory name instead

// Get the absolute path to the directory from the Devfile context
path := devfile.Ctx.GetAbsPath()
if path == "" {
return "", fmt.Errorf("unable to get the absolute path to the directory: %q", path)
}

name, err := alizer.DetectName(path)
if err != nil {
return "", fmt.Errorf("detecting name using alizer: %w", err)
}

if name == "" {
return "", fmt.Errorf("unable to detect the name")
}

return o.askerClient.AskName(fmt.Sprintf("my-%s-app", devfile.GetMetadataName()))
//return o.askerClient.AskName(fmt.Sprintf("%s", name))
}

func (o *InteractiveBackend) PersonalizeDevfileConfig(devfileobj parser.DevfileObj) (parser.DevfileObj, error) {
Expand Down
37 changes: 20 additions & 17 deletions pkg/init/backend/interactive_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package backend

import (
"io/ioutil"
"log"
"os"
"reflect"
"testing"

Expand Down Expand Up @@ -198,13 +201,16 @@ func TestInteractiveBackend_SelectStarterProject(t *testing.T) {
}
}

// TestInteractiveBackend_PersonalizeName will check to see if the personalize name works with Alizer.
// for this, we create a temporary file.
// If we use a "fake" filesystem in memory, the alizer library will not be able to find the file / end in a nil pointer.
func TestInteractiveBackend_PersonalizeName(t *testing.T) {
type fields struct {
asker func(ctrl *gomock.Controller) asker.Asker
registryClient registry.Client
}
type args struct {
devfile func(fs filesystem.Filesystem) parser.DevfileObj
devfile func(tmpDevfile string) parser.DevfileObj
flags map[string]string
}
tests := []struct {
Expand All @@ -215,23 +221,19 @@ func TestInteractiveBackend_PersonalizeName(t *testing.T) {
checkResult func(newName string, args args) bool
}{
{
name: "no flag",
fields: fields{
asker: func(ctrl *gomock.Controller) asker.Asker {
client := asker.NewMockAsker(ctrl)
client.EXPECT().AskName(gomock.Any()).Return("aname", nil)
return client
},
},
name: "no flag",
fields: fields{},
args: args{
devfile: func(fs filesystem.Filesystem) parser.DevfileObj {
devfile: func(tmpDevfile string) parser.DevfileObj {
devfileData, _ := data.NewDevfileData(string(data.APISchemaVersion200))

obj := parser.DevfileObj{
Ctx: parsercontext.FakeContext(fs, "/tmp/devfile.yaml"),
Ctx: parsercontext.FakeContext(filesystem.DefaultFs{}, tmpDevfile),
Data: devfileData,
}
return obj
},

flags: map[string]string{},
},
wantErr: false,
Expand All @@ -241,17 +243,18 @@ func TestInteractiveBackend_PersonalizeName(t *testing.T) {
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
var askerClient asker.Asker
if tt.fields.asker != nil {
askerClient = tt.fields.asker(ctrl)
}
o := &InteractiveBackend{
askerClient: askerClient,
registryClient: tt.fields.registryClient,
}
fs := filesystem.NewFakeFs()
newName, err := o.PersonalizeName(tt.args.devfile(fs), tt.args.flags)

tmpFile, err := ioutil.TempFile(os.TempDir(), "prefix-")
if err != nil {
log.Fatal("Cannot create temporary file", err)
}

newName, err := o.PersonalizeName(tt.args.devfile(tmpFile.Name()), tt.args.flags)
if (err != nil) != tt.wantErr {
t.Errorf("InteractiveBackend.PersonalizeName() error = %v, wantErr %v", err, tt.wantErr)
return
Expand Down
1 change: 1 addition & 0 deletions pkg/init/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ func (o *InitClient) DownloadStarterProject(starter *v1alpha2.StarterProject, de
// PersonalizeName calls PersonalizeName methods of the adequate backend
func (o *InitClient) PersonalizeName(devfile parser.DevfileObj, flags map[string]string) (string, error) {
var backend backend.InitBackend

if len(flags) == 0 {
backend = o.interactiveBackend
} else {
Expand Down
42 changes: 42 additions & 0 deletions tests/integration/interactive_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,48 @@ var _ = Describe("odo init interactive command tests", func() {
})
})
})

When("alizer detection of javascript name", func() {

BeforeEach(func() {
helper.CopyExample(filepath.Join("source", "nodejs"), commonVar.Context)
Expect(helper.ListFilesInDir(commonVar.Context)).To(
SatisfyAll(
HaveLen(3),
ContainElements("Dockerfile", "package.json", "server.js")))
})

It("should display node-echo name", func() {
language := "javascript"
projectType := "nodejs"

output, err := helper.RunInteractive([]string{"odo", "init"}, nil, func(ctx helper.InteractiveContext) {
helper.ExpectString(ctx, "Based on the files in the current directory odo detected")

helper.ExpectString(ctx, fmt.Sprintf("Language: %s", language))

helper.ExpectString(ctx, fmt.Sprintf("Project type: %s", projectType))

helper.ExpectString(ctx,
fmt.Sprintf("The devfile \"%s\" from the registry \"DefaultDevfileRegistry\" will be downloaded.", projectType))

helper.ExpectString(ctx, "Is this correct")
helper.SendLine(ctx, "\n")

helper.ExpectString(ctx, "Select container for which you want to change configuration")
helper.SendLine(ctx, "\n")

helper.ExpectString(ctx, "Your new component 'node-echo' is ready in the current directory")
})
Expect(err).To(BeNil())

lines, err := helper.ExtractLines(output)
Expect(err).To(BeNil())
Expect(len(lines)).To(BeNumerically(">", 2))
Expect(lines[len(lines)-1]).To(Equal("Your new component 'node-echo' is ready in the current directory"))

})
})
})

It("should start downloading starter project only after all interactive questions have been asked", func() {
Expand Down

0 comments on commit 9d83656

Please sign in to comment.