Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide local artifacts for software installation #36

Merged
merged 7 commits into from
Sep 12, 2022
23 changes: 23 additions & 0 deletions internal/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ package feature

import (
"fmt"
"strings"
"sync"
"time"

Expand All @@ -27,6 +28,10 @@ import (
const (
defaultDisconnectTimeout = 250 * time.Millisecond
defaultKeepAlive = 20 * time.Second

modeStrict = "strict"
modeScoped = "scoped"
modeLax = "lax"
)

var (
Expand All @@ -50,6 +55,8 @@ type ScriptBasedSoftwareUpdatableConfig struct {
ServerCert string
DownloadRetryCount int
DownloadRetryInterval durationTime
InstallDirs pathArgs
Mode string
InstallCommand command
}

Expand All @@ -65,6 +72,8 @@ type ScriptBasedSoftwareUpdatable struct {
serverCert string
downloadRetryCount int
downloadRetryInterval time.Duration
installDirs []string
accessMode string
installCommand *command
}

Expand All @@ -89,6 +98,10 @@ func InitScriptBasedSU(scriptSUPConfig *ScriptBasedSoftwareUpdatableConfig) (*Ed
downloadRetryCount: scriptSUPConfig.DownloadRetryCount,
// Interval between download reattempts
downloadRetryInterval: time.Duration(scriptSUPConfig.DownloadRetryInterval),
// Install locations for local artifacts
installDirs: scriptSUPConfig.InstallDirs.args,
// Access mode for local artifacts
accessMode: initAccessMode(scriptSUPConfig.Mode),
// Define the module artifact(s) type: archive or plane
artifactType: scriptSUPConfig.ArtifactType,
// Create queue with size 10
Expand Down Expand Up @@ -149,5 +162,15 @@ func (scriptSUPConfig *ScriptBasedSoftwareUpdatableConfig) Validate() error {
if scriptSUPConfig.DownloadRetryCount < 0 {
return fmt.Errorf("negative download retry count value - %d", scriptSUPConfig.DownloadRetryCount)
}
if !strings.EqualFold(modeStrict, scriptSUPConfig.Mode) && !strings.EqualFold(modeScoped, scriptSUPConfig.Mode) && !strings.EqualFold(modeLax, scriptSUPConfig.Mode) {
return fmt.Errorf("invalid mode value, must be either strict, scoped or lax")
}
return nil
}

func initAccessMode(accessMode string) string {
if accessMode == "" {
return modeStrict
}
return accessMode
}
7 changes: 5 additions & 2 deletions internal/feature_download.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func (f *ScriptBasedSoftwareUpdatable) downloadModules(
// downloadModule returns true if canceled!
func (f *ScriptBasedSoftwareUpdatable) downloadModule(
cid string, module *storage.Module, toDir string, su *hawkbit.SoftwareUpdatable) bool {
// Download module to direcotry.
// Download module to directory.
logger.Infof("Download module [%s.%s] to directory: %s", module.Name, module.Version, toDir)
// Create few useful variables.
id := module.Name + ":" + module.Version
Expand Down Expand Up @@ -123,8 +123,11 @@ Started:
Downloading:
if opError = f.store.DownloadModule(toDir, module, func(percent int) {
setLastOS(su, newOS(cid, module, hawkbit.StatusDownloading).WithProgress(percent))
}, f.serverCert, f.downloadRetryCount, f.downloadRetryInterval); opError != nil {
}, f.serverCert, f.downloadRetryCount, f.downloadRetryInterval, func() error {
return f.validateLocalArtifacts(module)
}); opError != nil {
opErrorMsg = errDownload
logger.Errorf("error downloading module [%s.%s] - %v", module.Name, module.Version, opError)
return opError == storage.ErrCancel
}

Expand Down
69 changes: 48 additions & 21 deletions internal/feature_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"

"github.com/eclipse-kanto/software-update/hawkbit"
Expand Down Expand Up @@ -63,14 +64,16 @@ func (f *ScriptBasedSoftwareUpdatable) installModules(
// installModule returns true if canceled!
func (f *ScriptBasedSoftwareUpdatable) installModule(
cid string, module *storage.Module, dir string, su *hawkbit.SoftwareUpdatable) bool {
// Install module to direcotry.
// Install module to directory.
logger.Infof("Install module [%s.%s] from directory: %s", module.Name, module.Version, dir)
// Create few useful variables.
id := module.Name + ":" + module.Version
s := filepath.Join(dir, storage.InternalStatusName)
var opError error
opErrorMsg := errRuntime

execInstallScriptDir := dir

// Process final operation status in defer to also catch potential panic calls.
defer func() {
if opError == storage.ErrCancel {
Expand All @@ -84,15 +87,15 @@ func (f *ScriptBasedSoftwareUpdatable) installModule(
if exiterr, ok := opError.(*exec.ExitError); ok {
logger.Errorf("failed to install module [%s.%s][ExitCode: %v]: %v",
module.Name, module.Version, exiterr.ExitCode(), opError)
setLastOS(su, newFileOS(dir, cid, module, hawkbit.StatusFinishedError).
setLastOS(su, newFileOS(execInstallScriptDir, cid, module, hawkbit.StatusFinishedError).
WithStatusCode(strconv.Itoa(exiterr.ExitCode())).
WithMessage(opErrorMsg))
} else {
logger.Errorf("failed to install module [%s.%s]: %v", module.Name, module.Version, opError)
setLastOS(su, newOS(cid, module, hawkbit.StatusFinishedError).WithMessage(opErrorMsg))
}
} else { // Success
setLastOS(su, newFileOS(dir, cid, module, hawkbit.StatusFinishedSuccess))
setLastOS(su, newFileOS(execInstallScriptDir, cid, module, hawkbit.StatusFinishedSuccess))
}
}()

Expand Down Expand Up @@ -130,8 +133,11 @@ Started:
Downloading:
if opError = f.store.DownloadModule(dir, module, func(progress int) {
setLastOS(su, newOS(cid, module, hawkbit.StatusDownloading).WithProgress(progress))
}, f.serverCert, f.downloadRetryCount, f.downloadRetryInterval); opError != nil {
}, f.serverCert, f.downloadRetryCount, f.downloadRetryInterval, func() error {
return f.validateLocalArtifacts(module)
}); opError != nil {
opErrorMsg = errDownload
logger.Errorf("error downloading module [%s.%s] - %v", module.Name, module.Version, opError)
return opError == storage.ErrCancel
}

Expand All @@ -147,17 +153,6 @@ Downloaded:
storage.WriteLn(s, string(hawkbit.StatusInstalling))
Installing:

// Monitor install progress
monitor, err := (&monitor{
status: hawkbit.StatusInstalling,
su: su,
cid: cid,
module: &hawkbit.SoftwareModuleID{Name: module.Name, Version: module.Version},
}).waitFor(dir)
if err != nil {
logger.Debugf("fail to start progress monitor: %v", err)
}

// Get artifact type
artifactType := f.artifactType
if module.Metadata != nil && module.Metadata["artifact-type"] != "" {
Expand All @@ -166,19 +161,51 @@ Installing:
if artifactType == "archive" { // Extract if needed
if len(module.Artifacts) > 1 { // Only one archive/artifact is allowed in archive modules
opErrorMsg = errMultiArchives
opError = fmt.Errorf("archive modules cannot have multiples artifacts")
opError = fmt.Errorf(opErrorMsg)
return false
}
logger.Debugf("[%s.%s] Extract module archive(s) to: ", module.Name, module.Version)
if opError = storage.ExtractArchive(dir); opError != nil {
opErrorMsg = errExtractArchive
return false
}
} else {
var installScriptExtLocation string
isWindows := runtime.GOOS == "windows"
for _, sa := range module.Artifacts {
if (isWindows && sa.FileName == "install.bat") || (!isWindows && sa.FileName == "install.sh") {
if sa.Local && !sa.Copy {
installScriptExtLocation = sa.Link
break
}
}
}
if installScriptExtLocation != "" {
absExecPath, err := filepath.Abs(installScriptExtLocation)
if err != nil {
opErrorMsg = errDetermineAbsolutePath
opError = fmt.Errorf(opErrorMsg, err)
return false
}
execInstallScriptDir = filepath.Dir(absExecPath)
logger.Debugf("install script %s will be ran in its original folder", installScriptExtLocation)
}
}

// Monitor install progress
monitor, err := (&monitor{
status: hawkbit.StatusInstalling,
su: su,
cid: cid,
module: &hawkbit.SoftwareModuleID{Name: module.Name, Version: module.Version},
}).waitFor(execInstallScriptDir)
if err != nil {
logger.Errorf("fail to start progress monitor: %v", err)
}

// Start install script
logger.Debugf("[%s.%s] Run module install script", module.Name, module.Version)
if opError = f.installCommand.run(dir, "install"); opError != nil {
logger.Debugf("[%s.%s] Run module install script in %s", module.Name, module.Version, execInstallScriptDir)
if opError = f.installCommand.run(execInstallScriptDir, "install"); opError != nil {
opErrorMsg = errInstallScript
return false
}
Expand All @@ -189,14 +216,14 @@ Installing:
}

// Move the predefined installed dependencies
if opError = f.store.MoveInstalledDeps(dir, module.Metadata); opError != nil {
opErrorMsg = errInstalledDepsSsave
if opError = f.store.MoveInstalledDeps(execInstallScriptDir, module.Metadata); opError != nil {
opErrorMsg = errInstalledDepsSave
return false
}

// Installed
logger.Debugf("[%s.%s] Module installed", module.Name, module.Version)
setLastOS(su, newFileOS(dir, cid, module, hawkbit.StatusInstalled))
setLastOS(su, newFileOS(execInstallScriptDir, cid, module, hawkbit.StatusInstalled))

// Update installed dependencies
deps, err := f.store.LoadInstalledDeps()
Expand Down
33 changes: 25 additions & 8 deletions internal/feature_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ import (
)

const (
errRuntime = "internal runtime error"
errMultiArchives = "archive modules cannot have multiples artifacts"
errDownload = "fail to download module"
errExtractArchive = "fail to extract module archive"
errInstallScript = "fail to execute install script"
errInstalledDepsSsave = "fail to save installed dependencies"
errInstalledDepsRefresh = "fail to refresh installed dependencies"
errRuntime = "internal runtime error"
errMultiArchives = "archive modules cannot have multiple artifacts"
errDownload = "fail to download module"
errExtractArchive = "fail to extract module archive"
errInstallScript = "fail to execute install script"
errInstalledDepsSave = "fail to save installed dependencies"
errInstalledDepsRefresh = "fail to refresh installed dependencies"
errDetermineAbsolutePath = "fail to determine absolute path of install script %s - %v"
)

// opw is an operation wrapper function.
Expand Down Expand Up @@ -200,9 +201,25 @@ func newFileOS(dir string, cid string, module *storage.Module, status hawkbit.St
return ops
}

// setLastOS sets the last operatino status and log an error on error.
// setLastOS sets the last operation status and log an error on error.
func setLastOS(su *hawkbit.SoftwareUpdatable, os *hawkbit.OperationStatus) {
if err := su.SetLastOperation(os); err != nil {
logger.Errorf("fail to send last operation status: %v", err)
}
}

func (f *ScriptBasedSoftwareUpdatable) validateLocalArtifacts(module *storage.Module) error {
gboyvalenkov-bosch marked this conversation as resolved.
Show resolved Hide resolved
logger.Debugf("validating local artifacts of module - %v", module)
for _, sa := range module.Artifacts {
if !sa.Local {
continue
}
location, err := f.resolveLocalArtifact(sa.Link)
if err != nil {
return err
}
logger.Infof("resolved local artifact link [%s] to %s", sa.Link, location)
sa.Link = location
}
return nil
}
12 changes: 6 additions & 6 deletions internal/feature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const (
// TestScriptBasedConstructor tests NewScriptBasedSU with wrong broker URL.
func TestScriptBasedConstructor(t *testing.T) {
// Prepare
dir := assertPath(t, testDirFeature, true)
dir := assertDirs(t, testDirFeature, true)
// Remove temporary directory at the end.
defer os.RemoveAll(dir)

Expand All @@ -65,7 +65,7 @@ func TestScriptBasedConstructor(t *testing.T) {
// when invalid config field featureID is provided to HawkBit
func TestNewScriptBasedInitHawkBitValidation(t *testing.T) {
// Prepare
dir := assertPath(t, testDirFeature, false)
dir := assertDirs(t, testDirFeature, false)
// Remove temporary directory at the end.
defer os.RemoveAll(dir)

Expand All @@ -81,7 +81,7 @@ func TestNewScriptBasedInitHawkBitValidation(t *testing.T) {
// with invalid mandatory field in dependencies on load
func TestScriptBasedInitLoadDependencies(t *testing.T) {
// Prepare
dir := assertPath(t, testDirFeature, false)
dir := assertDirs(t, testDirFeature, false)
// Remove temporary directory at the end.
defer os.RemoveAll(dir)

Expand All @@ -105,7 +105,7 @@ func TestScriptBasedInitLoadDependencies(t *testing.T) {
// TestScriptBasedInit tests the ScriptBasedSoftwareUpdatable initialization when the client is not connected
func TestScriptBasedInit(t *testing.T) {
// Prepare
dir := assertPath(t, testDirFeature, false)
dir := assertDirs(t, testDirFeature, false)
// Remove temporary directory at the end.
defer os.RemoveAll(dir)

Expand All @@ -130,7 +130,7 @@ func TestScriptBasedDownloadAndInstallResume(t *testing.T) {

func testScriptBasedSoftwareUpdatableOperations(noResume bool, t *testing.T) {
// Prepare
dir := assertPath(t, testDirFeature, false)
dir := assertDirs(t, testDirFeature, false)
// Remove temporary directory at the end.
defer os.RemoveAll(dir)

Expand Down Expand Up @@ -191,7 +191,7 @@ func testDisconnectWhileRunningOperation(feature *ScriptBasedSoftwareUpdatable,

statuses = append(statuses, pullStatusChanges(mc, postDisconnectEventCount)...)
waitDisconnect.Wait()
defer feature.Connect(mc, supConfig, edgeCfg)
defer connectFeature(t, mc, feature, getDefaultFlagValue(t, flagFeatureID))
if install {
checkInstallStatusEvents(statuses, t)
} else {
Expand Down
Loading