Skip to content

Commit

Permalink
⚠️ Add "owner" and "contributors" columns to application import CSV f…
Browse files Browse the repository at this point in the history
…ormat. (#551)

Adds two fields to the import CSV immediately before tags. One is for
the Owner and should be empty or contain a single value in the form
'John Doe <jdoe@example.com>'. The second is for contributors, and
should be empty or a list of one or more values in the previous format,
separated by commas. As this would be a comma-delimited field within a
CSV, the whole value should be surrounded by quotes. This breaks
compatibility with any existing import CSVs since it adds columns in the
middle (necessary since the number of tags is unbounded and as such the
tag list must come last).

Fixes #538

@jortel Do you think we can get away with adding this to migration 11,
or do you think I should create a new one?

---------

Signed-off-by: Sam Lucidi <slucidi@redhat.com>
  • Loading branch information
mansam authored Nov 8, 2023
1 parent 402c4e2 commit 57bb3f0
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 10 deletions.
12 changes: 9 additions & 3 deletions api/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const (
RecordTypeDependency = "2"
)

const (
ExpectedFieldCount = 17
)

//
// Import Statuses
const (
Expand Down Expand Up @@ -270,8 +274,8 @@ func (h ImportHandler) UploadCSV(ctx *gin.Context) {
var imp model.Import
switch row[0] {
case RecordTypeApplication:
// Check row format - length, expecting 15 fields + tags
if len(row) < 15 {
// Check row format - length, expecting 17 fields + tags
if len(row) < ExpectedFieldCount {
h.Respond(ctx, http.StatusBadRequest, gin.H{"errorMessage": "Invalid Application Import CSV format."})
return
}
Expand Down Expand Up @@ -396,10 +400,12 @@ func (h ImportHandler) applicationFromRow(fileName string, row []string) (app mo
RepositoryURL: row[12],
RepositoryBranch: row[13],
RepositoryPath: row[14],
Owner: row[15],
Contributors: row[16],
}

// Tags
for i := 15; i < len(row); i++ {
for i := ExpectedFieldCount; i < len(row); i++ {
if i%2 == 0 {
tag := model.ImportTag{
Name: row[i],
Expand Down
84 changes: 84 additions & 0 deletions importer/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,54 @@ func (m *Manager) createApplication(imp *model.Import) (ok bool) {
}
}

if imp.Owner != "" {
name, email, parsed := parseStakeholder(imp.Owner)
if !parsed {
imp.ErrorMessage = fmt.Sprintf("Could not parse Owner '%s'.", imp.Owner)
return
}
owner, found := m.findStakeholder(email)
if !found {
if imp.ImportSummary.CreateEntities {
var err error
owner, err = m.createStakeholder(name, email)
if err != nil {
imp.ErrorMessage = fmt.Sprintf("Owner '%s' could not be created.", imp.Owner)
return
}
} else {
imp.ErrorMessage = fmt.Sprintf("Owner '%s' could not be found.", imp.Owner)
return
}
}
app.OwnerID = &owner.ID
}
if imp.Contributors != "" {
fields := strings.Split(imp.Contributors, ",")
for _, f := range fields {
name, email, parsed := parseStakeholder(f)
if !parsed {
imp.ErrorMessage = fmt.Sprintf("Could not parse Contributor '%s'.", f)
return
}
contributor, found := m.findStakeholder(email)
if !found {
if imp.ImportSummary.CreateEntities {
var err error
contributor, err = m.createStakeholder(name, email)
if err != nil {
imp.ErrorMessage = fmt.Sprintf("Contributor '%s' could not be created.", imp.Owner)
return
}
} else {
imp.ErrorMessage = fmt.Sprintf("Contributor '%s' could not be found.", imp.Owner)
return
}
}
app.Contributors = append(app.Contributors, contributor)
}
}

result := m.DB.Create(app)
if result.Error != nil {
imp.ErrorMessage = result.Error.Error()
Expand All @@ -295,6 +343,25 @@ func (m *Manager) createApplication(imp *model.Import) (ok bool) {
return
}

func (m *Manager) createStakeholder(name string, email string) (stakeholder model.Stakeholder, err error) {
stakeholder.Name = name
stakeholder.Email = email
err = m.DB.Create(&stakeholder).Error
if err != nil {
err = liberr.Wrap(err)
}
return
}

func (m *Manager) findStakeholder(email string) (stakeholder model.Stakeholder, found bool) {
result := m.DB.First(&stakeholder, "email = ?", email)
if result.Error != nil {
return
}
found = true
return
}

//
// normalizedName transforms given name to be comparable as same with similar names
// Example: normalizedName(" F oo-123 bar! ") returns "foo123bar!"
Expand All @@ -304,3 +371,20 @@ func normalizedName(name string) (normName string) {
normName = invalidSymbols.ReplaceAllString(normName, "")
return
}

//
// parseStakeholder attempts to parse a stakeholder's name and an email address
// out of a string like `John Smith <jsmith@example.com>`. The pattern is very
// simple and treats anything before the first bracket as the name,
// and anything within the brackets as the email.
func parseStakeholder(s string) (name string, email string, parsed bool) {
pattern := regexp.MustCompile("(.+)\\s<(.+@.+)>")
matches := pattern.FindStringSubmatch(strings.TrimSpace(s))
if len(matches) != 3 {
return
}
parsed = true
name = matches[1]
email = strings.ToLower(matches[2])
return
}
2 changes: 2 additions & 0 deletions migration/v11/model/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ type Import struct {
RepositoryURL string
RepositoryBranch string
RepositoryPath string
Owner string
Contributors string
}

func (r *Import) AsMap() (m map[string]interface{}) {
Expand Down
20 changes: 17 additions & 3 deletions test/api/importcsv/api_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package importcsv

import (
"github.com/konveyor/tackle2-hub/api"
"github.com/konveyor/tackle2-hub/binding"
"github.com/konveyor/tackle2-hub/test/assert"
"io/ioutil"
"os"
"testing"
"time"
"github.com/konveyor/tackle2-hub/api"
"github.com/konveyor/tackle2-hub/binding"
"github.com/konveyor/tackle2-hub/test/assert"
)

func TestImportCSV(t *testing.T) {
Expand Down Expand Up @@ -68,6 +68,20 @@ func TestImportCSV(t *testing.T) {
if r.ExpectedApplications[i].BusinessService.Name != gotApp.BusinessService.Name {
t.Errorf("Mismatch in name of the BusinessService of imported Application: Expected %s, Actual %s", r.ExpectedApplications[i].BusinessService.Name, gotApp.BusinessService.Name)
}
if gotApp.Owner == nil || r.ExpectedApplications[i].Owner == nil {
if gotApp.Owner != r.ExpectedApplications[i].Owner {
t.Errorf("Mismatch in value of Owner on imported Application: Expected %v, Actual %v", r.ExpectedApplications[i].Owner, gotApp.BusinessService)
}
} else if r.ExpectedApplications[i].Owner.Name != gotApp.Owner.Name {
t.Errorf("Mismatch in name of the Owner of imported Application: Expected %s, Actual %s", r.ExpectedApplications[i].Owner.Name, gotApp.BusinessService.Name)
}
if len(gotApp.Contributors) != len(r.ExpectedApplications[i].Contributors) {
t.Errorf("Mismatch in number of Contributors: Expected %d, Actual %d", len(r.ExpectedApplications[i].Contributors), len(gotApp.Contributors))
} else {
for j, contributor := range gotApp.Contributors {
if contributor.Name != r.ExpectedApplications[i].Contributors[j].Name {}
}
}
}
}

Expand Down
22 changes: 22 additions & 0 deletions test/api/importcsv/samples.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ var (
BusinessService: &api.Ref{
Name: "Retail",
},
Owner: &api.Ref{
Name: "John Doe",
},
},
{
Name: "Inventory",
Expand Down Expand Up @@ -82,6 +85,14 @@ var (
BusinessService: &api.Ref{
Name: "Retail",
},
Contributors: []api.Ref{
{
Name: "John Doe",
},
{
Name: "Jane Smith",
},
},
},
{
Name: "Gateway",
Expand Down Expand Up @@ -112,6 +123,17 @@ var (
BusinessService: &api.Ref{
Name: "Retail",
},
Owner: &api.Ref{
Name: "John Doe",
},
Contributors: []api.Ref{
{
Name: "John Doe",
},
{
Name: "Jane Smith",
},
},
},
},
ExpectedDependencies: []api.Dependency{
Expand Down
8 changes: 4 additions & 4 deletions test/api/importcsv/template_application_import.csv
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Record Type 1,Application Name,Description,Comments,Business Service,Dependency,Dependency Direction,Binary Group,Binary Artifact,Binary Version,Binary Packaging,Repository Type,Repository URL,Repository Branch,Repository Path,Tag Category 1,Tag 1,Tag Category 2,Tag 2,Tag Category 3,Tag 3,Tag Category 4,Tag 4,Tag Category 5,Tag 5,Tag Category 6,Tag 6,Tag Category 7,Tag 7,Tag Category 8,Tag 8,Tag Category 9,Tag 9,Tag Category 10,Tag 10,Tag Category 11,Tag 11,Tag Category 12,Tag 12,Tag Category 13,Tag 13,Tag Category 14,Tag 14,Tag Category 15,Tag 15,Tag Category 16,Tag 16,Tag Category 17,Tag 17,Tag Category 18,Tag 18,Tag Category 19,Tag 19,Tag Category 20,Tag 20
1,Customers,Legacy Customers management service,,Retail,,,corp.acme.demo,customers-tomcat,0.0.1-SNAPSHOT,war,git,https://git-acme.local/customers.git,,,Operating System,RHEL 8,Database,Oracle,Language,Java,Runtime,Tomcat,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
1,Inventory,Inventory service,,Retail,,,corp.acme.demo,inventory,0.1.1-SNAPSHOT,war,git,https://git-acme.local/inventory.git,,,Operating System,RHEL 8,Database,Postgresql,Language,Java,Runtime,Quarkus,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
1,Gateway,API Gateway,,Retail,,,corp.acme.demo,gateway,0.1.1-SNAPSHOT,war,git,https://git-acme.local/gateway.git,,,Operating System,RHEL 8,,,Language,Java,Runtime,Spring Boot,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
Record Type 1,Application Name,Description,Comments,Business Service,Dependency,Dependency Direction,Binary Group,Binary Artifact,Binary Version,Binary Packaging,Repository Type,Repository URL,Repository Branch,Repository Path,Owner,Contributors,Tag Category 1,Tag 1,Tag Category 2,Tag 2,Tag Category 3,Tag 3,Tag Category 4,Tag 4,Tag Category 5,Tag 5,Tag Category 6,Tag 6,Tag Category 7,Tag 7,Tag Category 8,Tag 8,Tag Category 9,Tag 9,Tag Category 10,Tag 10,Tag Category 11,Tag 11,Tag Category 12,Tag 12,Tag Category 13,Tag 13,Tag Category 14,Tag 14,Tag Category 15,Tag 15,Tag Category 16,Tag 16,Tag Category 17,Tag 17,Tag Category 18,Tag 18,Tag Category 19,Tag 19,Tag Category 20,Tag 20
1,Customers,Legacy Customers management service,,Retail,,,corp.acme.demo,customers-tomcat,0.0.1-SNAPSHOT,war,git,https://git-acme.local/customers.git,,,John Doe <jdoe@example.com>,,Operating System,RHEL 8,Database,Oracle,Language,Java,Runtime,Tomcat,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
1,Inventory,Inventory service,,Retail,,,corp.acme.demo,inventory,0.1.1-SNAPSHOT,war,git,https://git-acme.local/inventory.git,,,,"John Doe <jdoe@example.com>, Jane Smith <jsmith@example.com>",Operating System,RHEL 8,Database,Postgresql,Language,Java,Runtime,Quarkus,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
1,Gateway,API Gateway,,Retail,,,corp.acme.demo,gateway,0.1.1-SNAPSHOT,war,git,https://git-acme.local/gateway.git,,,John Doe <jdoe@example.com>,"John Doe <jdoe@example.com>, Jane Smith <jsmith@example.com>",Operating System,RHEL 8,,,Language,Java,Runtime,Spring Boot,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
2,Gateway,,,,Inventory,southbound
2,Gateway,,,,Customers,southbound

0 comments on commit 57bb3f0

Please sign in to comment.