blob: 491c2e34e5f5f08191d4dac4ac4a608930c24d86 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package config handles configuration state passed across instances of the
// device manager.
//
// The State object captures setting that the device manager needs to be aware
// of when it starts. This is passed to the first invocation of the device
// manager, and then passed down from old device manager to new device manager
// upon update. The device manager has an implementation-dependent mechanism
// for parsing and passing state, which is encapsulated by the state sub-package
// (currently, the mechanism uses environment variables). When instantiating a
// new instance of the device manager service, the developer needs to pass in a
// copy of State. They can obtain this by calling Load, which captures any
// config state passed by a previous version of device manager during update.
// Any new version of the device manager must be able to decode a previous
// version's config state, even if the new version changes the mechanism for
// passing this state (that is, device manager implementations must be
// backward-compatible as far as accepting and passing config state goes).
// TODO(caprita): add config state versioning?
package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"v.io/v23/services/application"
"v.io/v23/verror"
"v.io/x/ref"
)
const pkgPath = "v.io/x/ref/services/device/internal/config"
var (
errNeedName = verror.Register(pkgPath+".errNeedName", verror.NoRetry, "{1:}{2:} Name cannot be empty{:_}")
errNeedRoot = verror.Register(pkgPath+".errNeedRoot", verror.NoRetry, "{1:}{2:} Root cannot be empty{:_}")
errNeedCurrentLink = verror.Register(pkgPath+".errNeedCurrentLink", verror.NoRetry, "{1:}{2:} CurrentLink cannot be empty{:_}")
errNeedHelper = verror.Register(pkgPath+".errNeedHelper", verror.NoRetry, "{1:}{2:} Helper must be specified{:_}")
errCantDecodeEnvelope = verror.Register(pkgPath+".errCantDecodeEnvelope", verror.NoRetry, "{1:}{2:} failed to decode envelope from {3}{:_}")
errCantEncodeEnvelope = verror.Register(pkgPath+".errCantEncodeEnvelope", verror.NoRetry, "{1:}{2:} failed to encode envelope {3}{:_}")
errEvalSymlinksFailed = verror.Register(pkgPath+".errEvalSymlinksFailed", verror.NoRetry, "{1:}{2:} EvalSymlinks failed{:_}")
)
// State specifies how the device manager is configured. This should
// encapsulate what the device manager needs to know and/or be able to mutate
// about its environment.
type State struct {
// Name is the device manager's object name. Must be non-empty.
Name string
// Envelope is the device manager's application envelope. If nil, any
// envelope fetched from the application repository will trigger an
// update.
Envelope *application.Envelope
// Previous holds the local path to the previous version of the device
// manager. If empty, revert is disabled.
Previous string
// Root is the directory on the local filesystem that contains
// the applications' workspaces. Must be non-empty.
Root string
// Origin is the application repository object name for the device
// manager application. If empty, update is disabled.
Origin string
// CurrentLink is the local filesystem soft link that should point to
// the version of the device manager binary/script through which device
// manager is started. Device manager is expected to mutate this during
// a self-update. Must be non-empty.
CurrentLink string
// Helper is the path to the setuid helper for running applications as
// specific users.
Helper string
}
// Validate checks the config state.
func (c *State) Validate() error {
if c.Name == "" {
return verror.New(errNeedName, nil)
}
if c.Root == "" {
return verror.New(errNeedRoot, nil)
}
if c.CurrentLink == "" {
return verror.New(errNeedCurrentLink, nil)
}
if c.Helper == "" {
return verror.New(errNeedHelper, nil)
}
return nil
}
// Load reconstructs the config state passed to the device manager (presumably
// by the parent device manager during an update). Currently, this is done via
// environment variables.
func Load() (*State, error) {
var env *application.Envelope
if jsonEnvelope := os.Getenv(EnvelopeEnv); jsonEnvelope != "" {
env = new(application.Envelope)
if err := json.Unmarshal([]byte(jsonEnvelope), env); err != nil {
return nil, verror.New(errCantDecodeEnvelope, nil, jsonEnvelope, err)
}
}
return &State{
Envelope: env,
Previous: os.Getenv(PreviousEnv),
Root: os.Getenv(RootEnv),
Origin: os.Getenv(OriginEnv),
CurrentLink: os.Getenv(CurrentLinkEnv),
Helper: os.Getenv(HelperEnv),
}, nil
}
// Save serializes the config state meant to be passed to a child device manager
// during an update, returning a slice of "key=value" strings, which are
// expected to be stuffed into environment variable settings by the caller.
func (c *State) Save(envelope *application.Envelope) ([]string, error) {
var jsonEnvelope []byte
if envelope != nil {
var err error
if jsonEnvelope, err = json.Marshal(envelope); err != nil {
return nil, verror.New(errCantEncodeEnvelope, nil, envelope, err)
}
}
var currScript string
if _, err := os.Lstat(c.CurrentLink); !os.IsNotExist(err) {
if currScript, err = filepath.EvalSymlinks(c.CurrentLink); err != nil {
return nil, verror.New(errEvalSymlinksFailed, nil, err)
}
}
settings := map[string]string{
EnvelopeEnv: string(jsonEnvelope),
PreviousEnv: currScript,
RootEnv: c.Root,
OriginEnv: c.Origin,
CurrentLinkEnv: c.CurrentLink,
HelperEnv: c.Helper,
}
// We need to manually pass the namespace roots to the child, since we
// currently don't have a way for the child to obtain this information
// from a config service at start-up.
roots, _ := ref.EnvNamespaceRoots()
var ret []string
for k, v := range roots {
ret = append(ret, k+"="+v)
}
for k, v := range settings {
ret = append(ret, k+"="+v)
}
return ret, nil
}
// QuoteEnv wraps environment variable values in double quotes, making them
// suitable for inclusion in a bash script.
func QuoteEnv(env []string) (ret []string) {
for _, e := range env {
if eqIdx := strings.Index(e, "="); eqIdx > 0 {
ret = append(ret, fmt.Sprintf("%s=%q", e[:eqIdx], e[eqIdx+1:]))
} else {
ret = append(ret, e)
}
}
return
}