From 4db2f91ea53d5052d3df838d0fcee97fe0ddba7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iris=20Marie=20K=C3=B6ster?= Date: Thu, 2 Dec 2021 18:37:52 +0100 Subject: [PATCH] Fix duplication: synchronous duplication, proper duplication of widgets which use IDs, reuse duplicated IC (if alive) --- .gitlab-ci.yml | 2 + routes/user/authenticate_endpoint.go | 5 +- routes/user/scenario_duplication.go | 326 +++++++++++++++------- routes/user/user_test.go | 392 +++++++++++++++++++++++---- 4 files changed, 569 insertions(+), 156 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c776a3e..9cba5fa 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -100,5 +100,7 @@ deploy: --dockerfile ${CI_PROJECT_DIR}/Dockerfile --destination ${DOCKER_IMAGE}:${DOCKER_TAG} --snapshotMode=redo + --single-snapshot dependencies: - test + diff --git a/routes/user/authenticate_endpoint.go b/routes/user/authenticate_endpoint.go index 4b05564..a11ae4e 100644 --- a/routes/user/authenticate_endpoint.go +++ b/routes/user/authenticate_endpoint.go @@ -287,10 +287,7 @@ func authenticateExternal(c *gin.Context) (User, error) { } if groupedScenario.Duplicate { - if err := <-duplicateScenarioForUser(so, &myUser.User, ""); err != nil { - log.Println(err) - } - + duplicateScenarioForUser(so, &myUser.User, "") } else { // add user to scenario err = db.Model(&so).Association("Users").Append(&(myUser.User)).Error if err != nil { diff --git a/routes/user/scenario_duplication.go b/routes/user/scenario_duplication.go index 0dc4bc9..3d90dad 100644 --- a/routes/user/scenario_duplication.go +++ b/routes/user/scenario_duplication.go @@ -27,102 +27,109 @@ import ( "fmt" "log" "strconv" + "strings" "time" "git.rwth-aachen.de/acs/public/villas/web-backend-go/database" "github.com/google/uuid" + "github.com/jinzhu/gorm/dialects/postgres" ) -func duplicateScenarioForUser(s database.Scenario, user *database.User, uuidstr string) <-chan error { - errs := make(chan error, 1) +func duplicateScenarioForUser(s database.Scenario, user *database.User, uuidstr string) { - go func() { + // get all component configs of the scenario + db := database.GetDB() + var configs []database.ComponentConfiguration + err := db.Order("ID asc").Model(s).Related(&configs, "ComponentConfigurations").Error + if err != nil { + log.Printf("Warning: scenario to duplicate (id=%d) has no component configurations", s.ID) + } + + icIdmap := make(map[uint]uint) // key: original IC id, value: duplicated IC id + duplicatedICuuids := make(map[uint]string) // key: original icID; value: UUID of duplicate + + // iterate over component configs to check for ICs to duplicate + for _, config := range configs { + icID := config.ICID + + if _, ok := duplicatedICuuids[icID]; ok { // this IC was already added + log.Println("IC already added while ranging configs") + continue + } + + var ic database.InfrastructureComponent + err = db.Find(&ic, icID).Error - // get all component configs of the scenario - db := database.GetDB() - var configs []database.ComponentConfiguration - err := db.Order("ID asc").Model(s).Related(&configs, "ComponentConfigurations").Error if err != nil { - log.Printf("Warning: scenario to duplicate (id=%d) has no component configurations", s.ID) + log.Printf("Cannot find IC with id %d in DB, will not duplicate for User %s: %s", icID, user.Username, err) + continue } - // iterate over component configs to check for ICs to duplicate - duplicatedICuuids := make(map[uint]string) // key: original icID; value: UUID of duplicate - var externalUUIDs []string // external ICs to wait for - for _, config := range configs { - icID := config.ICID - if duplicatedICuuids[icID] != "" { // this IC was already added - continue - } - - var ic database.InfrastructureComponent - err = db.Find(&ic, icID).Error - + alreadyDuplicated, dupID := isICalreadyDuplicated(ic, user.Username) + // create new kubernetes simulator OR use existing IC + if ic.Category == "simulator" && ic.Type == "kubernetes" && !alreadyDuplicated { + duplicateUUID, err := duplicateIC(ic, user.Username, uuidstr) if err != nil { - log.Printf("Cannot find IC with id %d in DB, will not duplicate for User %s: %s", icID, user.Username, err) + log.Printf("duplication of IC (id=%d) unsuccessful, err: %s", icID, err) continue } - // create new kubernetes simulator OR use existing IC - if ic.Category == "simulator" && ic.Type == "kubernetes" { - duplicateUUID, err := duplicateIC(ic, user.Username, uuidstr) - if err != nil { - errs <- fmt.Errorf("duplication of IC (id=%d) unsuccessful, err: %s", icID, err) - continue - } + duplicatedICuuids[ic.ID] = duplicateUUID + } else if alreadyDuplicated { + duplicatedICuuids[ic.ID] = "alreadyduplicated" + icIdmap[ic.ID] = dupID + err = nil + } else { // use existing IC + duplicatedICuuids[ic.ID] = "useoriginal" + icIdmap[ic.ID] = ic.ID + err = nil + } + } - duplicatedICuuids[ic.ID] = duplicateUUID - externalUUIDs = append(externalUUIDs, duplicateUUID) - } else { // use existing IC - duplicatedICuuids[ic.ID] = "" - err = nil + // copy scenario after all new external ICs are in DB + icsToWaitFor := len(duplicatedICuuids) + var timeout = 20 // seconds + + for i := 0; i < timeout; i++ { + // duplicate scenario after all duplicated ICs have been found in the DB + log.Printf("Number of ICs to wait for: %d", icsToWaitFor) + if icsToWaitFor <= 0 { + err := duplicateScenario(s, icIdmap, user) + if err != nil { + log.Printf("duplicate scenario %v fails with error %v", s.Name, err.Error()) } + + return + } else { + time.Sleep(1 * time.Second) } - // copy scenario after all new external ICs are in DB - icsToWaitFor := len(externalUUIDs) - var timeout = 20 // seconds + // check for new ICs with previously created UUIDs + for icId, uuid_r := range duplicatedICuuids { + if uuid_r == "alreadyduplicated" || uuid_r == "useoriginal" { + icsToWaitFor-- + continue + } + log.Printf("Looking for duplicated IC with UUID %s", uuid_r) - for i := 0; i < timeout; i++ { - // duplicate scenario after all duplicated ICs have been found in the DB - if icsToWaitFor == 0 { - err := duplicateScenario(s, duplicatedICuuids, user) - if err != nil { - errs <- fmt.Errorf("duplicate scenario %v fails with error %v", s.Name, err.Error()) - } - - close(errs) - return + // a new IC is searched by UUID, otherwise by string comparison of IC name + // this makes sense in testing and also in reality: if an external IC was requested + // it should be checked for, checking shouldn't be omitted due to old duplicated IC + var duplicatedIC database.InfrastructureComponent + err = db.Find(&duplicatedIC, "UUID = ?", uuid_r).Error + if err != nil { + log.Printf("Didn't find duplicated IC: %s, keep waiting for it..", err) } else { - time.Sleep(1 * time.Second) - } - - // check for new ICs with previously created UUIDs - for _, uuid_r := range externalUUIDs { - if uuid_r == "" { - continue - } - log.Printf("Looking for duplicated IC with UUID %s", uuid_r) - var duplicatedIC database.InfrastructureComponent - err = db.Find(&duplicatedIC, "UUID = ?", uuid_r).Error - if err != nil { - errs <- fmt.Errorf("Error looking up duplicated IC: %s", err) - } else { - icsToWaitFor-- - uuid_r = "" - } + log.Printf("Found duplicated IC! Original IC id: %d, duplicated IC id: %d", icId, duplicatedIC.ID) + icsToWaitFor-- + icIdmap[icId] = duplicatedIC.ID + uuid_r = "" } } - - errs <- fmt.Errorf("ALERT! Timed out while waiting for IC duplication, scenario not properly duplicated") - close(errs) - - }() - - return errs + } } -func duplicateScenario(s database.Scenario, icIds map[uint]string, user *database.User) error { +func duplicateScenario(s database.Scenario, icIds map[uint]uint, user *database.User) error { db := database.GetDB() @@ -145,28 +152,34 @@ func duplicateScenario(s database.Scenario, icIds map[uint]string, user *databas // duplicate files var files []database.File + fileidmap := make(map[uint]uint) // key: original file id, value: duplicated file id err = db.Order("ID asc").Model(s).Related(&files, "Files").Error if err != nil { log.Printf("error getting files for scenario %d", s.ID) } for _, f := range files { - err = duplicateFile(f, duplicateSo.ID) + duplicateFileID, err := duplicateFile(f, duplicateSo.ID) if err != nil { log.Printf("error creating duplicate file %d: %s", f.ID, err) continue + } else { + fileidmap[f.ID] = duplicateFileID } } var configs []database.ComponentConfiguration + configidmap := make(map[uint]uint) // key: original config id, value: duplicated config id // map existing signal IDs to duplicated signal IDs for widget duplication signalMap := make(map[uint]uint) err = db.Order("ID asc").Model(s).Related(&configs, "ComponentConfigurations").Error if err == nil { for _, c := range configs { - err = duplicateComponentConfig(c, duplicateSo.ID, icIds, &signalMap) + duplicatConfigID, err := duplicateComponentConfig(c, duplicateSo.ID, icIds, &signalMap) if err != nil { log.Printf("Error duplicating component config %d: %s", c.ID, err) continue + } else { + configidmap[c.ID] = duplicatConfigID } } } else { @@ -180,7 +193,7 @@ func duplicateScenario(s database.Scenario, icIds map[uint]string, user *databas } for _, dab := range dabs { - err = duplicateDashboard(dab, duplicateSo.ID, signalMap) + err = duplicateDashboard(dab, duplicateSo.ID, signalMap, configidmap, fileidmap, icIds) if err != nil { log.Printf("Error duplicating dashboard %d: %s", dab.ID, err) continue @@ -190,7 +203,7 @@ func duplicateScenario(s database.Scenario, icIds map[uint]string, user *databas return err } -func duplicateFile(f database.File, scenarioID uint) error { +func duplicateFile(f database.File, scenarioID uint) (uint, error) { var dup database.File dup.Name = f.Name @@ -209,7 +222,7 @@ func duplicateFile(f database.File, scenarioID uint) error { db := database.GetDB() err := db.Create(&dup).Error if err != nil { - return err + return 0, err } // Create association of duplicate file to scenario ID of duplicate file @@ -217,15 +230,15 @@ func duplicateFile(f database.File, scenarioID uint) error { var so database.Scenario err = db.Find(&so, scenarioID).Error if err != nil { - return err + return 0, err } err = db.Model(&so).Association("Files").Append(&dup).Error - return err + return dup.ID, err } -func duplicateComponentConfig(m database.ComponentConfiguration, scenarioID uint, icIds map[uint]string, signalMap *map[uint]uint) error { +func duplicateComponentConfig(m database.ComponentConfiguration, scenarioID uint, icIds map[uint]uint, signalMap *map[uint]uint) (uint, error) { db := database.GetDB() @@ -234,53 +247,53 @@ func duplicateComponentConfig(m database.ComponentConfiguration, scenarioID uint dup.StartParameters = m.StartParameters dup.ScenarioID = scenarioID - if icIds[m.ICID] == "" { - dup.ICID = m.ICID - } else { + if val, ok := icIds[m.ICID]; ok { var duplicatedIC database.InfrastructureComponent - err := db.Find(&duplicatedIC, "UUID = ?", icIds[m.ICID]).Error + err := db.Find(&duplicatedIC, "ID = ?", val).Error if err != nil { log.Print(err) - return err + return 0, err } dup.ICID = duplicatedIC.ID + } else { + dup.ICID = m.ICID } // save duplicate to DB and create associations with IC and scenario var so database.Scenario err := db.Find(&so, scenarioID).Error if err != nil { - return err + return 0, err } // save component configuration to DB err = db.Create(&dup).Error if err != nil { - return err + return 0, err } // associate IC with component configuration var ic database.InfrastructureComponent err = db.Find(&ic, dup.ICID).Error if err != nil { - return err + return 0, err } err = db.Model(&ic).Association("ComponentConfigurations").Append(&dup).Error if err != nil { - return err + return 0, err } // associate component configuration with scenario err = db.Model(&so).Association("ComponentConfigurations").Append(&dup).Error if err != nil { - return err + return 0, err } // duplication of signals var sigs []database.Signal err = db.Order("ID asc").Model(&m).Related(&sigs, "OutputMapping").Error if err != nil { - return err + return 0, err } smap := *signalMap for _, s := range sigs { @@ -295,7 +308,7 @@ func duplicateComponentConfig(m database.ComponentConfiguration, scenarioID uint // save signal to DB err = db.Create(&sigDup).Error if err != nil { - return err + return 0, err } // associate signal with component configuration in correct direction @@ -306,16 +319,17 @@ func duplicateComponentConfig(m database.ComponentConfiguration, scenarioID uint } if err != nil { - return err + return 0, err } smap[s.ID] = sigDup.ID } - return nil + return dup.ID, nil } -func duplicateDashboard(d database.Dashboard, scenarioID uint, signalMap map[uint]uint) error { +func duplicateDashboard(d database.Dashboard, scenarioID uint, signalMap map[uint]uint, + configIDmap map[uint]uint, fileIDmap map[uint]uint, icIds map[uint]uint) error { var duplicateD database.Dashboard duplicateD.Grid = d.Grid @@ -351,7 +365,7 @@ func duplicateDashboard(d database.Dashboard, scenarioID uint, signalMap map[uin } for _, w := range widgets { - err = duplicateWidget(w, duplicateD.ID, signalMap) + err = duplicateWidget(w, duplicateD.ID, signalMap, configIDmap, fileIDmap, icIds) if err != nil { log.Printf("error creating duplicate for widget %d: %s", w.ID, err) continue @@ -361,10 +375,11 @@ func duplicateDashboard(d database.Dashboard, scenarioID uint, signalMap map[uin return nil } -func duplicateWidget(w database.Widget, dashboardID uint, signalMap map[uint]uint) error { +func duplicateWidget(w database.Widget, dashboardID uint, signalMap map[uint]uint, + configIDmap map[uint]uint, fileIDmap map[uint]uint, icIds map[uint]uint) error { + var duplicateW database.Widget duplicateW.DashboardID = dashboardID - duplicateW.CustomProperties = w.CustomProperties duplicateW.Height = w.Height duplicateW.Width = w.Width duplicateW.MinHeight = w.MinHeight @@ -380,6 +395,16 @@ func duplicateWidget(w database.Widget, dashboardID uint, signalMap map[uint]uin duplicateW.SignalIDs = append(duplicateW.SignalIDs, int64(signalMap[uint(id)])) } + if w.Type == "ICstatus" { + duplicateW.CustomProperties = duplicateICStatusCustomProps(w.CustomProperties, icIds) + } else if w.Type == "Player" { + duplicateW.CustomProperties = duplicatePlayerCustomProps(w.CustomProperties, configIDmap) + } else if w.Type == "Image" { + duplicateW.CustomProperties = duplicateImageCustomProps(w.CustomProperties, fileIDmap) + } else { + duplicateW.CustomProperties = w.CustomProperties + } + db := database.GetDB() var dab database.Dashboard err := db.Find(&dab, duplicateW.DashboardID).Error @@ -398,6 +423,92 @@ func duplicateWidget(w database.Widget, dashboardID uint, signalMap map[uint]uin return err } +func duplicateICStatusCustomProps(customProps postgres.Jsonb, icIds map[uint]uint) postgres.Jsonb { + type ICstatusCustomProps struct { + CheckedIDs []uint `json:"checkedIDs"` + } + + var props ICstatusCustomProps + err := json.Unmarshal(customProps.RawMessage, &props) + if err != nil { + log.Printf("ICstatus duplication, unmarshalling failed: err: %s", err) + return customProps + } + + var IDs []string + for _, id := range props.CheckedIDs { + IDs = append(IDs, strconv.FormatUint(uint64(icIds[id]), 10)) + } + + customProperties := fmt.Sprintf(`{"checkedIDs": [%s]}`, strings.Join(IDs, ",")) + + return postgres.Jsonb{RawMessage: json.RawMessage(customProperties)} +} + +func duplicatePlayerCustomProps(customProps postgres.Jsonb, configIDmap map[uint]uint) postgres.Jsonb { + type PlayerCustomProps struct { + ConfigID string `json:"configID"` + ConfigIDs []int `json:"configIDs"` + UploadResults bool `json:"uploadResults"` + } + + var props PlayerCustomProps + err := json.Unmarshal(customProps.RawMessage, &props) + if err != nil { + log.Printf("Player duplication, unmarshalling failed: err: %v", err) + return customProps + } + + // get configID of original config + u, err := strconv.ParseUint(props.ConfigID, 10, 64) + if err != nil { + log.Printf("Player duplication, parsing file ID failed: err: %v", err) + return customProps + } + + // get configID of duplicated config, save it in PlayerCustomProps struct + props.ConfigID = strconv.FormatUint(uint64(configIDmap[uint(u)]), 10) + customProperties, err := json.Marshal(props) + if err != nil { + log.Printf("Player duplication, marshalling failed: err: %v", err) + return customProps + } + + return postgres.Jsonb{RawMessage: customProperties} +} + +func duplicateImageCustomProps(customProps postgres.Jsonb, fileIDmap map[uint]uint) postgres.Jsonb { + type ImageCustomProps struct { + File string `json:"file"` + Update bool `json:"update"` + Lock bool `json:"lockAspect"` + } + + var props ImageCustomProps + err := json.Unmarshal(customProps.RawMessage, &props) + + if err != nil { + log.Printf("Image duplication, unmarshalling failed: err: %v", err) + return customProps + } + + // get fileID of original file + u, err := strconv.ParseUint(props.File, 10, 64) + if err != nil { + log.Printf("Image duplication, parsing file ID failed: err: %v", err) + return customProps + } + // get fileID of duplicated file, save it in ImageCustomProps struct + props.File = strconv.FormatUint(uint64(fileIDmap[uint(u)]), 10) + customProperties, err := json.Marshal(props) + if err != nil { + log.Printf("Image duplication, marshalling failed: err: %v", err) + return customProps + } + + return postgres.Jsonb{RawMessage: customProperties} +} + type Container struct { Name string `json:"name"` Image string `json:"image"` @@ -512,3 +623,24 @@ func duplicateIC(ic database.InfrastructureComponent, userName string, uuidstr s } } + +func isICalreadyDuplicated(ic database.InfrastructureComponent, username string) (bool, uint) { + db := database.GetDB() + var duplicateICs []database.InfrastructureComponent + duplicateName := fmt.Sprintf("%s %s", ic.Name, username) + err := db.Find(&duplicateICs, "Name = ?", duplicateName).Error + if err != nil { + log.Printf("Error looking for duplicated ICs: %s", err) + } + + // return the first duplicated IC that was recently updated + for _, value := range duplicateICs { + lastUpdate := value.UpdatedAt + diff := time.Since(lastUpdate) + if diff.Seconds() < 60 { + return true, value.ID + } + } + + return false, 0 +} diff --git a/routes/user/user_test.go b/routes/user/user_test.go index c76e580..cf2abe6 100644 --- a/routes/user/user_test.go +++ b/routes/user/user_test.go @@ -884,14 +884,26 @@ func TestDuplicateScenarioForUser(t *testing.T) { assert.NoError(t, err) assert.NotEqual(t, 99, fileID) - // add IC - err = addIC(session, token) + // add ICs (kubernetes simulator plus other) + err = addICs(session, token) assert.NoError(t, err) - // add component config - err = addComponentConfig(scenarioID, token) + // add two component configs with the same IC (ID 1) + // plus one component config with IC != kubernetes simulator (ID 2) + err = addComponentConfigs(scenarioID, token) assert.NoError(t, err) + /* + * component/IC test setup: + * C0 --> IC0 + * C1 --> IC0 + * C2 --> IC1 + * ---- after duplication: ---- + * C3 --> IC2 + * C4 --> IC2 + * C5 --> IC1 + */ + // add signals err = addSignals(token) assert.NoError(t, err) @@ -919,9 +931,7 @@ func TestDuplicateScenarioForUser(t *testing.T) { err = addFakeIC(uuidDup, session, token) assert.NoError(t, err) - if err := <-duplicateScenarioForUser(originalSo, &myUser.User, uuidDup); err != nil { - t.Fail() - } + duplicateScenarioForUser(originalSo, &myUser.User, uuidDup) /*** Check duplicated scenario for correctness ***/ var dplScenarios []database.Scenario @@ -947,15 +957,26 @@ func TestDuplicateScenarioForUser(t *testing.T) { var configs []database.ComponentConfiguration err = db.Find(&configs).Error assert.NoError(t, err) - assert.Equal(t, 2, len(configs)) - assert.Equal(t, configs[0].Name, configs[1].Name) - assert.Equal(t, configs[0].FileIDs, configs[1].FileIDs) - assert.Equal(t, configs[0].InputMapping, configs[1].InputMapping) - assert.Equal(t, configs[0].OutputMapping, configs[1].OutputMapping) - assert.Equal(t, configs[0].StartParameters, configs[1].StartParameters) - assert.NotEqual(t, configs[0].ScenarioID, configs[1].ScenarioID) - assert.NotEqual(t, configs[0].ICID, configs[1].ICID) - assert.NotEqual(t, configs[0].ID, configs[1].ID) + assert.Equal(t, 6, len(configs)) + assert.Equal(t, configs[0].Name, configs[3].Name) + assert.Equal(t, configs[0].FileIDs, configs[3].FileIDs) + assert.Equal(t, configs[0].InputMapping, configs[3].InputMapping) + assert.Equal(t, configs[0].OutputMapping, configs[3].OutputMapping) + assert.Equal(t, configs[0].StartParameters, configs[3].StartParameters) + assert.NotEqual(t, configs[0].ScenarioID, configs[3].ScenarioID) + assert.NotEqual(t, configs[0].ICID, configs[3].ICID) // original and duplicated IC + assert.NotEqual(t, configs[0].ID, configs[3].ID) + + assert.Equal(t, configs[0].ScenarioID, configs[1].ScenarioID) + assert.Equal(t, configs[0].ScenarioID, configs[2].ScenarioID) + assert.Equal(t, configs[3].ScenarioID, configs[4].ScenarioID) + assert.Equal(t, configs[3].ScenarioID, configs[5].ScenarioID) + + assert.Equal(t, configs[0].ICID, configs[1].ICID) // same IC for both configs + assert.Equal(t, configs[3].ICID, configs[4].ICID) // same duplicated IC + assert.Equal(t, configs[2].ICID, configs[5].ICID) // reused (not duplicated) IC + assert.NotEqual(t, configs[0].ICID, configs[2].ICID) + assert.NotEqual(t, configs[0].ICID, configs[3].ICID) // compare original and duplicated signals var signals []database.Signal @@ -983,17 +1004,28 @@ func TestDuplicateScenarioForUser(t *testing.T) { // compare original and duplicated infrastructure component var ics []database.InfrastructureComponent - err = db.Find(&ics).Error + err = db.Order("created_at asc").Find(&ics).Error assert.NoError(t, err) - assert.Equal(t, 2, len(ics)) - assert.Equal(t, ics[0].Name, ics[1].Name) - assert.Equal(t, ics[0].Category, ics[1].Category) - assert.Equal(t, ics[0].Type, ics[1].Type) - assert.Equal(t, ics[0].CreateParameterSchema, ics[1].CreateParameterSchema) - assert.Equal(t, ics[0].StartParameterSchema, ics[1].StartParameterSchema) - assert.Equal(t, ics[0].Manager, ics[1].Manager) - assert.NotEqual(t, ics[0].UUID, ics[1].UUID) - assert.NotEqual(t, ics[0].ID, ics[1].ID) + assert.Equal(t, 3, len(ics)) + assert.Equal(t, ics[0].Category, ics[2].Category) + assert.Equal(t, ics[0].Type, ics[2].Type) + assert.Equal(t, ics[0].CreateParameterSchema, ics[2].CreateParameterSchema) + assert.Equal(t, ics[0].StartParameterSchema, ics[2].StartParameterSchema) + assert.Equal(t, ics[0].Manager, ics[2].Manager) + assert.NotEqual(t, ics[0].UUID, ics[2].UUID) + assert.NotEqual(t, ics[0].ID, ics[2].ID) + + assert.NotEqual(t, ics[0].Name, ics[1].Name) + assert.NotEqual(t, ics[0].Category, ics[1].Category) + assert.NotEqual(t, ics[0].Type, ics[1].Type) + + // check associations between component configs and ICs + assert.Equal(t, configs[0].ICID, ics[0].ID) + assert.Equal(t, configs[1].ICID, ics[0].ID) + assert.Equal(t, configs[2].ICID, ics[1].ID) + assert.Equal(t, configs[3].ICID, ics[2].ID) + assert.Equal(t, configs[4].ICID, ics[2].ID) + assert.Equal(t, configs[5].ICID, ics[1].ID) // compare original and duplicated dashboards err = db.Order("created_at asc").Find(&dashboards).Error @@ -1011,13 +1043,187 @@ func TestDuplicateScenarioForUser(t *testing.T) { assert.NotEqual(t, dashboards[1].ScenarioID, dashboards[3].ScenarioID) assert.NotEqual(t, dashboards[1].ID, dashboards[3].ID) - // compare original and duplicated widget + // compare original and duplicated widget (ICstatus) var widgets []database.Widget err = db.Find(&widgets).Error assert.NoError(t, err) assert.Equal(t, 2, len(widgets)) assert.Equal(t, widgets[0].Name, widgets[1].Name) - assert.Equal(t, widgets[0].CustomProperties, widgets[1].CustomProperties) + assert.NotEqual(t, widgets[0].CustomProperties, widgets[1].CustomProperties) + assert.Equal(t, widgets[0].MinHeight, widgets[1].MinHeight) + assert.Equal(t, widgets[0].MinWidth, widgets[1].MinWidth) + assert.NotEqual(t, widgets[0].DashboardID, widgets[1].DashboardID) + assert.NotEqual(t, widgets[0].ID, widgets[1].ID) +} + +func TestScenarioDuplicationAlreadyDuplicatedIC(t *testing.T) { + /* + * Test: IC was already duplicated, exists in DB at the time of duplication + **/ + database.DropTables() + database.MigrateModels() + assert.NoError(t, database.AddTestUsers()) + + // connect AMQP client + // Make sure that AMQP_HOST, AMQP_USER, AMQP_PASS are set + host, _ := configuration.GlobalConfig.String("amqp.host") + usr, _ := configuration.GlobalConfig.String("amqp.user") + pass, _ := configuration.GlobalConfig.String("amqp.pass") + amqpURI := "amqp://" + usr + ":" + pass + "@" + host + + // AMQP Connection startup is tested here + // Not repeated in other tests because it is only needed once + session = helper.NewAMQPSession("villas-test-session", amqpURI, "villas", infrastructure_component.ProcessMessage) + SetAMQPSession(session) + infrastructure_component.SetAMQPSession(session) + + // authenticate as admin (needed to create original IC) + token, err := helper.AuthenticateForTest(router, database.AdminCredentials) + assert.NoError(t, err) + + /*** Create original scenario and entities ***/ + scenarioID := addScenario(token) + var originalSo database.Scenario + db := database.GetDB() + err = db.Find(&originalSo, scenarioID).Error + assert.NoError(t, err) + + // add file to scenario + fileID, err := addFile(scenarioID, token) + assert.NoError(t, err) + assert.NotEqual(t, 99, fileID) + + // add ICs (kubernetes simulator plus other) + err = addICs(session, token) + assert.NoError(t, err) + + // add two component configs with the same IC (ID 1) + // plus one component config with IC != kubernetes simulator (ID 2) + err = addComponentConfigs(scenarioID, token) + assert.NoError(t, err) + + /* + * component/IC test setup: + * C0 --> IC0 + * C1 --> IC0 + * C2 --> IC1 + * x --> IC2 + * ---- after duplication: ---- + * C3 --> IC2 + * C4 --> IC2 + * C5 --> IC1 + */ + + // add dashboards to scenario + err = addTwoDashboards(scenarioID, token) + assert.NoError(t, err) + var dashboards []database.Dashboard + err = db.Find(&dashboards).Error + assert.NoError(t, err) + assert.Equal(t, 2, len(dashboards)) + + // add widgets + dashboardID_forAddingWidget := uint(1) + err = addWidget(dashboardID_forAddingWidget, token) + assert.NoError(t, err) + + /*** Duplicate scenario for new user ***/ + username := "Schnittlauch" + myUser, err := NewUser(username, "", "schnitti@lauch.de", "User", true) + assert.NoError(t, err) + + // add IC which will be seen as the duplicate IC + err = addDuplicateIC(token, username) + assert.NoError(t, err) + log.Println("--------ICs before duplication ------------") + var icsqq []database.InfrastructureComponent + err = db.Order("created_at asc").Find(&icsqq).Error + assert.NoError(t, err) + assert.Equal(t, 3, len(icsqq)) + for _, ic := range icsqq { + log.Printf("IC (id/name): (%d/%s)", ic.ID, ic.Name) + } + log.Println("------------------") + + duplicateScenarioForUser(originalSo, &myUser.User, "") + + /*** Check duplicated scenario for correctness ***/ + var dplScenarios []database.Scenario + err = db.Find(&dplScenarios, "name = ?", originalSo.Name+" "+username).Error + assert.NoError(t, err) + assert.Equal(t, 1, len(dplScenarios)) + assert.Equal(t, originalSo.StartParameters, dplScenarios[0].StartParameters) + + // compare original and duplicated component configs + var configs []database.ComponentConfiguration + err = db.Find(&configs).Error + assert.NoError(t, err) + assert.Equal(t, 6, len(configs)) + assert.Equal(t, configs[0].Name, configs[3].Name) + assert.Equal(t, configs[0].FileIDs, configs[3].FileIDs) + assert.Equal(t, configs[0].InputMapping, configs[3].InputMapping) + assert.Equal(t, configs[0].OutputMapping, configs[3].OutputMapping) + assert.Equal(t, configs[0].StartParameters, configs[3].StartParameters) + assert.NotEqual(t, configs[0].ScenarioID, configs[3].ScenarioID) + assert.NotEqual(t, configs[0].ICID, configs[3].ICID) // original and duplicated IC + assert.NotEqual(t, configs[0].ID, configs[3].ID) + + assert.Equal(t, configs[0].ScenarioID, configs[1].ScenarioID) + assert.Equal(t, configs[0].ScenarioID, configs[2].ScenarioID) + assert.Equal(t, configs[3].ScenarioID, configs[4].ScenarioID) + assert.Equal(t, configs[3].ScenarioID, configs[5].ScenarioID) + + assert.Equal(t, configs[0].ICID, configs[1].ICID) // same IC for both configs + assert.Equal(t, configs[3].ICID, configs[4].ICID) // same duplicated IC + assert.Equal(t, configs[2].ICID, configs[5].ICID) // reused (not duplicated) IC + log.Println(configs[2].ICID) + log.Println(configs[5].ICID) + assert.NotEqual(t, configs[0].ICID, configs[2].ICID) + assert.NotEqual(t, configs[0].ICID, configs[3].ICID) + + // compare original and duplicated infrastructure component + var ics []database.InfrastructureComponent + err = db.Order("created_at asc").Find(&ics).Error + assert.NoError(t, err) + assert.Equal(t, 3, len(ics)) + log.Println("--------ICs after duplication ------------") + + for _, ic := range ics { + log.Printf("IC (id/name): (%d/%s)", ic.ID, ic.Name) + } + log.Println("------------------") + + // check associations between component configs and ICs + assert.Equal(t, configs[0].ICID, ics[0].ID) + assert.Equal(t, configs[1].ICID, ics[0].ID) + assert.Equal(t, configs[2].ICID, ics[1].ID) + assert.Equal(t, configs[3].ICID, ics[2].ID) + assert.Equal(t, configs[4].ICID, ics[2].ID) + assert.Equal(t, configs[5].ICID, ics[1].ID) + + // compare original and duplicated dashboards + err = db.Order("created_at asc").Find(&dashboards).Error + assert.NoError(t, err) + assert.Equal(t, 4, len(dashboards)) + assert.Equal(t, dashboards[0].Name, dashboards[2].Name) + assert.Equal(t, dashboards[0].Grid, dashboards[2].Grid) + assert.Equal(t, dashboards[0].Height, dashboards[2].Height) + assert.NotEqual(t, dashboards[0].ScenarioID, dashboards[2].ScenarioID) + assert.NotEqual(t, dashboards[0].ID, dashboards[2].ID) + + assert.Equal(t, dashboards[1].Name, dashboards[3].Name) + assert.Equal(t, dashboards[1].Grid, dashboards[3].Grid) + assert.Equal(t, dashboards[1].Height, dashboards[3].Height) + assert.NotEqual(t, dashboards[1].ScenarioID, dashboards[3].ScenarioID) + assert.NotEqual(t, dashboards[1].ID, dashboards[3].ID) + + // compare original and duplicated widget (ICstatus) + var widgets []database.Widget + err = db.Find(&widgets).Error + assert.NoError(t, err) + assert.Equal(t, 2, len(widgets)) + assert.Equal(t, widgets[0].Name, widgets[1].Name) + assert.NotEqual(t, widgets[0].CustomProperties, widgets[1].CustomProperties) assert.Equal(t, widgets[0].MinHeight, widgets[1].MinHeight) assert.Equal(t, widgets[0].MinWidth, widgets[1].MinWidth) assert.NotEqual(t, widgets[0].DashboardID, widgets[1].DashboardID) @@ -1155,17 +1361,17 @@ func addWidget(dashboardID uint, token string) error { } newWidget := WidgetRequest{ - Name: "My label", - Type: "Label", + Name: "IC Status", + Type: "ICstatus", Width: 100, - Height: 50, - MinWidth: 40, - MinHeight: 80, + Height: 100, + MinWidth: 0, + MinHeight: 0, X: 10, Y: 10, Z: 200, IsLocked: false, - CustomProperties: postgres.Jsonb{RawMessage: json.RawMessage(`{"textSize" : "20", "fontColor" : "#4287f5", "fontColor_opacity": 1}`)}, + CustomProperties: postgres.Jsonb{RawMessage: json.RawMessage(`{"checkedIDs" : [1]}`)}, SignalIDs: []int64{}, } @@ -1177,29 +1383,29 @@ func addWidget(dashboardID uint, token string) error { return err } -func newTrue() *bool { - b := true +func newFalse() *bool { + b := false return &b } -func addIC(session *helper.AMQPsession, token string) error { - type ICRequest struct { - UUID string `json:"uuid,omitempty"` - WebsocketURL string `json:"websocketurl,omitempty"` - APIURL string `json:"apiurl,omitempty"` - Type string `json:"type,omitempty"` - Name string `json:"name,omitempty"` - Category string `json:"category,omitempty"` - State string `json:"state,omitempty"` - Location string `json:"location,omitempty"` - Description string `json:"description,omitempty"` - StartParameterSchema postgres.Jsonb `json:"startparameterschema,omitempty"` - CreateParameterSchema postgres.Jsonb `json:"createparameterschema,omitempty"` - ManagedExternally *bool `json:"managedexternally"` - Manager string `json:"manager,omitempty"` - } +type ICRequest struct { + UUID string `json:"uuid,omitempty"` + WebsocketURL string `json:"websocketurl,omitempty"` + APIURL string `json:"apiurl,omitempty"` + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + Category string `json:"category,omitempty"` + State string `json:"state,omitempty"` + Location string `json:"location,omitempty"` + Description string `json:"description,omitempty"` + StartParameterSchema postgres.Jsonb `json:"startparameterschema,omitempty"` + CreateParameterSchema postgres.Jsonb `json:"createparameterschema,omitempty"` + ManagedExternally *bool `json:"managedexternally"` + Manager string `json:"manager,omitempty"` +} - // create IC +func addICs(session *helper.AMQPsession, token string) error { + // create ICs var newIC = ICRequest{ UUID: "7be0322d-354e-431e-84bd-ae4c9633138b", WebsocketURL: "https://villas.k8s.eonerc.rwth-aachen.de/ws/ws_sig", @@ -1212,7 +1418,23 @@ func addIC(session *helper.AMQPsession, token string) error { Description: "A kubernetes simulator for testing purposes", StartParameterSchema: postgres.Jsonb{json.RawMessage(`{"startprop1" : "a nice prop"}`)}, CreateParameterSchema: postgres.Jsonb{json.RawMessage(`{"createprop1" : "a really nice prop"}`)}, - ManagedExternally: newTrue(), + ManagedExternally: newFalse(), + Manager: "7be0322d-354e-431e-84bd-ae4c9633beef", + } + + var newIC2 = ICRequest{ + UUID: "7be0322d-354e-431e-84bd-ae4c9635558b", + WebsocketURL: "https://villas.k8s.eonerc.rwth-aachen.de/ws/ws_sig", + APIURL: "https://villas.k8s.eonerc.rwth-aachen.de/ws/api/v2", + Type: "villas-node", + Name: "ACS Demo Signals", + Category: "gateway", + State: "idle", + Location: "k8s", + Description: "A signal generator for testing purposes", + StartParameterSchema: postgres.Jsonb{RawMessage: json.RawMessage(`{"startprop1" : "a nice prop"}`)}, + CreateParameterSchema: postgres.Jsonb{RawMessage: json.RawMessage(`{"createprop1" : "a really nice prop"}`)}, + ManagedExternally: newFalse(), Manager: "7be0322d-354e-431e-84bd-ae4c9633beef", } @@ -1244,10 +1466,42 @@ func addIC(session *helper.AMQPsession, token string) error { err = session.Send(payload, newIC.Manager) time.Sleep(2 * time.Second) + + if err != nil { + return err + } + + _, _, err = helper.TestEndpoint(router, token, + "/api/v2/ic", "POST", helper.KeyModels{"ic": newIC2}) return err } -func addComponentConfig(scenarioID uint, token string) error { +func addDuplicateIC(token string, username string) error { + + // create IC + var newIC = ICRequest{ + UUID: "7aaa322d-354e-431e-84bd-ae4c9633138b", + WebsocketURL: "https://villas.k8s.eonerc.rwth-aachen.de/ws/ws_sig", + APIURL: "https://villas.k8s.eonerc.rwth-aachen.de/ws/api/v2", + Type: "kubernetes", + Name: "Kubernetes Simulator " + username, + Category: "simulator", + State: "idle", + Location: "k8s", + Description: "A kubernetes simulator for testing purposes", + StartParameterSchema: postgres.Jsonb{json.RawMessage(`{"startprop1" : "a nice prop"}`)}, + CreateParameterSchema: postgres.Jsonb{json.RawMessage(`{"createprop1" : "a really nice prop"}`)}, + ManagedExternally: newFalse(), + Manager: "7be0322d-354e-431e-84bd-ae4c9633beef", + } + + _, _, err := helper.TestEndpoint(router, token, + "/api/v2/ic", "POST", helper.KeyModels{"ic": newIC}) + + return err +} + +func addComponentConfigs(scenarioID uint, token string) error { type ConfigRequest struct { Name string `json:"name,omitempty"` ScenarioID uint `json:"scenarioID,omitempty"` @@ -1264,8 +1518,36 @@ func addComponentConfig(scenarioID uint, token string) error { FileIDs: []int64{}, } + var newConfig2 = ConfigRequest{ + Name: "Example for Signal generator", + ScenarioID: scenarioID, + ICID: 1, + StartParameters: postgres.Jsonb{RawMessage: json.RawMessage(`{"parameter1" : "testValue1A", "parameter2" : "testValue2A", "parameter3" : 42}`)}, + FileIDs: []int64{}, + } + + var newConfig3 = ConfigRequest{ + Name: "Example for Signal generator", + ScenarioID: scenarioID, + ICID: 2, + StartParameters: postgres.Jsonb{RawMessage: json.RawMessage(`{"parameter1" : "testValue1A", "parameter2" : "testValue2A", "parameter3" : 42}`)}, + FileIDs: []int64{}, + } + _, _, err := helper.TestEndpoint(router, token, "/api/v2/configs", "POST", helper.KeyModels{"config": newConfig1}) + if err != nil { + return err + } + + _, _, err = helper.TestEndpoint(router, token, + "/api/v2/configs", "POST", helper.KeyModels{"config": newConfig2}) + if err != nil { + return err + } + + _, _, err = helper.TestEndpoint(router, token, + "/api/v2/configs", "POST", helper.KeyModels{"config": newConfig3}) return err }