Skip to content

Commit 57bb3f0

Browse files
authored
⚠️ Add "owner" and "contributors" columns to application import CSV format. (#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 <[email protected]>'. 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 <[email protected]>
1 parent 402c4e2 commit 57bb3f0

File tree

6 files changed

+138
-10
lines changed

6 files changed

+138
-10
lines changed

api/import.go

+9-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ const (
1818
RecordTypeDependency = "2"
1919
)
2020

21+
const (
22+
ExpectedFieldCount = 17
23+
)
24+
2125
//
2226
// Import Statuses
2327
const (
@@ -270,8 +274,8 @@ func (h ImportHandler) UploadCSV(ctx *gin.Context) {
270274
var imp model.Import
271275
switch row[0] {
272276
case RecordTypeApplication:
273-
// Check row format - length, expecting 15 fields + tags
274-
if len(row) < 15 {
277+
// Check row format - length, expecting 17 fields + tags
278+
if len(row) < ExpectedFieldCount {
275279
h.Respond(ctx, http.StatusBadRequest, gin.H{"errorMessage": "Invalid Application Import CSV format."})
276280
return
277281
}
@@ -396,10 +400,12 @@ func (h ImportHandler) applicationFromRow(fileName string, row []string) (app mo
396400
RepositoryURL: row[12],
397401
RepositoryBranch: row[13],
398402
RepositoryPath: row[14],
403+
Owner: row[15],
404+
Contributors: row[16],
399405
}
400406

401407
// Tags
402-
for i := 15; i < len(row); i++ {
408+
for i := ExpectedFieldCount; i < len(row); i++ {
403409
if i%2 == 0 {
404410
tag := model.ImportTag{
405411
Name: row[i],

importer/manager.go

+84
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,54 @@ func (m *Manager) createApplication(imp *model.Import) (ok bool) {
277277
}
278278
}
279279

280+
if imp.Owner != "" {
281+
name, email, parsed := parseStakeholder(imp.Owner)
282+
if !parsed {
283+
imp.ErrorMessage = fmt.Sprintf("Could not parse Owner '%s'.", imp.Owner)
284+
return
285+
}
286+
owner, found := m.findStakeholder(email)
287+
if !found {
288+
if imp.ImportSummary.CreateEntities {
289+
var err error
290+
owner, err = m.createStakeholder(name, email)
291+
if err != nil {
292+
imp.ErrorMessage = fmt.Sprintf("Owner '%s' could not be created.", imp.Owner)
293+
return
294+
}
295+
} else {
296+
imp.ErrorMessage = fmt.Sprintf("Owner '%s' could not be found.", imp.Owner)
297+
return
298+
}
299+
}
300+
app.OwnerID = &owner.ID
301+
}
302+
if imp.Contributors != "" {
303+
fields := strings.Split(imp.Contributors, ",")
304+
for _, f := range fields {
305+
name, email, parsed := parseStakeholder(f)
306+
if !parsed {
307+
imp.ErrorMessage = fmt.Sprintf("Could not parse Contributor '%s'.", f)
308+
return
309+
}
310+
contributor, found := m.findStakeholder(email)
311+
if !found {
312+
if imp.ImportSummary.CreateEntities {
313+
var err error
314+
contributor, err = m.createStakeholder(name, email)
315+
if err != nil {
316+
imp.ErrorMessage = fmt.Sprintf("Contributor '%s' could not be created.", imp.Owner)
317+
return
318+
}
319+
} else {
320+
imp.ErrorMessage = fmt.Sprintf("Contributor '%s' could not be found.", imp.Owner)
321+
return
322+
}
323+
}
324+
app.Contributors = append(app.Contributors, contributor)
325+
}
326+
}
327+
280328
result := m.DB.Create(app)
281329
if result.Error != nil {
282330
imp.ErrorMessage = result.Error.Error()
@@ -295,6 +343,25 @@ func (m *Manager) createApplication(imp *model.Import) (ok bool) {
295343
return
296344
}
297345

346+
func (m *Manager) createStakeholder(name string, email string) (stakeholder model.Stakeholder, err error) {
347+
stakeholder.Name = name
348+
stakeholder.Email = email
349+
err = m.DB.Create(&stakeholder).Error
350+
if err != nil {
351+
err = liberr.Wrap(err)
352+
}
353+
return
354+
}
355+
356+
func (m *Manager) findStakeholder(email string) (stakeholder model.Stakeholder, found bool) {
357+
result := m.DB.First(&stakeholder, "email = ?", email)
358+
if result.Error != nil {
359+
return
360+
}
361+
found = true
362+
return
363+
}
364+
298365
//
299366
// normalizedName transforms given name to be comparable as same with similar names
300367
// Example: normalizedName(" F oo-123 bar! ") returns "foo123bar!"
@@ -304,3 +371,20 @@ func normalizedName(name string) (normName string) {
304371
normName = invalidSymbols.ReplaceAllString(normName, "")
305372
return
306373
}
374+
375+
//
376+
// parseStakeholder attempts to parse a stakeholder's name and an email address
377+
// out of a string like `John Smith <[email protected]>`. The pattern is very
378+
// simple and treats anything before the first bracket as the name,
379+
// and anything within the brackets as the email.
380+
func parseStakeholder(s string) (name string, email string, parsed bool) {
381+
pattern := regexp.MustCompile("(.+)\\s<(.+@.+)>")
382+
matches := pattern.FindStringSubmatch(strings.TrimSpace(s))
383+
if len(matches) != 3 {
384+
return
385+
}
386+
parsed = true
387+
name = matches[1]
388+
email = strings.ToLower(matches[2])
389+
return
390+
}

migration/v11/model/application.go

+2
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,8 @@ type Import struct {
258258
RepositoryURL string
259259
RepositoryBranch string
260260
RepositoryPath string
261+
Owner string
262+
Contributors string
261263
}
262264

263265
func (r *Import) AsMap() (m map[string]interface{}) {

test/api/importcsv/api_test.go

+17-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package importcsv
22

33
import (
4+
"github.com/konveyor/tackle2-hub/api"
5+
"github.com/konveyor/tackle2-hub/binding"
6+
"github.com/konveyor/tackle2-hub/test/assert"
47
"io/ioutil"
58
"os"
69
"testing"
710
"time"
8-
"github.com/konveyor/tackle2-hub/api"
9-
"github.com/konveyor/tackle2-hub/binding"
10-
"github.com/konveyor/tackle2-hub/test/assert"
1111
)
1212

1313
func TestImportCSV(t *testing.T) {
@@ -68,6 +68,20 @@ func TestImportCSV(t *testing.T) {
6868
if r.ExpectedApplications[i].BusinessService.Name != gotApp.BusinessService.Name {
6969
t.Errorf("Mismatch in name of the BusinessService of imported Application: Expected %s, Actual %s", r.ExpectedApplications[i].BusinessService.Name, gotApp.BusinessService.Name)
7070
}
71+
if gotApp.Owner == nil || r.ExpectedApplications[i].Owner == nil {
72+
if gotApp.Owner != r.ExpectedApplications[i].Owner {
73+
t.Errorf("Mismatch in value of Owner on imported Application: Expected %v, Actual %v", r.ExpectedApplications[i].Owner, gotApp.BusinessService)
74+
}
75+
} else if r.ExpectedApplications[i].Owner.Name != gotApp.Owner.Name {
76+
t.Errorf("Mismatch in name of the Owner of imported Application: Expected %s, Actual %s", r.ExpectedApplications[i].Owner.Name, gotApp.BusinessService.Name)
77+
}
78+
if len(gotApp.Contributors) != len(r.ExpectedApplications[i].Contributors) {
79+
t.Errorf("Mismatch in number of Contributors: Expected %d, Actual %d", len(r.ExpectedApplications[i].Contributors), len(gotApp.Contributors))
80+
} else {
81+
for j, contributor := range gotApp.Contributors {
82+
if contributor.Name != r.ExpectedApplications[i].Contributors[j].Name {}
83+
}
84+
}
7185
}
7286
}
7387

test/api/importcsv/samples.go

+22
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ var (
4848
BusinessService: &api.Ref{
4949
Name: "Retail",
5050
},
51+
Owner: &api.Ref{
52+
Name: "John Doe",
53+
},
5154
},
5255
{
5356
Name: "Inventory",
@@ -82,6 +85,14 @@ var (
8285
BusinessService: &api.Ref{
8386
Name: "Retail",
8487
},
88+
Contributors: []api.Ref{
89+
{
90+
Name: "John Doe",
91+
},
92+
{
93+
Name: "Jane Smith",
94+
},
95+
},
8596
},
8697
{
8798
Name: "Gateway",
@@ -112,6 +123,17 @@ var (
112123
BusinessService: &api.Ref{
113124
Name: "Retail",
114125
},
126+
Owner: &api.Ref{
127+
Name: "John Doe",
128+
},
129+
Contributors: []api.Ref{
130+
{
131+
Name: "John Doe",
132+
},
133+
{
134+
Name: "Jane Smith",
135+
},
136+
},
115137
},
116138
},
117139
ExpectedDependencies: []api.Dependency{
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
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
2-
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,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
3-
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,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
4-
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,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
1+
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
2+
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 <[email protected]>,,Operating System,RHEL 8,Database,Oracle,Language,Java,Runtime,Tomcat,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
3+
1,Inventory,Inventory service,,Retail,,,corp.acme.demo,inventory,0.1.1-SNAPSHOT,war,git,https://git-acme.local/inventory.git,,,,"John Doe <[email protected]>, Jane Smith <[email protected]>",Operating System,RHEL 8,Database,Postgresql,Language,Java,Runtime,Quarkus,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
4+
1,Gateway,API Gateway,,Retail,,,corp.acme.demo,gateway,0.1.1-SNAPSHOT,war,git,https://git-acme.local/gateway.git,,,John Doe <[email protected]>,"John Doe <[email protected]>, Jane Smith <[email protected]>",Operating System,RHEL 8,,,Language,Java,Runtime,Spring Boot,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
55
2,Gateway,,,,Inventory,southbound
66
2,Gateway,,,,Customers,southbound

0 commit comments

Comments
 (0)