diff --git a/internal/core/metadata/application/device.go b/internal/core/metadata/application/device.go index 8caf62d33b..ee67afea6b 100644 --- a/internal/core/metadata/application/device.go +++ b/internal/core/metadata/application/device.go @@ -42,7 +42,7 @@ const minAutoEventInterval = 1 * time.Millisecond // The AddDevice function accepts the new device model from the controller function // and then invokes AddDevice function of infrastructure layer to add new device -func AddDevice(d models.Device, ctx context.Context, dic *di.Container, bypassValidation bool) (id string, edgeXerr errors.EdgeX) { +func AddDevice(d models.Device, ctx context.Context, dic *di.Container, bypassValidation bool, force bool) (id string, edgeXerr errors.EdgeX) { dbClient := container.DBClientFrom(dic.Get) lc := bootstrapContainer.LoggingClientFrom(dic.Get) @@ -59,9 +59,23 @@ func AddDevice(d models.Device, ctx context.Context, dic *di.Container, bypassVa return "", errors.NewCommonEdgeXWrapper(err) } - // Execute the Device Service Validation when bypassValidation is false by default - // Skip the Device Service Validation if bypassValidation is true - if !bypassValidation { + // check if device name already exists + exists, err = dbClient.DeviceNameExists(d.Name) + if err != nil { + return "", errors.NewCommonEdgeXWrapper(err) + } + if exists { + if force { + // invoke updateDevice if force flag is enabled + return updateDevice(d, ctx, dic) + } else { + return "", errors.NewCommonEdgeX(errors.KindDuplicateName, fmt.Sprintf("device name %s already exists", d.Name), nil) + } + } + + // Execute the Device Service Validation when both bypassValidation/force values are false by default + // Skip the Device Service Validation if either bypassValidation or force is true + if !(bypassValidation || force) { err = validateDeviceCallback(dtos.FromDeviceModelToDTO(d), dic) if err != nil { return "", errors.NewCommonEdgeXWrapper(err) @@ -90,6 +104,39 @@ func AddDevice(d models.Device, ctx context.Context, dic *di.Container, bypassVa return addedDevice.Id, nil } +// updateDevice accepts the updated device model from AddDevice function if force flag is enabled +// and then invokes UpdateDevice function of infrastructure layer to update the existing device +// the "update device" system events will be published to the msg bus at last +func updateDevice(d models.Device, ctx context.Context, dic *di.Container) (id string, edgeXerr errors.EdgeX) { + dbClient := container.DBClientFrom(dic.Get) + + oldDevice, err := dbClient.DeviceByName(d.Name) + if err != nil { + return "", errors.NewCommonEdgeXWrapper(edgeXerr) + } + + // set the id and created fields from the old device + if d.Id == "" { + d.Id = oldDevice.Id + } + if d.Created == 0 { + d.Created = oldDevice.Created + } + + // Old service name is used for invoking callback + var oldServiceName string + if d.ServiceName != "" && d.ServiceName != oldDevice.ServiceName { + oldServiceName = oldDevice.ServiceName + } + + err = updateDeviceInDB(d, oldServiceName, ctx, dic) + if err != nil { + return "", errors.NewCommonEdgeXWrapper(err) + } + + return d.Id, nil +} + // DeleteDeviceByName deletes the device by name func DeleteDeviceByName(name string, ctx context.Context, dic *di.Container) errors.EdgeX { if name == "" { @@ -147,7 +194,6 @@ func DeviceNameExists(name string, dic *di.Container) (exists bool, err errors.E // PatchDevice executes the PATCH operation with the device DTO to replace the old data func PatchDevice(dto dtos.UpdateDevice, ctx context.Context, dic *di.Container, bypassValidation bool) errors.EdgeX { dbClient := container.DBClientFrom(dic.Get) - lc := bootstrapContainer.LoggingClientFrom(dic.Get) // Check the existence of device service before device validation if dto.ServiceName != nil { @@ -188,7 +234,16 @@ func PatchDevice(dto dtos.UpdateDevice, ctx context.Context, dic *di.Container, } } - err = dbClient.UpdateDevice(device) + return updateDeviceInDB(device, oldServiceName, ctx, dic) +} + +// updateDeviceInDB calls the UpdateDevice method from the infrastructure layer and validate the device auto events +// and publish the "update device" system event at last +func updateDeviceInDB(device models.Device, oldServiceName string, ctx context.Context, dic *di.Container) errors.EdgeX { + dbClient := container.DBClientFrom(dic.Get) + lc := bootstrapContainer.LoggingClientFrom(dic.Get) + + err := dbClient.UpdateDevice(device) if err != nil { return errors.NewCommonEdgeXWrapper(err) } @@ -199,10 +254,11 @@ func PatchDevice(dto dtos.UpdateDevice, ctx context.Context, dic *di.Container, } lc.Debugf( - "Device patched on DB successfully. Correlation-ID: %s ", + "Device updated on DB successfully. Correlation-ID: %s ", correlation.FromContext(ctx), ) + deviceDTO := dtos.FromDeviceModelToDTO(device) if oldServiceName != "" { go publishSystemEvent(common.DeviceSystemEventType, common.SystemEventActionUpdate, oldServiceName, deviceDTO, ctx, dic) } diff --git a/internal/core/metadata/application/device_test.go b/internal/core/metadata/application/device_test.go index bed0995f0a..1d97f75a63 100644 --- a/internal/core/metadata/application/device_test.go +++ b/internal/core/metadata/application/device_test.go @@ -6,15 +6,23 @@ package application import ( - "github.com/edgexfoundry/edgex-go/internal/core/metadata/container" + "context" "testing" + "github.com/edgexfoundry/edgex-go/internal/core/metadata/config" + "github.com/edgexfoundry/edgex-go/internal/core/metadata/container" "github.com/edgexfoundry/edgex-go/internal/core/metadata/infrastructure/interfaces/mocks" + "github.com/edgexfoundry/edgex-go/internal/pkg/correlation" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v3/di" + "github.com/edgexfoundry/go-mod-core-contracts/v3/clients/logger" + "github.com/edgexfoundry/go-mod-core-contracts/v3/errors" "github.com/edgexfoundry/go-mod-core-contracts/v3/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" ) func TestValidateAutoEvents(t *testing.T) { @@ -100,3 +108,59 @@ func TestValidateAutoEvents(t *testing.T) { }) } } + +func TestForceAddDevice(t *testing.T) { + invalidDeviceName := "invalidDevice" + validDeviceName := "validDevice" + invalidDeviceName2 := "invalidDevice2" + invalidDevice := models.Device{Name: invalidDeviceName} + invalidDevice2 := models.Device{Name: invalidDeviceName2} + returnedDevice := models.Device{Name: validDeviceName} + + dic := di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger.NewMockClient() + }, + container.ConfigurationName: func(get di.Get) interface{} { + return &config.ConfigurationStruct{ + Writable: config.WritableInfo{ + LogLevel: "DEBUG", + }, + } + }, + }) + + dbClientMock := &mocks.DBClient{} + dbClientMock.On("DeviceByName", invalidDeviceName).Return(models.Device{}, errors.NewCommonEdgeX(errors.KindDatabaseError, "failed to query", nil)) + dbClientMock.On("DeviceByName", validDeviceName).Return(returnedDevice, nil) + dbClientMock.On("DeviceByName", invalidDeviceName2).Return(invalidDevice2, nil) + dbClientMock.On("UpdateDevice", returnedDevice).Return(nil) + dbClientMock.On("UpdateDevice", invalidDevice2).Return(errors.NewCommonEdgeX(errors.KindDatabaseError, "failed to update", nil)) + dic.Update(di.ServiceConstructorMap{ + container.DBClientInterfaceName: func(get di.Get) interface{} { + return dbClientMock + }, + }) + + tests := []struct { + name string + device models.Device + errorExpected bool + }{ + {"invalid - DeviceByName error", invalidDevice, true}, + {"valid", returnedDevice, false}, + {"invalid - UpdateDevice error", invalidDevice2, true}, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + ctx, _ := correlation.FromContextOrNew(context.Background()) + result, err := updateDevice(testCase.device, ctx, dic) + if testCase.errorExpected { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, returnedDevice.Id, result) + } + }) + } +} diff --git a/internal/core/metadata/controller/http/device.go b/internal/core/metadata/controller/http/device.go index 2fdbe76032..68b5a1e590 100644 --- a/internal/core/metadata/controller/http/device.go +++ b/internal/core/metadata/controller/http/device.go @@ -27,7 +27,10 @@ import ( "github.com/labstack/echo/v4" ) -const bypassValidationQueryParam = "bypassValidation" // query param to specify whether to skip the Device Service Validation API call +const ( + bypassValidationQueryParam = "bypassValidation" // query param to specify whether to skip the Device Service Validation API call + forceQueryParam = "force" // query param to specify whether to force add a device +) type DeviceController struct { reader io.DtoReader @@ -54,12 +57,17 @@ func (dc *DeviceController) AddDevice(c echo.Context) error { ctx := r.Context() correlationId := correlation.FromContext(ctx) - var bypassValidation bool + var bypassValidation, force bool // parse URL query string for bypassValidation bypassValidationParamStr := utils.ParseQueryStringToString(r, bypassValidationQueryParam, common.ValueFalse) if bypassValidationParamStr == common.ValueTrue { bypassValidation = true } + // parse URL query string for force add device + forceParamStr := utils.ParseQueryStringToString(r, forceQueryParam, common.ValueFalse) + if forceParamStr == common.ValueTrue { + force = true + } var reqDTOs []requests.AddDeviceRequest err := dc.reader.Read(r.Body, &reqDTOs) @@ -72,7 +80,7 @@ func (dc *DeviceController) AddDevice(c echo.Context) error { for i, d := range devices { var response interface{} reqId := reqDTOs[i].RequestId - newId, err := application.AddDevice(d, ctx, dc.dic, bypassValidation) + newId, err := application.AddDevice(d, ctx, dc.dic, bypassValidation, force) if err != nil { lc.Error(err.Error(), common.CorrelationHeader, correlationId) lc.Debug(err.DebugMessages(), common.CorrelationHeader, correlationId) diff --git a/internal/core/metadata/controller/http/device_test.go b/internal/core/metadata/controller/http/device_test.go index 0df310cfa2..1575734c91 100644 --- a/internal/core/metadata/controller/http/device_test.go +++ b/internal/core/metadata/controller/http/device_test.go @@ -130,9 +130,17 @@ func TestAddDevice(t *testing.T) { valid := testDevice dbClientMock.On("DeviceServiceNameExists", deviceModel.ServiceName).Return(true, nil) + dbClientMock.On("DeviceNameExists", deviceModel.Name).Return(false, nil) dbClientMock.On("AddDevice", deviceModel).Return(deviceModel, nil) dbClientMock.On("DeviceProfileByName", mock.Anything).Return(models.DeviceProfile{Name: "test-profile", DeviceResources: []models.DeviceResource{{Name: "TestResource"}}}, nil) + validForceAdd := testDevice + validForceAdd.Device.Name = "forceAdd" + forceAddDm := dtos.ToDeviceModel(validForceAdd.Device) + dbClientMock.On("DeviceNameExists", validForceAdd.Device.Name).Return(true, nil) + dbClientMock.On("DeviceByName", validForceAdd.Device.Name).Return(forceAddDm, nil) + dbClientMock.On("UpdateDevice", forceAddDm).Return(nil) + notFoundProfile := testDevice notFoundProfile.Device.ProfileName = "notFoundProfile" notFoundProfileDeviceModel := requests.AddDeviceReqToDeviceModels([]requests.AddDeviceRequest{notFoundProfile})[0] @@ -190,23 +198,25 @@ func TestAddDevice(t *testing.T) { expectedResponseCode int expectedValidation bool expectedSystemEvent bool + forceAdd bool }{ - {"Valid", []requests.AddDeviceRequest{valid}, http.StatusMultiStatus, http.StatusCreated, true, true}, - {"Valid - bypassValidation", []requests.AddDeviceRequest{valid}, http.StatusMultiStatus, http.StatusCreated, false, true}, - {"Valid - no profile name and no auto events", []requests.AddDeviceRequest{noProfileAndAutoEvents}, http.StatusMultiStatus, http.StatusCreated, true, true}, - {"Invalid - not found profile", []requests.AddDeviceRequest{notFoundProfile}, http.StatusMultiStatus, http.StatusNotFound, true, false}, - {"Invalid - no name", []requests.AddDeviceRequest{noName}, http.StatusBadRequest, http.StatusBadRequest, false, false}, - {"Invalid - no adminState", []requests.AddDeviceRequest{noAdminState}, http.StatusBadRequest, http.StatusBadRequest, false, false}, - {"Invalid - no operatingState", []requests.AddDeviceRequest{noOperatingState}, http.StatusBadRequest, http.StatusBadRequest, false, false}, - {"Invalid - invalid adminState", []requests.AddDeviceRequest{invalidAdminState}, http.StatusBadRequest, http.StatusBadRequest, false, false}, - {"Invalid - invalid operatingState", []requests.AddDeviceRequest{invalidOperatingState}, http.StatusBadRequest, http.StatusBadRequest, false, false}, - {"Invalid - no service name", []requests.AddDeviceRequest{noServiceName}, http.StatusBadRequest, http.StatusBadRequest, false, false}, - {"Valid - no profile name", []requests.AddDeviceRequest{noProfileName}, http.StatusMultiStatus, http.StatusCreated, true, true}, - {"Invalid - no protocols", []requests.AddDeviceRequest{noProtocols}, http.StatusBadRequest, http.StatusBadRequest, false, false}, - {"Valid - empty protocols", []requests.AddDeviceRequest{emptyProtocols}, http.StatusMultiStatus, http.StatusCreated, true, true}, - {"Invalid - invalid protocols", []requests.AddDeviceRequest{invalidProtocols}, http.StatusMultiStatus, http.StatusInternalServerError, true, false}, - {"Invalid - not found device service", []requests.AddDeviceRequest{notFoundService}, http.StatusMultiStatus, http.StatusBadRequest, false, false}, - {"Invalid - device service unavailable", []requests.AddDeviceRequest{valid}, http.StatusMultiStatus, http.StatusServiceUnavailable, true, false}, + {"Valid", []requests.AddDeviceRequest{valid}, http.StatusMultiStatus, http.StatusCreated, true, true, false}, + {"Valid - bypassValidation", []requests.AddDeviceRequest{valid}, http.StatusMultiStatus, http.StatusCreated, false, true, false}, + {"Valid - no profile name and no auto events", []requests.AddDeviceRequest{noProfileAndAutoEvents}, http.StatusMultiStatus, http.StatusCreated, true, true, false}, + {"Invalid - not found profile", []requests.AddDeviceRequest{notFoundProfile}, http.StatusMultiStatus, http.StatusNotFound, true, false, false}, + {"Invalid - no name", []requests.AddDeviceRequest{noName}, http.StatusBadRequest, http.StatusBadRequest, false, false, false}, + {"Invalid - no adminState", []requests.AddDeviceRequest{noAdminState}, http.StatusBadRequest, http.StatusBadRequest, false, false, false}, + {"Invalid - no operatingState", []requests.AddDeviceRequest{noOperatingState}, http.StatusBadRequest, http.StatusBadRequest, false, false, false}, + {"Invalid - invalid adminState", []requests.AddDeviceRequest{invalidAdminState}, http.StatusBadRequest, http.StatusBadRequest, false, false, false}, + {"Invalid - invalid operatingState", []requests.AddDeviceRequest{invalidOperatingState}, http.StatusBadRequest, http.StatusBadRequest, false, false, false}, + {"Invalid - no service name", []requests.AddDeviceRequest{noServiceName}, http.StatusBadRequest, http.StatusBadRequest, false, false, false}, + {"Valid - no profile name", []requests.AddDeviceRequest{noProfileName}, http.StatusMultiStatus, http.StatusCreated, true, true, false}, + {"Invalid - no protocols", []requests.AddDeviceRequest{noProtocols}, http.StatusBadRequest, http.StatusBadRequest, false, false, false}, + {"Valid - empty protocols", []requests.AddDeviceRequest{emptyProtocols}, http.StatusMultiStatus, http.StatusCreated, true, true, false}, + {"Invalid - invalid protocols", []requests.AddDeviceRequest{invalidProtocols}, http.StatusMultiStatus, http.StatusInternalServerError, true, false, false}, + {"Invalid - not found device service", []requests.AddDeviceRequest{notFoundService}, http.StatusMultiStatus, http.StatusBadRequest, false, false, false}, + {"Invalid - device service unavailable", []requests.AddDeviceRequest{valid}, http.StatusMultiStatus, http.StatusServiceUnavailable, true, false, false}, + {"Valid - force add device", []requests.AddDeviceRequest{validForceAdd}, http.StatusMultiStatus, http.StatusCreated, false, true, true}, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { @@ -259,6 +269,12 @@ func TestAddDevice(t *testing.T) { req.URL.RawQuery = query.Encode() } + if testCase.forceAdd { + query := req.URL.Query() + query.Add(forceQueryParam, common.ValueTrue) + req.URL.RawQuery = query.Encode() + } + // Act recorder := httptest.NewRecorder() c := e.NewContext(req, recorder) diff --git a/openapi/v3/core-metadata.yaml b/openapi/v3/core-metadata.yaml index 7de2383d80..6e7905e5bb 100644 --- a/openapi/v3/core-metadata.yaml +++ b/openapi/v3/core-metadata.yaml @@ -1152,6 +1152,14 @@ components: type: boolean description: "Indicates whether to skip the Device Service Validation API call." default: false + forceParam: + in: query + name: force + required: false + schema: + type: boolean + description: "Indicates whether to force add the device if device name already exists." + default: false headers: correlatedResponseHeader: description: "A response header that returns the unique correlation ID used to initiate the request." @@ -1661,6 +1669,8 @@ paths: - $ref: '#/components/parameters/bypassValidationParam' post: summary: "Allows provisioning of a new device" + parameters: + - $ref: '#/components/parameters/forceParam' requestBody: required: true content: