diff --git a/charmbits/httprelation/provider_test.go b/charmbits/httprelation/provider_test.go index dbd5967..3228791 100644 --- a/charmbits/httprelation/provider_test.go +++ b/charmbits/httprelation/provider_test.go @@ -1,7 +1,6 @@ package httprelation_test import ( - "encoding/json" "sort" "testing" @@ -118,56 +117,19 @@ type context struct { func (ctxt *context) runHook(c *gc.C, hookName string, relId hook.RelationId, relUnit hook.UnitId, register func(*httprelation.Provider, *hook.Registry)) [][]string { var p httprelation.Provider - r := hook.NewRegistry() - p.Register(r, "foo", ctxt.withHTTPS) - if register != nil { - register(&p, r) - } - hook.RegisterMainHooks(r) runner := &hooktest.Runner{ - RunFunc: func(cmd string, args ...string) ([]byte, error) { - c.Logf("RunFunc %s %q", cmd, args) - if cmd != "config-get" { - return nil, nil + RegisterHooks: func(r *hook.Registry) { + p.Register(r, "foo", ctxt.withHTTPS) + if register != nil { + register(&p, r) } - var val interface{} - if len(args) < 4 { - val = ctxt.config - } else { - key := args[3] - val = ctxt.config[key] - } - data, err := json.Marshal(val) - c.Assert(err, gc.IsNil) - c.Logf("returning %s", data) - return data, nil }, - Logger: c, - } - hctxt := &hook.Context{ - UUID: hooktest.UUID, - Unit: "wordpress/0", - CharmDir: "/nowhere", - HookName: hookName, - Runner: runner, Relations: ctxt.relations, RelationIds: ctxt.relationIds, + Config: ctxt.config, + Logger: c, } - if relId != "" { - hctxt.RelationId = relId - hctxt.RemoteUnit = relUnit - loop: - for name, ids := range ctxt.relationIds { - for _, id := range ids { - if id == hctxt.RelationId { - hctxt.RelationName = name - break loop - } - } - } - c.Assert(hctxt.RelationName, gc.Not(gc.Equals), "") - } - err := hook.Main(r, hctxt, ctxt.state) + err := runner.RunHook(hookName, relId, relUnit) c.Assert(err, gc.IsNil) return runner.Record } diff --git a/charmbits/service/service.go b/charmbits/service/service.go index 853ba4d..fae0bda 100644 --- a/charmbits/service/service.go +++ b/charmbits/service/service.go @@ -13,14 +13,23 @@ import ( "path/filepath" "time" - serviceCommon "github.com/juju/juju/service/common" - "github.com/juju/juju/service/upstart" "github.com/juju/utils" "gopkg.in/errgo.v1" "github.com/juju/gocharm/hook" ) +// OSService defines the interface provided by an +// operating system service. It is implemented by +// *upstart.Service (from github.com/juju/juju/service/upstart). +type OSService interface { + Install() error + StopAndRemove() error + Running() bool + Stop() error + Start() error +} + // Service represents a long running service that runs // outside of the usual charm hook context. type Service struct { @@ -83,7 +92,7 @@ func (svc *Service) Start(args ...string) error { return errgo.Notef(err, "cannot create state directory") } svc.ctxt.Logf("starting service") - usvc := svc.upstartService(args) + usvc := svc.osService(args) // Note: Install will restart the service if the configuration // file has changed. if err := usvc.Install(); err != nil { @@ -102,7 +111,7 @@ func (svc *Service) Start(args ...string) error { // Stop stops the service running. func (svc *Service) Stop() error { - if err := svc.upstartService(nil).Stop(); err != nil { + if err := svc.osService(nil).Stop(); err != nil { return errgo.Mask(err) } return nil @@ -110,7 +119,7 @@ func (svc *Service) Stop() error { // Started reports whether the service has been started. func (svc *Service) Started() bool { - return svc.upstartService(nil).Running() + return svc.osService(nil).Running() } // StopAndRemove stops and removes the service completely. @@ -118,7 +127,7 @@ func (svc *Service) StopAndRemove() error { if !svc.state.Installed { return nil } - if err := svc.upstartService(nil).StopAndRemove(); err != nil { + if err := svc.osService(nil).StopAndRemove(); err != nil { return errgo.Mask(err) } svc.state.Installed = false @@ -160,19 +169,7 @@ func (svc *Service) Call(method string, args interface{}, reply interface{}) err return nil } -func dialRPC(path string) (*rpc.Client, error) { - c, err := net.Dial("unix", path) - if err != nil { - return nil, errgo.Mask(err) - } - return rpc.NewClientWithCodec(jsonrpc.NewClientCodec(c)), nil -} - -func (svc *Service) socketPath() string { - return "@" + filepath.Join(svc.ctxt.StateDir(), "service") -} - -func (svc *Service) upstartService(args []string) *upstart.Service { +func (svc *Service) osService(args []string) OSService { exe := filepath.Join(svc.ctxt.CharmDir, "bin", "runhook") serviceName := svc.serviceName if serviceName == "" { @@ -187,16 +184,26 @@ func (svc *Service) upstartService(args []string) *upstart.Service { if err != nil { panic(errgo.Notef(err, "cannot marshal parameters")) } - cmd := exe + " " + - svc.ctxt.CommandName() + " " + - base64.StdEncoding.EncodeToString(pdata) - return &upstart.Service{ - Name: serviceName, - Conf: serviceCommon.Conf{ - InitDir: "/etc/init", - Desc: fmt.Sprintf("service for juju unit %q", svc.ctxt.Unit), - Cmd: cmd, - Out: filepath.Join(svc.ctxt.StateDir(), "servicelog.out"), + return NewService(OSServiceParams{ + Name: serviceName, + Description: fmt.Sprintf("service for juju unit %q", svc.ctxt.Unit), + Exe: exe, + Args: []string{ + svc.ctxt.CommandName(), + base64.StdEncoding.EncodeToString(pdata), }, + Output: filepath.Join(svc.ctxt.StateDir(), "servicelog.out"), + }) +} + +func dialRPC(path string) (*rpc.Client, error) { + c, err := net.Dial("unix", path) + if err != nil { + return nil, errgo.Mask(err) } + return rpc.NewClientWithCodec(jsonrpc.NewClientCodec(c)), nil +} + +func (svc *Service) socketPath() string { + return "@" + filepath.Join(svc.ctxt.StateDir(), "service") } diff --git a/charmbits/service/upstart.go b/charmbits/service/upstart.go new file mode 100644 index 0000000..ba9c84e --- /dev/null +++ b/charmbits/service/upstart.go @@ -0,0 +1,45 @@ +package service + +import ( + "strings" + + "github.com/juju/juju/service/common" + "github.com/juju/juju/service/upstart" +) + +// OSServiceParams holds the parameters for +// creating a new service. +type OSServiceParams struct { + // Name holds the name of the service. + Name string + + // Description holds the description of the service. + Description string + + // Output holds the file where output will be put. + Output string + + // Exe holds the name of the executable to run. + Exe string + + // Args holds any arguments to the executable, + // which should be OK to to pass to the shell + // without quoting. + Args []string +} + +// NewService is used to create a new service. +// It is defined as a variable so that it can be +// replaced for testing purposes. +var NewService = func(p OSServiceParams) OSService { + cmd := p.Exe + " " + strings.Join(p.Args, " ") + return &upstart.Service{ + Name: p.Name, + Conf: common.Conf{ + InitDir: "/etc/init", + Desc: p.Description, + Cmd: cmd, + Out: p.Output, + }, + } +} diff --git a/hook/hooktest/hooktest.go b/hook/hooktest/hooktest.go index 2798164..0c8875b 100644 --- a/hook/hooktest/hooktest.go +++ b/hook/hooktest/hooktest.go @@ -1,14 +1,41 @@ package hooktest +import ( + "encoding/json" + + "github.com/juju/gocharm/hook" +) + // Find out which commands will be generated by which Context methods. // Runner is implemention of hook.Runner suitable for use in tests. // It calls the given RunFunc function whenever the Run method -// and records all the calls in the Record field. +// and records all the calls in the Record field, with the +// exception of the calls mentioned below. +// // Any calls to juju-log are logged using Logger, but otherwise ignored. -// Any calls to config-get or unit-get are passed through to RunFunc but -// not recorded. +// Calls to config-get from the Config field and not invoked through RunFunc. +// Likewise, calls to unit-get will be satisfied from the PublicAddress +// and PrivateAddress fields. type Runner struct { + RegisterHooks func(r *hook.Registry) + // The following fields hold information that will + // be available through the hook context. + Relations map[hook.RelationId]map[hook.UnitId]map[string]string + RelationIds map[string][]hook.RelationId + Config map[string]interface{} + + PublicAddress string + PrivateAddress string + + // State holds the persistent state. + // If it is nil, it will be set to a hooktest.MemState + // instance. + State hook.PersistentState + + // RunFunc is called when a hook tool runs. + // It may be nil, in which case it will be assumed + // that the hook tool runs successfully with no output. RunFunc func(string, ...string) ([]byte, error) Record [][]string Logger interface { @@ -19,21 +46,91 @@ type Runner struct { Closed bool } +// RunHook runs a hook in the context of the Runner. If it's a relation +// hook, then relId should hold the current relation id and +// relUnit should hold the unit that the relation hook is running for. +// +// Any hook tools that have been run will be stored in r.Record. +func (runner *Runner) RunHook(hookName string, relId hook.RelationId, relUnit hook.UnitId) error { + if runner.State == nil { + runner.State = make(MemState) + } + r := hook.NewRegistry() + runner.RegisterHooks(r) + hook.RegisterMainHooks(r) + hctxt := &hook.Context{ + UUID: UUID, + Unit: "someunit/0", + CharmDir: "/nowhere", + HookName: hookName, + Runner: runner, + Relations: runner.Relations, + RelationIds: runner.RelationIds, + } + if relId != "" { + hctxt.RelationId = relId + hctxt.RemoteUnit = relUnit + loop: + for name, ids := range runner.RelationIds { + for _, id := range ids { + if id == hctxt.RelationId { + hctxt.RelationName = name + break loop + } + } + } + if hctxt.RelationName == "" { + panic("relation id not found") + } + } + return hook.Main(r, hctxt, runner.State) +} + // Run implements hook.Runner.Run. func (r *Runner) Run(cmd string, args ...string) ([]byte, error) { if cmd == "juju-log" { if len(args) != 1 { - panic("expected only one argument to juju-log") + panic("expected exactly one argument to juju-log") } r.Logger.Logf("%s", args[0]) return nil, nil } - if cmd != "config-get" && cmd != "unit-get" { - rec := []string{cmd} - rec = append(rec, args...) - r.Record = append(r.Record, rec) + switch cmd { + case "config-get": + var val interface{} + if len(args) < 4 { + // config-get --format json + val = r.Config + } else { + // config-get --format json -- key + key := args[3] + val = r.Config[key] + } + data, err := json.Marshal(val) + if err != nil { + panic(err) + } + return data, nil + case "unit-get": + if len(args) != 1 { + panic("expected exactly one argument to unit-get") + } + switch args[0] { + case "public-address": + return []byte(r.PublicAddress), nil + case "private-address": + return []byte(r.PrivateAddress), nil + default: + panic("unexpected argument to unit-get") + } + } + rec := []string{cmd} + rec = append(rec, args...) + r.Record = append(r.Record, rec) + if r.RunFunc != nil { + return r.RunFunc(cmd, args...) } - return r.RunFunc(cmd, args...) + return nil, nil } // Run implements hook.Runner.Close.