1// Copyright (C) 2020 Storj Labs, Inc.
2// See LICENSE for copying information.
3
4package main
5
6import (
7	"log"
8	"os"
9	"runtime"
10	"strings"
11	"time"
12
13	"github.com/spf13/cobra"
14	"go.uber.org/zap"
15
16	"storj.io/common/errs2"
17	"storj.io/common/fpath"
18	"storj.io/common/identity"
19	"storj.io/common/storj"
20	"storj.io/common/sync2"
21	"storj.io/private/cfgstruct"
22	"storj.io/private/process"
23	"storj.io/private/version"
24	_ "storj.io/storj/private/version" // This attaches version information during release builds.
25	"storj.io/storj/private/version/checker"
26)
27
28const (
29	updaterServiceName = "storagenode-updater"
30	minCheckInterval   = time.Minute
31)
32
33var (
34	// TODO: replace with config value of random bytes in storagenode config.
35	nodeID storj.NodeID
36
37	updaterBinaryPath string
38
39	rootCmd = &cobra.Command{
40		Use:   "storagenode-updater",
41		Short: "Version updater for storage node",
42	}
43	runCmd = &cobra.Command{
44		Use:   "run",
45		Short: "Run the storagenode-updater for storage node",
46		Args:  cobra.OnlyValidArgs,
47		RunE:  cmdRun,
48	}
49	restartCmd = &cobra.Command{
50		Use:   "restart-service <new binary path>",
51		Short: "Restart service with the new binary",
52		Args:  cobra.ExactArgs(1),
53		RunE:  cmdRestart,
54	}
55
56	runCfg struct {
57		Identity identity.Config
58		Version  checker.Config
59
60		BinaryLocation string `help:"the storage node executable binary location" default:"storagenode"`
61		ServiceName    string `help:"storage node OS service name" default:"storagenode"`
62		// deprecated
63		Log string `help:"deprecated, use --log.output" default:""`
64	}
65
66	confDir     string
67	identityDir string
68)
69
70func init() {
71	defaults := cfgstruct.DefaultsFlag(rootCmd)
72	defaultConfDir := fpath.ApplicationDir("storj", "storagenode")
73	defaultIdentityDir := fpath.ApplicationDir("storj", "identity", "storagenode")
74	cfgstruct.SetupFlag(zap.L(), rootCmd, &confDir, "config-dir", defaultConfDir, "main directory for storagenode configuration")
75	cfgstruct.SetupFlag(zap.L(), rootCmd, &identityDir, "identity-dir", defaultIdentityDir, "main directory for storagenode identity credentials")
76
77	rootCmd.AddCommand(runCmd)
78	rootCmd.AddCommand(restartCmd)
79
80	process.Bind(runCmd, &runCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
81	process.Bind(restartCmd, &runCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
82}
83
84func cmdRun(cmd *cobra.Command, args []string) (err error) {
85	if runCfg.Log != "" {
86		if err = openLog(runCfg.Log); err != nil {
87			zap.L().Error("Error creating new logger.", zap.Error(err))
88		}
89	}
90
91	updaterBinaryPath, err = os.Executable()
92	if err != nil {
93		zap.L().Fatal("Unable to find storage node updater binary path.")
94	}
95
96	if !fileExists(runCfg.BinaryLocation) {
97		zap.L().Fatal("Unable to find storage node executable binary.")
98	}
99
100	ident, err := runCfg.Identity.Load()
101	if err != nil {
102		zap.L().Fatal("Error loading identity.", zap.Error(err))
103	}
104	nodeID = ident.ID
105	if nodeID.IsZero() {
106		zap.L().Fatal("Empty node ID.")
107	}
108
109	zap.L().Info("Running on version",
110		zap.String("Service", updaterServiceName),
111		zap.String("Version", version.Build.Version.String()),
112	)
113
114	ctx, _ := process.Ctx(cmd)
115
116	switch {
117	case runCfg.Version.CheckInterval <= 0:
118		err = loopFunc(ctx)
119	case runCfg.Version.CheckInterval < minCheckInterval:
120		zap.L().Error("Check interval below minimum. Overriding it minimum.",
121			zap.Stringer("Check Interval", runCfg.Version.CheckInterval),
122			zap.Stringer("Minimum Check Interval", minCheckInterval),
123		)
124		runCfg.Version.CheckInterval = minCheckInterval
125		fallthrough
126	default:
127		loop := sync2.NewCycle(runCfg.Version.CheckInterval)
128		err = loop.Run(ctx, loopFunc)
129	}
130	if err != nil && !errs2.IsCanceled(err) {
131		log.Fatal(err)
132	}
133
134	return nil
135}
136
137func fileExists(filename string) bool {
138	info, err := os.Stat(filename)
139	if os.IsNotExist(err) {
140		return false
141	}
142	return info.Mode().IsRegular()
143}
144
145func openLog(logPath string) error {
146	if runtime.GOOS == "windows" && !strings.HasPrefix(logPath, "winfile:///") {
147		logPath = "winfile:///" + logPath
148	}
149
150	logger, err := process.NewLoggerWithOutputPaths("storagenode-updater", logPath)
151	if err != nil {
152		return err
153	}
154
155	zap.ReplaceGlobals(logger)
156	return nil
157}
158