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

Installing/updating configuration files via the containers update agent. #232

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions containerm/containers/types/mount_point.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const (

// MountPoint specifies a mount point from the host to the container
type MountPoint struct {
Destination string `json:"destination"` // path in container
Source string `json:"source"` // path in host
PropagationMode string `json:"propagation_mode"` // propagation mode to use in the spec
Source string `json:"source,omitempty"`
Destination string `json:"destination,omitempty"`
PropagationMode string `json:"propagation_mode,omitempty"`
Data string `json:"data,omitempty"`
}
5 changes: 5 additions & 0 deletions containerm/mgr/mgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ package mgr

import (
"context"
"errors"
"os"
"reflect"
"strings"
Expand Down Expand Up @@ -478,6 +479,10 @@ func (mgr *containerMgr) Remove(ctx context.Context, id string, force bool, stop

mgr.removeContainerRestartManager(container)
mgr.removeContainerFromCache(id)
//remove configuration files too when removing the container.
if _, err := os.Stat(container.Mounts[len(container.Mounts)-1].Source + "/" + container.Name + "_config.json"); !errors.Is(err, os.ErrNotExist) {
os.Remove(container.Mounts[len(container.Mounts)-1].Source + "/" + container.Name + "_config.json")
}

if err != nil {
log.WarnErr(err, "failed to Delete container file with id: %s", id)
Expand Down
19 changes: 16 additions & 3 deletions containerm/updateagent/to_containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package updateagent

import (
"os"
"strconv"
"time"

Expand Down Expand Up @@ -45,7 +46,6 @@ func toContainer(component *types.ComponentWithConfig) (*ctrtypes.Container, err
portMappings []ctrtypes.PortMapping
deviceMappings []ctrtypes.DeviceMapping
)

config := make(map[string]string, len(component.Config))
for _, keyValuePair := range component.Config {
switch keyValuePair.Key {
Expand All @@ -67,9 +67,21 @@ func toContainer(component *types.ComponentWithConfig) (*ctrtypes.Container, err
extraHosts = append(extraHosts, keyValuePair.Value)
case keyMount:
mountPoint, err := util.ParseMountPoint(keyValuePair.Value)
if err != nil {
log.WarnErr(err, "Ignoring invalid mount point")
//defer to remove config backup file after the entire process
defer func() {
os.Remove(mountPoint.Source + "/" + component.ID + "_config_backup.json")
}()
if err != nil || len(mountPoint.Data) == 0 {
log.WarnErr(err, "ignoring invalid mount point")
} else {
err = util.MakeAtomicCopy(mountPoint.Source+"/"+component.ID+"_config.json", mountPoint.Source+"/"+component.ID+"_config_backup.json")
if err != nil && !os.IsNotExist(err) {
log.Fatal("could not create backup, exiting out of process, no changes will be made to the container will be made ", err)
}
err = util.WriteAtomicFile(mountPoint.Source+"/"+component.ID+"_config.json", []byte(mountPoint.Data), 0755)
if err != nil {
log.WarnErr(err, "error writing to file, rolling back to previous configuration file data")
}
mountPoints = append(mountPoints, *mountPoint)
}
case keyEnv:
Expand Down Expand Up @@ -146,6 +158,7 @@ func toContainer(component *types.ComponentWithConfig) (*ctrtypes.Container, err
}

return container, nil

}

func parseBool(key string, config map[string]string) bool {
Expand Down
20 changes: 19 additions & 1 deletion containerm/updateagent/update_operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ package updateagent

import (
"context"
"errors"
"fmt"
"os"
"strings"

ctrtypes "github.com/eclipse-kanto/container-management/containerm/containers/types"
Expand Down Expand Up @@ -381,7 +383,7 @@ func activate(o *operation, baselineAction *baselineAction) {
}
}

// ActionCreate: removes the newly created container instance (from DOWNLOAD phase)
// ActionCreate: removes the newly created container instance (from DOWNLOAD phase).
// ActionRecreate: removes the newly created container instance (from DOWNLOAD phase) and restarts the old existing container instance.
// ActionUpdate: restores the old configuration to the existing container and ensures it is started.
func rollback(o *operation, baselineAction *baselineAction) {
Expand Down Expand Up @@ -409,6 +411,7 @@ func rollback(o *operation, baselineAction *baselineAction) {
if action.actionType == util.ActionUpdate {
o.updateBaselineActionStatus(baselineAction, types.BaselineStatusRollback, action, types.ActionStatusUpdating, action.feedbackAction.Message)
if err := o.updateContainer(action.current, action.current); err != nil {
o.configurationFileRollBack(action.current, &action.current.Mounts[len(action.current.Mounts)-1])
lastActionMessage = err.Error()
failure = true
continue
Expand Down Expand Up @@ -554,6 +557,7 @@ func (o *operation) createContainer(desired *ctrtypes.Container) error {
_, err := o.updateManager.mgr.Create(o.ctx, desired)
if err != nil {
log.ErrorErr(err, "could not create container [%s]", desired.Name)
o.configurationFileRollBack(desired, &desired.Mounts[len(desired.Mounts)-1])
return err
}
log.Debug("successfully created container [%s]", desired.Name)
Expand Down Expand Up @@ -637,3 +641,17 @@ func (o *operation) stopContainer(container *ctrtypes.Container) error {
log.Debug("successfully stopped outdated container [%s]", container.Name)
return nil
}

func (o *operation) configurationFileRollBack(container *ctrtypes.Container, mount *ctrtypes.MountPoint) {
if _, err := os.Stat(mount.Source + "/" + container.Name + "_config.json"); errors.Is(err, os.ErrNotExist) {
return
}
err := util.MakeAtomicCopy(mount.Source+"/"+container.Name+"_config_backup.json", mount.Source+"/"+container.Name+"_config.json")
if os.IsNotExist(err) {
log.WarnErr(err, "earlier configuration does not exist, removing any configuration files")
os.Remove(mount.Source + "/" + container.Name + "_config.json")
} else {
log.Debug("configuration file rolled back to previous configuration")
}
// os.Remove(mount.Source + "/" + container.Name + "_config_backup.json") - final step of container update, irrespective of success or failure.
}
86 changes: 86 additions & 0 deletions containerm/util/atomic_file_operations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package util

import (
"fmt"
"io"
"os"
"path/filepath"

"github.com/eclipse-kanto/container-management/containerm/log"
)

// WriteAtomicFile writes data to a temporary file, then renames it into filename. If the
// target filename already exists but is not a regular file, WriteAtomicFile will return an error.
func WriteAtomicFile(filename string, data []byte, perm os.FileMode) (err error) {
fi, err := os.Stat(filename)
if err == nil && !fi.Mode().IsRegular() {
log.WarnErr(err, "file already exists and is not a regular file.")
return err
}
f, err := os.CreateTemp(filepath.Dir(filename), filepath.Base(filename)+".tmp")
if err != nil {
return err
}
tmpName := f.Name()
defer func() {
if err != nil {
f.Close()
os.Remove(tmpName)
}
}()
if _, err := f.Write(data); err != nil {
return err
}
if err := f.Sync(); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
return os.Rename(tmpName, filename)
}

// MakeAtomicCopy makes a copy of an existing file given a source and destionation file.
func MakeAtomicCopy(sourcepath, destinationpath string) (err error) {
_, err = os.Stat(sourcepath)
if os.IsNotExist(err) {
log.Debug("configuration file does not exist to create a backup", err)
return err
}
if err != nil {
log.WarnErr(err, "error reading from previous configuration")
return err
}

tmpFile, err := os.CreateTemp(filepath.Dir(destinationpath), "tmp_")
if err != nil {
log.WarnErr(err, "error creating temporary file")
return err
}
tmpName := tmpFile.Name()
defer os.Remove(tmpName)

src, err := os.Open(sourcepath)
if err != nil {
log.WarnErr(err, "error opening current state configuration file")
return err
}
defer src.Close()

_, err = io.Copy(tmpFile, src)
if err != nil {
log.WarnErr(err, "error copying the contents to the temporary file")
return err
}

tmpFile.Close()

err = os.Rename(tmpName, destinationpath)
if err != nil {
log.WarnErr(err, "error renaming file")
return err
}

fmt.Printf("File copied successfully from %s to %s\n", sourcepath, destinationpath)
return nil
}
35 changes: 22 additions & 13 deletions containerm/util/containers_parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
package util

import (
"encoding/base64"
"encoding/json"
"net"
"strconv"
"strings"
Expand Down Expand Up @@ -83,25 +85,32 @@ func ParseMountPoints(mps []string) ([]types.MountPoint, error) {
}

// ParseMountPoint converts a single string representation of a container's mount to a structured MountPoint instance.
// Format: source:destination[:propagation_mode].
// Format:
// "Source":"mount_source",
// "Destination":"mount_destination",
// "Propagation_Mode": "propagation_mode",
// "Data":"configuration_data"
// If the propagation mode parameter is omitted, rprivate will be set by default.
// Available propagation modes are: rprivate, private, rshared, shared, rslave, slave.
func ParseMountPoint(mp string) (*types.MountPoint, error) {
mount := strings.Split(strings.TrimSpace(mp), ":")
if len(mount) < 2 || len(mount) > 3 {
return nil, log.NewErrorf("Incorrect number of parameters of the mount point %s", mp)
var config types.MountPoint
if err := json.Unmarshal([]byte(mp), &config); err != nil {
return nil, log.NewErrorf("error unmarshalling json:", err)
}
mountPoint := &types.MountPoint{
Destination: mount[1],
Source: mount[0],
if config.Source == "" || config.Destination == "" {
return nil, log.NewErrorf("either mount source, %s or mount destination, %s is invalid", config.Source, config.Destination)
}
if len(mount) == 2 {
// if propagation mode is omitted, "rprivate" is set as default
mountPoint.PropagationMode = types.RPrivatePropagationMode
} else {
mountPoint.PropagationMode = mount[2]
if config.PropagationMode == "" {
config.PropagationMode = types.RPrivatePropagationMode
}
configInfo, err := base64.StdEncoding.DecodeString(config.Data)
if err != nil {
log.WarnErr(err, "error decoding bin64 string, ignoring configuration data", config.Data)
config.Data = ""
return &config, nil
}
return mountPoint, nil
config.Data = string(configInfo)
return &config, nil
}

// ParsePortMappings converts string representations of container's port mappings to structured PortMapping instances.
Expand Down