From aed93e2fbab135fb79c79440a425dc906d58aba4 Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Tue, 21 Jan 2025 10:50:19 +0100 Subject: [PATCH 01/13] feat: edit upf Signed-off-by: Patricia Reinoso --- backend/webui_service/webui_init.go | 5 + configapi/api_default.go | 2 +- configapi/api_inventory.go | 215 +++++++++++++----- configapi/api_inventory_test.go | 299 ++++++++++++++++++++++---- configapi/routers.go | 8 +- configmodels/config_msg.go | 1 - configmodels/model_inventory.go | 10 + dbadapter/db_adapter.go | 27 +++ docs/docs.go | 140 ++++++++---- docs/docs.go.license | 2 + go.mod | 6 +- go.sum | 12 +- proto/server/configEvtHandler.go | 16 -- proto/server/configEvtHandler_test.go | 26 --- 14 files changed, 574 insertions(+), 195 deletions(-) create mode 100644 docs/docs.go.license diff --git a/backend/webui_service/webui_init.go b/backend/webui_service/webui_init.go index 25d6f82c..079d28f8 100644 --- a/backend/webui_service/webui_init.go +++ b/backend/webui_service/webui_init.go @@ -157,6 +157,11 @@ func (webui *WEBUI) Start() { dbadapter.ConnectMongo(mongodb.AuthUrl, mongodb.AuthKeysDbName, &dbadapter.AuthDBClient) } + resp, err := dbadapter.CommonDBClient.CreateIndex(configmodels.UpfDataColl, "hostname") + if !resp || err != nil { + logger.InitLog.Errorf("error creating UPF index in commonDB %v", err) + } + logger.InitLog.Infoln("WebUI server started") /* First HTTP Server running at port to receive Config from ROC */ diff --git a/configapi/api_default.go b/configapi/api_default.go index f3f8e2cb..55948371 100644 --- a/configapi/api_default.go +++ b/configapi/api_default.go @@ -240,7 +240,7 @@ func NetworkSliceSliceNameDelete(c *gin.Context) { // @Failure 401 {object} nil "Authorization failed" // @Failure 403 {object} nil "Forbidden" // @Failure 500 {object} nil "Error creating network slice" -// @Router /config/v1/network-slice/{sliceName [post] +// @Router /config/v1/network-slice/{sliceName} [post] func NetworkSliceSliceNamePost(c *gin.Context) { logger.ConfigLog.Debugf("Received NetworkSliceSliceNamePost ") if ret := NetworkSlicePostHandler(c, configmodels.Post_op); ret { diff --git a/configapi/api_inventory.go b/configapi/api_inventory.go index e92f4375..d92a3fb0 100644 --- a/configapi/api_inventory.go +++ b/configapi/api_inventory.go @@ -4,9 +4,11 @@ package configapi import ( + "context" "encoding/json" "fmt" "net/http" + "strconv" "strings" "github.com/gin-gonic/gin" @@ -14,11 +16,7 @@ import ( "github.com/omec-project/webconsole/configmodels" "github.com/omec-project/webconsole/dbadapter" "go.mongodb.org/mongo-driver/bson" -) - -const ( - gnbDataColl = "webconsoleData.snapshots.gnbData" - upfDataColl = "webconsoleData.snapshots.upfData" + "go.mongodb.org/mongo-driver/mongo" ) func setInventoryCorsHeader(c *gin.Context) { @@ -45,7 +43,7 @@ func GetGnbs(c *gin.Context) { var gnbs []*configmodels.Gnb gnbs = make([]*configmodels.Gnb, 0) - rawGnbs, errGetMany := dbadapter.CommonDBClient.RestfulAPIGetMany(gnbDataColl, bson.M{}) + rawGnbs, errGetMany := dbadapter.CommonDBClient.RestfulAPIGetMany(configmodels.GnbDataColl, bson.M{}) if errGetMany != nil { logger.DbLog.Errorln(errGetMany) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve gNBs"}) @@ -177,7 +175,7 @@ func GetUpfs(c *gin.Context) { var upfs []*configmodels.Upf upfs = make([]*configmodels.Upf, 0) - rawUpfs, errGetMany := dbadapter.CommonDBClient.RestfulAPIGetMany(upfDataColl, bson.M{}) + rawUpfs, errGetMany := dbadapter.CommonDBClient.RestfulAPIGetMany(configmodels.UpfDataColl, bson.M{}) if errGetMany != nil { logger.DbLog.Errorln(errGetMany) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve UPFs"}) @@ -200,21 +198,150 @@ func GetUpfs(c *gin.Context) { // @Description Create a new UPF // @Tags UPFs // @Produce json -// @Param upf-hostname path string true "Name of the UPF" -// @Param port body configmodels.PostUpfRequest true "Port of the UPF" +// @Param upf body configmodels.PostUpfRequest true "Hostname and port of the UPF to create" // @Security BearerAuth -// @Success 200 {object} nil "UPF created" -// @Failure 400 {object} nil "Failed to create the UPF" +// @Success 201 {object} nil "UPF successfully created" +// @Failure 400 {object} nil "Bad request" // @Failure 401 {object} nil "Authorization failed" // @Failure 403 {object} nil "Forbidden" -// @Router /config/v1/inventory/upf/{upf-hostname} [post] +// @Failure 500 {object} nil "Error creating UPF" +// @Router /config/v1/inventory/upf/ [post] func PostUpf(c *gin.Context) { - setInventoryCorsHeader(c) - if err := handlePostUpf(c); err == nil { + logger.WebUILog.Infoln("received a POST UPF request") + var postUpfParams configmodels.PostUpfRequest + err := c.ShouldBindJSON(&postUpfParams) + if err != nil { + logger.ConfigLog.Errorln("invalid UPF POST input parameters. Error:", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON format"}) + return + } + if postUpfParams.Hostname == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "UPF hostname must be provided"}) + return + } + if _, err := strconv.Atoi(postUpfParams.Port); err != nil { + errorMessage := "UPF port cannot be converted to integer or it was not provided" + logger.WebUILog.Errorln(errorMessage) + c.JSON(http.StatusBadRequest, gin.H{"error": errorMessage}) + return + } + filter := bson.M{"hostname": postUpfParams.Hostname} + upfDataBson := configmodels.ToBsonM(postUpfParams) + err = dbadapter.CommonDBClient.RestfulAPIPostMany(configmodels.UpfDataColl, filter, []interface{}{upfDataBson}) + if err != nil { + if strings.Contains(err.Error(), "E11000") { + logger.DbLog.Errorln("Duplicate hostname found:", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "UPF already exists"}) + return + } + logger.DbLog.Errorln(err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create UPF"}) + return + } + logger.WebUILog.Infof("successfully executed POST UPF %v request", postUpfParams.Hostname) + c.JSON(http.StatusCreated, gin.H{}) +} + +// PutUpf godoc +// +// @Description Create or update a UPF +// @Tags UPFs +// @Produce json +// @Param upf-hostname path string true "Name of the UPF to update" +// @Param port body configmodels.PutUpfRequest true "Port of the UPF to update" +// @Security BearerAuth +// @Success 200 {object} nil "UPF successfully updated" +// @Success 201 {object} nil "UPF successfully created" +// @Failure 400 {object} nil "Bad request" +// @Failure 401 {object} nil "Authorization failed" +// @Failure 403 {object} nil "Forbidden" +// @Failure 500 {object} nil "Error updating UPF" +// @Router /config/v1/inventory/upf/{upf-hostname} [put] +func PutUpf(c *gin.Context) { + logger.WebUILog.Infoln("received a PUT UPF request") + hostname, exists := c.Params.Get("upf-hostname") + if !exists { + errorMessage := "put UPF request is missing path param `upf-hostname`" + logger.WebUILog.Errorln(errorMessage) + c.JSON(http.StatusBadRequest, gin.H{"error": errorMessage}) + return + } + var putUpfParams configmodels.PutUpfRequest + err := c.ShouldBindJSON(&putUpfParams) + if err != nil { + logger.WebUILog.Errorw("invalid UPF PUT input parameters", "hostname", hostname, "error", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON format"}) + return + } + if _, err := strconv.Atoi(putUpfParams.Port); err != nil { + errorMessage := "UPF port cannot be converted to integer or it was not provided" + logger.WebUILog.Errorln(errorMessage) + c.JSON(http.StatusBadRequest, gin.H{"error": errorMessage}) + return + } + filter := bson.M{"hostname": hostname} + putUpf := configmodels.Upf{ + Hostname: hostname, + Port: putUpfParams.Port, + } + ctx, err := handlePutUpfTransaction(c.Request.Context(), filter, putUpf) + if err != nil { + logger.WebUILog.Errorw("failed to PUT UPF", "hostname", hostname, "error", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to PUT UPF"}) + return + } + c.Request = c.Request.WithContext(ctx) + logger.WebUILog.Infof("successfully executed PUT UPF %v request", hostname) + + if existed, ok := ctx.Value("existed").(bool); ok && existed { c.JSON(http.StatusOK, gin.H{}) - } else { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusCreated, gin.H{}) +} + +func handlePutUpfTransaction(ctx context.Context, filter bson.M, upf configmodels.Upf) (context.Context, error) { + session, err := dbadapter.CommonDBClient.StartSession() + if err != nil { + return ctx, fmt.Errorf("failed to initialize DB session: %w", err) } + defer session.EndSession(ctx) + return ctx, mongo.WithSession(ctx, session, func(sc mongo.SessionContext) error { + if err := session.StartTransaction(); err != nil { + return fmt.Errorf("failed to start transaction: %w", err) + } + upfDataBson := configmodels.ToBsonM(upf) + existed, err := dbadapter.CommonDBClient.RestfulAPIPutOneWithContext(configmodels.UpfDataColl, filter, upfDataBson, sc) + if err != nil { + if abortErr := session.AbortTransaction(sc); abortErr != nil { + logger.DbLog.Errorw("failed to abort transaction", "error", abortErr) + } + return err + } + patchJSON := []byte(fmt.Sprintf(`[ + { + "op": "replace", + "path": "/site-info/upf", + "value": { + "upf-name": "%s", + "upf-port": "%s" + } + } + ]`, upf.Hostname, upf.Port)) + err = updateUpfInNetworkSlices(upf.Hostname, patchJSON, sc) + if err != nil { + if abortErr := session.AbortTransaction(sc); abortErr != nil { + logger.DbLog.Errorw("failed to abort transaction", "error", abortErr) + } + return fmt.Errorf("failed to update network slices: %w", err) + } + + if err = session.CommitTransaction(sc); err != nil { + return fmt.Errorf("failed to commit transaction: %w", err) + } + ctx = context.WithValue(ctx, "existed", existed) + return nil + }) } // DeleteUpf godoc @@ -238,42 +365,6 @@ func DeleteUpf(c *gin.Context) { } } -func handlePostUpf(c *gin.Context) error { - upfHostname, exists := c.Params.Get("upf-hostname") - if !exists { - errorMessage := "post UPF request is missing upf-hostname" - logger.ConfigLog.Errorln(errorMessage) - return fmt.Errorf("%s", errorMessage) - } - logger.ConfigLog.Infof("received UPF %v", upfHostname) - if !strings.HasPrefix(c.GetHeader("Content-Type"), "application/json") { - return fmt.Errorf("invalid header") - } - var postUpfRequest configmodels.PostUpfRequest - err := c.ShouldBindJSON(&postUpfRequest) - if err != nil { - logger.ConfigLog.Errorf("err %v", err) - return fmt.Errorf("invalid JSON format") - } - if postUpfRequest.Port == "" { - errorMessage := "post UPF request body is missing port" - logger.ConfigLog.Errorln(errorMessage) - return fmt.Errorf("%s", errorMessage) - } - postUpf := configmodels.Upf{ - Hostname: upfHostname, - Port: postUpfRequest.Port, - } - msg := configmodels.ConfigMessage{ - MsgType: configmodels.Inventory, - MsgMethod: configmodels.Post_op, - Upf: &postUpf, - } - configChannel <- &msg - logger.ConfigLog.Infof("successfully added UPF [%v] to config channel", upfHostname) - return nil -} - func handleDeleteUpf(c *gin.Context) error { upfHostname, exists := c.Params.Get("upf-hostname") if !exists { @@ -291,3 +382,23 @@ func handleDeleteUpf(c *gin.Context) error { logger.ConfigLog.Infof("successfully added UPF [%v] with delete_op to config channel", upfHostname) return nil } + +func updateUpfInNetworkSlices(hostname string, patchJSON []byte, context context.Context) error { + filterByUpf := bson.M{"site-info.upf.upf-name": hostname} + rawNetworkSlices, err := dbadapter.CommonDBClient.RestfulAPIGetMany(sliceDataColl, filterByUpf) + if err != nil { + return fmt.Errorf("failed to fetch network slices: %w", err) + } + for _, rawNetworkSlice := range rawNetworkSlices { + sliceName, ok := rawNetworkSlice["slice-name"].(string) + if !ok { + return fmt.Errorf("invalid slice-name in network slice: %v", rawNetworkSlice) + } + filterBySliceName := bson.M{"slice-name": sliceName} + err = dbadapter.CommonDBClient.RestfulAPIJSONPatchWithContext(sliceDataColl, filterBySliceName, patchJSON, context) + if err != nil { + return err + } + } + return nil +} diff --git a/configapi/api_inventory_test.go b/configapi/api_inventory_test.go index f3e5ce6f..083fec7d 100644 --- a/configapi/api_inventory_test.go +++ b/configapi/api_inventory_test.go @@ -4,7 +4,9 @@ package configapi import ( + "context" "encoding/json" + "errors" "net/http" "net/http/httptest" "strings" @@ -14,8 +16,28 @@ import ( "github.com/omec-project/webconsole/configmodels" "github.com/omec-project/webconsole/dbadapter" "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" ) +type MockSession struct { + mongo.Session +} + +func (m *MockSession) StartTransaction(opts ...*options.TransactionOptions) error { + return nil +} + +func (m *MockSession) AbortTransaction(ctx context.Context) error { + return nil +} + +func (m *MockSession) CommitTransaction(ctx context.Context) error { + return nil +} + +func (m *MockSession) EndSession(ctx context.Context) {} + type MockMongoClientOneGnb struct { dbadapter.DBInterface } @@ -32,6 +54,60 @@ type MockMongoClientManyUpfs struct { dbadapter.DBInterface } +type MockMongoClientPutOneUpf struct { + dbadapter.DBInterface +} + +func (db *MockMongoClientPutOneUpf) RestfulAPIGetMany(collName string, filter bson.M) ([]map[string]interface{}, error) { + return []map[string]interface{}{}, nil +} + +func (db *MockMongoClientEmptyDB) RestfulAPIDeleteOneWithContext(collName string, filter bson.M, context context.Context) error { + return nil +} +func (db *MockMongoClientDBError) RestfulAPIDeleteOneWithContext(collName string, filter bson.M, context context.Context) error { + return errors.New("DB error") +} + +func (db *MockMongoClientEmptyDB) RestfulAPIJSONPatchWithContext(collName string, filter bson.M, patchJSON []byte, context context.Context) error { + return nil +} +func (db *MockMongoClientDBError) RestfulAPIJSONPatchWithContext(collName string, filter bson.M, patchJSON []byte, context context.Context) error { + return errors.New("DB error") +} + +func (db *MockMongoClientDBError) RestfulAPIPostMany(collName string, filter bson.M, postDataArray []interface{}) error { + return errors.New("DB error") +} + +func (db *MockMongoClientEmptyDB) RestfulAPIPutOneWithContext(collName string, filter bson.M, putData map[string]interface{}, context context.Context) (bool, error) { + return false, nil +} + +func (db *MockMongoClientPutOneUpf) RestfulAPIPutOneWithContext(collName string, filter bson.M, putData map[string]interface{}, context context.Context) (bool, error) { + return true, nil +} + +func (db *MockMongoClientDBError) RestfulAPIPutOneWithContext(collName string, filter bson.M, putData map[string]interface{}, context context.Context) (bool, error) { + return false, errors.New("DB error") +} + +func (m *MockMongoClientEmptyDB) StartSession() (mongo.Session, error) { + return &MockSession{}, nil +} + +func (m *MockMongoClientOneUpf) StartSession() (mongo.Session, error) { + return &MockSession{}, nil +} + +func (m *MockMongoClientPutOneUpf) StartSession() (mongo.Session, error) { + return &MockSession{}, nil +} + +func (m *MockMongoClientDBError) StartSession() (mongo.Session, error) { + return &MockSession{}, nil +} + func (m *MockMongoClientOneGnb) RestfulAPIGetMany(coll string, filter bson.M) ([]map[string]interface{}, error) { var results []map[string]interface{} gnb := configmodels.Gnb{ @@ -186,7 +262,7 @@ func TestInventoryGetHandlers(t *testing.T) { } } -func TestInventoryPostHandlers_Failure(t *testing.T) { +func TestGnbPostHandlers_Failure(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.Default() AddConfigV1Service(router) @@ -219,27 +295,6 @@ func TestInventoryPostHandlers_Failure(t *testing.T) { header: "application", expectedBody: `{"error":"invalid header"}`, }, - { - name: "Port is not a string", - route: "/config/v1/inventory/upf/upf1", - inputData: `{"port": 1234}`, - header: "application/json", - expectedBody: `{"error":"invalid JSON format"}`, - }, - { - name: "Missing port", - route: "/config/v1/inventory/upf/upf1", - inputData: `{"some_param": "123"}`, - header: "application/json", - expectedBody: `{"error":"post UPF request body is missing port"}`, - }, - { - name: "UpfInvalidHeader", - route: "/config/v1/inventory/upf/upf1", - inputData: `{"port": "123"}`, - header: "application", - expectedBody: `{"error":"invalid header"}`, - }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { @@ -271,7 +326,7 @@ func TestInventoryPostHandlers_Failure(t *testing.T) { } } -func TestInventoryPostHandlers_Success(t *testing.T) { +func TestGnbPostHandlers_Success(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.Default() AddConfigV1Service(router) @@ -295,19 +350,6 @@ func TestInventoryPostHandlers_Success(t *testing.T) { }, }, }, - { - name: "PostUpf", - route: "/config/v1/inventory/upf/upf1", - inputData: `{"port": "123"}`, - expectedMessage: configmodels.ConfigMessage{ - MsgType: configmodels.Inventory, - MsgMethod: configmodels.Post_op, - Upf: &configmodels.Upf{ - Hostname: "upf1", - Port: "123", - }, - }, - }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { @@ -355,14 +397,6 @@ func TestInventoryPostHandlers_Success(t *testing.T) { t.Errorf("expected gNB %+v, but got %+v", tc.expectedMessage.Gnb, msg.Gnb) } } - if tc.expectedMessage.Upf != nil { - if msg.Upf == nil { - t.Errorf("expected UPF %+v, but got nil", tc.expectedMessage.Upf) - } - if tc.expectedMessage.Upf.Hostname != msg.Upf.Hostname || tc.expectedMessage.Upf.Port != msg.Upf.Port { - t.Errorf("expected UPF %+v, but got %+v", tc.expectedMessage.Upf, msg.Upf) - } - } default: t.Error("expected message in configChannel, but none received") } @@ -370,6 +404,180 @@ func TestInventoryPostHandlers_Success(t *testing.T) { } } +func TestUpfPostHandler(t *testing.T) { + gin.SetMode(gin.TestMode) + router := gin.Default() + AddConfigV1Service(router) + + testCases := []struct { + name string + route string + dbAdapter dbadapter.DBInterface + inputData string + expectedCode int + expectedBody string + }{ + { + name: "Create a new UPF success", + route: "/config/v1/inventory/upf", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"hostname": "host", "port": "123"}`, + expectedCode: http.StatusCreated, + expectedBody: "{}", + }, + { + name: "Create an existing UPF expects failure", + route: "/config/v1/inventory/upf", + dbAdapter: &MockMongoClientDuplicateCreation{}, + inputData: `{"hostname": "upf1", "port": "123"}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"UPF already exists"}`, + }, + { + name: "Port is not a string expects failure", + route: "/config/v1/inventory/upf", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"hostname": "host", "port": 1234}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"invalid JSON format"}`, + }, + { + name: "Missing port expects failure", + route: "/config/v1/inventory/upf", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"hostname": "host", "some_param": "123"}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"UPF port cannot be converted to integer or it was not provided"}`, + }, + { + name: "DB POST operation fails expects failure", + route: "/config/v1/inventory/upf", + dbAdapter: &MockMongoClientDBError{}, + inputData: `{"hostname": "host", "port": "123"}`, + expectedCode: http.StatusInternalServerError, + expectedBody: `{"error":"failed to create UPF"}`, + }, + { + name: "Port cannot be converted to int expects failure", + route: "/config/v1/inventory/upf", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"hostname": "host", "port": "a"}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"UPF port cannot be converted to integer or it was not provided"}`, + }, + { + name: "Hostname not provided expects failure", + route: "/config/v1/inventory/upf", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"port": "a"}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"UPF hostname must be provided"}`, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dbadapter.CommonDBClient = tc.dbAdapter + req, err := http.NewRequest(http.MethodPost, tc.route, strings.NewReader(tc.inputData)) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + if tc.expectedCode != w.Code { + t.Errorf("Expected `%v`, got `%v`", tc.expectedCode, w.Code) + } + if tc.expectedBody != w.Body.String() { + t.Errorf("Expected `%v`, got `%v`", tc.expectedBody, w.Body.String()) + } + }) + } +} + +func TestUpfPutHandler(t *testing.T) { + gin.SetMode(gin.TestMode) + router := gin.Default() + AddConfigV1Service(router) + + testCases := []struct { + name string + route string + dbAdapter dbadapter.DBInterface + inputData string + expectedCode int + expectedBody string + }{ + { + name: "Put a new UPF expects Created status", + route: "/config/v1/inventory/upf/upf1", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"port": "123"}`, + expectedCode: http.StatusCreated, + expectedBody: "{}", + }, + { + name: "Put an existing UPF expects a OK status", + route: "/config/v1/inventory/upf/upf1", + dbAdapter: &MockMongoClientPutOneUpf{}, + inputData: `{"port": "123"}`, + expectedCode: http.StatusOK, + expectedBody: "{}", + }, + { + name: "Port is not a string expects failure", + route: "/config/v1/inventory/upf/upf1", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"port": 1234}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"invalid JSON format"}`, + }, + { + name: "Missing port expects failure", + route: "/config/v1/inventory/upf/upf1", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"some_param": "123"}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"UPF port cannot be converted to integer or it was not provided"}`, + }, + { + name: "DB PUT operation fails expects failure", + route: "/config/v1/inventory/upf/upf1", + dbAdapter: &MockMongoClientDBError{}, + inputData: `{"port": "123"}`, + expectedCode: http.StatusInternalServerError, + expectedBody: `{"error":"failed to PUT UPF"}`, + }, + { + name: "Port cannot be converted to int expects failure", + route: "/config/v1/inventory/upf/upf1", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"port": "a"}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"UPF port cannot be converted to integer or it was not provided"}`, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dbadapter.CommonDBClient = tc.dbAdapter + req, err := http.NewRequest(http.MethodPut, tc.route, strings.NewReader(tc.inputData)) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + if tc.expectedCode != w.Code { + t.Errorf("Expected `%v`, got `%v`", tc.expectedCode, w.Code) + } + if tc.expectedBody != w.Body.String() { + t.Errorf("Expected `%v`, got `%v`", tc.expectedBody, w.Body.String()) + } + }) + } +} + func TestInventoryDeleteHandlers_Success(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.Default() @@ -439,9 +647,6 @@ func TestInventoryDeleteHandlers_Success(t *testing.T) { if msg.Gnb != nil { t.Errorf("expected gNB nil, but got %+v", msg.Gnb) } - if msg.Upf != nil { - t.Errorf("expected UPF nil, but got %+v", msg.Upf) - } default: t.Error("expected message in configChannel, but none received") } diff --git a/configapi/routers.go b/configapi/routers.go index bb4082a5..587f73a1 100644 --- a/configapi/routers.go +++ b/configapi/routers.go @@ -180,9 +180,15 @@ var routes = Routes{ { "PostUpf", http.MethodPost, - "/inventory/upf/:upf-hostname", + "/inventory/upf", PostUpf, }, + { + "PutUpf", + http.MethodPut, + "/inventory/upf/:upf-hostname", + PutUpf, + }, { "DeleteUpf", http.MethodDelete, diff --git a/configmodels/config_msg.go b/configmodels/config_msg.go index 38023486..fc1f7b5b 100644 --- a/configmodels/config_msg.go +++ b/configmodels/config_msg.go @@ -27,7 +27,6 @@ type ConfigMessage struct { Slice *Slice AuthSubData *models.AuthenticationSubscription Gnb *Gnb - Upf *Upf DevGroupName string SliceName string Imsi string diff --git a/configmodels/model_inventory.go b/configmodels/model_inventory.go index 4e8e9a85..44ada97e 100644 --- a/configmodels/model_inventory.go +++ b/configmodels/model_inventory.go @@ -3,6 +3,11 @@ package configmodels +const ( + GnbDataColl = "webconsoleData.snapshots.gnbData" + UpfDataColl = "webconsoleData.snapshots.upfData" +) + type Gnb struct { Name string `json:"name"` Tac string `json:"tac"` @@ -18,5 +23,10 @@ type Upf struct { } type PostUpfRequest struct { + Hostname string `json:"hostname"` + Port string `json:"port"` +} + +type PutUpfRequest struct { Port string `json:"port"` } diff --git a/dbadapter/db_adapter.go b/dbadapter/db_adapter.go index eb69284b..e56a8dc4 100644 --- a/dbadapter/db_adapter.go +++ b/dbadapter/db_adapter.go @@ -6,12 +6,14 @@ package dbadapter import ( + "context" "time" "github.com/omec-project/util/mongoapi" "github.com/omec-project/webconsole/backend/logger" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" ) type DBInterface interface { @@ -19,17 +21,22 @@ type DBInterface interface { RestfulAPIGetMany(collName string, filter bson.M) ([]map[string]interface{}, error) RestfulAPIPutOneTimeout(collName string, filter bson.M, putData map[string]interface{}, timeout int32, timeField string) bool RestfulAPIPutOne(collName string, filter bson.M, putData map[string]interface{}) (bool, error) + RestfulAPIPutOneWithContext(collName string, filter bson.M, putData map[string]interface{}, context context.Context) (bool, error) RestfulAPIPutOneNotUpdate(collName string, filter bson.M, putData map[string]interface{}) (bool, error) RestfulAPIPutMany(collName string, filterArray []primitive.M, putDataArray []map[string]interface{}) error RestfulAPIDeleteOne(collName string, filter bson.M) error + RestfulAPIDeleteOneWithContext(collName string, filter bson.M, context context.Context) error RestfulAPIDeleteMany(collName string, filter bson.M) error RestfulAPIMergePatch(collName string, filter bson.M, patchData map[string]interface{}) error RestfulAPIJSONPatch(collName string, filter bson.M, patchJSON []byte) error + RestfulAPIJSONPatchWithContext(collName string, filter bson.M, patchJSON []byte, context context.Context) error RestfulAPIJSONPatchExtend(collName string, filter bson.M, patchJSON []byte, dataName string) error RestfulAPIPost(collName string, filter bson.M, postData map[string]interface{}) (bool, error) RestfulAPIPostMany(collName string, filter bson.M, postDataArray []interface{}) error RestfulAPICount(collName string, filter bson.M) (int64, error) CreateIndex(collName string, keyField string) (bool, error) + StartSession() (mongo.Session, error) + SupportsTransactions() (bool, error) } var ( @@ -88,6 +95,10 @@ func (db *MongoDBClient) RestfulAPIPutOne(collName string, filter bson.M, putDat return db.MongoClient.RestfulAPIPutOne(collName, filter, putData) } +func (db *MongoDBClient) RestfulAPIPutOneWithContext(collName string, filter bson.M, putData map[string]interface{}, context context.Context) (bool, error) { + return db.MongoClient.RestfulAPIPutOneWithContext(collName, filter, putData, context) +} + func (db *MongoDBClient) RestfulAPIPutOneNotUpdate(collName string, filter bson.M, putData map[string]interface{}) (bool, error) { return db.MongoClient.RestfulAPIPutOneNotUpdate(collName, filter, putData) } @@ -100,6 +111,10 @@ func (db *MongoDBClient) RestfulAPIDeleteOne(collName string, filter bson.M) err return db.MongoClient.RestfulAPIDeleteOne(collName, filter) } +func (db *MongoDBClient) RestfulAPIDeleteOneWithContext(collName string, filter bson.M, context context.Context) error { + return db.MongoClient.RestfulAPIDeleteOneWithContext(collName, filter, context) +} + func (db *MongoDBClient) RestfulAPIDeleteMany(collName string, filter bson.M) error { return db.MongoClient.RestfulAPIDeleteMany(collName, filter) } @@ -112,6 +127,10 @@ func (db *MongoDBClient) RestfulAPIJSONPatch(collName string, filter bson.M, pat return db.MongoClient.RestfulAPIJSONPatch(collName, filter, patchJSON) } +func (db *MongoDBClient) RestfulAPIJSONPatchWithContext(collName string, filter bson.M, patchJSON []byte, context context.Context) error { + return db.MongoClient.RestfulAPIJSONPatchWithContext(collName, filter, patchJSON, context) +} + func (db *MongoDBClient) RestfulAPIJSONPatchExtend(collName string, filter bson.M, patchJSON []byte, dataName string) error { return db.MongoClient.RestfulAPIJSONPatchExtend(collName, filter, patchJSON, dataName) } @@ -131,3 +150,11 @@ func (db *MongoDBClient) RestfulAPICount(collName string, filter bson.M) (int64, func (db *MongoDBClient) CreateIndex(collName string, keyField string) (bool, error) { return db.MongoClient.CreateIndex(collName, keyField) } + +func (db *MongoDBClient) StartSession() (mongo.Session, error) { + return db.MongoClient.StartSession() +} + +func (db *MongoDBClient) SupportsTransactions() (bool, error) { + return db.MongoClient.SupportsTransactions() +} diff --git a/docs/docs.go b/docs/docs.go index 156c3e17..de635354 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2024 Canonical Ltd - // Package docs Code generated by swaggo/swag. DO NOT EDIT package docs @@ -728,7 +725,7 @@ const docTemplate = `{ } } }, - "/config/v1/inventory/upf/{upf-hostname}": { + "/config/v1/inventory/upf/": { "post": { "security": [ { @@ -742,36 +739,86 @@ const docTemplate = `{ "tags": [ "UPFs" ], + "parameters": [ + { + "description": "Hostname and port of the UPF to create", + "name": "upf", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/configmodels.PostUpfRequest" + } + } + ], + "responses": { + "201": { + "description": "UPF successfully created" + }, + "400": { + "description": "Bad request" + }, + "401": { + "description": "Authorization failed" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Error creating UPF" + } + } + } + }, + "/config/v1/inventory/upf/{upf-hostname}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create or update a UPF", + "produces": [ + "application/json" + ], + "tags": [ + "UPFs" + ], "parameters": [ { "type": "string", - "description": "Name of the UPF", + "description": "Name of the UPF to update", "name": "upf-hostname", "in": "path", "required": true }, { - "description": "Port of the UPF", + "description": "Port of the UPF to update", "name": "port", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/configmodels.PostUpfRequest" + "$ref": "#/definitions/configmodels.PutUpfRequest" } } ], "responses": { "200": { - "description": "UPF created" + "description": "UPF successfully updated" + }, + "201": { + "description": "UPF successfully created" }, "400": { - "description": "Failed to create the UPF" + "description": "Bad request" }, "401": { "description": "Authorization failed" }, "403": { "description": "Forbidden" + }, + "500": { + "description": "Error updating UPF" } } }, @@ -849,14 +896,17 @@ const docTemplate = `{ } } }, - "/config/v1/network-slice/{sliceName": { - "post": { + "/config/v1/network-slice/{sliceName}": { + "get": { "security": [ { "BearerAuth": [] } ], - "description": "Create a new network slice", + "description": "Return the network slice", + "produces": [ + "application/json" + ], "tags": [ "Network Slices" ], @@ -867,23 +917,14 @@ const docTemplate = `{ "name": "sliceName", "in": "path", "required": true - }, - { - "description": " ", - "name": "content", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/configmodels.Slice" - } } ], "responses": { "200": { - "description": "Network slice created" - }, - "400": { - "description": "Invalid network slice content" + "description": "Network slice", + "schema": { + "$ref": "#/definitions/configmodels.Slice" + } }, "401": { "description": "Authorization failed" @@ -891,23 +932,21 @@ const docTemplate = `{ "403": { "description": "Forbidden" }, + "404": { + "description": "Network slices not found" + }, "500": { - "description": "Error creating network slice" + "description": "Error retrieving network slice" } } - } - }, - "/config/v1/network-slice/{sliceName}": { - "get": { + }, + "post": { "security": [ { "BearerAuth": [] } ], - "description": "Return the network slice", - "produces": [ - "application/json" - ], + "description": "Create a new network slice", "tags": [ "Network Slices" ], @@ -918,14 +957,23 @@ const docTemplate = `{ "name": "sliceName", "in": "path", "required": true + }, + { + "description": " ", + "name": "content", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/configmodels.Slice" + } } ], "responses": { "200": { - "description": "Network slice", - "schema": { - "$ref": "#/definitions/configmodels.Slice" - } + "description": "Network slice created" + }, + "400": { + "description": "Invalid network slice content" }, "401": { "description": "Authorization failed" @@ -933,11 +981,8 @@ const docTemplate = `{ "403": { "description": "Forbidden" }, - "404": { - "description": "Network slices not found" - }, "500": { - "description": "Error retrieving network slice" + "description": "Error creating network slice" } } }, @@ -1193,6 +1238,17 @@ const docTemplate = `{ } }, "configmodels.PostUpfRequest": { + "type": "object", + "properties": { + "hostname": { + "type": "string" + }, + "port": { + "type": "string" + } + } + }, + "configmodels.PutUpfRequest": { "type": "object", "properties": { "port": { diff --git a/docs/docs.go.license b/docs/docs.go.license new file mode 100644 index 00000000..001eccf1 --- /dev/null +++ b/docs/docs.go.license @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 Canonical Ltd diff --git a/go.mod b/go.mod index 40d44c18..d1b0dcdf 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/omec-project/config5g v1.5.5 github.com/omec-project/openapi v1.3.2 - github.com/omec-project/util v1.2.8 + github.com/omec-project/util v1.2.10 github.com/prometheus/client_golang v1.20.5 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 @@ -49,7 +49,7 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.7.6 // indirect @@ -72,7 +72,7 @@ require ( github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/arch v0.12.0 // indirect - golang.org/x/net v0.33.0 // indirect + golang.org/x/net v0.34.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect diff --git a/go.sum b/go.sum index 18a1a346..250263bd 100644 --- a/go.sum +++ b/go.sum @@ -77,8 +77,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= @@ -116,8 +116,8 @@ github.com/omec-project/config5g v1.5.5 h1:9ouNkogjNBPi4Qa0gz12Ab4pl8gp5XWvELTCD github.com/omec-project/config5g v1.5.5/go.mod h1:r5K7D2jMigypWWcWJDUZlgpcH8gOAvPT6phtKESGzNs= github.com/omec-project/openapi v1.3.2 h1:LhUA/vBKCswN0S8fc/dN5/xuUltLgoak6au2MAKwUkk= github.com/omec-project/openapi v1.3.2/go.mod h1:JByn0faBQbiWX/q3JA+FHOdQS9m6KdyHDF5VVjCGMms= -github.com/omec-project/util v1.2.8 h1:yiR/YfItbmm7to3chyLr+YxjcN3BvQUNre6fu75yFDI= -github.com/omec-project/util v1.2.8/go.mod h1:wTonG4t81Cf4zSNid3/SGYtEEWd926LBOXbD827/Bs0= +github.com/omec-project/util v1.2.10 h1:FaLz5Msn6wqq8tHREEE0F/GPGbZmz0dRLlWp3E82B5Y= +github.com/omec-project/util v1.2.10/go.mod h1:P+WFVCtuLfsrfTgWZLaR5S6QCnA94PT7KdV6KSAFbw8= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -203,8 +203,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= diff --git a/proto/server/configEvtHandler.go b/proto/server/configEvtHandler.go index 82cbe2d2..f6903b9a 100644 --- a/proto/server/configEvtHandler.go +++ b/proto/server/configEvtHandler.go @@ -102,11 +102,6 @@ func configHandler(configMsgChan chan *configmodels.ConfigMessage, configReceive handleGnbPost(configMsg.Gnb) } - if configMsg.Upf != nil { - logger.ConfigLog.Infof("received UPF [%v] configuration from config channel", configMsg.UpfHostname) - handleUpfPost(configMsg.Upf) - } - // loop through all clients and send this message to all clients if len(clientNFPool) == 0 { logger.ConfigLog.Infoln("no client available. No need to send config") @@ -264,17 +259,6 @@ func handleGnbDelete(gnbName string) { rwLock.Unlock() } -func handleUpfPost(upf *configmodels.Upf) { - rwLock.Lock() - filter := bson.M{"hostname": upf.Hostname} - upfDataBson := configmodels.ToBsonM(upf) - _, errPost := dbadapter.CommonDBClient.RestfulAPIPost(upfDataColl, filter, upfDataBson) - if errPost != nil { - logger.DbLog.Warnln(errPost) - } - rwLock.Unlock() -} - func handleUpfDelete(upfHostname string) { rwLock.Lock() filter := bson.M{"hostname": upfHostname} diff --git a/proto/server/configEvtHandler_test.go b/proto/server/configEvtHandler_test.go index 4cb8e269..836fce43 100644 --- a/proto/server/configEvtHandler_test.go +++ b/proto/server/configEvtHandler_test.go @@ -507,29 +507,3 @@ func TestPostGnb(t *testing.T) { t.Errorf("Expected port %v, got %v", newGnb.Tac, result["tac"]) } } - -func TestPostUpf(t *testing.T) { - upfHostname := "some-upf" - newUpf := configmodels.Upf{ - Hostname: upfHostname, - Port: "1233", - } - postData = make([]map[string]interface{}, 0) - dbadapter.CommonDBClient = &MockMongoPost{} - handleUpfPost(&newUpf) - - expected_collection := "webconsoleData.snapshots.upfData" - if postData[0]["coll"] != expected_collection { - t.Errorf("Expected collection %v, got %v", expected_collection, postData[0]["coll"]) - } - - expected_filter := bson.M{"hostname": upfHostname} - if !reflect.DeepEqual(postData[0]["filter"], expected_filter) { - t.Errorf("Expected filter %v, got %v", expected_filter, postData[0]["filter"]) - } - - var result map[string]interface{} = postData[0]["data"].(map[string]interface{}) - if result["port"] != newUpf.Port { - t.Errorf("Expected port %v, got %v", newUpf.Port, result["port"]) - } -} From c57383007f6b783e5b70db3ea77b9952d80fa676 Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Tue, 21 Jan 2025 12:14:57 +0100 Subject: [PATCH 02/13] implement transaction for POST Signed-off-by: Patricia Reinoso --- configapi/api_inventory.go | 55 +++++++++++++++------------------ configapi/api_inventory_test.go | 8 +++-- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/configapi/api_inventory.go b/configapi/api_inventory.go index d92a3fb0..521cb486 100644 --- a/configapi/api_inventory.go +++ b/configapi/api_inventory.go @@ -211,7 +211,7 @@ func PostUpf(c *gin.Context) { var postUpfParams configmodels.PostUpfRequest err := c.ShouldBindJSON(&postUpfParams) if err != nil { - logger.ConfigLog.Errorln("invalid UPF POST input parameters. Error:", err) + logger.WebUILog.Errorw("invalid UPF POST input parameters", "error", err) c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON format"}) return } @@ -225,16 +225,14 @@ func PostUpf(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": errorMessage}) return } - filter := bson.M{"hostname": postUpfParams.Hostname} - upfDataBson := configmodels.ToBsonM(postUpfParams) - err = dbadapter.CommonDBClient.RestfulAPIPostMany(configmodels.UpfDataColl, filter, []interface{}{upfDataBson}) - if err != nil { + upf := configmodels.Upf(postUpfParams) + if err = handleUpfTransaction(c.Request.Context(), upf, postUpfOperation); err != nil { if strings.Contains(err.Error(), "E11000") { - logger.DbLog.Errorln("Duplicate hostname found:", err) + logger.WebUILog.Errorw("duplicate hostname found:", "error", err) c.JSON(http.StatusBadRequest, gin.H{"error": "UPF already exists"}) return } - logger.DbLog.Errorln(err.Error()) + logger.WebUILog.Errorw("failed to create UPF", "hostname", postUpfParams.Hostname, "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create UPF"}) return } @@ -251,7 +249,6 @@ func PostUpf(c *gin.Context) { // @Param port body configmodels.PutUpfRequest true "Port of the UPF to update" // @Security BearerAuth // @Success 200 {object} nil "UPF successfully updated" -// @Success 201 {object} nil "UPF successfully created" // @Failure 400 {object} nil "Bad request" // @Failure 401 {object} nil "Authorization failed" // @Failure 403 {object} nil "Forbidden" @@ -279,40 +276,43 @@ func PutUpf(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": errorMessage}) return } - filter := bson.M{"hostname": hostname} putUpf := configmodels.Upf{ Hostname: hostname, Port: putUpfParams.Port, } - ctx, err := handlePutUpfTransaction(c.Request.Context(), filter, putUpf) - if err != nil { + if err := handleUpfTransaction(c.Request.Context(), putUpf, putUpfOperation); err != nil { logger.WebUILog.Errorw("failed to PUT UPF", "hostname", hostname, "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to PUT UPF"}) return } - c.Request = c.Request.WithContext(ctx) - logger.WebUILog.Infof("successfully executed PUT UPF %v request", hostname) + c.JSON(http.StatusOK, gin.H{}) +} - if existed, ok := ctx.Value("existed").(bool); ok && existed { - c.JSON(http.StatusOK, gin.H{}) - return - } - c.JSON(http.StatusCreated, gin.H{}) +func postUpfOperation(upf configmodels.Upf, sc mongo.SessionContext) error { + filter := bson.M{"hostname": upf.Hostname} + upfDataBson := configmodels.ToBsonM(upf) + return dbadapter.CommonDBClient.RestfulAPIPostMany(configmodels.UpfDataColl, filter, []interface{}{upfDataBson}) +} + +func putUpfOperation(upf configmodels.Upf, sc mongo.SessionContext) error { + filter := bson.M{"hostname": upf.Hostname} + upfDataBson := configmodels.ToBsonM(upf) + _, err := dbadapter.CommonDBClient.RestfulAPIPutOneWithContext(configmodels.UpfDataColl, filter, upfDataBson, sc) + return err } -func handlePutUpfTransaction(ctx context.Context, filter bson.M, upf configmodels.Upf) (context.Context, error) { +func handleUpfTransaction(ctx context.Context, upf configmodels.Upf, operation func(configmodels.Upf, mongo.SessionContext) error) error { session, err := dbadapter.CommonDBClient.StartSession() if err != nil { - return ctx, fmt.Errorf("failed to initialize DB session: %w", err) + return fmt.Errorf("failed to initialize DB session: %w", err) } defer session.EndSession(ctx) - return ctx, mongo.WithSession(ctx, session, func(sc mongo.SessionContext) error { + + return mongo.WithSession(ctx, session, func(sc mongo.SessionContext) error { if err := session.StartTransaction(); err != nil { return fmt.Errorf("failed to start transaction: %w", err) } - upfDataBson := configmodels.ToBsonM(upf) - existed, err := dbadapter.CommonDBClient.RestfulAPIPutOneWithContext(configmodels.UpfDataColl, filter, upfDataBson, sc) - if err != nil { + if err := operation(upf, sc); err != nil { if abortErr := session.AbortTransaction(sc); abortErr != nil { logger.DbLog.Errorw("failed to abort transaction", "error", abortErr) } @@ -335,12 +335,7 @@ func handlePutUpfTransaction(ctx context.Context, filter bson.M, upf configmodel } return fmt.Errorf("failed to update network slices: %w", err) } - - if err = session.CommitTransaction(sc); err != nil { - return fmt.Errorf("failed to commit transaction: %w", err) - } - ctx = context.WithValue(ctx, "existed", existed) - return nil + return session.CommitTransaction(sc) }) } diff --git a/configapi/api_inventory_test.go b/configapi/api_inventory_test.go index 083fec7d..ce573fbc 100644 --- a/configapi/api_inventory_test.go +++ b/configapi/api_inventory_test.go @@ -108,6 +108,10 @@ func (m *MockMongoClientDBError) StartSession() (mongo.Session, error) { return &MockSession{}, nil } +func (m *MockMongoClientDuplicateCreation) StartSession() (mongo.Session, error) { + return &MockSession{}, nil +} + func (m *MockMongoClientOneGnb) RestfulAPIGetMany(coll string, filter bson.M) ([]map[string]interface{}, error) { var results []map[string]interface{} gnb := configmodels.Gnb{ @@ -509,11 +513,11 @@ func TestUpfPutHandler(t *testing.T) { expectedBody string }{ { - name: "Put a new UPF expects Created status", + name: "Put a new UPF expects OK status", route: "/config/v1/inventory/upf/upf1", dbAdapter: &MockMongoClientEmptyDB{}, inputData: `{"port": "123"}`, - expectedCode: http.StatusCreated, + expectedCode: http.StatusOK, expectedBody: "{}", }, { From 6bb6b1175ab1401dc7a94e1a13ec450d8246ab70 Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Tue, 21 Jan 2025 12:16:29 +0100 Subject: [PATCH 03/13] update docs Signed-off-by: Patricia Reinoso --- docs/docs.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index de635354..336c0894 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -805,9 +805,6 @@ const docTemplate = `{ "200": { "description": "UPF successfully updated" }, - "201": { - "description": "UPF successfully created" - }, "400": { "description": "Bad request" }, From 0aa17fb25c190baa8cecf754736203b8a77c3bf7 Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Tue, 21 Jan 2025 12:50:39 +0100 Subject: [PATCH 04/13] refactor tests Signed-off-by: Patricia Reinoso --- configapi/api_inventory_test.go | 124 ++++++---------------- configapi/common_mock_db.go | 131 ++++++++++++++++++++++++ configapi/handlers_user_account_test.go | 63 ------------ 3 files changed, 164 insertions(+), 154 deletions(-) create mode 100644 configapi/common_mock_db.go diff --git a/configapi/api_inventory_test.go b/configapi/api_inventory_test.go index ce573fbc..851b5e1c 100644 --- a/configapi/api_inventory_test.go +++ b/configapi/api_inventory_test.go @@ -6,7 +6,6 @@ package configapi import ( "context" "encoding/json" - "errors" "net/http" "net/http/httptest" "strings" @@ -17,101 +16,12 @@ import ( "github.com/omec-project/webconsole/dbadapter" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" ) -type MockSession struct { - mongo.Session -} - -func (m *MockSession) StartTransaction(opts ...*options.TransactionOptions) error { - return nil -} - -func (m *MockSession) AbortTransaction(ctx context.Context) error { - return nil -} - -func (m *MockSession) CommitTransaction(ctx context.Context) error { - return nil -} - -func (m *MockSession) EndSession(ctx context.Context) {} - type MockMongoClientOneGnb struct { dbadapter.DBInterface } -type MockMongoClientManyGnbs struct { - dbadapter.DBInterface -} - -type MockMongoClientOneUpf struct { - dbadapter.DBInterface -} - -type MockMongoClientManyUpfs struct { - dbadapter.DBInterface -} - -type MockMongoClientPutOneUpf struct { - dbadapter.DBInterface -} - -func (db *MockMongoClientPutOneUpf) RestfulAPIGetMany(collName string, filter bson.M) ([]map[string]interface{}, error) { - return []map[string]interface{}{}, nil -} - -func (db *MockMongoClientEmptyDB) RestfulAPIDeleteOneWithContext(collName string, filter bson.M, context context.Context) error { - return nil -} -func (db *MockMongoClientDBError) RestfulAPIDeleteOneWithContext(collName string, filter bson.M, context context.Context) error { - return errors.New("DB error") -} - -func (db *MockMongoClientEmptyDB) RestfulAPIJSONPatchWithContext(collName string, filter bson.M, patchJSON []byte, context context.Context) error { - return nil -} -func (db *MockMongoClientDBError) RestfulAPIJSONPatchWithContext(collName string, filter bson.M, patchJSON []byte, context context.Context) error { - return errors.New("DB error") -} - -func (db *MockMongoClientDBError) RestfulAPIPostMany(collName string, filter bson.M, postDataArray []interface{}) error { - return errors.New("DB error") -} - -func (db *MockMongoClientEmptyDB) RestfulAPIPutOneWithContext(collName string, filter bson.M, putData map[string]interface{}, context context.Context) (bool, error) { - return false, nil -} - -func (db *MockMongoClientPutOneUpf) RestfulAPIPutOneWithContext(collName string, filter bson.M, putData map[string]interface{}, context context.Context) (bool, error) { - return true, nil -} - -func (db *MockMongoClientDBError) RestfulAPIPutOneWithContext(collName string, filter bson.M, putData map[string]interface{}, context context.Context) (bool, error) { - return false, errors.New("DB error") -} - -func (m *MockMongoClientEmptyDB) StartSession() (mongo.Session, error) { - return &MockSession{}, nil -} - -func (m *MockMongoClientOneUpf) StartSession() (mongo.Session, error) { - return &MockSession{}, nil -} - -func (m *MockMongoClientPutOneUpf) StartSession() (mongo.Session, error) { - return &MockSession{}, nil -} - -func (m *MockMongoClientDBError) StartSession() (mongo.Session, error) { - return &MockSession{}, nil -} - -func (m *MockMongoClientDuplicateCreation) StartSession() (mongo.Session, error) { - return &MockSession{}, nil -} - func (m *MockMongoClientOneGnb) RestfulAPIGetMany(coll string, filter bson.M) ([]map[string]interface{}, error) { var results []map[string]interface{} gnb := configmodels.Gnb{ @@ -126,6 +36,10 @@ func (m *MockMongoClientOneGnb) RestfulAPIGetMany(coll string, filter bson.M) ([ return results, nil } +type MockMongoClientManyGnbs struct { + dbadapter.DBInterface +} + func (m *MockMongoClientManyGnbs) RestfulAPIGetMany(coll string, filter bson.M) ([]map[string]interface{}, error) { var results []map[string]interface{} names := []string{"gnb0", "gnb1", "gnb2"} @@ -144,6 +58,10 @@ func (m *MockMongoClientManyGnbs) RestfulAPIGetMany(coll string, filter bson.M) return results, nil } +type MockMongoClientOneUpf struct { + dbadapter.DBInterface +} + func (m *MockMongoClientOneUpf) RestfulAPIGetMany(coll string, filter bson.M) ([]map[string]interface{}, error) { var results []map[string]interface{} upf := configmodels.Upf{ @@ -158,6 +76,14 @@ func (m *MockMongoClientOneUpf) RestfulAPIGetMany(coll string, filter bson.M) ([ return results, nil } +func (m *MockMongoClientOneUpf) StartSession() (mongo.Session, error) { + return &MockSession{}, nil +} + +type MockMongoClientManyUpfs struct { + dbadapter.DBInterface +} + func (m *MockMongoClientManyUpfs) RestfulAPIGetMany(coll string, filter bson.M) ([]map[string]interface{}, error) { var results []map[string]interface{} names := []string{"upf0", "upf1", "upf2"} @@ -176,6 +102,22 @@ func (m *MockMongoClientManyUpfs) RestfulAPIGetMany(coll string, filter bson.M) return results, nil } +type MockMongoClientPutExistingUpf struct { + dbadapter.DBInterface +} + +func (db *MockMongoClientPutExistingUpf) RestfulAPIGetMany(collName string, filter bson.M) ([]map[string]interface{}, error) { + return []map[string]interface{}{}, nil +} + +func (m *MockMongoClientPutExistingUpf) StartSession() (mongo.Session, error) { + return &MockSession{}, nil +} + +func (db *MockMongoClientPutExistingUpf) RestfulAPIPutOneWithContext(collName string, filter bson.M, putData map[string]interface{}, context context.Context) (bool, error) { + return true, nil +} + func TestInventoryGetHandlers(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.Default() @@ -523,7 +465,7 @@ func TestUpfPutHandler(t *testing.T) { { name: "Put an existing UPF expects a OK status", route: "/config/v1/inventory/upf/upf1", - dbAdapter: &MockMongoClientPutOneUpf{}, + dbAdapter: &MockMongoClientPutExistingUpf{}, inputData: `{"port": "123"}`, expectedCode: http.StatusOK, expectedBody: "{}", diff --git a/configapi/common_mock_db.go b/configapi/common_mock_db.go new file mode 100644 index 00000000..8fa0b9d2 --- /dev/null +++ b/configapi/common_mock_db.go @@ -0,0 +1,131 @@ +package configapi + +import ( + "context" + "errors" + + "github.com/omec-project/webconsole/dbadapter" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type MockSession struct { + mongo.Session +} + +func (m *MockSession) StartTransaction(opts ...*options.TransactionOptions) error { + return nil +} + +func (m *MockSession) AbortTransaction(ctx context.Context) error { + return nil +} + +func (m *MockSession) CommitTransaction(ctx context.Context) error { + return nil +} + +func (m *MockSession) EndSession(ctx context.Context) {} + +type MockMongoClientDBError struct { + dbadapter.DBInterface +} + +func (db *MockMongoClientDBError) RestfulAPIGetOne(coll string, filter bson.M) (map[string]interface{}, error) { + return nil, errors.New("DB error") +} + +func (db *MockMongoClientDBError) RestfulAPIGetMany(coll string, filter bson.M) ([]map[string]interface{}, error) { + return nil, errors.New("DB error") +} + +func (db *MockMongoClientDBError) RestfulAPIPutOneWithContext(collName string, filter bson.M, putData map[string]interface{}, context context.Context) (bool, error) { + return false, errors.New("DB error") +} + +func (db *MockMongoClientDBError) RestfulAPIDeleteOneWithContext(collName string, filter bson.M, context context.Context) error { + return errors.New("DB error") +} + +func (db *MockMongoClientDBError) RestfulAPIJSONPatchWithContext(collName string, filter bson.M, patchJSON []byte, context context.Context) error { + return errors.New("DB error") +} + +func (db *MockMongoClientDBError) RestfulAPIPost(collName string, filter bson.M, postData map[string]interface{}) (bool, error) { + return false, errors.New("DB error") +} + +func (db *MockMongoClientDBError) RestfulAPIPostMany(collName string, filter bson.M, postDataArray []interface{}) error { + return errors.New("DB error") +} + +func (db *MockMongoClientDBError) RestfulAPICount(collName string, filter bson.M) (int64, error) { + return 0, errors.New("DB error") +} + +func (m *MockMongoClientDBError) StartSession() (mongo.Session, error) { + return &MockSession{}, nil +} + +type MockMongoClientEmptyDB struct { + dbadapter.DBInterface +} + +func (db *MockMongoClientEmptyDB) RestfulAPIGetOne(collName string, filter bson.M) (map[string]interface{}, error) { + return map[string]interface{}{}, nil +} + +func (db *MockMongoClientEmptyDB) RestfulAPIGetMany(coll string, filter bson.M) ([]map[string]interface{}, error) { + var results []map[string]interface{} + return results, nil +} + +func (db *MockMongoClientEmptyDB) RestfulAPIPutOneWithContext(collName string, filter bson.M, putData map[string]interface{}, context context.Context) (bool, error) { + return false, nil +} + +func (db *MockMongoClientEmptyDB) RestfulAPIPost(collName string, filter bson.M, postData map[string]interface{}) (bool, error) { + return true, nil +} + +func (db *MockMongoClientEmptyDB) RestfulAPIPostMany(collName string, filter bson.M, postDataArray []interface{}) error { + return nil +} + +func (db *MockMongoClientEmptyDB) RestfulAPIDeleteOneWithContext(collName string, filter bson.M, context context.Context) error { + return nil +} + +func (db *MockMongoClientEmptyDB) RestfulAPIJSONPatchWithContext(collName string, filter bson.M, patchJSON []byte, context context.Context) error { + return nil +} + +func (db *MockMongoClientEmptyDB) RestfulAPICount(collName string, filter bson.M) (int64, error) { + return 0, nil +} + +func (m *MockMongoClientEmptyDB) StartSession() (mongo.Session, error) { + return &MockSession{}, nil +} + +type MockMongoClientDuplicateCreation struct { + dbadapter.DBInterface +} + +func (db *MockMongoClientDuplicateCreation) RestfulAPIGetMany(coll string, filter bson.M) ([]map[string]interface{}, error) { + var results []map[string]interface{} + return results, nil +} + +func (db *MockMongoClientDuplicateCreation) RestfulAPIPostMany(collName string, filter bson.M, postDataArray []interface{}) error { + return errors.New("E11000") +} + +func (db *MockMongoClientDuplicateCreation) RestfulAPICount(collName string, filter bson.M) (int64, error) { + return 1, nil +} + +func (m *MockMongoClientDuplicateCreation) StartSession() (mongo.Session, error) { + return &MockSession{}, nil +} diff --git a/configapi/handlers_user_account_test.go b/configapi/handlers_user_account_test.go index 46d7ab83..0939fcdd 100644 --- a/configapi/handlers_user_account_test.go +++ b/configapi/handlers_user_account_test.go @@ -4,7 +4,6 @@ package configapi import ( - "errors" "fmt" "net/http" "net/http/httptest" @@ -17,14 +16,6 @@ import ( "golang.org/x/crypto/bcrypt" ) -type MockMongoClientEmptyDB struct { - dbadapter.DBInterface -} - -type MockMongoClientDBError struct { - dbadapter.DBInterface -} - type MockMongoClientInvalidUser struct { dbadapter.DBInterface } @@ -37,10 +28,6 @@ type MockMongoClientRegularUser struct { dbadapter.DBInterface } -type MockMongoClientDuplicateCreation struct { - dbadapter.DBInterface -} - func hashPassword(password string) string { hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { @@ -49,43 +36,6 @@ func hashPassword(password string) string { return string(hashed) } -func (db *MockMongoClientEmptyDB) RestfulAPIGetOne(collName string, filter bson.M) (map[string]interface{}, error) { - return map[string]interface{}{}, nil -} - -func (db *MockMongoClientEmptyDB) RestfulAPIGetMany(coll string, filter bson.M) ([]map[string]interface{}, error) { - var results []map[string]interface{} - return results, nil -} - -func (db *MockMongoClientEmptyDB) RestfulAPIPost(collName string, filter bson.M, postData map[string]interface{}) (bool, error) { - return true, nil -} - -func (db *MockMongoClientEmptyDB) RestfulAPIPostMany(collName string, filter bson.M, postDataArray []interface{}) error { - return nil -} - -func (db *MockMongoClientEmptyDB) RestfulAPICount(collName string, filter bson.M) (int64, error) { - return 0, nil -} - -func (db *MockMongoClientDBError) RestfulAPIGetOne(coll string, filter bson.M) (map[string]interface{}, error) { - return nil, errors.New("DB error") -} - -func (db *MockMongoClientDBError) RestfulAPIGetMany(coll string, filter bson.M) ([]map[string]interface{}, error) { - return nil, errors.New("DB error") -} - -func (db *MockMongoClientDBError) RestfulAPIPost(collName string, filter bson.M, postData map[string]interface{}) (bool, error) { - return false, errors.New("DB error") -} - -func (db *MockMongoClientDBError) RestfulAPICount(collName string, filter bson.M) (int64, error) { - return 0, errors.New("DB error") -} - func (db *MockMongoClientInvalidUser) RestfulAPIGetOne(collName string, filter bson.M) (map[string]interface{}, error) { rawUser := map[string]interface{}{ "username": "johndoe", @@ -137,19 +87,6 @@ func (db *MockMongoClientRegularUser) RestfulAPIDeleteOne(collName string, filte return nil } -func (db *MockMongoClientDuplicateCreation) RestfulAPIGetMany(coll string, filter bson.M) ([]map[string]interface{}, error) { - var results []map[string]interface{} - return results, nil -} - -func (db *MockMongoClientDuplicateCreation) RestfulAPIPostMany(collName string, filter bson.M, postDataArray []interface{}) error { - return errors.New("E11000") -} - -func (db *MockMongoClientDuplicateCreation) RestfulAPICount(collName string, filter bson.M) (int64, error) { - return 1, nil -} - func TestGetUserAccountsHandler(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.Default() From 192c5963490b609d209f83e2f4f8834bcf9fa3cf Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Tue, 21 Jan 2025 12:54:35 +0100 Subject: [PATCH 05/13] add license Signed-off-by: Patricia Reinoso --- configapi/common_mock_db.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/configapi/common_mock_db.go b/configapi/common_mock_db.go index 8fa0b9d2..65fa998c 100644 --- a/configapi/common_mock_db.go +++ b/configapi/common_mock_db.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Canonical Ltd. + package configapi import ( From acf7fe4bdb3df86599ff421563699ad4a891d421 Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Wed, 22 Jan 2025 16:07:20 +0100 Subject: [PATCH 06/13] use context in post operation Signed-off-by: Patricia Reinoso --- configapi/api_inventory.go | 2 +- configapi/common_mock_db.go | 12 ++++++++++++ dbadapter/db_adapter.go | 10 ++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/configapi/api_inventory.go b/configapi/api_inventory.go index 521cb486..b9898e62 100644 --- a/configapi/api_inventory.go +++ b/configapi/api_inventory.go @@ -291,7 +291,7 @@ func PutUpf(c *gin.Context) { func postUpfOperation(upf configmodels.Upf, sc mongo.SessionContext) error { filter := bson.M{"hostname": upf.Hostname} upfDataBson := configmodels.ToBsonM(upf) - return dbadapter.CommonDBClient.RestfulAPIPostMany(configmodels.UpfDataColl, filter, []interface{}{upfDataBson}) + return dbadapter.CommonDBClient.RestfulAPIPostManyWithContext(configmodels.UpfDataColl, filter, []interface{}{upfDataBson}, sc) } func putUpfOperation(upf configmodels.Upf, sc mongo.SessionContext) error { diff --git a/configapi/common_mock_db.go b/configapi/common_mock_db.go index 65fa998c..e860f7c9 100644 --- a/configapi/common_mock_db.go +++ b/configapi/common_mock_db.go @@ -63,6 +63,10 @@ func (db *MockMongoClientDBError) RestfulAPIPostMany(collName string, filter bso return errors.New("DB error") } +func (db *MockMongoClientDBError) RestfulAPIPostManyWithContext(collName string, filter bson.M, postDataArray []interface{}, context context.Context) error { + return errors.New("DB error") +} + func (db *MockMongoClientDBError) RestfulAPICount(collName string, filter bson.M) (int64, error) { return 0, errors.New("DB error") } @@ -96,6 +100,10 @@ func (db *MockMongoClientEmptyDB) RestfulAPIPostMany(collName string, filter bso return nil } +func (db *MockMongoClientEmptyDB) RestfulAPIPostManyWithContext(collName string, filter bson.M, postDataArray []interface{}, context context.Context) error { + return nil +} + func (db *MockMongoClientEmptyDB) RestfulAPIDeleteOneWithContext(collName string, filter bson.M, context context.Context) error { return nil } @@ -125,6 +133,10 @@ func (db *MockMongoClientDuplicateCreation) RestfulAPIPostMany(collName string, return errors.New("E11000") } +func (db *MockMongoClientDuplicateCreation) RestfulAPIPostManyWithContext(collName string, filter bson.M, postDataArray []interface{}, context context.Context) error { + return errors.New("E11000") +} + func (db *MockMongoClientDuplicateCreation) RestfulAPICount(collName string, filter bson.M) (int64, error) { return 1, nil } diff --git a/dbadapter/db_adapter.go b/dbadapter/db_adapter.go index e56a8dc4..261b6c76 100644 --- a/dbadapter/db_adapter.go +++ b/dbadapter/db_adapter.go @@ -32,7 +32,9 @@ type DBInterface interface { RestfulAPIJSONPatchWithContext(collName string, filter bson.M, patchJSON []byte, context context.Context) error RestfulAPIJSONPatchExtend(collName string, filter bson.M, patchJSON []byte, dataName string) error RestfulAPIPost(collName string, filter bson.M, postData map[string]interface{}) (bool, error) + RestfulAPIPostWithContext(collName string, filter bson.M, postData map[string]interface{}, context context.Context) (bool, error) RestfulAPIPostMany(collName string, filter bson.M, postDataArray []interface{}) error + RestfulAPIPostManyWithContext(collName string, filter bson.M, postDataArray []interface{}, context context.Context) error RestfulAPICount(collName string, filter bson.M) (int64, error) CreateIndex(collName string, keyField string) (bool, error) StartSession() (mongo.Session, error) @@ -139,10 +141,18 @@ func (db *MongoDBClient) RestfulAPIPost(collName string, filter bson.M, postData return db.MongoClient.RestfulAPIPost(collName, filter, postData) } +func (db *MongoDBClient) RestfulAPIPostWithContext(collName string, filter bson.M, postData map[string]interface{}, context context.Context) (bool, error) { + return db.MongoClient.RestfulAPIPostWithContext(collName, filter, postData, context) +} + func (db *MongoDBClient) RestfulAPIPostMany(collName string, filter bson.M, postDataArray []interface{}) error { return db.MongoClient.RestfulAPIPostMany(collName, filter, postDataArray) } +func (db *MongoDBClient) RestfulAPIPostManyWithContext(collName string, filter bson.M, postDataArray []interface{}, context context.Context) error { + return db.MongoClient.RestfulAPIPostManyWithContext(collName, filter, postDataArray, context) +} + func (db *MongoDBClient) RestfulAPICount(collName string, filter bson.M) (int64, error) { return db.MongoClient.RestfulAPICount(collName, filter) } From 75bc80b9c400438c7b573d48ccf865dbd977d7c6 Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Fri, 24 Jan 2025 17:00:51 +0100 Subject: [PATCH 07/13] fix merge Signed-off-by: Patricia Reinoso --- configapi/api_inventory.go | 12 ++++++------ configapi/api_inventory_test.go | 20 -------------------- configapi/common_mock_db.go | 18 +++++++++--------- 3 files changed, 15 insertions(+), 35 deletions(-) diff --git a/configapi/api_inventory.go b/configapi/api_inventory.go index 967ff58c..307d0327 100644 --- a/configapi/api_inventory.go +++ b/configapi/api_inventory.go @@ -42,7 +42,7 @@ func GetGnbs(c *gin.Context) { logger.WebUILog.Infoln("received a GET gNBs request") var gnbs []*configmodels.Gnb gnbs = make([]*configmodels.Gnb, 0) - rawGnbs, err := dbadapter.CommonDBClient.RestfulAPIGetMany(gnbDataColl, bson.M{}) + rawGnbs, err := dbadapter.CommonDBClient.RestfulAPIGetMany(configmodels.GnbDataColl, bson.M{}) if err != nil { logger.DbLog.Errorw("failed to retrieve gNBs", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve gNBs"}) @@ -128,7 +128,7 @@ func handleDeleteGnbTransaction(ctx context.Context, filter bson.M, gnbName stri if err := session.StartTransaction(); err != nil { return fmt.Errorf("failed to start transaction: %w", err) } - if err = dbadapter.CommonDBClient.RestfulAPIDeleteOneWithContext(sc, gnbDataColl, filter); err != nil { + if err = dbadapter.CommonDBClient.RestfulAPIDeleteOneWithContext(sc, configmodels.GnbDataColl, filter); err != nil { if abortErr := session.AbortTransaction(sc); abortErr != nil { logger.DbLog.Errorw("failed to abort transaction", "error", abortErr) } @@ -196,7 +196,7 @@ func GetUpfs(c *gin.Context) { logger.WebUILog.Infoln("received a GET UPFs request") var upfs []*configmodels.Upf upfs = make([]*configmodels.Upf, 0) - rawUpfs, err := dbadapter.CommonDBClient.RestfulAPIGetMany(upfDataColl, bson.M{}) + rawUpfs, err := dbadapter.CommonDBClient.RestfulAPIGetMany(configmodels.UpfDataColl, bson.M{}) if err != nil { logger.DbLog.Errorw("failed to retrieve UPFs", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve UPFs"}) @@ -313,13 +313,13 @@ func PutUpf(c *gin.Context) { func postUpfOperation(upf configmodels.Upf, sc mongo.SessionContext) error { filter := bson.M{"hostname": upf.Hostname} upfDataBson := configmodels.ToBsonM(upf) - return dbadapter.CommonDBClient.RestfulAPIPostManyWithContext(configmodels.UpfDataColl, filter, []interface{}{upfDataBson}, sc) + return dbadapter.CommonDBClient.RestfulAPIPostManyWithContext(sc, configmodels.UpfDataColl, filter, []interface{}{upfDataBson}) } func putUpfOperation(upf configmodels.Upf, sc mongo.SessionContext) error { filter := bson.M{"hostname": upf.Hostname} upfDataBson := configmodels.ToBsonM(upf) - _, err := dbadapter.CommonDBClient.RestfulAPIPutOneWithContext(configmodels.UpfDataColl, filter, upfDataBson, sc) + _, err := dbadapter.CommonDBClient.RestfulAPIPutOneWithContext(sc, configmodels.UpfDataColl, filter, upfDataBson) return err } @@ -406,7 +406,7 @@ func handleDeleteUpfTransaction(ctx context.Context, filter bson.M, hostname str if err := session.StartTransaction(); err != nil { return fmt.Errorf("failed to start transaction: %w", err) } - if err = dbadapter.CommonDBClient.RestfulAPIDeleteOneWithContext(sc, upfDataColl, filter); err != nil { + if err = dbadapter.CommonDBClient.RestfulAPIDeleteOneWithContext(sc, configmodels.UpfDataColl, filter); err != nil { if abortErr := session.AbortTransaction(sc); abortErr != nil { logger.DbLog.Errorw("failed to abort transaction", "error", abortErr) } diff --git a/configapi/api_inventory_test.go b/configapi/api_inventory_test.go index 98457d46..7480d643 100644 --- a/configapi/api_inventory_test.go +++ b/configapi/api_inventory_test.go @@ -6,7 +6,6 @@ package configapi import ( "context" "encoding/json" - "errors" "net/http" "net/http/httptest" "strings" @@ -17,27 +16,8 @@ import ( "github.com/omec-project/webconsole/dbadapter" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" ) -type MockSession struct { - mongo.Session -} - -func (m *MockSession) StartTransaction(opts ...*options.TransactionOptions) error { - return nil -} - -func (m *MockSession) AbortTransaction(ctx context.Context) error { - return nil -} - -func (m *MockSession) CommitTransaction(ctx context.Context) error { - return nil -} - -func (m *MockSession) EndSession(ctx context.Context) {} - type MockMongoClientOneGnb struct { dbadapter.DBInterface } diff --git a/configapi/common_mock_db.go b/configapi/common_mock_db.go index e860f7c9..5dbae963 100644 --- a/configapi/common_mock_db.go +++ b/configapi/common_mock_db.go @@ -43,15 +43,15 @@ func (db *MockMongoClientDBError) RestfulAPIGetMany(coll string, filter bson.M) return nil, errors.New("DB error") } -func (db *MockMongoClientDBError) RestfulAPIPutOneWithContext(collName string, filter bson.M, putData map[string]interface{}, context context.Context) (bool, error) { +func (db *MockMongoClientDBError) RestfulAPIPutOneWithContext(context context.Context, collName string, filter bson.M, putData map[string]interface{}) (bool, error) { return false, errors.New("DB error") } -func (db *MockMongoClientDBError) RestfulAPIDeleteOneWithContext(collName string, filter bson.M, context context.Context) error { +func (db *MockMongoClientDBError) RestfulAPIDeleteOneWithContext(context context.Context, collName string, filter bson.M) error { return errors.New("DB error") } -func (db *MockMongoClientDBError) RestfulAPIJSONPatchWithContext(collName string, filter bson.M, patchJSON []byte, context context.Context) error { +func (db *MockMongoClientDBError) RestfulAPIJSONPatchWithContext(context context.Context, collName string, filter bson.M, patchJSON []byte) error { return errors.New("DB error") } @@ -63,7 +63,7 @@ func (db *MockMongoClientDBError) RestfulAPIPostMany(collName string, filter bso return errors.New("DB error") } -func (db *MockMongoClientDBError) RestfulAPIPostManyWithContext(collName string, filter bson.M, postDataArray []interface{}, context context.Context) error { +func (db *MockMongoClientDBError) RestfulAPIPostManyWithContext(context context.Context, collName string, filter bson.M, postDataArray []interface{}) error { return errors.New("DB error") } @@ -88,7 +88,7 @@ func (db *MockMongoClientEmptyDB) RestfulAPIGetMany(coll string, filter bson.M) return results, nil } -func (db *MockMongoClientEmptyDB) RestfulAPIPutOneWithContext(collName string, filter bson.M, putData map[string]interface{}, context context.Context) (bool, error) { +func (db *MockMongoClientEmptyDB) RestfulAPIPutOneWithContext(context context.Context, collName string, filter bson.M, putData map[string]interface{}) (bool, error) { return false, nil } @@ -100,15 +100,15 @@ func (db *MockMongoClientEmptyDB) RestfulAPIPostMany(collName string, filter bso return nil } -func (db *MockMongoClientEmptyDB) RestfulAPIPostManyWithContext(collName string, filter bson.M, postDataArray []interface{}, context context.Context) error { +func (db *MockMongoClientEmptyDB) RestfulAPIPostManyWithContext(context context.Context, collName string, filter bson.M, postDataArray []interface{}) error { return nil } -func (db *MockMongoClientEmptyDB) RestfulAPIDeleteOneWithContext(collName string, filter bson.M, context context.Context) error { +func (db *MockMongoClientEmptyDB) RestfulAPIDeleteOneWithContext(context context.Context, collName string, filter bson.M) error { return nil } -func (db *MockMongoClientEmptyDB) RestfulAPIJSONPatchWithContext(collName string, filter bson.M, patchJSON []byte, context context.Context) error { +func (db *MockMongoClientEmptyDB) RestfulAPIJSONPatchWithContext(context context.Context, collName string, filter bson.M, patchJSON []byte) error { return nil } @@ -133,7 +133,7 @@ func (db *MockMongoClientDuplicateCreation) RestfulAPIPostMany(collName string, return errors.New("E11000") } -func (db *MockMongoClientDuplicateCreation) RestfulAPIPostManyWithContext(collName string, filter bson.M, postDataArray []interface{}, context context.Context) error { +func (db *MockMongoClientDuplicateCreation) RestfulAPIPostManyWithContext(context context.Context, collName string, filter bson.M, postDataArray []interface{}) error { return errors.New("E11000") } From bdecd0359257994995f73c29ae79498abf7c1231 Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Mon, 27 Jan 2025 09:58:11 +0100 Subject: [PATCH 08/13] refactoring to reduce duplication Signed-off-by: Patricia Reinoso --- configapi/api_inventory.go | 153 +++++++++++++------------------- configapi/api_inventory_test.go | 4 + 2 files changed, 64 insertions(+), 93 deletions(-) diff --git a/configapi/api_inventory.go b/configapi/api_inventory.go index 307d0327..e44de784 100644 --- a/configapi/api_inventory.go +++ b/configapi/api_inventory.go @@ -248,7 +248,8 @@ func PostUpf(c *gin.Context) { return } upf := configmodels.Upf(postUpfParams) - if err = handleUpfTransaction(c.Request.Context(), upf, postUpfOperation); err != nil { + patchJSON := getEditUpfPatchJSON(upf) + if err = handleUpfTransaction(c.Request.Context(), upf, patchJSON, postUpfOperation); err != nil { if strings.Contains(err.Error(), "E11000") { logger.WebUILog.Errorw("duplicate hostname found:", "error", err) c.JSON(http.StatusBadRequest, gin.H{"error": "UPF already exists"}) @@ -302,7 +303,8 @@ func PutUpf(c *gin.Context) { Hostname: hostname, Port: putUpfParams.Port, } - if err := handleUpfTransaction(c.Request.Context(), putUpf, putUpfOperation); err != nil { + patchJSON := getEditUpfPatchJSON(putUpf) + if err := handleUpfTransaction(c.Request.Context(), putUpf, patchJSON, putUpfOperation); err != nil { logger.WebUILog.Errorw("failed to PUT UPF", "hostname", hostname, "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to PUT UPF"}) return @@ -310,55 +312,17 @@ func PutUpf(c *gin.Context) { c.JSON(http.StatusOK, gin.H{}) } -func postUpfOperation(upf configmodels.Upf, sc mongo.SessionContext) error { - filter := bson.M{"hostname": upf.Hostname} - upfDataBson := configmodels.ToBsonM(upf) - return dbadapter.CommonDBClient.RestfulAPIPostManyWithContext(sc, configmodels.UpfDataColl, filter, []interface{}{upfDataBson}) -} - -func putUpfOperation(upf configmodels.Upf, sc mongo.SessionContext) error { - filter := bson.M{"hostname": upf.Hostname} - upfDataBson := configmodels.ToBsonM(upf) - _, err := dbadapter.CommonDBClient.RestfulAPIPutOneWithContext(sc, configmodels.UpfDataColl, filter, upfDataBson) - return err -} - -func handleUpfTransaction(ctx context.Context, upf configmodels.Upf, operation func(configmodels.Upf, mongo.SessionContext) error) error { - session, err := dbadapter.CommonDBClient.StartSession() - if err != nil { - return fmt.Errorf("failed to initialize DB session: %w", err) - } - defer session.EndSession(ctx) - - return mongo.WithSession(ctx, session, func(sc mongo.SessionContext) error { - if err := session.StartTransaction(); err != nil { - return fmt.Errorf("failed to start transaction: %w", err) - } - if err := operation(upf, sc); err != nil { - if abortErr := session.AbortTransaction(sc); abortErr != nil { - logger.DbLog.Errorw("failed to abort transaction", "error", abortErr) +func getEditUpfPatchJSON(upf configmodels.Upf) []byte { + return []byte(fmt.Sprintf(`[ + { + "op": "replace", + "path": "/site-info/upf", + "value": { + "upf-name": "%s", + "upf-port": "%s" } - return err } - patchJSON := []byte(fmt.Sprintf(`[ - { - "op": "replace", - "path": "/site-info/upf", - "value": { - "upf-name": "%s", - "upf-port": "%s" - } - } - ]`, upf.Hostname, upf.Port)) - err = updateUpfInNetworkSlices(upf.Hostname, patchJSON, sc) - if err != nil { - if abortErr := session.AbortTransaction(sc); abortErr != nil { - logger.DbLog.Errorw("failed to abort transaction", "error", abortErr) - } - return fmt.Errorf("failed to update network slices: %w", err) - } - return session.CommitTransaction(sc) - }) + ]`, upf.Hostname, upf.Port)) } // DeleteUpf godoc @@ -384,9 +348,12 @@ func DeleteUpf(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": errorMessage}) return } - filter := bson.M{"hostname": hostname} - err := handleDeleteUpfTransaction(c.Request.Context(), filter, hostname) - if err != nil { + + upf := configmodels.Upf{ + Hostname: hostname, + } + patchJSON := []byte(`[{"op": "remove", "path": "/site-info/upf"}]`) + if err := handleUpfTransaction(c.Request.Context(), upf, patchJSON, deleteUpfOperation); err != nil { logger.WebUILog.Errorw("failed to delete UPF", "hostname", hostname, "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete UPF"}) return @@ -395,34 +362,6 @@ func DeleteUpf(c *gin.Context) { c.JSON(http.StatusOK, gin.H{}) } -func handleDeleteUpfTransaction(ctx context.Context, filter bson.M, hostname string) error { - session, err := dbadapter.CommonDBClient.StartSession() - if err != nil { - return fmt.Errorf("failed to initialize DB session: %w", err) - } - defer session.EndSession(ctx) - - return mongo.WithSession(ctx, session, func(sc mongo.SessionContext) error { - if err := session.StartTransaction(); err != nil { - return fmt.Errorf("failed to start transaction: %w", err) - } - if err = dbadapter.CommonDBClient.RestfulAPIDeleteOneWithContext(sc, configmodels.UpfDataColl, filter); err != nil { - if abortErr := session.AbortTransaction(sc); abortErr != nil { - logger.DbLog.Errorw("failed to abort transaction", "error", abortErr) - } - return err - } - patchJSON := []byte(`[{"op": "remove", "path": "/site-info/upf"}]`) - if err = updateUpfInNetworkSlices(hostname, patchJSON, sc); err != nil { - if abortErr := session.AbortTransaction(sc); abortErr != nil { - logger.DbLog.Errorw("failed to abort transaction", "error", abortErr) - } - return fmt.Errorf("failed to update network slices: %w", err) - } - return session.CommitTransaction(sc) - }) -} - func updateGnbInNetworkSlices(gnbName string, context context.Context) error { filterByGnb := bson.M{ "site-info.gNodeBs": bson.M{ @@ -461,22 +400,50 @@ func updateGnbInNetworkSlices(gnbName string, context context.Context) error { return nil } -func updateUpfInNetworkSlices(hostname string, patchJSON []byte, context context.Context) error { - filterByUpf := bson.M{"site-info.upf.upf-name": hostname} - rawNetworkSlices, err := dbadapter.CommonDBClient.RestfulAPIGetMany(sliceDataColl, filterByUpf) +func postUpfOperation(upf configmodels.Upf, sc mongo.SessionContext) error { + filter := bson.M{"hostname": upf.Hostname} + upfDataBson := configmodels.ToBsonM(upf) + return dbadapter.CommonDBClient.RestfulAPIPostManyWithContext(sc, configmodels.UpfDataColl, filter, []interface{}{upfDataBson}) +} + +func putUpfOperation(upf configmodels.Upf, sc mongo.SessionContext) error { + filter := bson.M{"hostname": upf.Hostname} + upfDataBson := configmodels.ToBsonM(upf) + _, err := dbadapter.CommonDBClient.RestfulAPIPutOneWithContext(sc, configmodels.UpfDataColl, filter, upfDataBson) + return err +} + +func deleteUpfOperation(upf configmodels.Upf, sc mongo.SessionContext) error { + filter := bson.M{"hostname": upf.Hostname} + return dbadapter.CommonDBClient.RestfulAPIDeleteOneWithContext(sc, configmodels.UpfDataColl, filter) +} + +func handleUpfTransaction(ctx context.Context, upf configmodels.Upf, patchJSON []byte, operation func(configmodels.Upf, mongo.SessionContext) error) error { + session, err := dbadapter.CommonDBClient.StartSession() if err != nil { - return fmt.Errorf("failed to fetch network slices: %w", err) + return fmt.Errorf("failed to initialize DB session: %w", err) } - for _, rawNetworkSlice := range rawNetworkSlices { - sliceName, ok := rawNetworkSlice["slice-name"].(string) - if !ok { - return fmt.Errorf("invalid slice-name in network slice: %v", rawNetworkSlice) + defer session.EndSession(ctx) + + return mongo.WithSession(ctx, session, func(sc mongo.SessionContext) error { + if err := session.StartTransaction(); err != nil { + return fmt.Errorf("failed to start transaction: %w", err) } - filterBySliceName := bson.M{"slice-name": sliceName} - err = dbadapter.CommonDBClient.RestfulAPIJSONPatchWithContext(context, sliceDataColl, filterBySliceName, patchJSON) - if err != nil { + if err := operation(upf, sc); err != nil { + if abortErr := session.AbortTransaction(sc); abortErr != nil { + logger.DbLog.Errorw("failed to abort transaction", "error", abortErr) + } return err } - } - return nil + + filterByUpf := bson.M{"site-info.upf.upf-name": upf.Hostname} + err = dbadapter.CommonDBClient.RestfulAPIJSONPatchWithContext(sc, sliceDataColl, filterByUpf, patchJSON) + if err != nil { + if abortErr := session.AbortTransaction(sc); abortErr != nil { + logger.DbLog.Errorw("failed to abort transaction", "error", abortErr) + } + return fmt.Errorf("failed to update network slices: %w", err) + } + return session.CommitTransaction(sc) + }) } diff --git a/configapi/api_inventory_test.go b/configapi/api_inventory_test.go index 7480d643..3dfc1432 100644 --- a/configapi/api_inventory_test.go +++ b/configapi/api_inventory_test.go @@ -118,6 +118,10 @@ func (db *MockMongoClientPutExistingUpf) RestfulAPIPutOneWithContext(context con return true, nil } +func (db *MockMongoClientPutExistingUpf) RestfulAPIJSONPatchWithContext(context context.Context, collName string, filter bson.M, patchJSON []byte) error { + return nil +} + func TestInventoryGetHandlers(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.Default() From 2202d3825fd00406e1e8114c552705e45ef0c6d2 Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Mon, 27 Jan 2025 10:05:23 +0100 Subject: [PATCH 09/13] change param order Signed-off-by: Patricia Reinoso --- configapi/api_inventory.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/configapi/api_inventory.go b/configapi/api_inventory.go index e44de784..9e0c8840 100644 --- a/configapi/api_inventory.go +++ b/configapi/api_inventory.go @@ -400,25 +400,25 @@ func updateGnbInNetworkSlices(gnbName string, context context.Context) error { return nil } -func postUpfOperation(upf configmodels.Upf, sc mongo.SessionContext) error { +func postUpfOperation(sc mongo.SessionContext, upf configmodels.Upf) error { filter := bson.M{"hostname": upf.Hostname} upfDataBson := configmodels.ToBsonM(upf) return dbadapter.CommonDBClient.RestfulAPIPostManyWithContext(sc, configmodels.UpfDataColl, filter, []interface{}{upfDataBson}) } -func putUpfOperation(upf configmodels.Upf, sc mongo.SessionContext) error { +func putUpfOperation(sc mongo.SessionContext, upf configmodels.Upf) error { filter := bson.M{"hostname": upf.Hostname} upfDataBson := configmodels.ToBsonM(upf) _, err := dbadapter.CommonDBClient.RestfulAPIPutOneWithContext(sc, configmodels.UpfDataColl, filter, upfDataBson) return err } -func deleteUpfOperation(upf configmodels.Upf, sc mongo.SessionContext) error { +func deleteUpfOperation(sc mongo.SessionContext, upf configmodels.Upf) error { filter := bson.M{"hostname": upf.Hostname} return dbadapter.CommonDBClient.RestfulAPIDeleteOneWithContext(sc, configmodels.UpfDataColl, filter) } -func handleUpfTransaction(ctx context.Context, upf configmodels.Upf, patchJSON []byte, operation func(configmodels.Upf, mongo.SessionContext) error) error { +func handleUpfTransaction(ctx context.Context, upf configmodels.Upf, patchJSON []byte, operation func(mongo.SessionContext, configmodels.Upf) error) error { session, err := dbadapter.CommonDBClient.StartSession() if err != nil { return fmt.Errorf("failed to initialize DB session: %w", err) @@ -429,7 +429,7 @@ func handleUpfTransaction(ctx context.Context, upf configmodels.Upf, patchJSON [ if err := session.StartTransaction(); err != nil { return fmt.Errorf("failed to start transaction: %w", err) } - if err := operation(upf, sc); err != nil { + if err := operation(sc, upf); err != nil { if abortErr := session.AbortTransaction(sc); abortErr != nil { logger.DbLog.Errorw("failed to abort transaction", "error", abortErr) } From 7042f2725e1a5d04ea7fec830a6b351fcd5a38e5 Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Mon, 27 Jan 2025 15:07:31 +0100 Subject: [PATCH 10/13] implement validation Signed-off-by: Patricia Reinoso --- configapi/api_inventory.go | 31 ++++++++------ configapi/api_inventory_test.go | 50 ++++++++++++++-------- configapi/validators.go | 36 ++++++++++++++++ configapi/validators_test.go | 76 +++++++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 31 deletions(-) create mode 100644 configapi/validators.go create mode 100644 configapi/validators_test.go diff --git a/configapi/api_inventory.go b/configapi/api_inventory.go index 9e0c8840..4f6f0a88 100644 --- a/configapi/api_inventory.go +++ b/configapi/api_inventory.go @@ -8,7 +8,6 @@ import ( "encoding/json" "fmt" "net/http" - "strconv" "strings" "github.com/gin-gonic/gin" @@ -229,6 +228,7 @@ func GetUpfs(c *gin.Context) { // @Failure 500 {object} nil "Error creating UPF" // @Router /config/v1/inventory/upf/ [post] func PostUpf(c *gin.Context) { + setInventoryCorsHeader(c) logger.WebUILog.Infoln("received a POST UPF request") var postUpfParams configmodels.PostUpfRequest err := c.ShouldBindJSON(&postUpfParams) @@ -237,19 +237,21 @@ func PostUpf(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON format"}) return } - if postUpfParams.Hostname == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "UPF hostname must be provided"}) + if !isValidFQDN(postUpfParams.Hostname) { + errorMessage := fmt.Sprintf("invalid UPF hostname '%s'. Hostname needs to represent a valid FQDN", postUpfParams.Hostname) + logger.WebUILog.Errorln(errorMessage) + c.JSON(http.StatusBadRequest, gin.H{"error": errorMessage}) return } - if _, err := strconv.Atoi(postUpfParams.Port); err != nil { - errorMessage := "UPF port cannot be converted to integer or it was not provided" + if !isValidUpfPort(postUpfParams.Port) { + errorMessage := fmt.Sprintf("invalid UPF port '%s'. Port must be a numeric string within the range [0, 65535]", postUpfParams.Port) logger.WebUILog.Errorln(errorMessage) c.JSON(http.StatusBadRequest, gin.H{"error": errorMessage}) return } upf := configmodels.Upf(postUpfParams) patchJSON := getEditUpfPatchJSON(upf) - if err = handleUpfTransaction(c.Request.Context(), upf, patchJSON, postUpfOperation); err != nil { + if err = executeUpfTransaction(c.Request.Context(), upf, patchJSON, postUpfOperation); err != nil { if strings.Contains(err.Error(), "E11000") { logger.WebUILog.Errorw("duplicate hostname found:", "error", err) c.JSON(http.StatusBadRequest, gin.H{"error": "UPF already exists"}) @@ -278,10 +280,11 @@ func PostUpf(c *gin.Context) { // @Failure 500 {object} nil "Error updating UPF" // @Router /config/v1/inventory/upf/{upf-hostname} [put] func PutUpf(c *gin.Context) { + setInventoryCorsHeader(c) logger.WebUILog.Infoln("received a PUT UPF request") - hostname, exists := c.Params.Get("upf-hostname") - if !exists { - errorMessage := "put UPF request is missing path param `upf-hostname`" + hostname, _ := c.Params.Get("upf-hostname") + if !isValidFQDN(hostname) { + errorMessage := fmt.Sprintf("invalid UPF hostname '%s'. Hostname needs to represent a valid FQDN", hostname) logger.WebUILog.Errorln(errorMessage) c.JSON(http.StatusBadRequest, gin.H{"error": errorMessage}) return @@ -293,8 +296,8 @@ func PutUpf(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON format"}) return } - if _, err := strconv.Atoi(putUpfParams.Port); err != nil { - errorMessage := "UPF port cannot be converted to integer or it was not provided" + if !isValidUpfPort(putUpfParams.Port) { + errorMessage := fmt.Sprintf("invalid UPF port '%s'. Port must be a numeric string within the range [0, 65535]", putUpfParams.Port) logger.WebUILog.Errorln(errorMessage) c.JSON(http.StatusBadRequest, gin.H{"error": errorMessage}) return @@ -304,7 +307,7 @@ func PutUpf(c *gin.Context) { Port: putUpfParams.Port, } patchJSON := getEditUpfPatchJSON(putUpf) - if err := handleUpfTransaction(c.Request.Context(), putUpf, patchJSON, putUpfOperation); err != nil { + if err := executeUpfTransaction(c.Request.Context(), putUpf, patchJSON, putUpfOperation); err != nil { logger.WebUILog.Errorw("failed to PUT UPF", "hostname", hostname, "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to PUT UPF"}) return @@ -353,7 +356,7 @@ func DeleteUpf(c *gin.Context) { Hostname: hostname, } patchJSON := []byte(`[{"op": "remove", "path": "/site-info/upf"}]`) - if err := handleUpfTransaction(c.Request.Context(), upf, patchJSON, deleteUpfOperation); err != nil { + if err := executeUpfTransaction(c.Request.Context(), upf, patchJSON, deleteUpfOperation); err != nil { logger.WebUILog.Errorw("failed to delete UPF", "hostname", hostname, "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete UPF"}) return @@ -418,7 +421,7 @@ func deleteUpfOperation(sc mongo.SessionContext, upf configmodels.Upf) error { return dbadapter.CommonDBClient.RestfulAPIDeleteOneWithContext(sc, configmodels.UpfDataColl, filter) } -func handleUpfTransaction(ctx context.Context, upf configmodels.Upf, patchJSON []byte, operation func(mongo.SessionContext, configmodels.Upf) error) error { +func executeUpfTransaction(ctx context.Context, upf configmodels.Upf, patchJSON []byte, operation func(mongo.SessionContext, configmodels.Upf) error) error { session, err := dbadapter.CommonDBClient.StartSession() if err != nil { return fmt.Errorf("failed to initialize DB session: %w", err) diff --git a/configapi/api_inventory_test.go b/configapi/api_inventory_test.go index 3dfc1432..d8cdb60d 100644 --- a/configapi/api_inventory_test.go +++ b/configapi/api_inventory_test.go @@ -365,7 +365,7 @@ func TestUpfPostHandler(t *testing.T) { name: "Create a new UPF success", route: "/config/v1/inventory/upf", dbAdapter: &MockMongoClientEmptyDB{}, - inputData: `{"hostname": "host", "port": "123"}`, + inputData: `{"hostname": "upf1.my-domain.com", "port": "123"}`, expectedCode: http.StatusCreated, expectedBody: "{}", }, @@ -373,7 +373,7 @@ func TestUpfPostHandler(t *testing.T) { name: "Create an existing UPF expects failure", route: "/config/v1/inventory/upf", dbAdapter: &MockMongoClientDuplicateCreation{}, - inputData: `{"hostname": "upf1", "port": "123"}`, + inputData: `{"hostname": "upf1.my-domain.com", "port": "123"}`, expectedCode: http.StatusBadRequest, expectedBody: `{"error":"UPF already exists"}`, }, @@ -381,7 +381,7 @@ func TestUpfPostHandler(t *testing.T) { name: "Port is not a string expects failure", route: "/config/v1/inventory/upf", dbAdapter: &MockMongoClientEmptyDB{}, - inputData: `{"hostname": "host", "port": 1234}`, + inputData: `{"hostname": "upf1.my-domain.com", "port": 1234}`, expectedCode: http.StatusBadRequest, expectedBody: `{"error":"invalid JSON format"}`, }, @@ -389,15 +389,15 @@ func TestUpfPostHandler(t *testing.T) { name: "Missing port expects failure", route: "/config/v1/inventory/upf", dbAdapter: &MockMongoClientEmptyDB{}, - inputData: `{"hostname": "host", "some_param": "123"}`, + inputData: `{"hostname": "upf1.my-domain.com", "some_param": "123"}`, expectedCode: http.StatusBadRequest, - expectedBody: `{"error":"UPF port cannot be converted to integer or it was not provided"}`, + expectedBody: `{"error":"invalid UPF port ''. Port must be a numeric string within the range [0, 65535]"}`, }, { name: "DB POST operation fails expects failure", route: "/config/v1/inventory/upf", dbAdapter: &MockMongoClientDBError{}, - inputData: `{"hostname": "host", "port": "123"}`, + inputData: `{"hostname": "upf1.my-domain.com", "port": "123"}`, expectedCode: http.StatusInternalServerError, expectedBody: `{"error":"failed to create UPF"}`, }, @@ -405,9 +405,9 @@ func TestUpfPostHandler(t *testing.T) { name: "Port cannot be converted to int expects failure", route: "/config/v1/inventory/upf", dbAdapter: &MockMongoClientEmptyDB{}, - inputData: `{"hostname": "host", "port": "a"}`, + inputData: `{"hostname": "upf1.my-domain.com", "port": "a"}`, expectedCode: http.StatusBadRequest, - expectedBody: `{"error":"UPF port cannot be converted to integer or it was not provided"}`, + expectedBody: `{"error":"invalid UPF port 'a'. Port must be a numeric string within the range [0, 65535]"}`, }, { name: "Hostname not provided expects failure", @@ -415,7 +415,15 @@ func TestUpfPostHandler(t *testing.T) { dbAdapter: &MockMongoClientEmptyDB{}, inputData: `{"port": "a"}`, expectedCode: http.StatusBadRequest, - expectedBody: `{"error":"UPF hostname must be provided"}`, + expectedBody: `{"error":"invalid UPF hostname ''. Hostname needs to represent a valid FQDN"}`, + }, + { + name: "Invalid UPF hostname expects failure", + route: "/config/v1/inventory/upf", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"hostname": "upf1", "port": "123"}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"invalid UPF hostname 'upf1'. Hostname needs to represent a valid FQDN"}`, }, } for _, tc := range testCases { @@ -454,7 +462,7 @@ func TestUpfPutHandler(t *testing.T) { }{ { name: "Put a new UPF expects OK status", - route: "/config/v1/inventory/upf/upf1", + route: "/config/v1/inventory/upf/upf1.my-domain.com", dbAdapter: &MockMongoClientEmptyDB{}, inputData: `{"port": "123"}`, expectedCode: http.StatusOK, @@ -462,7 +470,7 @@ func TestUpfPutHandler(t *testing.T) { }, { name: "Put an existing UPF expects a OK status", - route: "/config/v1/inventory/upf/upf1", + route: "/config/v1/inventory/upf/upf1.my-domain.com", dbAdapter: &MockMongoClientPutExistingUpf{}, inputData: `{"port": "123"}`, expectedCode: http.StatusOK, @@ -470,7 +478,7 @@ func TestUpfPutHandler(t *testing.T) { }, { name: "Port is not a string expects failure", - route: "/config/v1/inventory/upf/upf1", + route: "/config/v1/inventory/upf/upf1.my-domain.com", dbAdapter: &MockMongoClientEmptyDB{}, inputData: `{"port": 1234}`, expectedCode: http.StatusBadRequest, @@ -478,15 +486,15 @@ func TestUpfPutHandler(t *testing.T) { }, { name: "Missing port expects failure", - route: "/config/v1/inventory/upf/upf1", + route: "/config/v1/inventory/upf/upf1.my-domain.com", dbAdapter: &MockMongoClientEmptyDB{}, inputData: `{"some_param": "123"}`, expectedCode: http.StatusBadRequest, - expectedBody: `{"error":"UPF port cannot be converted to integer or it was not provided"}`, + expectedBody: `{"error":"invalid UPF port ''. Port must be a numeric string within the range [0, 65535]"}`, }, { name: "DB PUT operation fails expects failure", - route: "/config/v1/inventory/upf/upf1", + route: "/config/v1/inventory/upf/upf1.my-domain.com", dbAdapter: &MockMongoClientDBError{}, inputData: `{"port": "123"}`, expectedCode: http.StatusInternalServerError, @@ -494,11 +502,19 @@ func TestUpfPutHandler(t *testing.T) { }, { name: "Port cannot be converted to int expects failure", - route: "/config/v1/inventory/upf/upf1", + route: "/config/v1/inventory/upf/upf1.my-domain.com", dbAdapter: &MockMongoClientEmptyDB{}, inputData: `{"port": "a"}`, expectedCode: http.StatusBadRequest, - expectedBody: `{"error":"UPF port cannot be converted to integer or it was not provided"}`, + expectedBody: `{"error":"invalid UPF port 'a'. Port must be a numeric string within the range [0, 65535]"}`, + }, + { + name: "Invalid UPF hostname expects failure", + route: "/config/v1/inventory/upf/upf1", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"port": "123"}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"invalid UPF hostname 'upf1'. Hostname needs to represent a valid FQDN"}`, }, } for _, tc := range testCases { diff --git a/configapi/validators.go b/configapi/validators.go new file mode 100644 index 00000000..29bdf8e4 --- /dev/null +++ b/configapi/validators.go @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 Canonical Ltd + +package configapi + +import ( + "regexp" + "strconv" +) + +const NAME_PATTERN = "^[a-zA-Z0-9-_]+$" +const FQDN_PATTERN = "^([a-zA-Z0-9-]+\\.){2,}([a-zA-Z]{2,6})$" + +func isValidName(name string) bool { + nameMatch, err := regexp.MatchString(NAME_PATTERN, name) + if err != nil { + return false + } + return nameMatch +} + +func isValidFQDN(fqdn string) bool { + fqdnMatch, err := regexp.MatchString(FQDN_PATTERN, fqdn) + if err != nil { + return false + } + return fqdnMatch +} + +func isValidUpfPort(port string) bool { + portNum, err := strconv.Atoi(port) + if err != nil { + return false + } + return portNum >= 0 && portNum <= 65535 +} diff --git a/configapi/validators_test.go b/configapi/validators_test.go new file mode 100644 index 00000000..067c55d4 --- /dev/null +++ b/configapi/validators_test.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Canonical Ltd. + +package configapi + +import "testing" + +func TestValidateName(t *testing.T) { + var testCases = []struct { + name string + expected bool + }{ + {"validName", true}, + {"Valid-Name", true}, + {"Valid_Name", true}, + {"{invalid_name}", false}, + {"invalid&name", false}, + {"invalidName(R)", false}, + {"", false}, + } + + for _, tc := range testCases { + r := isValidName(tc.name) + if r != tc.expected { + t.Errorf("%s", tc.name) + } + } +} + +func TestValidateFQDN(t *testing.T) { + var testCases = []struct { + fqdn string + expected bool + }{ + {"upf-external.sdcore.svc.cluster.local", true}, + {"my-upf.my-domain.com", true}, + {"www.my-upf.com", true}, + {"some-upf-name", false}, + {"1.2.3.4", false}, + {"{upf-external}.sdcore.svc.cluster.local", false}, + {"http://my-upf.my-domain.com", false}, + {"my-domain.com/my-upf", false}, + {"", false}, + } + + for _, tc := range testCases { + r := isValidFQDN(tc.fqdn) + if r != tc.expected { + t.Errorf("%s", tc.fqdn) + } + } +} + +func TestValidateUpfPort(t *testing.T) { + var testCases = []struct { + port string + expected bool + }{ + {"123", true}, + {"7000", true}, + {"0", true}, + {"65535", true}, + {"-1", false}, + {"65536", false}, + {"invalid", false}, + {"123ad", false}, + {"", false}, + } + + for _, tc := range testCases { + r := isValidUpfPort(tc.port) + if r != tc.expected { + t.Errorf("%s", tc.port) + } + } +} From ded179cbf0bd8f7e7c6be0184c12d2a234fdef22 Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Mon, 27 Jan 2025 16:59:10 +0100 Subject: [PATCH 11/13] patch only works for one element Signed-off-by: Patricia Reinoso --- configapi/api_inventory.go | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/configapi/api_inventory.go b/configapi/api_inventory.go index 871cd94d..c15205b6 100644 --- a/configapi/api_inventory.go +++ b/configapi/api_inventory.go @@ -438,9 +438,7 @@ func executeUpfTransaction(ctx context.Context, upf configmodels.Upf, patchJSON } return err } - - filterByUpf := bson.M{"site-info.upf.upf-name": upf.Hostname} - err = dbadapter.CommonDBClient.RestfulAPIJSONPatchWithContext(sc, sliceDataColl, filterByUpf, patchJSON) + err = updateUpfInNetworkSlices(sc, upf.Hostname, patchJSON) if err != nil { if abortErr := session.AbortTransaction(sc); abortErr != nil { logger.DbLog.Errorw("failed to abort transaction", "error", abortErr) @@ -450,3 +448,23 @@ func executeUpfTransaction(ctx context.Context, upf configmodels.Upf, patchJSON return session.CommitTransaction(sc) }) } + +func updateUpfInNetworkSlices(context context.Context, hostname string, patchJSON []byte) error { + filterByUpf := bson.M{"site-info.upf.upf-name": hostname} + rawNetworkSlices, err := dbadapter.CommonDBClient.RestfulAPIGetMany(sliceDataColl, filterByUpf) + if err != nil { + return fmt.Errorf("failed to fetch network slices: %w", err) + } + for _, rawNetworkSlice := range rawNetworkSlices { + sliceName, ok := rawNetworkSlice["slice-name"].(string) + if !ok { + return fmt.Errorf("invalid slice-name in network slice: %v", rawNetworkSlice) + } + filterBySliceName := bson.M{"slice-name": sliceName} + err = dbadapter.CommonDBClient.RestfulAPIJSONPatchWithContext(context, sliceDataColl, filterBySliceName, patchJSON) + if err != nil { + return err + } + } + return nil +} From 8600c354c3c3758052e8d343270988093b8aedc0 Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Tue, 28 Jan 2025 14:45:50 +0100 Subject: [PATCH 12/13] use json encoder Signed-off-by: Patricia Reinoso --- configapi/api_inventory.go | 50 +++++++++++++++++++++++++++----------- dbadapter/db_adapter.go | 6 +++++ 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/configapi/api_inventory.go b/configapi/api_inventory.go index c15205b6..67c0fc84 100644 --- a/configapi/api_inventory.go +++ b/configapi/api_inventory.go @@ -250,7 +250,12 @@ func PostUpf(c *gin.Context) { return } upf := configmodels.Upf(postUpfParams) - patchJSON := getEditUpfPatchJSON(upf) + patchJSON, err := getEditUpfPatchJSON(upf) + if err != nil { + logger.WebUILog.Errorw("failed to serialize UPF", "hostname", upf.Hostname, "error", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to PUT UPF"}) + return + } if err = executeUpfTransaction(c.Request.Context(), upf, patchJSON, postUpfOperation); err != nil { if strings.Contains(err.Error(), "E11000") { logger.WebUILog.Errorw("duplicate hostname found:", "error", err) @@ -306,7 +311,12 @@ func PutUpf(c *gin.Context) { Hostname: hostname, Port: putUpfParams.Port, } - patchJSON := getEditUpfPatchJSON(putUpf) + patchJSON, err := getEditUpfPatchJSON(putUpf) + if err != nil { + logger.WebUILog.Errorw("failed to serialize UPF", "hostname", hostname, "error", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to PUT UPF"}) + return + } if err := executeUpfTransaction(c.Request.Context(), putUpf, patchJSON, putUpfOperation); err != nil { logger.WebUILog.Errorw("failed to PUT UPF", "hostname", hostname, "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to PUT UPF"}) @@ -315,17 +325,18 @@ func PutUpf(c *gin.Context) { c.JSON(http.StatusOK, gin.H{}) } -func getEditUpfPatchJSON(upf configmodels.Upf) []byte { - return []byte(fmt.Sprintf(`[ +func getEditUpfPatchJSON(upf configmodels.Upf) ([]byte, error) { + patch := []dbadapter.PatchOperation{ { - "op": "replace", - "path": "/site-info/upf", - "value": { - "upf-name": "%s", - "upf-port": "%s" - } - } - ]`, upf.Hostname, upf.Port)) + Op: "replace", + Path: "/site-info/upf", + Value: map[string]string{ + "upf-name": upf.Hostname, + "upf-port": upf.Port, + }, + }, + } + return json.Marshal(patch) } // DeleteUpf godoc @@ -351,11 +362,16 @@ func DeleteUpf(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": errorMessage}) return } - upf := configmodels.Upf{ Hostname: hostname, } - patchJSON := []byte(`[{"op": "remove", "path": "/site-info/upf"}]`) + patch := []dbadapter.PatchOperation{ + { + Op: "remove", + Path: "/site-info/upf", + }, + } + patchJSON, _ := json.Marshal(patch) if err := executeUpfTransaction(c.Request.Context(), upf, patchJSON, deleteUpfOperation); err != nil { logger.WebUILog.Errorw("failed to delete UPF", "hostname", hostname, "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete UPF"}) @@ -406,12 +422,18 @@ func updateGnbInNetworkSlices(gnbName string, context context.Context) error { func postUpfOperation(sc mongo.SessionContext, upf configmodels.Upf) error { filter := bson.M{"hostname": upf.Hostname} upfDataBson := configmodels.ToBsonM(upf) + if upfDataBson == nil { + return fmt.Errorf("failed to serialize UPF") + } return dbadapter.CommonDBClient.RestfulAPIPostManyWithContext(sc, configmodels.UpfDataColl, filter, []interface{}{upfDataBson}) } func putUpfOperation(sc mongo.SessionContext, upf configmodels.Upf) error { filter := bson.M{"hostname": upf.Hostname} upfDataBson := configmodels.ToBsonM(upf) + if upfDataBson == nil { + return fmt.Errorf("failed to serialize UPF") + } _, err := dbadapter.CommonDBClient.RestfulAPIPutOneWithContext(sc, configmodels.UpfDataColl, filter, upfDataBson) return err } diff --git a/dbadapter/db_adapter.go b/dbadapter/db_adapter.go index 2b47d432..69b7b676 100644 --- a/dbadapter/db_adapter.go +++ b/dbadapter/db_adapter.go @@ -54,6 +54,12 @@ type MongoDBClient struct { mongoapi.MongoClient } +type PatchOperation struct { + Op string `json:"op"` + Path string `json:"path"` + Value interface{} `json:"value,omitempty"` +} + func setDBClient(url, dbname string) (DBInterface, error) { mClient, errConnect := mongoapi.NewMongoClient(url, dbname) if mClient.Client != nil { From b1d559ee67c2195490f84d04cbca888ef5c4269d Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Tue, 28 Jan 2025 14:50:09 +0100 Subject: [PATCH 13/13] fix govet Signed-off-by: Patricia Reinoso --- dbadapter/db_adapter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbadapter/db_adapter.go b/dbadapter/db_adapter.go index 69b7b676..6a86b595 100644 --- a/dbadapter/db_adapter.go +++ b/dbadapter/db_adapter.go @@ -55,9 +55,9 @@ type MongoDBClient struct { } type PatchOperation struct { + Value interface{} `json:"value,omitempty"` Op string `json:"op"` Path string `json:"path"` - Value interface{} `json:"value,omitempty"` } func setDBClient(url, dbname string) (DBInterface, error) {