Skip to content

Commit

Permalink
[#68] Allow owner approval through things (#69)
Browse files Browse the repository at this point in the history
* [#68] Allow owner approval through things

---------

Signed-off-by: Dimitar Dimitrov <dimitar.dimitrov3@bosch.com>
  • Loading branch information
dimitar-dimitrow authored Jun 26, 2024
1 parent 0b23c81 commit dfd2129
Show file tree
Hide file tree
Showing 6 changed files with 400 additions and 25 deletions.
10 changes: 8 additions & 2 deletions cmd/update-manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,14 @@ func initUpdateManager(cfg *config.Config) (api.UpdateAgentClient, api.UpdateMan
}

if len(cfg.OwnerConsentCommands) != 0 {
if occ, err = mqtt.NewOwnerConsentClient(cfg.Domain, uac); err != nil {
return nil, nil, err
if cfg.ThingsEnabled {
if occ, err = mqtt.NewOwnerConsentThingsClient(cfg.Domain, uac); err != nil {
return nil, nil, err
}
} else {
if occ, err = mqtt.NewOwnerConsentClient(cfg.Domain, uac); err != nil {
return nil, nil, err
}
}
}
if um, err = orchestration.NewUpdateManager(version, cfg, uac, orchestration.NewUpdateOrchestrator(cfg, occ)); err != nil {
Expand Down
61 changes: 61 additions & 0 deletions mqtt/owner_consent_things_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) 2024 Contributors to the Eclipse Foundation
//
// See the NOTICE file(s) distributed with this work for additional
// information regarding copyright ownership.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0

package mqtt

import (
"fmt"

"github.com/eclipse-kanto/update-manager/api"
"github.com/eclipse-kanto/update-manager/api/types"
)

type ownerConsentThingsClient struct {
*updateAgentThingsClient
domain string
}

// NewOwnerConsentThingsClient instantiates a new client for handling owner consent approval through things.
func NewOwnerConsentThingsClient(domain string, updateAgent api.UpdateAgentClient) (api.OwnerConsentClient, error) {
var client *updateAgentThingsClient
switch v := updateAgent.(type) {
case *updateAgentThingsClient:
client = updateAgent.(*updateAgentThingsClient)
default:
return nil, fmt.Errorf("unexpected type: %T", v)
}
return &ownerConsentThingsClient{
updateAgentThingsClient: client,
domain: domain,
}, nil
}

func (client *ownerConsentThingsClient) Domain() string {
return client.domain
}

// Start sets consent handler to the UpdateManager feature.
func (client *ownerConsentThingsClient) Start(consentHandler api.OwnerConsentHandler) error {
client.umFeature.SetConsentHandler(consentHandler)
return nil
}

// Stop removes the consent handler from the UpdateManager feature.
func (client *ownerConsentThingsClient) Stop() error {
client.umFeature.SetConsentHandler(nil)
return nil
}

// SendOwnerConsent requests the owner consent through the UpdateManager feature.
func (client *ownerConsentThingsClient) SendOwnerConsent(activityID string, consent *types.OwnerConsent) error {
return client.umFeature.SendConsent(activityID, consent)
}
127 changes: 127 additions & 0 deletions mqtt/owner_consent_things_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright (c) 2024 Contributors to the Eclipse Foundation
//
// See the NOTICE file(s) distributed with this work for additional
// information regarding copyright ownership.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0

package mqtt

import (
"fmt"
"testing"

"github.com/eclipse-kanto/update-manager/api"
"github.com/eclipse-kanto/update-manager/api/types"
clientsmocks "github.com/eclipse-kanto/update-manager/mqtt/mocks"
"github.com/eclipse-kanto/update-manager/test"
"github.com/eclipse-kanto/update-manager/test/mocks"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)

func TestNewOwnerConsentThingsClient(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

mockPaho := clientsmocks.NewMockClient(mockCtrl)
mockClient := mocks.NewMockUpdateAgentClient(mockCtrl)

tests := map[string]struct {
client api.UpdateAgentClient
err string
}{
"test_update_agent_things_client": {
client: &updateAgentThingsClient{
updateAgentClient: &updateAgentClient{
mqttClient: newInternalClient("testDomain", &internalConnectionConfig{}, mockPaho),
},
},
},
"test_update_agent_client": {
client: &updateAgentClient{
mqttClient: newInternalClient("testDomain", &internalConnectionConfig{}, mockPaho),
},
err: fmt.Sprintf("unexpected type: %T", mockClient),
},
"test_error": {
client: mockClient,
err: fmt.Sprintf("unexpected type: %T", mockClient),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
client, err := NewOwnerConsentThingsClient("testDomain", test.client)
if test.err != "" {
assert.EqualError(t, err, fmt.Sprintf("unexpected type: %T", test.client))
} else {
assert.NoError(t, err)
assert.NotNil(t, client)
}
})
}
}

func TestOwnerConsentThingsClientStart(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

mockFeature := mocks.NewMockUpdateManagerFeature(mockCtrl)
mockHandler := mocks.NewMockOwnerConsentHandler(mockCtrl)
client := &ownerConsentThingsClient{
updateAgentThingsClient: &updateAgentThingsClient{
umFeature: mockFeature,
},
}
mockFeature.EXPECT().SetConsentHandler(mockHandler)
assert.Nil(t, client.Start(mockHandler))
}

func TestOwnerConsentThingsClientStop(t *testing.T) {

mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

mockFeature := mocks.NewMockUpdateManagerFeature(mockCtrl)
client := &ownerConsentThingsClient{
updateAgentThingsClient: &updateAgentThingsClient{
umFeature: mockFeature,
},
}
mockFeature.EXPECT().SetConsentHandler(nil)
assert.Nil(t, client.Stop())
}

func TestSendOwnerConsentThings(t *testing.T) {
tests := map[string]struct {
err error
}{
"test_send_owner_consent_ok": {},
"test_send_owner_consent_error": {err: fmt.Errorf("test error")},
}

mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

mockFeature := mocks.NewMockUpdateManagerFeature(mockCtrl)
client := &ownerConsentThingsClient{
updateAgentThingsClient: &updateAgentThingsClient{
umFeature: mockFeature,
},
}
testConsent := &types.OwnerConsent{
Command: types.CommandDownload,
}

for name, testCase := range tests {
t.Run(name, func(t *testing.T) {
mockFeature.EXPECT().SendConsent(test.ActivityID, testConsent).Return(testCase.err)
assert.Equal(t, testCase.err, client.SendOwnerConsent(test.ActivityID, testConsent))
})
}
}
27 changes: 27 additions & 0 deletions test/mocks/update_manager_feature_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

78 changes: 71 additions & 7 deletions things/update_manager_feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/eclipse/ditto-clients-golang/model"
"github.com/eclipse/ditto-clients-golang/protocol"
"github.com/eclipse/ditto-clients-golang/protocol/things"
"github.com/google/uuid"
)

const (
Expand All @@ -37,6 +38,7 @@ const (
updateManagerFeatureOperationRefresh = "refresh"
// outgoing messages
updateManagerFeatureMessageFeedback = "feedback"
updateManagerFeatureMessageConsent = "consent"
// properties
updateManagerFeaturePropertyDomain = "domain"
updateManagerFeaturePropertyActivityID = "activityId"
Expand All @@ -56,6 +58,16 @@ type feedback struct {
DesiredStateFeedback *types.DesiredStateFeedback `json:"desiredStateFeedback,omitempty"`
}

type consent struct {
base
OwnerConsent *types.OwnerConsent `json:"ownerConsent,omitempty"`
}

type consentFeedback struct {
base
OwnerConsentFeedback *types.OwnerConsentFeedback `json:"ownerConsentFeedback,omitempty"`
}

type updateManagerProperties struct {
base
Domain string `json:"domain"`
Expand All @@ -73,15 +85,19 @@ type UpdateManagerFeature interface {
Deactivate()
SetState(activityID string, currentState *types.Inventory) error
SendFeedback(activityID string, desiredStateFeedback *types.DesiredStateFeedback) error
SendConsent(activityID string, ownerConsent *types.OwnerConsent) error
SetConsentHandler(handler api.OwnerConsentHandler)
}

type updateManagerFeature struct {
sync.Mutex
active bool
thingID *model.NamespacedID
dittoClient ditto.Client
domain string
handler api.UpdateAgentHandler
active bool
thingID *model.NamespacedID
dittoClient ditto.Client
domain string
handler api.UpdateAgentHandler
consentHandler api.OwnerConsentHandler
consentCorrelationID string
}

// NewUpdateManagerFeature creates a new update manager feature representation.
Expand Down Expand Up @@ -163,6 +179,30 @@ func (um *updateManagerFeature) SendFeedback(activityID string, desiredStateFeed
return um.dittoClient.Send(message.Envelope(protocol.WithResponseRequired(false), protocol.WithContentType(jsonContent)))
}

// SendConsent issues an owner consent message to the cloud.
func (um *updateManagerFeature) SendConsent(activityID string, ownerConsent *types.OwnerConsent) error {
um.Lock()
defer um.Unlock()

if !um.active {
return nil
}
consent := &consent{
base: base{ActivityID: activityID, Timestamp: time.Now().UnixNano() / int64(time.Millisecond)},
OwnerConsent: ownerConsent,
}
um.consentCorrelationID = uuid.New().String()
message := things.NewMessage(um.thingID).Feature(updateManagerFeatureID).Outbox(updateManagerFeatureMessageConsent).WithPayload(consent)
return um.dittoClient.Send(message.Envelope(protocol.WithResponseRequired(true), protocol.WithContentType(jsonContent), protocol.WithCorrelationID(um.consentCorrelationID)))
}

func (um *updateManagerFeature) SetConsentHandler(handler api.OwnerConsentHandler) {
um.Lock()
defer um.Unlock()

um.consentHandler = handler
}

func (um *updateManagerFeature) messagesHandler(requestID string, msg *protocol.Envelope) {
um.Lock()
defer um.Unlock()
Expand All @@ -176,6 +216,8 @@ func (um *updateManagerFeature) messagesHandler(requestID string, msg *protocol.
um.processApply(requestID, msg)
} else if msg.Path == fmt.Sprintf("/features/%s/inbox/messages/%s", updateManagerFeatureID, updateManagerFeatureOperationRefresh) {
um.processRefresh(requestID, msg)
} else if msg.Path == fmt.Sprintf("/features/%s/inbox/messages/%s", updateManagerFeatureID, updateManagerFeatureMessageConsent) {
um.processConsent(requestID, msg)
} else {
logger.Debug("There is no handler for a message - skipping processing")
}
Expand All @@ -191,7 +233,7 @@ func (um *updateManagerFeature) processApply(requestID string, msg *protocol.Env
um.replySuccess(requestID, msg, updateManagerFeatureOperationApply)
go func(handler api.UpdateAgentHandler) {
logger.Trace("[%s][%s] processing apply operation", updateManagerFeatureID, um.domain)
if err := um.handler.HandleDesiredState(args.ActivityID, args.Timestamp, args.DesiredState); err != nil {
if err := handler.HandleDesiredState(args.ActivityID, args.Timestamp, args.DesiredState); err != nil {
logger.ErrorErr(err, "[%s][%s] error processing apply operation", updateManagerFeatureID, um.domain)
}
}(um.handler)
Expand All @@ -207,13 +249,35 @@ func (um *updateManagerFeature) processRefresh(requestID string, msg *protocol.E
um.replySuccess(requestID, msg, updateManagerFeatureOperationRefresh)
go func(handler api.UpdateAgentHandler) {
logger.Trace("[%s][%s] processing refresh operation", updateManagerFeatureID, um.domain)
if err := um.handler.HandleCurrentStateGet(args.ActivityID, args.Timestamp); err != nil {
if err := handler.HandleCurrentStateGet(args.ActivityID, args.Timestamp); err != nil {
logger.ErrorErr(err, "[%s][%s] error processing refresh operation", updateManagerFeatureID, um.domain)
}
}(um.handler)
}
}

func (um *updateManagerFeature) processConsent(requestID string, msg *protocol.Envelope) {
if um.consentHandler == nil {
um.replyError("owner consent handler not available", requestID, msg, updateManagerFeatureMessageConsent)
return
}
consentFeedback := &consentFeedback{}
if um.prepare(requestID, msg, updateManagerFeatureMessageConsent, consentFeedback) {
if msg.Headers.CorrelationID() != um.consentCorrelationID {
um.replyError("correlation id mismatch", requestID, msg, updateManagerFeatureMessageConsent)
return
}
um.consentCorrelationID = ""
um.replySuccess(requestID, msg, updateManagerFeatureMessageConsent)
go func(handler api.OwnerConsentHandler) {
logger.Trace("[%s][%s] processing consent operation", updateManagerFeatureID, um.domain)
if err := handler.HandleOwnerConsentFeedback(consentFeedback.ActivityID, consentFeedback.Timestamp, consentFeedback.OwnerConsentFeedback); err != nil {
logger.ErrorErr(err, "[%s][%s] error processing consent operation", updateManagerFeatureID, um.domain)
}
}(um.consentHandler)
}
}

func (um *updateManagerFeature) prepare(requestID string, msg *protocol.Envelope, operation string, to interface{}) bool {
logger.Trace("[%s][%s] parse message value: %v", updateManagerFeatureID, um.domain, msg.Value)

Expand Down
Loading

0 comments on commit dfd2129

Please sign in to comment.