Skip to content

Commit

Permalink
Autodetect project name via Alizer (redhat-developer#5989)
Browse files Browse the repository at this point in the history
* Autodetect project name via Alizer

<!--
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>

* update based on review

Signed-off-by: Charlie Drage <charlie@charliedrage.com>

Signed-off-by: Charlie Drage <charlie@charliedrage.com>
  • Loading branch information
cdrage authored Aug 22, 2022
1 parent 491e2bf commit 83ad3ee
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 2 deletions.
73 changes: 73 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,70 @@ 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
dir, err := os.Stat(path)
if err != nil {
return "", err
}

// Check to see if the path is a directory, and fail if it is not
if !dir.IsDir() {
return "", fmt.Errorf("alizer DetectName %q path is not a directory", path)
}

// 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
}
klog.V(4).Infof("Found components: %v", components)

// 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
// The directory name SHOULD be the path (we use a previous check to see if it's "itsdir"
if detectedName == "" {
detectedName = filepath.Base(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
}
61 changes: 61 additions & 0 deletions pkg/alizer/alizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,64 @@ func TestDetectFramework(t *testing.T) {
})
}
}

func TestDetectName(t *testing.T) {

type args struct {
path string
}
tests := []struct {
name string
args args
wantedName string
wantErr bool
}{
{
name: "Case 1: Detect Node.JS name through package.json",
args: args{
path: GetTestProjectPath("nodejs"),
},
wantedName: "node-echo",
wantErr: false,
},
{
// NOTE
// Alizer does NOT support Python yet, so this test is expected to fail once Python support
// is implemented
name: "Case 2: Detect Python name through DIRECTORY name",
args: args{
path: GetTestProjectPath("python"),
},
// Directory name is 'python' so expect that name to be returned
wantedName: "python",
wantErr: false,
},
{

// NOTE
// Returns "insultapp" instead of "InsultApp" as it does DNS1123 sanitization
// See DetectName function
name: "Case 3: Detect Java name through pom.xml",
args: args{
path: GetTestProjectPath("wildfly"),
},
wantedName: "insultapp",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

name, err := DetectName(tt.args.path)

if !tt.wantErr == (err != nil) {
t.Errorf("unexpected error %v, wantErr %v", err, tt.wantErr)
return
}

if name != tt.wantedName {
t.Errorf("unexpected name %q, wanted: %q", name, tt.wantedName)
}
})
}
}
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
32 changes: 31 additions & 1 deletion pkg/init/backend/interactive.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package backend

import (
"fmt"
"path/filepath"
"sort"
"strconv"

"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/pkg/devfile/parser"
parsercommon "github.com/devfile/library/pkg/devfile/parser/data/v2/common"
"k8s.io/klog"

"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 +107,34 @@ func (o *InteractiveBackend) SelectStarterProject(devfile parser.DevfileObj, fla
}

func (o *InteractiveBackend) PersonalizeName(devfile parser.DevfileObj, flags map[string]string) (string, error) {
return o.askerClient.AskName(fmt.Sprintf("my-%s-app", devfile.GetMetadataName()))

// 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)
}

// Pass in the BASE directory (not the file name of devfile.yaml)
// Convert path to base dir not file name
baseDir := filepath.Dir(path)

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

klog.V(4).Infof("Detected name via alizer: %q from path: %q", name, baseDir)

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

return o.askerClient.AskName(name)
}

func (o *InteractiveBackend) PersonalizeDevfileConfig(devfileobj parser.DevfileObj) (parser.DevfileObj, error) {
Expand Down
2 changes: 2 additions & 0 deletions pkg/init/backend/interactive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ func TestInteractiveBackend_SelectStarterProject(t *testing.T) {
}

func TestInteractiveBackend_PersonalizeName(t *testing.T) {

type fields struct {
asker func(ctrl *gomock.Controller) asker.Asker
registryClient registry.Client
Expand Down Expand Up @@ -226,6 +227,7 @@ func TestInteractiveBackend_PersonalizeName(t *testing.T) {
args: args{
devfile: func(fs filesystem.Filesystem) parser.DevfileObj {
devfileData, _ := data.NewDevfileData(string(data.APISchemaVersion200))

obj := parser.DevfileObj{
Ctx: parsercontext.FakeContext(fs, "/tmp/devfile.yaml"),
Data: devfileData,
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
46 changes: 46 additions & 0 deletions tests/integration/interactive_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,52 @@ 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"
projectName := "node-echo"

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, "Enter component name")
helper.SendLine(ctx, "\n")

helper.ExpectString(ctx, fmt.Sprintf("Your new component '%s' is ready in the current directory", projectName))
})
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(fmt.Sprintf("Your new component '%s' is ready in the current directory", projectName)))

})
})
})

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

0 comments on commit 83ad3ee

Please sign in to comment.