diff --git a/.travis.yml b/.travis.yml index 59dbef26a..09031e06b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,8 +21,8 @@ deploy: api_key: secure: NtpNjquqjnwpeVQQM1GTHTTU7YOo8fEIyoBtMf3Vf1ayZjuWVZxwNfM77E596TG52a8pnZtpapXyHT0M4e1zms7F5KVCrOEfOB0OrA4IDzoATelVqdONnN3lbRJeVJVdSmK8/FNKwjI24tQZTaTQcIOioNqh7ZRcrEYlatGCuAw= file: - - /home/travis/rpmbuild/RPMS/noarch/mackerel-agent-0.25.1-1.noarch.rpm - - packaging/mackerel-agent_0.25.1-1_all.deb + - /home/travis/rpmbuild/RPMS/noarch/mackerel-agent-0.26.2-1.noarch.rpm + - packaging/mackerel-agent_0.26.2-1_all.deb - snapshot/mackerel-agent_darwin_386.zip - snapshot/mackerel-agent_darwin_amd64.zip - snapshot/mackerel-agent_freebsd_386.zip diff --git a/CHANGELOG.md b/CHANGELOG.md index f26d84a3c..05dfd9d31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 0.26.2 (2015-12-10) + +* output success message to stderr when configtest succeed #178 (Songmu) + + +## 0.26.1 (2015-12-09) + +* fix deprecate message #176 (Songmu) + + +## 0.26.0 (2015-12-08) + +* Make HostID storage replacable #167 (motemen) +* Publicize command.Context's fields #168 (motemen) +* Configtest #169 (fujiwara) +* Refactor config loading and check if Apikey exists in configtest #171 (Songmu) +* fix exit status of debian init script. #172 (fujiwara) +* Deprecate version and once option #173 (Songmu) + + ## 0.25.1 (2015-11-25) * Go 1.5.1 #164 (Songmu) diff --git a/Makefile b/Makefile index 1166def72..b3d55922b 100644 --- a/Makefile +++ b/Makefile @@ -22,25 +22,15 @@ run: build deps: go get -d -v -t ./... - go get github.com/golang/lint/golint go get golang.org/x/tools/cmd/vet - go get golang.org/x/tools/cmd/cover + go get github.com/golang/lint/golint go get github.com/pierrre/gotestcover go get github.com/laher/goxc go get github.com/mattn/goveralls -LINT_RET = .golint.txt lint: deps - go vet ./... - rm -f $(LINT_RET) - for os in "$(BUILD_OS_TARGETS)"; do \ - if [ $$os != "windows" ]; then \ - GOOS=$$os golint ./... | grep -v '_string.go:' | tee -a $(LINT_RET); \ - else \ - GOOS=$$os golint --min_confidence=0.9 ./... | grep -v '_string.go:' | tee -a $(LINT_RET); \ - fi \ - done - test ! -s $(LINT_RET) + go tool vet -all . + tool/go-linter $(BUILD_OS_TARGETS) crossbuild: deps cp mackerel-agent.sample.conf mackerel-agent.conf diff --git a/appveyor.yml b/appveyor.yml index 2830587b5..0fb84d4e8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -26,7 +26,7 @@ test_script: - FOR /F "usebackq" %%w IN (`git rev-parse --show-cdup`) DO SET CDUP=%%w - cd %CDUP% - go get -d -v -t ./... -- go vet ./... +- go tool vet -all . - go test -short ./... notifications: - provider: Slack diff --git a/command/command.go b/command/command.go index fd1fb64f8..e0eff794f 100644 --- a/command/command.go +++ b/command/command.go @@ -4,11 +4,8 @@ import ( "crypto/sha1" "encoding/json" "fmt" - "io/ioutil" "math" "os" - "path/filepath" - "strings" "time" "github.com/Songmu/retry" @@ -25,52 +22,12 @@ import ( var logger = logging.GetLogger("command") var metricsInterval = 60 * time.Second -const idFileName = "id" - -func idFilePath(root string) string { - return filepath.Join(root, idFileName) -} - -// LoadHostID loads hostID -func LoadHostID(root string) (string, error) { - content, err := ioutil.ReadFile(idFilePath(root)) - if err != nil { - return "", err - } - return strings.TrimRight(string(content), "\r\n"), nil -} - -// RemoveIDFile removes idfile -func RemoveIDFile(root string) error { - return os.Remove(idFilePath(root)) -} - -func saveHostID(root string, id string) error { - err := os.MkdirAll(root, 0755) - if err != nil { - return err - } - - file, err := os.Create(idFilePath(root)) - if err != nil { - return err - } - defer file.Close() - - _, err = file.Write([]byte(id)) - if err != nil { - return err - } - - return nil -} - var retryNum uint = 20 var retryInterval = 3 * time.Second // prepareHost collects specs of the host and sends them to Mackerel server. // A unique host-id is returned by the server if one is not specified. -func prepareHost(root string, api *mackerel.API, roleFullnames []string, checks []string, displayName string, hostSt string) (*mackerel.Host, error) { +func prepareHost(conf *config.Config, api *mackerel.API) (*mackerel.Host, error) { // XXX this configuration should be moved to under spec/linux os.Setenv("PATH", "/sbin:/usr/sbin:/bin:/usr/bin:"+os.Getenv("PATH")) os.Setenv("LANG", "C") // prevent changing outputs of some command, e.g. ifconfig. @@ -96,11 +53,11 @@ func prepareHost(root string, api *mackerel.API, roleFullnames []string, checks } var result *mackerel.Host - if hostID, err := LoadHostID(root); err != nil { // create + if hostID, err := conf.LoadHostID(); err != nil { // create logger.Debugf("Registering new host on mackerel...") doRetry(func() error { - hostID, lastErr = api.CreateHost(hostname, meta, interfaces, roleFullnames, displayName) + hostID, lastErr = api.CreateHost(hostname, meta, interfaces, conf.Roles, conf.DisplayName) return filterErrorForRetry(lastErr) }) @@ -121,10 +78,14 @@ func prepareHost(root string, api *mackerel.API, roleFullnames []string, checks return filterErrorForRetry(lastErr) }) if lastErr != nil { - return nil, fmt.Errorf("Failed to find this host on mackerel (You may want to delete file \"%s\" to register this host to an another organization): %s", idFilePath(root), lastErr.Error()) + if fsStorage, ok := conf.HostIDStorage.(*config.FileSystemHostIDStorage); ok { + return nil, fmt.Errorf("Failed to find this host on mackerel (You may want to delete file \"%s\" to register this host to an another organization): %s", fsStorage.HostIDFile(), lastErr.Error()) + } + return nil, fmt.Errorf("Failed to find this host on mackerel: %s", lastErr.Error()) } } + hostSt := conf.HostStatus.OnStart if hostSt != "" && hostSt != result.Status { doRetry(func() error { lastErr = api.UpdateHostStatus(result.ID, hostSt) @@ -135,7 +96,7 @@ func prepareHost(root string, api *mackerel.API, roleFullnames []string, checks } } - lastErr = saveHostID(root, result.ID) + lastErr = conf.SaveHostID(result.ID) if lastErr != nil { return nil, fmt.Errorf("Failed to save host ID: %s", lastErr.Error()) } @@ -153,10 +114,10 @@ func delayByHost(host *mackerel.Host) int { // Context context object type Context struct { - ag *agent.Agent - conf *config.Config - host *mackerel.Host - api *mackerel.API + Agent *agent.Agent + Config *config.Config + Host *mackerel.Host + API *mackerel.API } type postValue struct { @@ -185,17 +146,17 @@ func loop(c *Context, termCh chan struct{}) int { // Periodically update host specs. go updateHostSpecsLoop(c, quit) - postQueue := make(chan *postValue, c.conf.Connection.PostMetricsBufferSize) + postQueue := make(chan *postValue, c.Config.Connection.PostMetricsBufferSize) go enqueueLoop(c, postQueue, quit) - postDelaySeconds := delayByHost(c.host) + postDelaySeconds := delayByHost(c.Host) initialDelay := postDelaySeconds / 2 logger.Debugf("wait %d seconds before initial posting.", initialDelay) select { case <-termCh: return 0 case <-time.After(time.Duration(initialDelay) * time.Second): - c.ag.InitPluginGenerators(c.api) + c.Agent.InitPluginGenerators(c.API) } termCheckerCh := make(chan struct{}) @@ -236,10 +197,10 @@ func loop(c *Context, termCh chan struct{}) int { case loopStateFirst: // request immediately to create graph defs of host // nop case loopStateQueued: - delaySeconds = c.conf.Connection.PostMetricsDequeueDelaySeconds + delaySeconds = c.Config.Connection.PostMetricsDequeueDelaySeconds case loopStateHadError: // TODO: better interval calculation. exponential backoff or so. - delaySeconds = c.conf.Connection.PostMetricsRetryDelaySeconds + delaySeconds = c.Config.Connection.PostMetricsRetryDelaySeconds case loopStateTerminating: // dequeue and post every one second when terminating. delaySeconds = 1 @@ -278,7 +239,7 @@ func loop(c *Context, termCh chan struct{}) int { for _, v := range origPostValues { postValues = append(postValues, v.values...) } - err := c.api.PostMetricsValues(postValues) + err := c.API.PostMetricsValues(postValues) if err != nil { logger.Errorf("Failed to post metrics value (will retry): %s", err.Error()) if lState != loopStateTerminating { @@ -289,7 +250,7 @@ func loop(c *Context, termCh chan struct{}) int { v.retryCnt++ // It is difficult to distinguish the error is server error or data error. // So, if retryCnt exceeded the configured limit, postValue is considered invalid and abandoned. - if v.retryCnt > c.conf.Connection.PostMetricsRetryMax { + if v.retryCnt > c.Config.Connection.PostMetricsRetryMax { json, err := json.Marshal(v.values) if err != nil { logger.Errorf("Something wrong with post values. marshaling failed.") @@ -325,7 +286,7 @@ func updateHostSpecsLoop(c *Context, quit chan struct{}) { } func enqueueLoop(c *Context, postQueue chan *postValue, quit chan struct{}) { - metricsResult := c.ag.Watch() + metricsResult := c.Agent.Watch() for { select { case <-quit: @@ -335,14 +296,14 @@ func enqueueLoop(c *Context, postQueue chan *postValue, quit chan struct{}) { creatingValues := [](*mackerel.CreatingMetricsValue){} for name, value := range (map[string]float64)(result.Values) { if math.IsNaN(value) || math.IsInf(value, 0) { - logger.Warningf("Invalid value: hostID = %s, name = %s, value = %f\n is not sent.", c.host.ID, name, value) + logger.Warningf("Invalid value: hostID = %s, name = %s, value = %f\n is not sent.", c.Host.ID, name, value) continue } creatingValues = append( creatingValues, &mackerel.CreatingMetricsValue{ - HostID: c.host.ID, + HostID: c.Host.ID, Name: name, Time: created, Value: value, @@ -363,7 +324,7 @@ func runCheckersLoop(c *Context, termCheckerCh <-chan struct{}, quit <-chan stru checkReportCh chan *checks.Report reportCheckImmediateCh chan struct{} ) - for _, checker := range c.ag.Checkers { + for _, checker := range c.Agent.Checkers { if checkReportCh == nil { checkReportCh = make(chan *checks.Report) reportCheckImmediateCh = make(chan struct{}) @@ -439,7 +400,7 @@ func runCheckersLoop(c *Context, termCheckerCh <-chan struct{}, quit <-chan stru continue } - err := c.api.ReportCheckMonitors(c.host.ID, reports) + err := c.API.ReportCheckMonitors(c.Host.ID, reports) if err != nil { logger.Errorf("ReportCheckMonitors: %s", err) @@ -496,13 +457,13 @@ func (c *Context) UpdateHostSpecs() { return } - err = c.api.UpdateHost(c.host.ID, mackerel.HostSpec{ + err = c.API.UpdateHost(c.Host.ID, mackerel.HostSpec{ Name: hostname, Meta: meta, Interfaces: interfaces, - RoleFullnames: c.conf.Roles, - Checks: c.conf.CheckNames(), - DisplayName: c.conf.DisplayName, + RoleFullnames: c.Config.Roles, + Checks: c.Config.CheckNames(), + DisplayName: c.Config.DisplayName, }) if err != nil { @@ -520,16 +481,16 @@ func Prepare(conf *config.Config) (*Context, error) { return nil, fmt.Errorf("Failed to prepare an api: %s", err.Error()) } - host, err := prepareHost(conf.Root, api, conf.Roles, conf.CheckNames(), conf.DisplayName, conf.HostStatus.OnStart) + host, err := prepareHost(conf, api) if err != nil { return nil, fmt.Errorf("Failed to prepare host: %s", err.Error()) } return &Context{ - ag: NewAgent(conf), - conf: conf, - host: host, - api: api, + Agent: NewAgent(conf), + Config: conf, + Host: host, + API: api, }, nil } @@ -588,12 +549,12 @@ func NewAgent(conf *config.Config) *agent.Agent { // Run starts the main metric collecting logic and this function will never return. func Run(c *Context, termCh chan struct{}) int { - logger.Infof("Start: apibase = %s, hostName = %s, hostID = %s", c.conf.Apibase, c.host.Name, c.host.ID) + logger.Infof("Start: apibase = %s, hostName = %s, hostID = %s", c.Config.Apibase, c.Host.Name, c.Host.ID) exitCode := loop(c, termCh) - if exitCode == 0 && c.conf.HostStatus.OnStop != "" { + if exitCode == 0 && c.Config.HostStatus.OnStop != "" { // TOOD error handling. supoprt retire(?) - err := c.api.UpdateHostStatus(c.host.ID, c.conf.HostStatus.OnStop) + err := c.API.UpdateHostStatus(c.Host.ID, c.Config.HostStatus.OnStop) if err != nil { logger.Errorf("Failed update host status on stop: %s", err) } diff --git a/command/command_test.go b/command/command_test.go index a52130053..a78b56477 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -107,8 +107,8 @@ func TestPrepareWithCreate(t *testing.T) { } c, _ := Prepare(&conf) - api := c.api - host := c.host + api := c.API + host := c.Host if api.BaseURL.String() != ts.URL { t.Errorf("Apibase mismatch: %s != %s", api.BaseURL, ts.URL) @@ -151,7 +151,7 @@ func TestPrepareWithUpdate(t *testing.T) { defer ts.Close() tempDir, _ := ioutil.TempDir("", "") conf.Root = tempDir - saveHostID(tempDir, "xxx12345678901") + conf.SaveHostID("xxx12345678901") mockHandlers["PUT /api/v0/hosts/xxx12345678901"] = func(req *http.Request) (int, jsonObject) { return 200, jsonObject{ @@ -171,8 +171,8 @@ func TestPrepareWithUpdate(t *testing.T) { } c, _ := Prepare(&conf) - api := c.api - host := c.host + api := c.API + host := c.Host if api.BaseURL.String() != ts.URL { t.Errorf("Apibase mismatch: %s != %s", api.BaseURL, ts.URL) @@ -311,10 +311,10 @@ func TestLoop(t *testing.T) { termCh := make(chan struct{}) exitCh := make(chan int) c := &Context{ - ag: ag, - conf: &conf, - api: api, - host: host, + Agent: ag, + Config: &conf, + API: api, + Host: host, } // Start looping! go func() { diff --git a/config/config.go b/config/config.go index 851fd5c68..d340990aa 100644 --- a/config/config.go +++ b/config/config.go @@ -2,7 +2,10 @@ package config import ( "fmt" + "io/ioutil" + "os" "path/filepath" + "strings" "time" "github.com/BurntSushi/toml" @@ -50,6 +53,9 @@ type Config struct { DeprecatedSensu map[string]PluginConfigs `toml:"sensu"` // DEPRECATED this is for backward compatibility Include string + + // Cannot exist in configuration files + HostIDStorage HostIDStorage } // PluginConfigs represents a set of [plugin..] sections in the configuration file @@ -209,3 +215,83 @@ func includeConfigFile(config *Config, include string) error { return nil } + +func (conf *Config) hostIDStorage() HostIDStorage { + if conf.HostIDStorage == nil { + conf.HostIDStorage = &FileSystemHostIDStorage{Root: conf.Root} + } + return conf.HostIDStorage +} + +// LoadHostID loads the previously saved host id. +func (conf *Config) LoadHostID() (string, error) { + return conf.hostIDStorage().LoadHostID() +} + +// SaveHostID saves the host id, which may be restored by LoadHostID. +func (conf *Config) SaveHostID(id string) error { + return conf.hostIDStorage().SaveHostID(id) +} + +// DeleteSavedHostID deletes the host id saved by SaveHostID. +func (conf *Config) DeleteSavedHostID() error { + return conf.hostIDStorage().DeleteSavedHostID() +} + +// HostIDStorage is an interface which maintains persistency +// of the "Host ID" for the current host where the agent is running on. +// The ID is always generated and given by Mackerel (mackerel.io). +type HostIDStorage interface { + LoadHostID() (string, error) + SaveHostID(id string) error + DeleteSavedHostID() error +} + +// FileSystemHostIDStorage is the default HostIDStorage +// which saves/loads the host id using an id file on the local filesystem. +// The file will be located at /var/lib/mackerel-agent/id by default on linux. +type FileSystemHostIDStorage struct { + Root string +} + +const idFileName = "id" + +// HostIDFile is the location of the host id file. +func (s FileSystemHostIDStorage) HostIDFile() string { + return filepath.Join(s.Root, idFileName) +} + +// LoadHostID loads the current host ID from the mackerel-agent's id file. +func (s FileSystemHostIDStorage) LoadHostID() (string, error) { + content, err := ioutil.ReadFile(s.HostIDFile()) + if err != nil { + return "", err + } + return strings.TrimRight(string(content), "\r\n"), nil +} + +// SaveHostID saves the host ID to the mackerel-agent's id file. +func (s FileSystemHostIDStorage) SaveHostID(id string) error { + err := os.MkdirAll(s.Root, 0755) + if err != nil { + return err + } + + file, err := os.Create(s.HostIDFile()) + if err != nil { + return err + } + defer file.Close() + + _, err = file.Write([]byte(id)) + if err != nil { + return err + } + + return nil +} + +// DeleteSavedHostID deletes the mackerel-agent's id file. +func (s FileSystemHostIDStorage) DeleteSavedHostID() error { + return os.Remove(s.HostIDFile()) +} diff --git a/config/config_test.go b/config/config_test.go index 1ca2c4ea1..38b5cd872 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -241,3 +241,34 @@ command = "bar" assert(t, config.Plugin["metrics"]["foo2"].Command == "foo2", "plugin.metrics.foo2 should exist") assert(t, config.Plugin["metrics"]["bar"].Command == "bar", "plugin.metrics.bar should be overwritten") } + +func TestFileSystemHostIDStorage(t *testing.T) { + root, err := ioutil.TempDir("", "mackerel-agent-test") + if err != nil { + t.Fatal(err) + } + + s := FileSystemHostIDStorage{Root: root} + err = s.SaveHostID("test-host-id") + assertNoError(t, err) + + hostID, err := s.LoadHostID() + assertNoError(t, err) + assert(t, hostID == "test-host-id", "SaveHostID and LoadHostID should preserve the host id") + + err = s.DeleteSavedHostID() + assertNoError(t, err) + + _, err = s.LoadHostID() + assert(t, err != nil, "LoadHostID after DeleteSavedHostID must fail") +} + +func TestConfig_HostIDStorage(t *testing.T) { + conf := Config{ + Root: "test-root", + } + + storage, ok := conf.hostIDStorage().(*FileSystemHostIDStorage) + assert(t, ok, "Default hostIDStorage must be *FileSystemHostIDStorage") + assert(t, storage.Root == "test-root", "FileSystemHostIDStorage must have the same Root of Config") +} diff --git a/main.go b/main.go index bc3893729..a3a037c23 100644 --- a/main.go +++ b/main.go @@ -58,9 +58,11 @@ const mainProcess = "" // subcommands and processes of the mackerel-agent var commands = map[string](func([]string) int){ - mainProcess: doMain, - "version": doVersion, - "retire": doRetire, + mainProcess: doMain, + "version": doVersion, + "retire": doRetire, + "configtest": doConfigtest, + "once": doOnce, } func doVersion(_ []string) int { @@ -69,6 +71,15 @@ func doVersion(_ []string) int { return exitStatusOK } +func doConfigtest(argv []string) int { + conf, otherOpts := resolveConfig(argv) + if conf == nil || otherOpts != nil { + return exitStatusError + } + fmt.Fprintf(os.Stderr, "%s Syntax OK\n", conf.Conffile) + return exitStatusOK +} + func doMain(argv []string) int { conf, otherOpts := resolveConfig(argv) if conf == nil { @@ -89,10 +100,6 @@ func doMain(argv []string) int { return exitStatusOK } - if conf.Apikey == "" { - logger.Criticalf("Apikey must be specified in the command-line flag or in the config file") - return exitStatusError - } return start(conf) } @@ -101,12 +108,8 @@ func doRetire(argv []string) int { if err != nil { return exitStatusError } - if conf.Apikey == "" { - logger.Criticalf("Apikey must be specified in the command-line flag or in the config file") - return exitStatusError - } - hostID, err := command.LoadHostID(conf.Root) + hostID, err := conf.LoadHostID() if err != nil { logger.Warningf("HostID file is not found") return exitStatusError @@ -130,50 +133,70 @@ func doRetire(argv []string) int { } logger.Infof("This host (hostID: %s) has been retired.", hostID) // just to try to remove hostID file. - err = command.RemoveIDFile(conf.Root) + err = conf.DeleteSavedHostID() if err != nil { logger.Warningf("Failed to remove HostID file: %s", err) } return exitStatusOK } -func resolveConfigForRetire(argv []string) (*config.Config, bool, error) { - fs := flag.NewFlagSet("mackerel-agent retire", flag.ExitOnError) - // Allow accepting unnecessary options, pidfile, diagnostic and role. - // Because, these options are potentially passed in initd script by using $OTHER_OPTS. dirty... - var ( - conffile = fs.String("conf", config.DefaultConfig.Conffile, "Config file path (Configs in this file are over-written by command line options)") - apibase = fs.String("apibase", config.DefaultConfig.Apibase, "API base") - _ = fs.String("pidfile", config.DefaultConfig.Pidfile, "(not used in retire)") - root = fs.String("root", config.DefaultConfig.Root, "Directory containing variable state information") - apikey = fs.String("apikey", "", "API key from mackerel.io web site") - force = fs.Bool("force", false, "force retirement without prompting") - _ = fs.Bool("diagnostic", false, "(not used in retire)") - ) - var roleFullnames roleFullnamesFlag - fs.Var(&roleFullnames, "role", "(not used in retire)") - var verbose bool - fs.BoolVar(&verbose, "verbose", config.DefaultConfig.Verbose, "Toggle verbosity") - fs.BoolVar(&verbose, "v", config.DefaultConfig.Verbose, "Toggle verbosity (shorthand)") - fs.Parse(argv) - conf, err := config.LoadConfig(*conffile) - if err != nil { - return nil, *force, err +func doOnce(argv []string) int { + // dirty hack `resolveConfig` required apikey so fill up + argvOpt := append(argv, "-apikey=dummy") + conf, _ := resolveConfig(argvOpt) + if conf == nil { + return exitStatusError } - // overwrite config from file by config from args - fs.Visit(func(f *flag.Flag) { - switch f.Name { - case "apibase": - conf.Apibase = *apibase - case "apikey": - conf.Apikey = *apikey - case "root": - conf.Root = *root - case "verbose", "v": - conf.Verbose = verbose + command.RunOnce(conf) + return exitStatusOK +} + +func printRetireUsage() { + usage := fmt.Sprintf(`Usage of mackerel-agent retire: + -conf string + Config file path (Configs in this file are over-written by command line options) + (default "%s") + -force + force retirement without prompting + -apibase string + API base (default "%s") + -apikey string + API key from mackerel.io web site`, + config.DefaultConfig.Conffile, + config.DefaultConfig.Apibase) + + fmt.Fprintln(os.Stderr, usage) + os.Exit(2) +} + +var helpReg = regexp.MustCompile(`^--?h(?:elp)?$`) +var forceReg = regexp.MustCompile(`^--?force$`) + +func resolveConfigForRetire(argv []string) (*config.Config, bool, error) { + optArgs := []string{} + isForce := false + for _, v := range argv { + if helpReg.MatchString(v) { + printRetireUsage() } - }) - return conf, *force, nil + if forceReg.MatchString(v) { + isForce = true + continue + } + optArgs = append(optArgs, v) + } + conf, otherOpts := resolveConfig(optArgs) + if conf == nil { + printRetireUsage() + } + + if otherOpts != nil { + msg := "can't use -vesion/-once option in retire" + logger.Errorf(msg) + return nil, isForce, fmt.Errorf(msg) + } + + return conf, isForce, nil } // resolveConfig parses command line arguments and loads config file to @@ -187,37 +210,43 @@ func resolveConfig(argv []string) (*config.Config, *otherOptions) { fs := flag.NewFlagSet("mackerel-agent", flag.ExitOnError) var ( - conffile = fs.String("conf", config.DefaultConfig.Conffile, "Config file path (Configs in this file are over-written by command line options)") - apibase = fs.String("apibase", config.DefaultConfig.Apibase, "API base") - pidfile = fs.String("pidfile", config.DefaultConfig.Pidfile, "File containing PID") - root = fs.String("root", config.DefaultConfig.Root, "Directory containing variable state information") - apikey = fs.String("apikey", "", "API key from mackerel.io web site") - diagnostic = fs.Bool("diagnostic", false, "Enables diagnostic features") - runOnce = fs.Bool("once", false, "Show spec and metrics to stdout once") - printVersion = fs.Bool("version", false, "Prints version and exit") + conffile = fs.String("conf", config.DefaultConfig.Conffile, "Config file path (Configs in this file are over-written by command line options)") + apibase = fs.String("apibase", config.DefaultConfig.Apibase, "API base") + pidfile = fs.String("pidfile", config.DefaultConfig.Pidfile, "File containing PID") + root = fs.String("root", config.DefaultConfig.Root, "Directory containing variable state information") + apikey = fs.String("apikey", "", "(DEPRECATED) API key from mackerel.io web site") + diagnostic = fs.Bool("diagnostic", false, "Enables diagnostic features") + verbose bool + roleFullnames roleFullnamesFlag ) - - var verbose bool fs.BoolVar(&verbose, "verbose", config.DefaultConfig.Verbose, "Toggle verbosity") fs.BoolVar(&verbose, "v", config.DefaultConfig.Verbose, "Toggle verbosity (shorthand)") // The value of "role" option is internally "roll fullname", // but we call it "role" here for ease. - var roleFullnames roleFullnamesFlag fs.Var(&roleFullnames, "role", "Set this host's roles (format: :)") + + // flags for otherOpts + var ( + runOnce = fs.Bool("once", false, "(DEPRECATED) Show spec and metrics to stdout once") + printVersion = fs.Bool("version", false, "(DEPRECATED) Prints version and exit") + ) fs.Parse(argv) if *printVersion { otherOptions.printVersion = true + logger.Warningf("-version option is deprecated. use subcommand (`%% mackerel-agent version`) instead") return conf, otherOptions } if *runOnce { otherOptions.runOnce = true + logger.Warningf("-once option is deprecated. use subcommand (`%% mackerel-agent once`) instead") return conf, otherOptions } conf, confErr := config.LoadConfig(*conffile) + conf.Conffile = *conffile if confErr != nil { logger.Criticalf("Failed to load the config file: %s", confErr) return nil, nil @@ -229,6 +258,7 @@ func resolveConfig(argv []string) (*config.Config, *otherOptions) { case "apibase": conf.Apibase = *apibase case "apikey": + logger.Warningf("-apikey option is deprecated. use config file instead") conf.Apikey = *apikey case "pidfile": conf.Pidfile = *pidfile @@ -252,6 +282,11 @@ func resolveConfig(argv []string) (*config.Config, *otherOptions) { } } conf.Roles = r + + if conf.Apikey == "" { + logger.Criticalf("Apikey must be specified in the command-line flag or in the config file") + return nil, nil + } return conf, nil } diff --git a/main_test.go b/main_test.go index a991efdcb..50d0e2fe3 100644 --- a/main_test.go +++ b/main_test.go @@ -112,6 +112,39 @@ func TestDetectForce(t *testing.T) { } } +func TestResolveConfigForRetire(t *testing.T) { + confFile, err := ioutil.TempFile("", "mackerel-config-test") + if err != nil { + t.Fatalf("Could not create temprary config file for test") + } + confFile.WriteString(`apikey="DUMMYAPIKEY" +`) + confFile.Sync() + confFile.Close() + defer os.Remove(confFile.Name()) + + // Allow accepting unnecessary options, pidfile, diagnostic and role. + // Because, these options are potentially passed in initd script by using $OTHER_OPTS. + argv := []string{ + "-conf=" + confFile.Name(), + "-apibase=https://mackerel.io", + "-pidfile=hoge", + "-root=hoge", + "-verbose", + "-diagnostic", + "-apikey=hogege", + "-role=hoge:fuga", + } + + conf, force, err := resolveConfigForRetire(argv) + if force { + t.Errorf("force should be false") + } + if conf.Apikey != "hogege" { + t.Errorf("Apikey should be 'hogege'") + } +} + func TestCreateAndRemovePidFile(t *testing.T) { file, err := ioutil.TempFile("", "") if err != nil { @@ -173,3 +206,65 @@ func TestSignalHandler(t *testing.T) { t.Errorf("Something went wrong") } } + +func TestConfigTestOK(t *testing.T) { + // prepare dummy config + confFile, err := ioutil.TempFile("", "mackerel-config-test") + if err != nil { + t.Fatalf("Could not create temprary config file for test") + } + confFile.WriteString(`apikey="DUMMYAPIKEY" +`) + confFile.Sync() + confFile.Close() + defer os.Remove(confFile.Name()) + + argv := []string{"-conf=" + confFile.Name()} + status := doConfigtest(argv) + + if status != exitStatusOK { + t.Errorf("configtest(ok) must be return exitStatusOK") + } +} + +func TestConfigTestNotFound(t *testing.T) { + // prepare dummy config + confFile, err := ioutil.TempFile("", "mackerel-config-test") + if err != nil { + t.Fatalf("Could not create temprary config file for test") + } + confFile.WriteString(`apikey="DUMMYAPIKEY" +`) + confFile.Sync() + confFile.Close() + defer os.Remove(confFile.Name()) + + argv := []string{"-conf=" + confFile.Name() + "xxx"} + status := doConfigtest(argv) + + if status != exitStatusError { + t.Errorf("configtest(failed) must be return existStatusError") + } +} + +func TestConfigTestInvalidFormat(t *testing.T) { + // prepare dummy config + confFile, err := ioutil.TempFile("", "mackerel-config-test") + if err != nil { + t.Fatalf("Could not create temprary config file for test") + } + confFile.WriteString(`apikey="DUMMYAPIKEY" +[plugin.checks.foo ] +command = "bar" +`) + confFile.Sync() + confFile.Close() + defer os.Remove(confFile.Name()) + + argv := []string{"-conf=" + confFile.Name()} + status := doConfigtest(argv) + + if status != exitStatusError { + t.Errorf("configtest(failed) must be return exitStatusError") + } +} diff --git a/packaging/deb/debian/changelog b/packaging/deb/debian/changelog index 55944e5c1..0c2c98f4a 100644 --- a/packaging/deb/debian/changelog +++ b/packaging/deb/debian/changelog @@ -1,3 +1,34 @@ +mackerel-agent (0.26.2-1) stable; urgency=low + + * output success message to stderr when configtest succeed (by Songmu) + + + -- Songmu Thu, 10 Dec 2015 11:15:21 +0900 + +mackerel-agent (0.26.1-1) stable; urgency=low + + * fix deprecate message (by Songmu) + + + -- Songmu Wed, 09 Dec 2015 15:20:47 +0900 + +mackerel-agent (0.26.0-1) stable; urgency=low + + * Make HostID storage replacable (by motemen) + + * Publicize command.Context's fields (by motemen) + + * Configtest (by fujiwara) + + * Refactor config loading and check if Apikey exists in configtest (by Songmu) + + * fix exit status of debian init script. (by fujiwara) + + * Deprecate version and once option (by Songmu) + + + -- Songmu Tue, 08 Dec 2015 11:17:07 +0900 + mackerel-agent (0.25.1-1) stable; urgency=low * Go 1.5.1 (by Songmu) diff --git a/packaging/deb/debian/mackerel-agent.initd b/packaging/deb/debian/mackerel-agent.initd index d12ab7ad1..7980d4c35 100755 --- a/packaging/deb/debian/mackerel-agent.initd +++ b/packaging/deb/debian/mackerel-agent.initd @@ -39,6 +39,12 @@ do_start() return $? } +do_configtest() +{ + $DAEMON configtest ${APIBASE:+--apibase=$APIBASE} ${APIKEY:+--apikey=$APIKEY} --pidfile=$PIDFILE --root=$ROOT $OTHER_OPTS >>$LOGFILE 2>&1 + return $? +} + do_retire() { $DAEMON retire -force ${APIBASE:+--apibase=$APIBASE} ${APIKEY:+--apikey=$APIKEY} --root=$ROOT $OTHER_OPTS >>$LOGFILE 2>&1 @@ -63,60 +69,75 @@ case "$1" in start) log_daemon_msg "Starting $NAME" do_start - case "$?" in + retval=$? + case "$retval" in 0) log_end_msg 0 ;; - *) log_end_msg 1 ;; + *) log_end_msg 1; exit $retval ;; esac ;; stop) log_daemon_msg "Stopping $DESC" "$NAME" do_stop retval=$? - [ "$AUTO_RETIREMENT" != "" ] && [ "$AUTO_RETIREMENT" != "0" ] && do_retire + [ "$AUTO_RETIREMENT" != "" ] && [ "$AUTO_RETIREMENT" != "0" ] && do_retire || exit $? case "$retval" in 0|1) log_end_msg 0 ;; - *) log_end_msg 1 ;; + *) log_end_msg 1; exit $retval ;; esac ;; status) status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? ;; reload) + do_configtest || exit $? log_daemon_msg "Restarting $DESC" "$NAME" do_stop - case "$?" in + retval=$? + case "$retval" in 0|1) do_start - case "$?" in + retval=$? + case "$retval" in 0) log_end_msg 0 ;; - *) log_end_msg 1 ;; # Failed to start + *) log_end_msg 1; exit $retval ;; # Failed to start esac ;; *) # Failed to stop - log_end_msg 1 + log_end_msg 1; exit $retval ;; esac ;; restart) log_daemon_msg "Restarting $DESC" "$NAME" do_stop - case "$?" in + retval=$? + case "$retval" in 0|1) do_start - case "$?" in + retval=$? + case "$retval" in 0) log_end_msg 0 ;; - *) log_end_msg 1 ;; # Failed to start + *) log_end_msg 1; exit $retval ;; # Failed to start esac ;; *) # Failed to stop - log_end_msg 1 + log_end_msg 1; exit $retval ;; esac ;; + configtest) + log_daemon_msg "Testing configuration of $NAME" + do_configtest + retval=$? + case "$retval" in + 0) log_end_msg 0 ;; + *) log_end_msg 1; exit $retval ;; + esac + ;; *) - echo "Usage: $SCRIPTNAME {start|stop|restart|reload|status}" >&2 + echo "Usage: $SCRIPTNAME {start|stop|restart|reload|status|configtest}" >&2 exit 3 ;; esac diff --git a/packaging/rpm/mackerel-agent.spec b/packaging/rpm/mackerel-agent.spec index 415d6cfd0..aaf9884b4 100644 --- a/packaging/rpm/mackerel-agent.spec +++ b/packaging/rpm/mackerel-agent.spec @@ -5,7 +5,7 @@ %define _localbindir /usr/local/bin Name: mackerel-agent -Version: 0.25.1 +Version: 0.26.2 Release: 1 License: Commercial Summary: mackerel.io agent @@ -71,6 +71,20 @@ fi %{_sysconfdir}/logrotate.d/%{name} %changelog +* Thu Dec 10 2015 - 0.26.2-1 +- output success message to stderr when configtest succeed (by Songmu) + +* Wed Dec 09 2015 - 0.26.1-1 +- fix deprecate message (by Songmu) + +* Tue Dec 08 2015 - 0.26.0-1 +- Make HostID storage replacable (by motemen) +- Publicize command.Context's fields (by motemen) +- Configtest (by fujiwara) +- Refactor config loading and check if Apikey exists in configtest (by Songmu) +- fix exit status of debian init script. (by fujiwara) +- Deprecate version and once option (by Songmu) + * Wed Nov 25 2015 - 0.25.1-1 - Go 1.5.1 (by Songmu) - logging STDERR of checker command (by Songmu) diff --git a/packaging/rpm/src/mackerel-agent.initd b/packaging/rpm/src/mackerel-agent.initd index 407c254b0..c161bc098 100644 --- a/packaging/rpm/src/mackerel-agent.initd +++ b/packaging/rpm/src/mackerel-agent.initd @@ -54,6 +54,24 @@ start() { return 0 } +configtest_q() { + $BIN configtest ${APIBASE:+--apibase=$APIBASE} ${APIKEY:+--apikey=$APIKEY} --pidfile=$PIDFILE --root=$ROOT $OTHER_OPTS >>$LOGFILE 2>&1 +} + +configtest() { + echo -n $"Testing configuration of $prog: " + + configtest_q + if [ "$?" -eq 0 ]; then + success + echo + else + failure + echo + return 1 + fi +} + stop() { echo -n $"Stopping $prog: " @@ -130,6 +148,7 @@ case "$1" in exit $retval ;; reload) + configtest_q || exit 1 restart ;; restart) @@ -138,8 +157,11 @@ case "$1" in status) rh_status ;; + configtest) + configtest + ;; *) - echo $"Usage: $0 {start|stop|restart|reload|status}" + echo $"Usage: $0 {start|stop|restart|reload|status|configtest}" exit 2 esac diff --git a/tool/go-linter b/tool/go-linter new file mode 100755 index 000000000..061f2f5d0 --- /dev/null +++ b/tool/go-linter @@ -0,0 +1,12 @@ +#!/bin/sh + +LINT_RET=.golint.txt +rm -f $LINT_RET +for os in $@; do + if [ $os != "windows" ]; then + GOOS=$os golint ./... | grep -v '_string.go:' | tee -a $LINT_RET + else + GOOS=$os golint --min_confidence=0.9 ./... | grep -v '_string.go:' | tee -a $LINT_RET + fi +done +exec test ! -s $LINT_RET diff --git a/tool/travis/autotag.sh b/tool/travis/autotag.sh index f430abd28..181df07c2 100755 --- a/tool/travis/autotag.sh +++ b/tool/travis/autotag.sh @@ -1,5 +1,5 @@ #!/bin/sh -set -ex +set -e deploykey=~/.ssh/deploy.key