Merge "vdl: temporary fix of issue 29: Generate a use of wiretype whenever there is an interface to temporarily fix errors on packages with just bootstrap types"
diff --git a/runtimes/google/ipc/stream/crypto/tls.go b/runtimes/google/ipc/stream/crypto/tls.go
index 314f7ed..8dfb09d 100644
--- a/runtimes/google/ipc/stream/crypto/tls.go
+++ b/runtimes/google/ipc/stream/crypto/tls.go
@@ -195,9 +195,11 @@
Certificates: []tls.Certificate{c},
InsecureSkipVerify: true,
ClientAuth: tls.NoClientCert,
- // TLS_ECDHE_RSA_WITH_RC4_128_SHA is 4-5X faster compared to
- // the other cipher suites and is what google.com seems to use.
- CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA},
+ // RC4_128_SHA is 4-5X faster compared to the other cipher suites
+ // and is what google.com seems to use.
+ // Allowing ECDHE_RSA for the key exchange since some older binaries
+ // have an RSA certificate hardcoded in them.
+ CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA},
}
}
@@ -206,45 +208,28 @@
//
// PEM-encoded certificates and keys used in the tests.
// One way to generate them is:
-// go run $GOROOT/src/pkg/crypto/tls/generate_cert.go --host=localhost
+// go run $GOROOT/src/pkg/crypto/tls/generate_cert.go --host=localhost --duration=87600h --ecdsa-curve=P256
+// (This generates a self-signed certificate valid for 10 years)
+// (The --ecdsa-curve flag has not yet been submitted back to the Go repository)
// which will create cert.pem and key.pem files.
const (
serverCert = `
-----BEGIN CERTIFICATE-----
-MIIC1jCCAj+gAwIBAgIJAOsQamnsz2kWMA0GCSqGSIb3DQEBBQUAMIGDMQswCQYD
-VQQGEwJERTEMMAoGA1UECAwDTlJXMQ4wDAYDVQQHDAVFYXJ0aDEXMBUGA1UECgwO
-UmFuZG9tIENvbXBhbnkxCzAJBgNVBAsMAklUMRcwFQYDVQQDDA53d3cucmFuZG9t
-LmNvbTEXMBUGCSqGSIb3DQEJARYIZ2F1dGhhbXQwHhcNMTMwNDIzMjEzMTA4WhcN
-MjMwNDIxMjEzMTA4WjCBgzELMAkGA1UEBhMCREUxDDAKBgNVBAgMA05SVzEOMAwG
-A1UEBwwFRWFydGgxFzAVBgNVBAoMDlJhbmRvbSBDb21wYW55MQswCQYDVQQLDAJJ
-VDEXMBUGA1UEAwwOd3d3LnJhbmRvbS5jb20xFzAVBgkqhkiG9w0BCQEWCGdhdXRo
-YW10MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBaDlmU0csZctqYP8AWJ60
-IYGPmT/gGWGo6p0B6jPy02LuY91jQAn0XkiAdjgdtkkkWQyRtQgaaGsGC6qT5qVX
-Ogx/5l/wb5hOa75gGiOdaGxStkzCjS8hAn4Lr0AbI/JmssUQ0xwNJr6t+aHBJ5Go
-gjG0TsedkLL3qw6ktQd47wIDAQABo1AwTjAdBgNVHQ4EFgQUh166SbXiiSTt+Tud
-rLWaA0sS3bQwHwYDVR0jBBgwFoAUh166SbXiiSTt+TudrLWaA0sS3bQwDAYDVR0T
-BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQB8550EwYMrcFXEwQHktpFrcaOEUWN+
-50NeS0lJ0IHwb31dcMywCX0xsteKyUwkXUCSjE8Ubnktjelo3KPMaur78Jy12pK1
-g3Ay6y3nBDKwpBDPcoy7Pt/pz0yL8Qy54fVnU2iQBiHMjTR/kmDsK+BwRksJfk9V
-MFLsr6ZAZxOPbg==
+MIIBbTCCAROgAwIBAgIQMD+Kzawjvhij1B/BmvHxLDAKBggqhkjOPQQDAjASMRAw
+DgYDVQQKEwdBY21lIENvMB4XDTE0MDcxODIzMTYxMloXDTI0MDcxNTIzMTYxMlow
+EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLiz
+Ajsly1DS8NJF2KE195V83TgidfgGEB7nudscdKWH3+5uQHgCc+2BV/7AGGj3yePR
+ZZLzYD95goJ/a7eet/2jSzBJMA4GA1UdDwEB/wQEAwIAoDATBgNVHSUEDDAKBggr
+BgEFBQcDATAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAKBggq
+hkjOPQQDAgNIADBFAiAb4tBxggEpnKdxv66TBVFxAUn3EBWX25XlL1G2GF8RkAIh
+AOAwys3mvzM4Td/2kV9QNyQPZ9kLLQr9A9ryB0H3N9Yz
-----END CERTIFICATE-----
`
serverKey = `
------BEGIN PRIVATE KEY-----
-MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMFoOWZTRyxly2pg
-/wBYnrQhgY+ZP+AZYajqnQHqM/LTYu5j3WNACfReSIB2OB22SSRZDJG1CBpoawYL
-qpPmpVc6DH/mX/BvmE5rvmAaI51obFK2TMKNLyECfguvQBsj8mayxRDTHA0mvq35
-ocEnkaiCMbROx52QsverDqS1B3jvAgMBAAECgYEAl2Xk+Orb3i9ZSs7fDwBQS6Wm
-7CgEzoJP5pCxk1woij9bRE28cgMhR7++dYEVcHzPSLrEkhLqYvG2RadAQkLczcy+
-NgXFm1I0HcZXbVT2rafaKS27GpT7NicIrIw48goncMwAI0+UB3Ply9RDwfs+VhDo
-G2a8JTVx2FNpJoIIJOECQQDl80AJPi17TbJehEByQOF0Q7KgfN4aD9hx+E6SLdPq
-ddn0xqnmsbBD1EPv25qeAtQ6sHRxjlP03gvhQ4CQQ0+nAkEA11ExtkqGXayf2hAe
-dMwi2JrAuIGtOCQHQOCAADYgIH+3/SIf05kk/PUiXFTlGkm69qpBmLPaiDSfHV6g
-taT1eQJAe9KClveOUilCdTbN5TgerxaNJ3JVvr7tlGFbHcfjpwsS9IXNk1X3Tm8M
-rioYliF72qaN7V/wwZiX2RMaNZSpXQJAXmuBlEG8CGoBsztsT6WRBlFef8qF7l+G
-OsH3/5+8mOPJCB0lvcGjgbXxenHUAaIhdbeVimQcSaxhthxf9ye+aQJAMstlAS7X
-4rJXYVJUL5JQISgz/D5BzM5pbgJivVRcHO2Qk3HZO2F95Sg3lpD1tdOWBtOhOyRS
-AS91NC8w9ruJeg==
------END PRIVATE KEY-----
+-----BEGIN ECDSA PRIVATE KEY-----
+MHcCAQEEIPLfwg+SVC2/xUcKq0bI9y2+SDEEdCeGuxuBz22BhAw1oAoGCCqGSM49
+AwEHoUQDQgAEuLMCOyXLUNLw0kXYoTX3lXzdOCJ1+AYQHue52xx0pYff7m5AeAJz
+7YFX/sAYaPfJ49FlkvNgP3mCgn9rt563/Q==
+-----END ECDSA PRIVATE KEY-----
`
)
diff --git a/services/mgmt/node/config/config.go b/services/mgmt/node/config/config.go
new file mode 100644
index 0000000..dc1fb78
--- /dev/null
+++ b/services/mgmt/node/config/config.go
@@ -0,0 +1,139 @@
+// Package config handles configuration state passed across instances of the
+// node manager.
+//
+// The State object captures setting that the node manager needs to be aware of
+// when it starts. This is passed to the first invocation of the node manager,
+// and then passed down from old node manager to new node manager upon update.
+// The node 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 node 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 node manager during update. Any new version of the
+// node 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,
+// node 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"
+
+ "veyron2/services/mgmt/application"
+)
+
+// State specifies how the node manager is configured. This should encapsulate
+// what the node manager needs to know and/or be able to mutate about its
+// environment.
+type State struct {
+ // Name is the node manager's object name. Must be non-empty.
+ Name string
+ // Envelope is the node 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 node
+ // 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 node
+ // manager application. If empty, update is disabled.
+ Origin string
+ // CurrentLink is the local filesystem soft link that should point to
+ // the version of the node manager binary/script through which node
+ // manager is started. Node manager is expected to mutate this during
+ // a self-update. Must be non-empty.
+ CurrentLink string
+}
+
+// Validate checks the config state.
+func (c *State) Validate() error {
+ if c.Name == "" {
+ return fmt.Errorf("Name cannot be empty")
+ }
+ if c.Root == "" {
+ return fmt.Errorf("Root cannot be empty")
+ }
+ if c.CurrentLink == "" {
+ return fmt.Errorf("CurrentLink cannot be empty")
+ }
+ return nil
+}
+
+// Load reconstructs the config state passed to the node manager (presumably by
+// the parent node 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, fmt.Errorf("failed to decode envelope from %v: %v", jsonEnvelope, err)
+ }
+ }
+ return &State{
+ Envelope: env,
+ Previous: os.Getenv(PreviousEnv),
+ Root: os.Getenv(RootEnv),
+ Origin: os.Getenv(OriginEnv),
+ CurrentLink: os.Getenv(CurrentLinkEnv),
+ }, nil
+}
+
+// Save serializes the config state meant to be passed to a child node 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) {
+ jsonEnvelope, err := json.Marshal(envelope)
+ if err != nil {
+ return nil, fmt.Errorf("failed to encode envelope %v: %v", envelope, err)
+ }
+ currScript, err := filepath.EvalSymlinks(c.CurrentLink)
+ if err != nil {
+ return nil, fmt.Errorf("EvalSymlink failed: %v", err)
+ }
+ settings := map[string]string{
+ EnvelopeEnv: string(jsonEnvelope),
+ PreviousEnv: currScript,
+ RootEnv: c.Root,
+ OriginEnv: c.Origin,
+ CurrentLinkEnv: c.CurrentLink,
+ }
+ // 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.
+ for _, ev := range os.Environ() {
+ p := strings.SplitN(ev, "=", 2)
+ if len(p) != 2 {
+ continue
+ }
+ k, v := p[0], p[1]
+ if strings.HasPrefix(k, "NAMESPACE_ROOT") {
+ settings[k] = v
+ }
+ }
+ var ret []string
+ 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
+}
diff --git a/services/mgmt/node/config/config_test.go b/services/mgmt/node/config/config_test.go
new file mode 100644
index 0000000..44264b3
--- /dev/null
+++ b/services/mgmt/node/config/config_test.go
@@ -0,0 +1,112 @@
+package config_test
+
+import (
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+
+ "veyron/services/mgmt/node/config"
+
+ "veyron2/services/mgmt/application"
+)
+
+// TestState checks that encoding/decoding State to child/from parent works
+// as expected.
+func TestState(t *testing.T) {
+ currScript := filepath.Join(os.TempDir(), "fido/was/here")
+ if err := os.MkdirAll(currScript, 0700); err != nil {
+ t.Fatalf("MkdirAll failed: %v", err)
+ }
+ defer os.RemoveAll(currScript)
+ currLink := filepath.Join(os.TempDir(), "familydog")
+ if err := os.Symlink(currScript, currLink); err != nil {
+ t.Fatalf("Symlink failed: %v", err)
+ }
+ defer os.Remove(currLink)
+ state := &config.State{
+ Name: "fido",
+ Previous: "doesn't matter",
+ Root: "fidos/doghouse",
+ Origin: "pet/store",
+ CurrentLink: currLink,
+ }
+ if err := state.Validate(); err != nil {
+ t.Errorf("Config state %v failed to validate: %v", state, err)
+ }
+ encoded, err := state.Save(&application.Envelope{
+ Title: "dog",
+ Args: []string{"roll-over", "play-dead"},
+ })
+ if err != nil {
+ t.Errorf("Config state %v Save failed: %v", state, err)
+ }
+ for _, e := range encoded {
+ pair := strings.SplitN(e, "=", 2)
+ os.Setenv(pair[0], pair[1])
+ }
+ decodedState, err := config.Load()
+ if err != nil {
+ t.Errorf("Config state Load failed: %v", err)
+ }
+ expectedState := state
+ expectedState.Envelope = &application.Envelope{
+ Title: "dog",
+ Args: []string{"roll-over", "play-dead"},
+ }
+ expectedState.Name = ""
+ expectedState.Previous = currScript
+ if !reflect.DeepEqual(decodedState, expectedState) {
+ t.Errorf("Decode state: want %v, got %v", expectedState, decodedState)
+ }
+}
+
+// TestValidate checks the Validate method of State.
+func TestValidate(t *testing.T) {
+ state := &config.State{
+ Name: "schweinsteiger",
+ Previous: "a",
+ Root: "b",
+ Origin: "c",
+ CurrentLink: "d",
+ }
+ if err := state.Validate(); err != nil {
+ t.Errorf("Config state %v failed to validate: %v", state, err)
+ }
+ state.Root = ""
+ if err := state.Validate(); err == nil {
+ t.Errorf("Config state %v should have failed to validate.", state)
+ }
+ state.Root, state.CurrentLink = "a", ""
+ if err := state.Validate(); err == nil {
+ t.Errorf("Confi stateg %v should have failed to validate.", state)
+ }
+ state.CurrentLink, state.Name = "d", ""
+ if err := state.Validate(); err == nil {
+ t.Errorf("Config state %v should have failed to validate.", state)
+ }
+}
+
+// TestQuoteEnv checks the QuoteEnv method.
+func TestQuoteEnv(t *testing.T) {
+ cases := []struct {
+ before, after string
+ }{
+ {`a=b`, `a="b"`},
+ {`a=`, `a=""`},
+ {`a`, `a`},
+ {`a=x y`, `a="x y"`},
+ {`a="x y"`, `a="\"x y\""`},
+ {`a='x y'`, `a="'x y'"`},
+ }
+ var input []string
+ var want []string
+ for _, c := range cases {
+ input = append(input, c.before)
+ want = append(want, c.after)
+ }
+ if got := config.QuoteEnv(input); !reflect.DeepEqual(want, got) {
+ t.Errorf("QuoteEnv(%v) wanted %v, got %v instead", input, want, got)
+ }
+}
diff --git a/services/mgmt/node/config/const.go b/services/mgmt/node/config/const.go
new file mode 100644
index 0000000..c57cd98
--- /dev/null
+++ b/services/mgmt/node/config/const.go
@@ -0,0 +1,21 @@
+package config
+
+const (
+ // EnvelopeEnv is the name of the environment variable that holds the
+ // serialized node manager application envelope.
+ EnvelopeEnv = "VEYRON_NM_ENVELOPE"
+ // PreviousEnv is the name of the environment variable that holds the
+ // path to the previous version of the node manager.
+ PreviousEnv = "VEYRON_NM_PREVIOUS"
+ // OriginEnv is the name of the environment variable that holds the
+ // object name of the application repository that can be used to
+ // retrieve the node manager application envelope.
+ OriginEnv = "VEYRON_NM_ORIGIN"
+ // RootEnv is the name of the environment variable that holds the
+ // path to the directory in which node manager workspaces are
+ // created.
+ RootEnv = "VEYRON_NM_ROOT"
+ // CurrentLinkEnv is the name of the environment variable that holds
+ // the path to the soft link that points to the current node manager.
+ CurrentLinkEnv = "VEYRON_NM_CURRENT"
+)
diff --git a/services/mgmt/node/doc.go b/services/mgmt/node/doc.go
new file mode 100644
index 0000000..2f98bea
--- /dev/null
+++ b/services/mgmt/node/doc.go
@@ -0,0 +1,34 @@
+// Package node contains the implementation for the veyron2/mgmt/node APIs.
+//
+// The node manager is a server that is expected to run on every Veyron-enabled
+// node, and it handles both node management and management of the applications
+// running on the node.
+//
+// The node manager is responsible for installing, updating, and launching
+// applications. It therefore sets up a footprint on the local filesystem, both
+// to maintain its internal state, and to provide applications with their own
+// private workspaces.
+//
+// The node manager is responsible for updating itself. The mechanism to do so
+// is implementation-dependent, though each node manager expects to be supplied
+// with the file path of a symbolic link file, which the node manager will then
+// update to point to the updated version of itself before terminating itself.
+// The node manager should therefore be launched via this symbolic link to
+// enable auto-updates. To enable updates, in addition to the symbolic link
+// path, the node manager needs to be told what its application metadata is
+// (such as command-line arguments and environment variables, i.e. the
+// application envelope defined in the veyron2/services/mgmt/application
+// package), as well as the object name for where it can fetch an updated
+// envelope, and the local filesystem path for its previous version (for
+// rollbacks).
+//
+// Finally, the node manager needs to know its own object name, so it can pass
+// that along to the applications that it starts.
+//
+// The impl subpackage contains the implementation of the node manager service.
+//
+// The config subpackage encapsulates the configuration settings that form the
+// node manager service's 'contract' with its environment.
+//
+// The noded subpackage contains the main driver.
+package node
diff --git a/services/mgmt/node/impl/callback.go b/services/mgmt/node/impl/callback.go
new file mode 100644
index 0000000..787425d
--- /dev/null
+++ b/services/mgmt/node/impl/callback.go
@@ -0,0 +1,34 @@
+package impl
+
+import (
+ "veyron/services/mgmt/lib/exec"
+ inode "veyron/services/mgmt/node"
+
+ "veyron2/mgmt"
+ "veyron2/rt"
+ "veyron2/vlog"
+)
+
+// InvokeCallback provides the parent node manager with the given name (which is
+// expected to be this node manager's object name).
+func InvokeCallback(name string) {
+ handle, err := exec.GetChildHandle()
+ switch err {
+ case nil:
+ // Node manager was started by self-update, notify the parent.
+ callbackName, err := handle.Config.Get(mgmt.ParentNodeManagerConfigKey)
+ if err != nil {
+ vlog.Fatalf("Failed to get callback name from config: %v", err)
+ }
+ nmClient, err := inode.BindNode(callbackName)
+ if err != nil {
+ vlog.Fatalf("BindNode(%v) failed: %v", callbackName, err)
+ }
+ if err := nmClient.Set(rt.R().NewContext(), mgmt.ChildNodeManagerConfigKey, name); err != nil {
+ vlog.Fatalf("Set(%v, %v) failed: %v", mgmt.ChildNodeManagerConfigKey, name, err)
+ }
+ case exec.ErrNoVersion:
+ default:
+ vlog.Fatalf("GetChildHandle() failed: %v", err)
+ }
+}
diff --git a/services/mgmt/node/impl/const.go b/services/mgmt/node/impl/const.go
deleted file mode 100644
index c3c3d7b..0000000
--- a/services/mgmt/node/impl/const.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package impl
-
-const (
- // BinaryEnv is the name of the environment variable that holds the
- // object name used for obtaining the node manager binary.
- BinaryEnv = "VEYRON_NM_BINARY"
- // PreviousEnv is the name of the environment variable that holds
- // the path to the workspace that contains the previous version of
- // the node manager.
- PreviousEnv = "VEYRON_NM_PREVIOUS"
- // OriginEnv is the name of the environment variable that holds the
- // object name of the application repository that can be used to
- // retrieve the node manager application envelope.
- OriginEnv = "VEYRON_NM_ORIGIN"
- // RootEnv is the name of the environment variable that holds the
- // path to the directory in which node manager workspaces are
- // created.
- RootEnv = "VEYRON_NM_ROOT"
-)
diff --git a/services/mgmt/node/impl/dispatcher.go b/services/mgmt/node/impl/dispatcher.go
index 19147f6..33abf91 100644
--- a/services/mgmt/node/impl/dispatcher.go
+++ b/services/mgmt/node/impl/dispatcher.go
@@ -1,39 +1,47 @@
package impl
import (
+ "fmt"
"sync"
"veyron/services/mgmt/node"
+ "veyron/services/mgmt/node/config"
"veyron2/ipc"
"veyron2/security"
- "veyron2/services/mgmt/application"
)
// dispatcher holds the state of the node manager dispatcher.
type dispatcher struct {
- auth security.Authorizer
- state *state
+ auth security.Authorizer
+ internal *internalState
+ config *config.State
}
// NewDispatcher is the node manager dispatcher factory.
-func NewDispatcher(auth security.Authorizer, envelope *application.Envelope, name, previous string) *dispatcher {
+func NewDispatcher(auth security.Authorizer, config *config.State) (*dispatcher, error) {
+ if err := config.Validate(); err != nil {
+ return nil, fmt.Errorf("Invalid config %v: %v", config, err)
+ }
+
return &dispatcher{
auth: auth,
- state: &state{
+ internal: &internalState{
channels: make(map[string]chan string),
channelsMutex: new(sync.Mutex),
- envelope: envelope,
- name: name,
- previous: previous,
updating: false,
updatingMutex: new(sync.Mutex),
},
- }
+ config: config,
+ }, nil
}
// DISPATCHER INTERFACE IMPLEMENTATION
func (d *dispatcher) Lookup(suffix string) (ipc.Invoker, security.Authorizer, error) {
- return ipc.ReflectInvoker(node.NewServerNode(NewInvoker(d.state, suffix))), d.auth, nil
+ return ipc.ReflectInvoker(node.NewServerNode(&invoker{
+ internal: d.internal,
+ config: d.config,
+ suffix: suffix,
+ })), d.auth, nil
}
diff --git a/services/mgmt/node/impl/impl_test.go b/services/mgmt/node/impl/impl_test.go
index adb226c..2146596 100644
--- a/services/mgmt/node/impl/impl_test.go
+++ b/services/mgmt/node/impl/impl_test.go
@@ -1,451 +1,341 @@
package impl_test
import (
- "crypto/md5"
- "encoding/hex"
- "errors"
"fmt"
- "io"
"io/ioutil"
"os"
+ goexec "os/exec"
"path/filepath"
"strings"
- "syscall"
"testing"
- "time"
"veyron/lib/signals"
- _ "veyron/lib/testutil"
"veyron/lib/testutil/blackbox"
- "veyron/lib/testutil/security"
"veyron/services/mgmt/lib/exec"
- "veyron/services/mgmt/node"
+ "veyron/services/mgmt/node/config"
"veyron/services/mgmt/node/impl"
- mtlib "veyron/services/mounttable/lib"
- "veyron2"
- "veyron2/ipc"
- "veyron2/mgmt"
"veyron2/naming"
"veyron2/rt"
"veyron2/services/mgmt/application"
- "veyron2/services/mgmt/binary"
- "veyron2/services/mgmt/repository"
"veyron2/verror"
"veyron2/vlog"
)
-const (
- testEnv = "VEYRON_NM_TEST"
-)
-
-var (
- errOperationFailed = errors.New("operation failed")
-)
-
-func init() {
- blackbox.CommandTable["nodeManager"] = nodeManager
-}
-
-// arInvoker holds the state of an application repository invocation
-// mock. On its first invocation of Match, this mock will return a bogus
-// application title. On the second invocation, it will return the correct
-// node manager app title. We make use of this behavior to test that Update
-// fails when the app title mismatches.
-type arInvoker struct {
- firstInvocation bool
-}
-
-// APPLICATION REPOSITORY INTERFACE IMPLEMENTATION
-
-func (i *arInvoker) Match(ipc.ServerContext, []string) (application.Envelope, error) {
- vlog.VI(0).Infof("Match(), first invocation: %t", i.firstInvocation)
- envelope := generateEnvelope()
- if i.firstInvocation {
- i.firstInvocation = false
- envelope.Title = "gibberish"
- } else {
- envelope.Title = application.NodeManagerTitle
- }
- envelope.Env = exec.Setenv(envelope.Env, testEnv, "child")
- envelope.Binary = "cr"
- return *envelope, nil
-}
-
-// crInvoker holds the state of a binary repository invocation mock.
-type crInvoker struct{}
-
-// BINARY REPOSITORY INTERFACE IMPLEMENTATION
-
-func (*crInvoker) Create(ipc.ServerContext, int32) error {
- vlog.VI(0).Infof("Create()")
- return nil
-}
-
-func (i *crInvoker) Delete(ipc.ServerContext) error {
- vlog.VI(0).Infof("Delete()")
- return nil
-}
-
-func (i *crInvoker) Download(_ ipc.ServerContext, _ int32, stream repository.BinaryServiceDownloadStream) error {
- vlog.VI(0).Infof("Download()")
- file, err := os.Open(os.Args[0])
- if err != nil {
- vlog.Errorf("Open() failed: %v", err)
- return errOperationFailed
- }
- defer file.Close()
- bufferLength := 4096
- buffer := make([]byte, bufferLength)
- for {
- n, err := file.Read(buffer)
- switch err {
- case io.EOF:
- return nil
- case nil:
- if err := stream.Send(buffer[:n]); err != nil {
- vlog.Errorf("Send() failed: %v", err)
- return errOperationFailed
- }
- default:
- vlog.Errorf("Read() failed: %v", err)
- return errOperationFailed
- }
- }
-}
-
-func (*crInvoker) DownloadURL(ipc.ServerContext) (string, int64, error) {
- vlog.VI(0).Infof("DownloadURL()")
- return "", 0, nil
-}
-
-func (*crInvoker) Stat(ipc.ServerContext) ([]binary.PartInfo, error) {
- vlog.VI(0).Infof("Stat()")
- h := md5.New()
- bytes, err := ioutil.ReadFile(os.Args[0])
- if err != nil {
- return []binary.PartInfo{}, errOperationFailed
- }
- h.Write(bytes)
- part := binary.PartInfo{Checksum: hex.EncodeToString(h.Sum(nil)), Size: int64(len(bytes))}
- return []binary.PartInfo{part}, nil
-}
-
-func (i *crInvoker) Upload(ipc.ServerContext, int32, repository.BinaryServiceUploadStream) error {
- vlog.VI(0).Infof("Upload()")
- return nil
-}
-
-func generateBinary(workspace string) string {
- path := filepath.Join(workspace, "noded")
- if err := os.Link(os.Args[0], path); err != nil {
- vlog.Fatalf("Link(%v, %v) failed: %v", os.Args[0], path, err)
- }
- return path
-}
-
-func generateEnvelope() *application.Envelope {
- envelope := &application.Envelope{}
- envelope.Args = os.Args[1:]
- for _, env := range os.Environ() {
- i := strings.Index(env, "=")
- envelope.Env = append(envelope.Env, fmt.Sprintf("%s=%q", env[:i], env[i+1:]))
- }
- return envelope
-}
-
-func generateLink(root, workspace string) {
- link := filepath.Join(root, impl.CurrentWorkspace)
- newLink := link + ".new"
- fi, err := os.Lstat(newLink)
- if err == nil {
- if err := os.Remove(fi.Name()); err != nil {
- vlog.Fatalf("Remove(%v) failed: %v", fi.Name(), err)
- }
- }
- if err := os.Symlink(workspace, newLink); err != nil {
- vlog.Fatalf("Symlink(%v, %v) failed: %v", workspace, newLink, err)
- }
- if err := os.Rename(newLink, link); err != nil {
- vlog.Fatalf("Rename(%v, %v) failed: %v", newLink, link, err)
- }
-}
-
-func generateScript(workspace, binary string) string {
- envelope := generateEnvelope()
- envelope.Env = exec.Setenv(envelope.Env, testEnv, "parent")
- output := "#!/bin/bash\n"
- output += strings.Join(envelope.Env, " ") + " "
- output += binary + " " + strings.Join(envelope.Args, " ")
- path := filepath.Join(workspace, "noded.sh")
- if err := ioutil.WriteFile(path, []byte(output), 0755); err != nil {
- vlog.Fatalf("WriteFile(%v) failed: %v", path, err)
- }
- return path
-}
-
-func invokeCallback(name string) {
- handle, err := exec.GetChildHandle()
- switch err {
- case nil:
- // Node manager was started by self-update, notify the parent
- // process that you are ready.
- handle.SetReady()
- callbackName, err := handle.Config.Get(mgmt.ParentNodeManagerConfigKey)
- if err != nil {
- vlog.Fatalf("Failed to get callback name from config: %v", err)
- }
- nmClient, err := node.BindNode(callbackName)
- if err != nil {
- vlog.Fatalf("BindNode(%v) failed: %v", callbackName, err)
- }
- if err := nmClient.Set(rt.R().NewContext(), mgmt.ChildNodeManagerConfigKey, name); err != nil {
- vlog.Fatalf("Set(%v, %v) failed: %v", mgmt.ChildNodeManagerConfigKey, name, err)
- }
- case exec.ErrNoVersion:
- vlog.Fatalf("invokeCallback should only be called from child node manager")
- default:
- vlog.Fatalf("NewChildHandle() failed: %v", err)
- }
-}
-
-func invokeUpdate(t *testing.T, name string, expectFail bool) {
- address := naming.JoinAddressName(name, "nm")
- stub, err := node.BindNode(address)
- if err != nil {
- t.Fatalf("BindNode(%v) failed: %v", address, err)
- }
- err = stub.Update(rt.R().NewContext())
- if expectFail {
- if !verror.Is(err, verror.BadArg) {
- t.Fatalf("Unexpected update error: %v", err)
- }
- } else if err != nil {
- t.Fatalf("Update() failed: %v", err)
- }
-}
-
-// nodeManager is an enclosure for setting up and starting the parent
-// and child node manager used by the TestUpdate() method.
-func nodeManager(argv []string) {
- root := os.Getenv(impl.RootEnv)
- switch os.Getenv(testEnv) {
- case "setup":
- workspace := filepath.Join(root, fmt.Sprintf("%v", time.Now().Format(time.RFC3339Nano)))
- perm := os.FileMode(0755)
- if err := os.MkdirAll(workspace, perm); err != nil {
- vlog.Fatalf("MkdirAll(%v, %v) failed: %v", workspace, perm, err)
- }
- binary := generateBinary(workspace)
- script := generateScript(workspace, binary)
- generateLink(root, workspace)
- argv, envv := []string{}, []string{}
- if err := syscall.Exec(script, argv, envv); err != nil {
- vlog.Fatalf("Exec(%v, %v, %v) failed: %v", script, argv, envv, err)
- }
- case "parent":
- runtime := rt.Init()
- defer runtime.Cleanup()
- // Set up a mock binary repository, a mock application repository, and a node manager.
- _, crCleanup := startBinaryRepository()
- defer crCleanup()
- _, arCleanup := startApplicationRepository()
- defer arCleanup()
- _, nmCleanup := startNodeManager()
- defer nmCleanup()
- // Wait until shutdown.
- <-signals.ShutdownOnSignals()
- blackbox.WaitForEOFOnStdin()
- case "child":
- runtime := rt.Init()
- defer runtime.Cleanup()
- // Set up a node manager.
- name, nmCleanup := startNodeManager()
- defer nmCleanup()
- invokeCallback(name)
- // Wait until shutdown.
- <-signals.ShutdownOnSignals()
- blackbox.WaitForEOFOnStdin()
- }
-}
-
-func spawnNodeManager(t *testing.T, mtName string, idFile string) *blackbox.Child {
- root := filepath.Join(os.TempDir(), "noded")
- child := blackbox.HelperCommand(t, "nodeManager")
- child.Cmd.Env = exec.Setenv(child.Cmd.Env, "NAMESPACE_ROOT", mtName)
- child.Cmd.Env = exec.Setenv(child.Cmd.Env, "VEYRON_IDENTITY", idFile)
- child.Cmd.Env = exec.Setenv(child.Cmd.Env, impl.OriginEnv, "ar")
- child.Cmd.Env = exec.Setenv(child.Cmd.Env, impl.RootEnv, root)
- child.Cmd.Env = exec.Setenv(child.Cmd.Env, testEnv, "setup")
- if err := child.Cmd.Start(); err != nil {
- t.Fatalf("Start() failed: %v", err)
- }
- return child
-}
-
-func startApplicationRepository() (string, func()) {
- server, err := rt.R().NewServer()
- if err != nil {
- vlog.Fatalf("NewServer() failed: %v", err)
- }
- dispatcher := ipc.SoloDispatcher(repository.NewServerApplication(&arInvoker{firstInvocation: true}), nil)
- protocol, hostname := "tcp", "localhost:0"
- endpoint, err := server.Listen(protocol, hostname)
- if err != nil {
- vlog.Fatalf("Listen(%v, %v) failed: %v", protocol, hostname, err)
- }
- vlog.VI(1).Infof("Application repository running at endpoint: %s", endpoint)
- name := "ar"
- if err := server.Serve(name, dispatcher); err != nil {
- vlog.Fatalf("Serve(%v) failed: %v", name, err)
- }
- return name, func() {
- if err := server.Stop(); err != nil {
- vlog.Fatalf("Stop() failed: %v", err)
- }
- }
-}
-
-func startBinaryRepository() (string, func()) {
- server, err := rt.R().NewServer()
- if err != nil {
- vlog.Fatalf("NewServer() failed: %v", err)
- }
- dispatcher := ipc.SoloDispatcher(repository.NewServerBinary(&crInvoker{}), nil)
- protocol, hostname := "tcp", "localhost:0"
- endpoint, err := server.Listen(protocol, hostname)
- if err != nil {
- vlog.Fatalf("Listen(%v, %v) failed: %v", protocol, hostname, err)
- }
- vlog.VI(1).Infof("Binary repository running at endpoint: %s", endpoint)
- name := "cr"
- if err := server.Serve(name, dispatcher); err != nil {
- vlog.Fatalf("Serve(%v) failed: %v", name, err)
- }
- return name, func() {
- if err := server.Stop(); err != nil {
- vlog.Fatalf("Stop() failed: %v", err)
- }
- }
-}
-
-func startMountTable(t *testing.T) (string, func()) {
- server, err := rt.R().NewServer(veyron2.ServesMountTableOpt(true))
- if err != nil {
- t.Fatalf("NewServer() failed: %v", err)
- }
- dispatcher, err := mtlib.NewMountTable("")
- if err != nil {
- t.Fatalf("NewMountTable() failed: %v", err)
- }
- protocol, hostname := "tcp", "localhost:0"
- endpoint, err := server.Listen(protocol, hostname)
- if err != nil {
- t.Fatalf("Listen(%v, %v) failed: %v", protocol, hostname, err)
- }
- if err := server.Serve("", dispatcher); err != nil {
- t.Fatalf("Serve(%v) failed: %v", dispatcher, err)
- }
- name := naming.JoinAddressName(endpoint.String(), "")
- vlog.VI(1).Infof("Mount table name: %v", name)
- return name, func() {
- if err := server.Stop(); err != nil {
- t.Fatalf("Stop() failed: %v", err)
- }
- }
-}
-
-func startNodeManager() (string, func()) {
- server, err := rt.R().NewServer()
- if err != nil {
- vlog.Fatalf("NewServer() failed: %v", err)
- }
- protocol, hostname := "tcp", "localhost:0"
- endpoint, err := server.Listen(protocol, hostname)
- if err != nil {
- vlog.Fatalf("Listen(%v, %v) failed: %v", protocol, hostname, err)
- }
- envelope := &application.Envelope{}
- name := naming.MakeTerminal(naming.JoinAddressName(endpoint.String(), ""))
- vlog.VI(0).Infof("Node manager name: %v", name)
- // TODO(jsimsa): Replace <PreviousEnv> with a command-line flag when
- // command-line flags in tests are supported.
- dispatcher := impl.NewDispatcher(nil, envelope, name, os.Getenv(impl.PreviousEnv))
- publishAs := "nm"
- if err := server.Serve(publishAs, dispatcher); err != nil {
- vlog.Fatalf("Serve(%v) failed: %v", publishAs, err)
- }
- fmt.Printf("ready\n")
- return name, func() {
- if err := server.Stop(); err != nil {
- vlog.Fatalf("Stop() failed: %v", err)
- }
- }
-}
-
+// TestHelperProcess is blackbox boilerplate.
func TestHelperProcess(t *testing.T) {
blackbox.HelperProcess(t)
}
-// TestUpdate checks that the node manager Update() method works as
-// expected. To that end, this test spawns a new process that invokes
-// the nodeManager() method. The behavior of this method depends on
-// the value of the VEYRON_NM_TEST environment variable:
-//
-// 1) Initially, the value of VEYRON_NM_TEST is set to be "setup",
-// which prompts the nodeManager() method to setup a new workspace
-// that mimics the structure used for storing the node manager
-// binary. The method then sets VEYRON_NM_TEST to "parent" and
-// restarts itself using syscall.Exec(), effectively becoming the
-// process described next.
-//
-// 2) The "parent" branch sets up a mock application and binary
-// repository and a node manager that is pointed to the mock
-// application repository for updates. When all three services start,
-// the TestUpdate() method is notified and it proceeds to invoke
-// Update() on the node manager. This in turn results in the node
-// manager downloading an application envelope from the mock
-// application repository and a binary from the mock binary
-// repository. These are identical to the application envelope of the
-// "parent" node manager, except for the VEYRON_NM_TEST variable,
-// which is set to "child". The Update() method then spawns the child
-// node manager, checks that it is a valid node manager, and
-// terminates.
-//
-// 3) The "child" branch sets up a node manager and then calls back to
-// the "parent" node manager. This prompts the parent node manager to
-// invoke the Revert() method on the child node manager, which
-// terminates the child.
-func TestUpdate(t *testing.T) {
- // Set up a mount table.
- runtime := rt.Init()
- mtName, mtCleanup := startMountTable(t)
- defer mtCleanup()
- ns := runtime.Namespace()
- // The local, client-side Namespace is now relative to the
- // MountTable server started above.
- ns.SetRoots(mtName)
- // Spawn a node manager with an identity blessed by the MountTable's
- // identity under the name "test", and obtain its address.
- //
- // TODO(ataly): Eventually we want to use the same identity the node
- // manager would have if it was running in production.
- idFile := security.SaveIdentityToFile(security.NewBlessedIdentity(runtime.Identity(), "test"))
- defer os.Remove(idFile)
- child := spawnNodeManager(t, mtName, idFile)
- defer child.Cleanup()
- child.Expect("ready")
- ctx, name := runtime.NewContext(), naming.Join(mtName, "nm")
- results, err := ns.Resolve(ctx, name)
+func init() {
+ // All the tests and the subprocesses they start require a runtime; so just
+ // create it here.
+ rt.Init()
+
+ blackbox.CommandTable["nodeManager"] = nodeManager
+ blackbox.CommandTable["execScript"] = execScript
+}
+
+// execScript launches the script passed as argument.
+func execScript(args []string) {
+ if want, got := 1, len(args); want != got {
+ vlog.Fatalf("execScript expected %d arguments, got %d instead", want, got)
+ }
+ script := args[0]
+ env := []string{}
+ if os.Getenv("PAUSE_BEFORE_STOP") == "1" {
+ env = append(env, "PAUSE_BEFORE_STOP=1")
+ }
+ cmd := goexec.Cmd{
+ Path: script,
+ Env: env,
+ Stderr: os.Stderr,
+ Stdout: os.Stdout,
+ }
+ go func() {
+ stdin, err := cmd.StdinPipe()
+ if err != nil {
+ vlog.Fatalf("Failed to get stdin pipe: %v", err)
+ }
+ blackbox.WaitForEOFOnStdin()
+ stdin.Close()
+ }()
+ if err := cmd.Run(); err != nil {
+ vlog.Fatalf("Run cmd %v failed: %v", cmd, err)
+ }
+}
+
+// nodeManager sets up a node manager server. It accepts the name to publish
+// the server under as an argument. Additional arguments can optionally specify
+// node manager config settings.
+func nodeManager(args []string) {
+ if len(args) == 0 {
+ vlog.Fatalf("nodeManager expected at least an argument")
+ }
+ publishName := args[0]
+
+ defer fmt.Printf("%v terminating\n", publishName)
+ defer rt.R().Cleanup()
+ server, endpoint := newServer()
+ defer server.Stop()
+ name := naming.MakeTerminal(naming.JoinAddressName(endpoint, ""))
+ vlog.VI(1).Infof("Node manager name: %v", name)
+
+ // Satisfy the contract described in doc.go by passing the config state
+ // through to the node manager dispatcher constructor.
+ configState, err := config.Load()
if err != nil {
- t.Fatalf("Resolve(%v) failed: %v", name, err)
+ vlog.Fatalf("Failed to decode config state: %v", err)
}
- if expected, got := 1, len(results); expected != got {
- t.Fatalf("Unexpected number of results: expected %d, got %d", expected, got)
+ configState.Name = name
+
+ // This exemplifies how to override or set specific config fields, if,
+ // for example, the node manager is invoked 'by hand' instead of via a
+ // script prepared by a previous version of the node manager.
+ if len(args) > 1 {
+ if want, got := 3, len(args)-1; want != got {
+ vlog.Fatalf("expected %d additional arguments, got %d instead", want, got)
+ }
+ configState.Root, configState.Origin, configState.CurrentLink = args[1], args[2], args[3]
}
- // First invocation will cause app repository mock to return a bogus
- // app title and hence the update should fail.
- invokeUpdate(t, name, true)
- invokeUpdate(t, name, false)
- child.Expect("ready")
+
+ dispatcher, err := impl.NewDispatcher(nil, configState)
+ if err != nil {
+ vlog.Fatalf("Failed to create node manager dispatcher: %v", err)
+ }
+ if err := server.Serve(publishName, dispatcher); err != nil {
+ vlog.Fatalf("Serve(%v) failed: %v", publishName, err)
+ }
+
+ impl.InvokeCallback(name)
+
+ fmt.Println("ready")
+ <-signals.ShutdownOnSignals()
+ if os.Getenv("PAUSE_BEFORE_STOP") == "1" {
+ blackbox.WaitForEOFOnStdin()
+ }
+}
+
+// generateScript is very similar in behavior to its namesake in invoker.go.
+// However, we chose to re-implement it here for two reasons: (1) avoid making
+// generateScript public; and (2) how the test choses to invoke the node manager
+// subprocess the first time should be independent of how node manager
+// implementation sets up its updated versions.
+func generateScript(t *testing.T, root string, cmd *goexec.Cmd) string {
+ output := "#!/bin/bash\n"
+ output += strings.Join(config.QuoteEnv(cmd.Env), " ") + " "
+ output += cmd.Args[0] + " " + strings.Join(cmd.Args[1:], " ")
+ if err := os.MkdirAll(filepath.Join(root, "factory"), 0755); err != nil {
+ t.Fatalf("MkdirAll failed: %v", err)
+ }
+ // Why pigeons? To show that the name we choose for the initial script
+ // doesn't matter and in particular is independent of how node manager
+ // names its updated version scripts (noded.sh).
+ path := filepath.Join(root, "factory", "pigeons.sh")
+ if err := ioutil.WriteFile(path, []byte(output), 0755); err != nil {
+ t.Fatalf("WriteFile(%v) failed: %v", path, err)
+ }
+ return path
+}
+
+// envelopeFromCmd returns an envelope that describes the given command object.
+func envelopeFromCmd(cmd *goexec.Cmd) *application.Envelope {
+ return &application.Envelope{
+ Title: application.NodeManagerTitle,
+ Args: cmd.Args[1:],
+ Env: cmd.Env,
+ Binary: "br",
+ }
+}
+
+// TestUpdateAndRevert makes the node manager go through the motions of updating
+// itself to newer versions (twice), and reverting itself back (twice). It also
+// checks that update and revert fail when they're supposed to. The initial
+// node manager is started 'by hand' via a blackbox command. Further versions
+// are started through the soft link that the node manager itself updates.
+func TestUpdateAndRevert(t *testing.T) {
+ // Set up mount table, application, and binary repositories.
+ defer setupLocalNamespace(t)()
+ envelope, cleanup := startApplicationRepository()
+ defer cleanup()
+ defer startBinaryRepository()()
+
+ // This is the local filesystem location that the node manager is told
+ // to use.
+ root := filepath.Join(os.TempDir(), "nodemanager")
+ defer os.RemoveAll(root)
+
+ // Current link does not have to live in the root dir.
+ currLink := filepath.Join(os.TempDir(), "testcurrent")
+ defer os.Remove(currLink)
+
+ // Set up the initial version of the node manager, the so-called
+ // "factory" version.
+ nm := blackbox.HelperCommand(t, "nodeManager", "factoryNM", root, "ar", currLink)
+ defer setupChildCommand(nm)()
+
+ // This is the script that we'll point the current link to initially.
+ scriptPathFactory := generateScript(t, root, nm.Cmd)
+
+ if err := os.Symlink(scriptPathFactory, currLink); err != nil {
+ t.Fatalf("Symlink(%q, %q) failed: %v", scriptPathFactory, currLink, err)
+ }
+ // We instruct the initial node manager that we run to pause before
+ // stopping its service, so that we get a chance to verify that
+ // attempting an update while another one is ongoing will fail.
+ nm.Cmd.Env = exec.Setenv(nm.Cmd.Env, "PAUSE_BEFORE_STOP", "1")
+
+ resolveExpectError(t, "factoryNM", verror.NotFound) // Ensure a clean slate.
+
+ // Start the node manager -- we use the blackbox-generated command to
+ // start it. We could have also used the scriptPathFactory to start it, but
+ // this demonstrates that the initial node manager could be started by
+ // hand as long as the right initial configuration is passed into the
+ // node manager implementation.
+ if err := nm.Cmd.Start(); err != nil {
+ t.Fatalf("Start() failed: %v", err)
+ }
+ deferrer := nm.Cleanup
+ defer func() {
+ if deferrer != nil {
+ deferrer()
+ }
+ }()
+ nm.Expect("ready")
+ resolve(t, "factoryNM") // Verify the node manager has published itself.
+
+ // Simulate an invalid envelope in the application repository.
+ *envelope = *envelopeFromCmd(nm.Cmd)
+ envelope.Title = "bogus"
+ updateExpectError(t, "factoryNM", verror.BadArg) // Incorrect title.
+ revertExpectError(t, "factoryNM", verror.NotFound) // No previous version available.
+
+ // Set up a second version of the node manager. We use the blackbox
+ // command solely to collect the args and env we need to provide the
+ // application repository with an envelope that will actually run the
+ // node manager subcommand. The blackbox command is never started by
+ // hand -- instead, the information in the envelope will be used by the
+ // node manager to stage the next version.
+ nmV2 := blackbox.HelperCommand(t, "nodeManager", "v2NM")
+ defer setupChildCommand(nmV2)()
+ *envelope = *envelopeFromCmd(nmV2.Cmd)
+ update(t, "factoryNM")
+
+ // Current link should have been updated to point to v2.
+ evalLink := func() string {
+ path, err := filepath.EvalSymlinks(currLink)
+ if err != nil {
+ t.Fatalf("EvalSymlinks(%v) failed: %v", currLink, err)
+ }
+ return path
+ }
+ scriptPathV2 := evalLink()
+ if scriptPathFactory == scriptPathV2 {
+ t.Errorf("current link didn't change")
+ }
+
+ // This is from the child node manager started by the node manager
+ // as an update test.
+ nm.Expect("ready")
+ nm.Expect("v2NM terminating")
+
+ updateExpectError(t, "factoryNM", verror.Exists) // Update already in progress.
+
+ nm.CloseStdin()
+ nm.Expect("factoryNM terminating")
+ deferrer = nil
+ nm.Cleanup()
+
+ // A successful update means the node manager has stopped itself. We
+ // relaunch it from the current link.
+ runNM := blackbox.HelperCommand(t, "execScript", currLink)
+ resolveExpectError(t, "v2NM", verror.NotFound) // Ensure a clean slate.
+ if err := runNM.Cmd.Start(); err != nil {
+ t.Fatalf("Start() failed: %v", err)
+ }
+ deferrer = runNM.Cleanup
+ runNM.Expect("ready")
+ resolve(t, "v2NM") // Current link should have been launching v2.
+
+ // Try issuing an update without changing the envelope in the application
+ // repository: this should fail, and current link should be unchanged.
+ updateExpectError(t, "v2NM", verror.NotFound)
+ if evalLink() != scriptPathV2 {
+ t.Errorf("script changed")
+ }
+
+ // Create a third version of the node manager and issue an update.
+ nmV3 := blackbox.HelperCommand(t, "nodeManager", "v3NM")
+ defer setupChildCommand(nmV3)()
+ *envelope = *envelopeFromCmd(nmV3.Cmd)
+ update(t, "v2NM")
+
+ scriptPathV3 := evalLink()
+ if scriptPathV3 == scriptPathV2 {
+ t.Errorf("current link didn't change")
+ }
+
+ // This is from the child node manager started by the node manager
+ // as an update test.
+ runNM.Expect("ready")
+ // Both the parent and child node manager should terminate upon successful
+ // update.
+ runNM.ExpectSet([]string{"v3NM terminating", "v2NM terminating"})
+
+ deferrer = nil
+ runNM.Cleanup()
+
+ // Re-lanuch the node manager from current link.
+ runNM = blackbox.HelperCommand(t, "execScript", currLink)
+ // We instruct the node manager to pause before stopping its server, so
+ // that we can verify that a second revert fails while a revert is in
+ // progress.
+ runNM.Cmd.Env = exec.Setenv(nm.Cmd.Env, "PAUSE_BEFORE_STOP", "1")
+ resolveExpectError(t, "v3NM", verror.NotFound) // Ensure a clean slate.
+ if err := runNM.Cmd.Start(); err != nil {
+ t.Fatalf("Start() failed: %v", err)
+ }
+ deferrer = runNM.Cleanup
+ runNM.Expect("ready")
+ resolve(t, "v3NM") // Current link should have been launching v3.
+
+ // Revert the node manager to its previous version (v2).
+ revert(t, "v3NM")
+ revertExpectError(t, "v3NM", verror.Exists) // Revert already in progress.
+ nm.CloseStdin()
+ runNM.Expect("v3NM terminating")
+ if evalLink() != scriptPathV2 {
+ t.Errorf("current link didn't change")
+ }
+ deferrer = nil
+ runNM.Cleanup()
+
+ // Re-launch the node manager from current link.
+ runNM = blackbox.HelperCommand(t, "execScript", currLink)
+ resolveExpectError(t, "v2NM", verror.NotFound) // Ensure a clean slate.
+ if err := runNM.Cmd.Start(); err != nil {
+ t.Fatalf("Start() failed: %v", err)
+ }
+ deferrer = runNM.Cleanup
+ runNM.Expect("ready")
+ resolve(t, "v2NM") // Current link should have been launching v2.
+
+ // Revert the node manager to its previous version (factory).
+ revert(t, "v2NM")
+ runNM.Expect("v2NM terminating")
+ if evalLink() != scriptPathFactory {
+ t.Errorf("current link didn't change")
+ }
+ deferrer = nil
+ runNM.Cleanup()
+
+ // Re-launch the node manager from current link.
+ runNM = blackbox.HelperCommand(t, "execScript", currLink)
+ resolveExpectError(t, "factoryNM", verror.NotFound) // Ensure a clean slate.
+ if err := runNM.Cmd.Start(); err != nil {
+ t.Fatalf("Start() failed: %v", err)
+ }
+ deferrer = runNM.Cleanup
+ runNM.Expect("ready")
+ resolve(t, "factoryNM") // Current link should have been launching factory version.
}
diff --git a/services/mgmt/node/impl/invoker.go b/services/mgmt/node/impl/invoker.go
index ed450c6..b7b4611 100644
--- a/services/mgmt/node/impl/invoker.go
+++ b/services/mgmt/node/impl/invoker.go
@@ -1,11 +1,9 @@
package impl
-// The implementation of the node manager expects that the node
-// manager installations are all organized in the following directory
-// structure:
+// The implementation of the node manager expects that the node manager
+// installations are all organized in the following directory structure:
//
// VEYRON_NM_ROOT/
-// current # symlink to one of the workspaces
// workspace-1/
// noded - the node manager binary
// noded.sh - a shell script to start the binary
@@ -14,15 +12,14 @@
// noded - the node manager binary
// noded.sh - a shell script to start the binary
//
-// The node manager is always expected to be started through
-// VEYRON_NM_ROOT/current/noded.sh, which is monitored by an init
-// daemon. This provides for simple and robust updates.
+// The node manager is always expected to be started through the symbolic link
+// passed in as config.CurrentLink, which is monitored by an init daemon. This
+// provides for simple and robust updates.
//
-// To update the node manager to a newer version, a new workspace is
-// created and the "current" symlink is updated
-// accordingly. Similarly, to revert the node manager to a previous
-// version, all that is required is to update the symlink to point to
-// the previous workspace.
+// To update the node manager to a newer version, a new workspace is created and
+// the symlink is updated to the new noded.sh script. Similarly, to revert the
+// node manager to a previous version, all that is required is to update the
+// symlink to point to the previous noded.sh script.
import (
"bytes"
@@ -43,6 +40,7 @@
"veyron/lib/config"
blib "veyron/services/mgmt/lib/binary"
vexec "veyron/services/mgmt/lib/exec"
+ iconfig "veyron/services/mgmt/node/config"
"veyron/services/mgmt/profile"
"veyron2/ipc"
@@ -58,24 +56,15 @@
"veyron2/vlog"
)
-const CurrentWorkspace = "current"
-
-// state wraps state shared between different node manager
+// internalState wraps state shared between different node manager
// invocations.
-type state struct {
+type internalState struct {
// channels maps callback identifiers to channels that are used to
// communicate information from child processes.
channels map[string]chan string
// channelsMutex is a lock for coordinating concurrent access to
// <channels>.
channelsMutex *sync.Mutex
- // envelope is the node manager application envelope.
- envelope *application.Envelope
- // name is the node manager name.
- name string
- // previous holds the local path to the previous version of the node
- // manager.
- previous string
// updating is a flag that records whether this instance of node
// manager is being updated.
updating bool
@@ -86,7 +75,11 @@
// invoker holds the state of a node manager invocation.
type invoker struct {
- state *state
+ // internal holds the node manager's internal state that persists across
+ // RPC requests.
+ internal *internalState
+ // config holds the node manager's (immutable) configuration state.
+ config *iconfig.State
// suffix is the suffix of the current invocation that is assumed to
// be used as a relative object name to identify an application,
// installation, or instance.
@@ -100,16 +93,9 @@
errOperationFailed = verror.Internalf("operation failed")
errUpdateInProgress = verror.Existsf("update in progress")
errIncompatibleUpdate = verror.BadArgf("update failed: mismatching app title")
+ errUpdateNoOp = verror.NotFoundf("no different version available")
)
-// NewInvoker is the invoker factory.
-func NewInvoker(state *state, suffix string) *invoker {
- return &invoker{
- state: state,
- suffix: suffix,
- }
-}
-
// NODE INTERFACE IMPLEMENTATION
// computeNodeProfile generates a description of the runtime
@@ -290,7 +276,7 @@
}
func (i *invoker) Describe(call ipc.ServerContext) (node.Description, error) {
- vlog.VI(0).Infof("%v.Describe()", i.suffix)
+ vlog.VI(1).Infof("%v.Describe()", i.suffix)
empty := node.Description{}
nodeProfile, err := i.computeNodeProfile()
if err != nil {
@@ -305,7 +291,7 @@
}
func (i *invoker) IsRunnable(call ipc.ServerContext, description binary.Description) (bool, error) {
- vlog.VI(0).Infof("%v.IsRunnable(%v)", i.suffix, description)
+ vlog.VI(1).Infof("%v.IsRunnable(%v)", i.suffix, description)
nodeProfile, err := i.computeNodeProfile()
if err != nil {
return false, err
@@ -323,7 +309,7 @@
}
func (i *invoker) Reset(call ipc.ServerContext, deadline uint64) error {
- vlog.VI(0).Infof("%v.Reset(%v)", i.suffix, deadline)
+ vlog.VI(1).Infof("%v.Reset(%v)", i.suffix, deadline)
// TODO(jsimsa): Implement.
return nil
}
@@ -377,16 +363,14 @@
// TODO(jsimsa): Replace <PreviousEnv> with a command-line flag when
// command-line flags in tests are supported.
-func generateScript(workspace string, envelope *application.Envelope) error {
+func generateScript(workspace string, configSettings []string, envelope *application.Envelope) error {
path, err := filepath.EvalSymlinks(os.Args[0])
if err != nil {
vlog.Errorf("EvalSymlinks(%v) failed: %v", os.Args[0], err)
return errOperationFailed
}
output := "#!/bin/bash\n"
- output += BinaryEnv + "=" + envelope.Binary + " "
- output += PreviousEnv + "=" + filepath.Dir(path) + " "
- output += strings.Join(envelope.Env, " ") + " "
+ output += strings.Join(iconfig.QuoteEnv(append(envelope.Env, configSettings...)), " ") + " "
output += filepath.Join(workspace, "noded") + " "
output += strings.Join(envelope.Args, " ")
path = filepath.Join(workspace, "noded.sh")
@@ -397,25 +381,25 @@
return nil
}
-// getCurrentFileInfo returns the os.FileInfo for both the symbolic
-// link $VEYRON_NM_ROOT/current and the workspace this link points to.
-func getCurrentFileInfo() (os.FileInfo, os.FileInfo, error) {
- path := filepath.Join(os.Getenv(RootEnv), CurrentWorkspace)
+// getCurrentFileInfo returns the os.FileInfo for both the symbolic link
+// CurrentLink, and the node script in the workspace that this link points to.
+func (i *invoker) getCurrentFileInfo() (os.FileInfo, string, error) {
+ path := i.config.CurrentLink
link, err := os.Lstat(path)
if err != nil {
vlog.Errorf("Lstat(%v) failed: %v", path, err)
- return nil, nil, err
+ return nil, "", err
}
- workspace, err := os.Stat(path)
+ scriptPath, err := filepath.EvalSymlinks(path)
if err != nil {
- vlog.Errorf("Stat(%v) failed: %v", path, err)
- return nil, nil, err
+ vlog.Errorf("EvalSymlinks(%v) failed: %v", path, err)
+ return nil, "", err
}
- return link, workspace, nil
+ return link, scriptPath, nil
}
-func updateLink(workspace string) error {
- link := filepath.Join(os.Getenv(RootEnv), CurrentWorkspace)
+func (i *invoker) updateLink(newScript string) error {
+ link := i.config.CurrentLink
newLink := link + ".new"
fi, err := os.Lstat(newLink)
if err == nil {
@@ -424,8 +408,8 @@
return errOperationFailed
}
}
- if err := os.Symlink(workspace, newLink); err != nil {
- vlog.Errorf("Symlink(%v, %v) failed: %v", workspace, newLink, err)
+ if err := os.Symlink(newScript, newLink); err != nil {
+ vlog.Errorf("Symlink(%v, %v) failed: %v", newScript, newLink, err)
return errOperationFailed
}
if err := os.Rename(newLink, link); err != nil {
@@ -436,13 +420,13 @@
}
func (i *invoker) registerCallback(id string, channel chan string) {
- i.state.channelsMutex.Lock()
- defer i.state.channelsMutex.Unlock()
- i.state.channels[id] = channel
+ i.internal.channelsMutex.Lock()
+ defer i.internal.channelsMutex.Unlock()
+ i.internal.channels[id] = channel
}
func (i *invoker) revertNodeManager() error {
- if err := updateLink(i.state.previous); err != nil {
+ if err := i.updateLink(i.config.Previous); err != nil {
return err
}
rt.R().Stop()
@@ -457,7 +441,7 @@
// Setup up the child process callback.
id := fmt.Sprintf("%d", rand.Int())
cfg := config.New()
- cfg.Set(mgmt.ParentNodeManagerConfigKey, naming.JoinAddressName(i.state.name, id))
+ cfg.Set(mgmt.ParentNodeManagerConfigKey, naming.MakeTerminal(naming.Join(i.config.Name, id)))
handle := vexec.NewParentHandle(cmd, vexec.ConfigOpt{cfg})
callbackChan := make(chan string)
i.registerCallback(id, callbackChan)
@@ -478,18 +462,18 @@
}
// Wait for the child process to invoke the Callback().
select {
- case address := <-callbackChan:
+ case childName := <-callbackChan:
// Check that invoking Update() succeeds.
- address = naming.JoinAddressName(address, "nm")
- nmClient, err := node.BindNode(address)
+ childName = naming.MakeTerminal(naming.Join(childName, "nm"))
+ nmClient, err := node.BindNode(childName)
if err != nil {
- vlog.Errorf("BindNode(%v) failed: %v", address, err)
+ vlog.Errorf("BindNode(%v) failed: %v", childName, err)
if err := handle.Clean(); err != nil {
vlog.Errorf("Clean() failed: %v", err)
}
return errOperationFailed
}
- linkOld, workspaceOld, err := getCurrentFileInfo()
+ linkOld, pathOld, err := i.getCurrentFileInfo()
if err != nil {
if err := handle.Clean(); err != nil {
vlog.Errorf("Clean() failed: %v", err)
@@ -498,7 +482,7 @@
}
// Since the resolution of mtime for files is seconds,
// the test sleeps for a second to make sure it can
- // check whether the "current" symlink is updated.
+ // check whether the current symlink is updated.
time.Sleep(time.Second)
if err := nmClient.Revert(rt.R().NewContext()); err != nil {
if err := handle.Clean(); err != nil {
@@ -506,23 +490,23 @@
}
return errOperationFailed
}
- linkNew, workspaceNew, err := getCurrentFileInfo()
+ linkNew, pathNew, err := i.getCurrentFileInfo()
if err != nil {
if err := handle.Clean(); err != nil {
vlog.Errorf("Clean() failed: %v", err)
}
return errOperationFailed
}
- // Check that the new node manager updated the symbolic link
- // $VEYRON_NM_ROOT/current.
+ // Check that the new node manager updated the current symbolic
+ // link.
if !linkOld.ModTime().Before(linkNew.ModTime()) {
vlog.Errorf("new node manager test failed")
return errOperationFailed
}
- // Check that the symbolic link $VEYRON_NM_ROOT/current points to
- // the same workspace.
- if workspaceOld.Name() != workspaceNew.Name() {
- updateLink(workspaceOld.Name())
+ // Ensure that the current symbolic link points to the same
+ // script.
+ if pathNew != pathOld {
+ i.updateLink(pathOld)
vlog.Errorf("new node manager test failed")
return errOperationFailed
}
@@ -537,144 +521,162 @@
}
func (i *invoker) unregisterCallback(id string) {
- i.state.channelsMutex.Lock()
- defer i.state.channelsMutex.Unlock()
- delete(i.state.channels, id)
+ i.internal.channelsMutex.Lock()
+ defer i.internal.channelsMutex.Unlock()
+ delete(i.internal.channels, id)
}
func (i *invoker) updateNodeManager() error {
- envelope, err := fetchEnvelope(os.Getenv(OriginEnv))
+ if len(i.config.Origin) == 0 {
+ return errUpdateNoOp
+ }
+ envelope, err := fetchEnvelope(i.config.Origin)
if err != nil {
return err
}
if envelope.Title != application.NodeManagerTitle {
return errIncompatibleUpdate
}
- if !reflect.DeepEqual(envelope, i.state.envelope) {
- // Create new workspace.
- workspace := filepath.Join(os.Getenv(RootEnv), fmt.Sprintf("%v", time.Now().Format(time.RFC3339Nano)))
- perm := os.FileMode(0755)
- if err := os.MkdirAll(workspace, perm); err != nil {
- vlog.Errorf("MkdirAll(%v, %v) failed: %v", workspace, perm, err)
- return errOperationFailed
- }
- // Populate the new workspace with a node manager binary.
- if err := generateBinary(workspace, envelope, envelope.Binary != i.state.envelope.Binary); err != nil {
- if err := os.RemoveAll(workspace); err != nil {
- vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
- }
- return err
- }
- // Populate the new workspace with a node manager script.
- if err := generateScript(workspace, envelope); err != nil {
- if err := os.RemoveAll(workspace); err != nil {
- vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
- }
- return err
- }
- if err := i.testNodeManager(workspace, envelope); err != nil {
- if err := os.RemoveAll(workspace); err != nil {
- vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
- }
- return err
- }
- // If the binary has changed, update the node manager symlink.
- if err := updateLink(workspace); err != nil {
- if err := os.RemoveAll(workspace); err != nil {
- vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
- }
- return err
- }
- rt.R().Stop()
+ if i.config.Envelope != nil && reflect.DeepEqual(envelope, i.config.Envelope) {
+ return errUpdateNoOp
}
+ // Create new workspace.
+ workspace := filepath.Join(i.config.Root, fmt.Sprintf("%v", time.Now().Format(time.RFC3339Nano)))
+ perm := os.FileMode(0755)
+ if err := os.MkdirAll(workspace, perm); err != nil {
+ vlog.Errorf("MkdirAll(%v, %v) failed: %v", workspace, perm, err)
+ return errOperationFailed
+ }
+ // Populate the new workspace with a node manager binary.
+ // TODO(caprita): match identical binaries on binary metadata
+ // rather than binary object name.
+ sameBinary := i.config.Envelope != nil && envelope.Binary == i.config.Envelope.Binary
+ if err := generateBinary(workspace, envelope, !sameBinary); err != nil {
+ if err := os.RemoveAll(workspace); err != nil {
+ vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
+ }
+ return err
+ }
+ // Populate the new workspace with a node manager script.
+ configSettings, err := i.config.Save(envelope)
+ if err != nil {
+ if err := os.RemoveAll(workspace); err != nil {
+ vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
+ }
+ return errOperationFailed
+ }
+ if err := generateScript(workspace, configSettings, envelope); err != nil {
+ if err := os.RemoveAll(workspace); err != nil {
+ vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
+ }
+ return err
+ }
+ if err := i.testNodeManager(workspace, envelope); err != nil {
+ if err := os.RemoveAll(workspace); err != nil {
+ vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
+ }
+ return err
+ }
+ // If the binary has changed, update the node manager symlink.
+ if err := i.updateLink(filepath.Join(workspace, "noded.sh")); err != nil {
+ if err := os.RemoveAll(workspace); err != nil {
+ vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
+ }
+ return err
+ }
+ rt.R().Stop()
return nil
}
func (i *invoker) Install(call ipc.ServerContext, von string) (string, error) {
- vlog.VI(0).Infof("%v.Install(%q)", i.suffix, von)
+ vlog.VI(1).Infof("%v.Install(%q)", i.suffix, von)
// TODO(jsimsa): Implement.
return "", nil
}
func (i *invoker) Refresh(call ipc.ServerContext) error {
- vlog.VI(0).Infof("%v.Refresh()", i.suffix)
+ vlog.VI(1).Infof("%v.Refresh()", i.suffix)
// TODO(jsimsa): Implement.
return nil
}
func (i *invoker) Restart(call ipc.ServerContext) error {
- vlog.VI(0).Infof("%v.Restart()", i.suffix)
+ vlog.VI(1).Infof("%v.Restart()", i.suffix)
// TODO(jsimsa): Implement.
return nil
}
func (i *invoker) Resume(call ipc.ServerContext) error {
- vlog.VI(0).Infof("%v.Resume()", i.suffix)
+ vlog.VI(1).Infof("%v.Resume()", i.suffix)
// TODO(jsimsa): Implement.
return nil
}
func (i *invoker) Revert(call ipc.ServerContext) error {
- vlog.VI(0).Infof("%v.Revert()", i.suffix)
- if i.state.previous == "" {
- return errOperationFailed
+ vlog.VI(1).Infof("%v.Revert()", i.suffix)
+ if i.config.Previous == "" {
+ return errUpdateNoOp
}
- i.state.updatingMutex.Lock()
- if i.state.updating {
- i.state.updatingMutex.Unlock()
+ i.internal.updatingMutex.Lock()
+ if i.internal.updating {
+ i.internal.updatingMutex.Unlock()
return errUpdateInProgress
} else {
- i.state.updating = true
+ i.internal.updating = true
}
- i.state.updatingMutex.Unlock()
+ i.internal.updatingMutex.Unlock()
err := i.revertNodeManager()
- i.state.updatingMutex.Lock()
- i.state.updating = false
- i.state.updatingMutex.Unlock()
+ if err != nil {
+ i.internal.updatingMutex.Lock()
+ i.internal.updating = false
+ i.internal.updatingMutex.Unlock()
+ }
return err
}
func (i *invoker) Start(call ipc.ServerContext) ([]string, error) {
- vlog.VI(0).Infof("%v.Start()", i.suffix)
+ vlog.VI(1).Infof("%v.Start()", i.suffix)
// TODO(jsimsa): Implement.
return make([]string, 0), nil
}
func (i *invoker) Stop(call ipc.ServerContext, deadline uint64) error {
- vlog.VI(0).Infof("%v.Stop(%v)", i.suffix, deadline)
+ vlog.VI(1).Infof("%v.Stop(%v)", i.suffix, deadline)
// TODO(jsimsa): Implement.
return nil
}
func (i *invoker) Suspend(call ipc.ServerContext) error {
- vlog.VI(0).Infof("%v.Suspend()", i.suffix)
+ vlog.VI(1).Infof("%v.Suspend()", i.suffix)
// TODO(jsimsa): Implement.
return nil
}
func (i *invoker) Uninstall(call ipc.ServerContext) error {
- vlog.VI(0).Infof("%v.Uninstall()", i.suffix)
+ vlog.VI(1).Infof("%v.Uninstall()", i.suffix)
// TODO(jsimsa): Implement.
return nil
}
func (i *invoker) Update(call ipc.ServerContext) error {
- vlog.VI(0).Infof("%v.Update()", i.suffix)
+ vlog.VI(1).Infof("%v.Update()", i.suffix)
switch {
case i.suffix == "nm":
// This branch attempts to update the node manager itself.
- i.state.updatingMutex.Lock()
- if i.state.updating {
- i.state.updatingMutex.Unlock()
+ i.internal.updatingMutex.Lock()
+ if i.internal.updating {
+ i.internal.updatingMutex.Unlock()
return errUpdateInProgress
} else {
- i.state.updating = true
+ i.internal.updating = true
}
- i.state.updatingMutex.Unlock()
+ i.internal.updatingMutex.Unlock()
err := i.updateNodeManager()
- i.state.updatingMutex.Lock()
- i.state.updating = false
- i.state.updatingMutex.Unlock()
+ if err != nil {
+ i.internal.updatingMutex.Lock()
+ i.internal.updating = false
+ i.internal.updatingMutex.Unlock()
+ }
return err
case appsSuffix.MatchString(i.suffix):
// TODO(jsimsa): Implement.
@@ -686,7 +688,7 @@
}
func (i *invoker) UpdateTo(call ipc.ServerContext, von string) error {
- vlog.VI(0).Infof("%v.UpdateTo(%q)", i.suffix, von)
+ vlog.VI(1).Infof("%v.UpdateTo(%q)", i.suffix, von)
// TODO(jsimsa): Implement.
return nil
}
@@ -694,15 +696,15 @@
// CONFIG INTERFACE IMPLEMENTATION
func (i *invoker) Set(_ ipc.ServerContext, key, value string) error {
- vlog.VI(0).Infof("%v.Set(%v, %v)", i.suffix, key, value)
+ vlog.VI(1).Infof("%v.Set(%v, %v)", i.suffix, key, value)
// For now, only handle the child node manager name. We'll add handling
// for the child's app cycle manager name later on.
if key != mgmt.ChildNodeManagerConfigKey {
return nil
}
- i.state.channelsMutex.Lock()
- channel, ok := i.state.channels[i.suffix]
- i.state.channelsMutex.Unlock()
+ i.internal.channelsMutex.Lock()
+ channel, ok := i.internal.channels[i.suffix]
+ i.internal.channelsMutex.Unlock()
if !ok {
return errInvalidSuffix
}
diff --git a/services/mgmt/node/impl/mock_repo_test.go b/services/mgmt/node/impl/mock_repo_test.go
new file mode 100644
index 0000000..28eaf4a
--- /dev/null
+++ b/services/mgmt/node/impl/mock_repo_test.go
@@ -0,0 +1,130 @@
+package impl_test
+
+import (
+ "crypto/md5"
+ "encoding/hex"
+ "errors"
+ "io"
+ "io/ioutil"
+ "os"
+
+ "veyron2/ipc"
+ "veyron2/services/mgmt/application"
+ "veyron2/services/mgmt/binary"
+ "veyron2/services/mgmt/repository"
+ "veyron2/vlog"
+)
+
+// arInvoker holds the state of an application repository invocation mock. The
+// mock returns the value of the wrapped envelope, which can be subsequently be
+// changed at any time. Client is responsible for synchronization if desired.
+type arInvoker struct {
+ envelope application.Envelope
+}
+
+// startApplicationRepository sets up a server running the application
+// repository. It returns a pointer to the envelope that the repository returns
+// to clients (so that it can be changed). It also returns a cleanup function.
+func startApplicationRepository() (*application.Envelope, func()) {
+ server, _ := newServer()
+ invoker := new(arInvoker)
+ dispatcher := ipc.SoloDispatcher(repository.NewServerApplication(invoker), nil)
+ name := "ar"
+ if err := server.Serve(name, dispatcher); err != nil {
+ vlog.Fatalf("Serve(%v) failed: %v", name, err)
+ }
+ return &invoker.envelope, func() {
+ if err := server.Stop(); err != nil {
+ vlog.Fatalf("Stop() failed: %v", err)
+ }
+ }
+}
+
+// APPLICATION REPOSITORY INTERFACE IMPLEMENTATION
+func (i *arInvoker) Match(ipc.ServerContext, []string) (application.Envelope, error) {
+ vlog.VI(1).Infof("Match()")
+ return i.envelope, nil
+}
+
+// brInvoker holds the state of a binary repository invocation mock. It always
+// serves the current running binary.
+type brInvoker struct{}
+
+// startBinaryRepository sets up a server running the binary repository and
+// returns a cleanup function.
+func startBinaryRepository() func() {
+ server, _ := newServer()
+ dispatcher := ipc.SoloDispatcher(repository.NewServerBinary(new(brInvoker)), nil)
+ name := "br"
+ if err := server.Serve(name, dispatcher); err != nil {
+ vlog.Fatalf("Serve(%q) failed: %v", name, err)
+ }
+ return func() {
+ if err := server.Stop(); err != nil {
+ vlog.Fatalf("Stop() failed: %v", err)
+ }
+ }
+}
+
+// BINARY REPOSITORY INTERFACE IMPLEMENTATION
+
+func (*brInvoker) Create(ipc.ServerContext, int32) error {
+ vlog.VI(1).Infof("Create()")
+ return nil
+}
+
+func (i *brInvoker) Delete(ipc.ServerContext) error {
+ vlog.VI(1).Infof("Delete()")
+ return nil
+}
+
+var errOperationFailed = errors.New("operation failed")
+
+func (i *brInvoker) Download(_ ipc.ServerContext, _ int32, stream repository.BinaryServiceDownloadStream) error {
+ vlog.VI(1).Infof("Download()")
+ file, err := os.Open(os.Args[0])
+ if err != nil {
+ vlog.Errorf("Open() failed: %v", err)
+ return errOperationFailed
+ }
+ defer file.Close()
+ bufferLength := 4096
+ buffer := make([]byte, bufferLength)
+ for {
+ n, err := file.Read(buffer)
+ switch err {
+ case io.EOF:
+ return nil
+ case nil:
+ if err := stream.Send(buffer[:n]); err != nil {
+ vlog.Errorf("Send() failed: %v", err)
+ return errOperationFailed
+ }
+ default:
+ vlog.Errorf("Read() failed: %v", err)
+ return errOperationFailed
+ }
+ }
+}
+
+func (*brInvoker) DownloadURL(ipc.ServerContext) (string, int64, error) {
+ vlog.VI(1).Infof("DownloadURL()")
+ return "", 0, nil
+}
+
+func (*brInvoker) Stat(ipc.ServerContext) ([]binary.PartInfo, error) {
+ vlog.VI(1).Infof("Stat()")
+ h := md5.New()
+ bytes, err := ioutil.ReadFile(os.Args[0])
+ if err != nil {
+ return []binary.PartInfo{}, errOperationFailed
+ }
+ h.Write(bytes)
+ part := binary.PartInfo{Checksum: hex.EncodeToString(h.Sum(nil)), Size: int64(len(bytes))}
+ return []binary.PartInfo{part}, nil
+}
+
+func (i *brInvoker) Upload(ipc.ServerContext, int32, repository.BinaryServiceUploadStream) error {
+ vlog.VI(1).Infof("Upload()")
+ return nil
+}
diff --git a/services/mgmt/node/impl/util_test.go b/services/mgmt/node/impl/util_test.go
new file mode 100644
index 0000000..3388b63
--- /dev/null
+++ b/services/mgmt/node/impl/util_test.go
@@ -0,0 +1,155 @@
+package impl_test
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ mtlib "veyron/services/mounttable/lib"
+
+ "veyron/lib/testutil/blackbox"
+ "veyron/lib/testutil/security"
+ "veyron/services/mgmt/lib/exec"
+
+ "veyron2"
+ "veyron2/ipc"
+ "veyron2/naming"
+ "veyron2/rt"
+ "veyron2/services/mgmt/node"
+ "veyron2/verror"
+ "veyron2/vlog"
+)
+
+// TODO(caprita): I've had to write one too many of these, let's move it to some
+// central utility library.
+
+// setupLocalNamespace sets up a mounttable and sets the local namespace root
+// to point to it. Returns a cleanup function.
+func setupLocalNamespace(t *testing.T) func() {
+ server, err := rt.R().NewServer(veyron2.ServesMountTableOpt(true))
+ if err != nil {
+ t.Fatalf("NewServer() failed: %v", err)
+ }
+ dispatcher, err := mtlib.NewMountTable("")
+ if err != nil {
+ t.Fatalf("NewMountTable() failed: %v", err)
+ }
+ protocol, hostname := "tcp", "localhost:0"
+ endpoint, err := server.Listen(protocol, hostname)
+ if err != nil {
+ t.Fatalf("Listen(%v, %v) failed: %v", protocol, hostname, err)
+ }
+ if err := server.Serve("", dispatcher); err != nil {
+ t.Fatalf("Serve(%v) failed: %v", dispatcher, err)
+ }
+ name := naming.JoinAddressName(endpoint.String(), "")
+ vlog.VI(1).Infof("Mount table object name: %v", name)
+ ns := rt.R().Namespace()
+ // Make the runtime's namespace rooted at the MountTable server started
+ // above.
+ ns.SetRoots(name)
+ return func() {
+ if err := server.Stop(); err != nil {
+ t.Fatalf("Stop() failed: %v", err)
+ }
+ // The runtime outlives the particular test case that invokes
+ // setupLocalNamespace. It's good practice to reset the
+ // runtime's state before the next test uses it.
+ ns.SetRoots()
+ }
+}
+
+// TODO(caprita): Move this setup into the blackbox lib.
+
+// setupChildCommand configures the child to use the right mounttable root
+// and identity. It returns a cleanup function.
+func setupChildCommand(child *blackbox.Child) func() {
+ cmd := child.Cmd
+ for i, root := range rt.R().Namespace().Roots() {
+ cmd.Env = exec.Setenv(cmd.Env, fmt.Sprintf("NAMESPACE_ROOT%d", i), root)
+ }
+ idFile := security.SaveIdentityToFile(security.NewBlessedIdentity(rt.R().Identity(), "test"))
+ cmd.Env = exec.Setenv(cmd.Env, "VEYRON_IDENTITY", idFile)
+ return func() {
+ os.Remove(idFile)
+ }
+}
+
+func newServer() (ipc.Server, string) {
+ server, err := rt.R().NewServer()
+ if err != nil {
+ vlog.Fatalf("NewServer() failed: %v", err)
+ }
+ protocol, hostname := "tcp", "localhost:0"
+ endpoint, err := server.Listen(protocol, hostname)
+ if err != nil {
+ vlog.Fatalf("Listen(%v, %v) failed: %v", protocol, hostname, err)
+ }
+ return server, endpoint.String()
+}
+
+// resolveExpectError verifies that the given name is not in the mounttable.
+func resolveExpectError(t *testing.T, name string, errID verror.ID) {
+ if results, err := rt.R().Namespace().Resolve(rt.R().NewContext(), name); err == nil {
+ t.Errorf("Resolve(%v) succeeded with results %v when it was expected to fail", name, results)
+ } else if !verror.Is(err, errID) {
+ t.Errorf("Resolve(%v) failed with error %v, expected error ID %v", err, errID)
+ }
+}
+
+// resolve looks up the given name in the mounttable.
+func resolve(t *testing.T, name string) string {
+ results, err := rt.R().Namespace().Resolve(rt.R().NewContext(), name)
+ if err != nil {
+ t.Fatalf("Resolve(%v) failed: %v", name, err)
+ }
+ if want, got := 1, len(results); want != got {
+ t.Fatalf("Resolve(%v) expected %d result(s), got %d instead", name, want, got)
+ }
+ return results[0]
+}
+
+// The following set of functions are convenience wrappers around Update and
+// Revert.
+
+func updateExpectError(t *testing.T, name string, errID verror.ID) {
+ if err := invokeUpdate(t, name); !verror.Is(err, errID) {
+ t.Errorf("Unexpected update error %v, expected error ID %v", err, errID)
+ }
+}
+
+func update(t *testing.T, name string) {
+ if err := invokeUpdate(t, name); err != nil {
+ t.Errorf("Update() failed: %v", err)
+ }
+}
+
+func invokeUpdate(t *testing.T, name string) error {
+ address := naming.Join(name, "nm")
+ stub, err := node.BindNode(address)
+ if err != nil {
+ t.Fatalf("BindNode(%v) failed: %v", address, err)
+ }
+ return stub.Update(rt.R().NewContext())
+}
+
+func revertExpectError(t *testing.T, name string, errID verror.ID) {
+ if err := invokeRevert(t, name); !verror.Is(err, errID) {
+ t.Errorf("Unexpected revert error %v, expected error ID %v", err, errID)
+ }
+}
+
+func revert(t *testing.T, name string) {
+ if err := invokeRevert(t, name); err != nil {
+ t.Errorf("Revert() failed: %v", err)
+ }
+}
+
+func invokeRevert(t *testing.T, name string) error {
+ address := naming.Join(name, "nm")
+ stub, err := node.BindNode(address)
+ if err != nil {
+ t.Fatalf("BindNode(%v) failed: %v", address, err)
+ }
+ return stub.Revert(rt.R().NewContext())
+}
diff --git a/services/mgmt/node/noded/main.go b/services/mgmt/node/noded/main.go
index 9deb603..1fe7ff4 100644
--- a/services/mgmt/node/noded/main.go
+++ b/services/mgmt/node/noded/main.go
@@ -2,30 +2,17 @@
import (
"flag"
- "os"
"veyron/lib/signals"
vflag "veyron/security/flag"
- "veyron/services/mgmt/lib/exec"
- "veyron/services/mgmt/node"
+ "veyron/services/mgmt/node/config"
"veyron/services/mgmt/node/impl"
- "veyron2/mgmt"
"veyron2/naming"
"veyron2/rt"
- "veyron2/services/mgmt/application"
"veyron2/vlog"
)
-func generateEnvelope() *application.Envelope {
- return &application.Envelope{
- Args: os.Args,
- Binary: os.Getenv(impl.BinaryEnv),
- Env: os.Environ(),
- Title: application.NodeManagerTitle,
- }
-}
-
func main() {
// TODO(rthellend): Remove the address and protocol flags when the config manager is working.
var address, protocol, publishAs string
@@ -33,9 +20,6 @@
flag.StringVar(&protocol, "protocol", "tcp", "network type to listen on")
flag.StringVar(&publishAs, "name", "", "name to publish the node manager at")
flag.Parse()
- if os.Getenv(impl.OriginEnv) == "" {
- vlog.Fatalf("Specify the node manager origin as environment variable %s=<name>", impl.OriginEnv)
- }
runtime := rt.Init()
defer runtime.Cleanup()
server, err := runtime.NewServer()
@@ -47,29 +31,26 @@
if err != nil {
vlog.Fatalf("Listen(%v, %v) failed: %v", protocol, address, err)
}
- suffix, envelope := "", generateEnvelope()
- name := naming.MakeTerminal(naming.JoinAddressName(endpoint.String(), suffix))
- vlog.VI(0).Infof("Node manager name: %v", name)
- // TODO(jsimsa): Replace <PreviousEnv> with a command-line flag when
- // command-line flags are supported in tests.
- dispatcher := impl.NewDispatcher(vflag.NewAuthorizerOrDie(), envelope, name, os.Getenv(impl.PreviousEnv))
+ name := naming.MakeTerminal(naming.JoinAddressName(endpoint.String(), ""))
+ vlog.VI(0).Infof("Node manager object name: %v", name)
+ configState, err := config.Load()
+ if err != nil {
+ vlog.Fatalf("Failed to load config passed from parent: %v", err)
+ return
+ }
+ configState.Name = name
+ // TODO(caprita): We need a way to set config fields outside of the
+ // update mechanism (since that should ideally be an opaque
+ // implementation detail).
+ dispatcher, err := impl.NewDispatcher(vflag.NewAuthorizerOrDie(), configState)
+ if err != nil {
+ vlog.Fatalf("Failed to create dispatcher: %v", err)
+ }
if err := server.Serve(publishAs, dispatcher); err != nil {
vlog.Fatalf("Serve(%v) failed: %v", publishAs, err)
}
- handle, _ := exec.GetChildHandle()
- if handle != nil {
- callbackName, err := handle.Config.Get(mgmt.ParentNodeManagerConfigKey)
- if err != nil {
- vlog.Fatalf("Couldn't get callback name from config: %v", err)
- }
- nmClient, err := node.BindConfig(callbackName)
- if err != nil {
- vlog.Fatalf("BindNode(%v) failed: %v", callbackName, err)
- }
- if err := nmClient.Set(rt.R().NewContext(), mgmt.ChildNodeManagerConfigKey, name); err != nil {
- vlog.Fatalf("Callback(%v, %v) failed: %v", mgmt.ChildNodeManagerConfigKey, name, err)
- }
- }
+ impl.InvokeCallback(name)
+
// Wait until shutdown.
<-signals.ShutdownOnSignals()
}
diff --git a/services/store/memstore/query/eval.go b/services/store/memstore/query/eval.go
index 73fa455..783fbd8 100644
--- a/services/store/memstore/query/eval.go
+++ b/services/store/memstore/query/eval.go
@@ -501,7 +501,6 @@
pos: p.Pos,
}
for i, a := range p.SubPipelines {
- // TODO(kash): Protect against aliases that have slashes in them?
e.subpipelines[i] = alias{convertPipeline(a.Pipeline), a.Alias, a.Hidden}
}
return e
@@ -580,10 +579,9 @@
case <-c.abort:
return false
case sub, ok := <-out:
- if !ok {
- return false
+ if ok {
+ value = sub.Value
}
- value = sub.Value
}
} else {
value = out
diff --git a/services/store/memstore/query/eval_test.go b/services/store/memstore/query/eval_test.go
index 6367a9f..65359b2 100644
--- a/services/store/memstore/query/eval_test.go
+++ b/services/store/memstore/query/eval_test.go
@@ -40,6 +40,9 @@
bettyID := put(t, sn, "/players/betty", player{"betty", 23})
bobID := put(t, sn, "/players/bob", player{"bob", 21})
+ put(t, sn, "/players/betty/bio", "")
+ put(t, sn, "/players/betty/bio/hometown", "Tampa")
+
put(t, sn, "/teams", "")
put(t, sn, "/teams/cardinals", team{"cardinals", "CA"})
put(t, sn, "/teams/sharks", team{"sharks", "NY"})
@@ -434,6 +437,31 @@
},
},
},
+ // Test for selection of a nested name ('bio/hometown'). Only betty has this
+ // nested name, so other players should get a nil value.
+ {
+ "", "'players/*' | type player | {Name, 'bio/hometown'} | ? Name == 'alfred' || Name == 'betty' | sort()",
+ []*store.QueryResult{
+ &store.QueryResult{
+ 0,
+ "players/alfred",
+ map[string]vdlutil.Any{
+ "Name": "alfred",
+ "bio/hometown": nil,
+ },
+ nil,
+ },
+ &store.QueryResult{
+ 0,
+ "players/betty",
+ map[string]vdlutil.Any{
+ "Name": "betty",
+ "bio/hometown": "Tampa",
+ },
+ nil,
+ },
+ },
+ },
}
for _, test := range tests {
vlog.VI(1).Infof("Testing %s\n", test.query)