Skip to content

Commit

Permalink
[#58] Request device owner's approval (#59)
Browse files Browse the repository at this point in the history
* [#58] Request device owner's approval
- add config before which phases a owner consent will be needed
- owner consent API
- add owner consent client, used internally in UM
- add owner consent agent client, to be used by the app getting the owner approval
- modify orchestrator to wait for a owner consent
- unit test

---------

Signed-off-by: Dimitar Dimitrov <dimitar.dimitrov3@bosch.com>
  • Loading branch information
dimitar-dimitrow authored May 13, 2024
1 parent 84d770b commit 7ba526a
Show file tree
Hide file tree
Showing 25 changed files with 957 additions and 186 deletions.
26 changes: 26 additions & 0 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,29 @@ type DesiredStateClient interface {
SendDesiredStateCommand(string, *types.DesiredStateCommand) error
SendCurrentStateGet(string) error
}

// OwnerConsentAgentHandler defines functions for handling the owner consent requests
type OwnerConsentAgentHandler interface {
HandleOwnerConsent(string, int64, *types.OwnerConsent) error
}

// OwnerConsentAgentClient defines an interface for handling for owner consent requests
type OwnerConsentAgentClient interface {
BaseClient

Start(OwnerConsentAgentHandler) error
SendOwnerConsentFeedback(string, *types.OwnerConsentFeedback) error
}

// OwnerConsentHandler defines functions for handling the owner consent feedback
type OwnerConsentHandler interface {
HandleOwnerConsentFeedback(string, int64, *types.OwnerConsentFeedback) error
}

// OwnerConsentClient defines an interface for triggering requests for owner consent
type OwnerConsentClient interface {
BaseClient

Start(OwnerConsentHandler) error
SendOwnerConsent(string, *types.OwnerConsent) error
}
34 changes: 34 additions & 0 deletions api/types/owner_consent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 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 types

// ConsentStatusType defines values for status within the owner consent
type ConsentStatusType string

const (
// StatusApproved denotes that the owner approved the update operation.
StatusApproved ConsentStatusType = "APPROVED"
// StatusDenied denotes that the owner denied the update operation.
StatusDenied ConsentStatusType = "DENIED"
)

// OwnerConsentFeedback defines the payload for Owner Consent Feedback.
type OwnerConsentFeedback struct {
Status ConsentStatusType `json:"status,omitempty"`
// time field for scheduling could be added here
}

// OwnerConsent defines the payload for Owner Consent.
type OwnerConsent struct {
Command CommandType `json:"command,omitempty"`
}
1 change: 1 addition & 0 deletions api/update_orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ type UpdateOrchestrator interface {
Apply(context.Context, map[string]UpdateManager, string, *types.DesiredState, DesiredStateFeedbackHandler) bool

DesiredStateFeedbackHandler
OwnerConsentHandler
}
40 changes: 30 additions & 10 deletions cmd/update-manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,9 @@ func main() {
}
defer loggerOut.Close()

var client api.UpdateAgentClient
if cfg.ThingsEnabled {
client, err = mqtt.NewUpdateAgentThingsClient(cfg.Domain, cfg.MQTT)
} else {
client, err = mqtt.NewUpdateAgentClient(cfg.Domain, cfg.MQTT)
}
uac, um, err := initUpdateManager(cfg)
if err == nil {
updateManager, err := orchestration.NewUpdateManager(version, cfg, client, orchestration.NewUpdateOrchestrator(cfg))
if err == nil {
err = app.Launch(cfg, client, updateManager)
}
err = app.Launch(cfg, uac, um)
}

if err != nil {
Expand All @@ -60,3 +52,31 @@ func main() {
os.Exit(1)
}
}

func initUpdateManager(cfg *config.Config) (api.UpdateAgentClient, api.UpdateManager, error) {
var (
uac api.UpdateAgentClient
occ api.OwnerConsentClient
um api.UpdateManager
err error
)

if cfg.ThingsEnabled {
uac, err = mqtt.NewUpdateAgentThingsClient(cfg.Domain, cfg.MQTT)
} else {
uac, err = mqtt.NewUpdateAgentClient(cfg.Domain, cfg.MQTT)
}
if err != nil {
return nil, nil, err
}

if len(cfg.OwnerConsentCommands) != 0 {
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 {
return nil, nil, err
}
return uac, um, nil
}
9 changes: 8 additions & 1 deletion config/config_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@

package config

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

const (
// default log config
Expand All @@ -29,6 +32,7 @@ const (
currentStateDelayDefault = "30s"
phaseTimeoutDefault = "10m"
readTimeoutDefault = "1m"
ownerConsentTimeoutDefault = "30m"

domainContainers = "containers"
)
Expand All @@ -42,6 +46,8 @@ type Config struct {
ReportFeedbackInterval string `json:"reportFeedbackInterval"`
CurrentStateDelay string `json:"currentStateDelay"`
PhaseTimeout string `json:"phaseTimeout"`
OwnerConsentCommands []types.CommandType `json:"ownerConsentCommands"`
OwnerConsentTimeout string `json:"ownerConsentTimeout"`
}

func newDefaultConfig() *Config {
Expand All @@ -53,6 +59,7 @@ func newDefaultConfig() *Config {
ReportFeedbackInterval: reportFeedbackIntervalDefault,
CurrentStateDelay: currentStateDelayDefault,
PhaseTimeout: phaseTimeoutDefault,
OwnerConsentTimeout: ownerConsentTimeoutDefault,
}
}

Expand Down
4 changes: 4 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"testing"

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

"github.com/eclipse-kanto/update-manager/logger"
"github.com/eclipse-kanto/update-manager/mqtt"
Expand Down Expand Up @@ -63,6 +64,7 @@ func TestNewDefaultConfig(t *testing.T) {
ReportFeedbackInterval: "1m",
CurrentStateDelay: "30s",
PhaseTimeout: "10m",
OwnerConsentTimeout: "30m",
}

cfg := newDefaultConfig()
Expand Down Expand Up @@ -136,6 +138,8 @@ func TestLoadConfigFromFile(t *testing.T) {
ReportFeedbackInterval: "2m",
CurrentStateDelay: "1m",
PhaseTimeout: "2m",
OwnerConsentTimeout: "4m",
OwnerConsentCommands: []types.CommandType{types.CommandDownload},
}
assert.True(t, reflect.DeepEqual(*cfg, expectedConfigValues))
})
Expand Down
28 changes: 24 additions & 4 deletions config/flags_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,31 @@ import (
"os"
"strings"

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

const (
// domains flag
domainsFlagID = "domains"
domainsFlagID = "domains"
domainsDesc = "Specify a comma-separated list of domains handled by the update manager"
ownerConsentCommandsFlagID = "owner-consent-commands"
ownerConsentCommandsDesc = "Specify a comma-separated list of commands, before which an owner consent should be granted. Possible values are: 'download', 'update', 'activate'"
)

// SetupAllUpdateManagerFlags adds all flags for the configuration of the update manager
func SetupAllUpdateManagerFlags(flagSet *flag.FlagSet, cfg *Config) {
SetupFlags(flagSet, cfg.BaseConfig)

flagSet.String(domainsFlagID, "", "Specify a comma-separated list of domains handled by the update manager")
flagSet.String(domainsFlagID, "", domainsDesc)

flagSet.BoolVar(&cfg.RebootEnabled, "reboot-enabled", EnvToBool("REBOOT_ENABLED", cfg.RebootEnabled), "Specify a flag that controls the enabling/disabling of the reboot process after successful update operation")
flagSet.StringVar(&cfg.RebootAfter, "reboot-after", EnvToString("REBOOT_AFTER", cfg.RebootAfter), "Specify the timeout in cron format to wait before a reboot process is initiated after successful update operation. Value should be a positive integer number followed by a unit suffix, such as '60s', '10m', etc")

flagSet.StringVar(&cfg.PhaseTimeout, "phase-timeout", EnvToString("PHASE_TIMEOUT", cfg.PhaseTimeout), "Specify the timeout for completing an Update Orchestration phase. Value should be a positive integer number followed by a unit suffix, such as '60s', '10m', etc")
flagSet.StringVar(&cfg.ReportFeedbackInterval, "report-feedback-interval", EnvToString("REPORT_FEEDBACK_INTERVAL", cfg.ReportFeedbackInterval), "Specify the time interval for reporting intermediate desired state feedback messages during an active update operation. Value should be a positive integer number followed by a unit suffix, such as '60s', '10m', etc")
flagSet.StringVar(&cfg.CurrentStateDelay, "current-state-delay", EnvToString("CURRENT_STATE_DELAY", cfg.CurrentStateDelay), "Specify the time delay for reporting current state messages. Value should be a positive integer number followed by a unit suffix, such as '60s', '10m', etc")

flagSet.StringVar(&cfg.OwnerConsentTimeout, "owner-consent-timeout", EnvToString("OWNER_CONSENT_TIMEOUT", cfg.OwnerConsentTimeout), "Specify the timeout to wait for owner consent. Value should be a positive integer number followed by a unit suffix, such as '60s', '10m', etc")
setupAgentsConfigFlags(flagSet, cfg)
}

Expand All @@ -53,6 +57,7 @@ func parseFlags(cfg *Config, version string) {
SetupAllUpdateManagerFlags(flagSet, cfg)

fVersion := flagSet.Bool("version", false, "Prints current version and exits")
listCommands := flagSet.String(ownerConsentCommandsFlagID, "", ownerConsentCommandsDesc)
if err := flagSet.Parse(os.Args[1:]); err != nil {
logger.ErrorErr(err, "Cannot parse command flags")
}
Expand All @@ -61,13 +66,28 @@ func parseFlags(cfg *Config, version string) {
fmt.Println(version)
os.Exit(0)
}

if len(*listCommands) != 0 {
cfg.OwnerConsentCommands = parseOwnerConsentCommandsFlag(*listCommands)
}
}

func parseOwnerConsentCommandsFlag(listCommands string) []types.CommandType {
var result []types.CommandType
for _, command := range strings.Split(listCommands, ",") {
c := strings.TrimSpace(command)
if len(c) > 0 {
result = append(result, types.CommandType(strings.ToUpper(c)))
}
}
return result
}

func parseDomainsFlag() map[string]bool {
var listDomains string
flagSet := flag.NewFlagSet("", flag.ContinueOnError)
flagSet.SetOutput(io.Discard)
flagSet.StringVar(&listDomains, domainsFlagID, EnvToString("DOMAINS", ""), "Specify a comma-separated list of domains handled by the update manager")
flagSet.StringVar(&listDomains, domainsFlagID, EnvToString("DOMAINS", ""), domainsDesc)
if err := flagSet.Parse(getFlagArgs(domainsFlagID)); err != nil {
logger.ErrorErr(err, "Cannot parse domain flag")
}
Expand Down
2 changes: 2 additions & 0 deletions config/testdata/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"reportFeedbackInterval": "2m",
"currentStateDelay": "1m",
"phaseTimeout": "2m",
"ownerConsentCommands": ["DOWNLOAD"],
"ownerConsentTimeout": "4m",
"agents": {
"self-update": {
"rebootRequired": false,
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/eclipse-kanto/update-manager

go 1.17
go 1.18

require (
github.com/eclipse/ditto-clients-golang v0.0.0-20230504175246-3e6e17510ac4
Expand Down
11 changes: 3 additions & 8 deletions mqtt/desired_state_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,9 @@ type desiredStateClient struct {

// NewDesiredStateClient instantiates a new client for triggering MQTT requests.
func NewDesiredStateClient(domain string, updateAgent api.UpdateAgentClient) (api.DesiredStateClient, error) {
var mqttClient *mqttClient
switch v := updateAgent.(type) {
case *updateAgentClient:
mqttClient = updateAgent.(*updateAgentClient).mqttClient
case *updateAgentThingsClient:
mqttClient = updateAgent.(*updateAgentThingsClient).mqttClient
default:
return nil, fmt.Errorf("Unexpected type: %T", v)
mqttClient, err := getMQTTClient(updateAgent)
if err != nil {
return nil, err
}
return &desiredStateClient{
mqttClient: newInternalClient(domain, mqttClient.mqttConfig, mqttClient.pahoClient),
Expand Down
4 changes: 2 additions & 2 deletions mqtt/desired_state_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,14 @@ func TestNewDesiredStateClient(t *testing.T) {
},
"test_error": {
client: mockClient,
err: fmt.Sprintf("Unexpected type: %T", mockClient),
err: fmt.Sprintf("unexpected type: %T", mockClient),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
client, err := NewDesiredStateClient("testDomain", test.client)
if test.err != "" {
assert.EqualError(t, err, fmt.Sprintf("Unexpected type: %T", test.client))
assert.EqualError(t, err, fmt.Sprintf("unexpected type: %T", test.client))
} else {
assert.NoError(t, err)
assert.NotNil(t, client)
Expand Down
Loading

0 comments on commit 7ba526a

Please sign in to comment.