Skip to content

Commit

Permalink
feat(tools/cosmovisor): create current symlink as relative
Browse files Browse the repository at this point in the history
Signed-off-by: Artur Troian <[email protected]>
  • Loading branch information
troian committed Oct 4, 2024
1 parent 080ff34 commit f4bb7c1
Show file tree
Hide file tree
Showing 9 changed files with 391 additions and 175 deletions.
1 change: 1 addition & 0 deletions tools/cosmovisor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Improvements

* [#21891](https://github.com/cosmos/cosmos-sdk/pull/21891) create `current` symlink as relative
* [#21462](https://github.com/cosmos/cosmos-sdk/pull/21462) Pass `stdin` to binary.

### Features
Expand Down
39 changes: 20 additions & 19 deletions tools/cosmovisor/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,19 @@ func (cfg *Config) UpgradeInfoFilePath() string {

// SymLinkToGenesis creates a symbolic link from "./current" to the genesis directory.
func (cfg *Config) SymLinkToGenesis() (string, error) {
genesis := filepath.Join(cfg.Root(), genesisDir)
link := filepath.Join(cfg.Root(), currentLink)
// workdir is set to cosmovisor directory so relative
// symlinks are getting resolved correctly
if err := os.Symlink(genesisDir, currentLink); err != nil {
return "", err
}

if err := os.Symlink(genesis, link); err != nil {
res, err := filepath.EvalSymlinks(cfg.GenesisBin())
if err != nil {
return "", err
}

// and return the genesis binary
return cfg.GenesisBin(), nil
return res, nil
}

// WaitRestartDelay will block and wait until the RestartDelay has elapsed.
Expand All @@ -134,27 +139,24 @@ func (cfg *Config) WaitRestartDelay() {
// This will resolve the symlink to the underlying directory to make it easier to debug
func (cfg *Config) CurrentBin() (string, error) {
cur := filepath.Join(cfg.Root(), currentLink)

// if nothing here, fallback to genesis
info, err := os.Lstat(cur)
if err != nil {
// Create symlink to the genesis
return cfg.SymLinkToGenesis()
}
// if it is there, ensure it is a symlink
if info.Mode()&os.ModeSymlink == 0 {
info, err := os.Lstat(cur)
if err != nil || (info.Mode()&os.ModeSymlink == 0) {
// Create symlink to the genesis
return cfg.SymLinkToGenesis()
}

// resolve it
dest, err := os.Readlink(cur)
res, err := filepath.EvalSymlinks(cur)
if err != nil {
// Create symlink to the genesis
return cfg.SymLinkToGenesis()
}

// and return the binary
binpath := filepath.Join(dest, "bin", cfg.Name)
binpath := filepath.Join(res, "bin", cfg.Name)

return binpath, nil
}

Expand Down Expand Up @@ -385,24 +387,23 @@ func (cfg *Config) SetCurrentUpgrade(u upgradetypes.Plan) (rerr error) {
}

// set a symbolic link
link := filepath.Join(cfg.Root(), currentLink)
safeName := url.PathEscape(u.Name)
upgrade := filepath.Join(cfg.Root(), upgradesDir, safeName)
upgrade := filepath.Join(upgradesDir, safeName)

// remove link if it exists
if _, err := os.Stat(link); err == nil {
if err := os.Remove(link); err != nil {
if _, err := os.Stat(currentLink); err == nil {
if err := os.Remove(currentLink); err != nil {
return fmt.Errorf("failed to remove existing link: %w", err)
}
}

// point to the new directory
if err := os.Symlink(upgrade, link); err != nil {
if err := os.Symlink(upgrade, currentLink); err != nil {
return fmt.Errorf("creating current symlink: %w", err)
}

cfg.currentUpgrade = u
f, err := os.Create(filepath.Join(upgrade, upgradetypes.UpgradeInfoFilename))
f, err := os.Create(filepath.Join(cfg.Root(), upgrade, upgradetypes.UpgradeInfoFilename))
if err != nil {
return err
}
Expand Down
58 changes: 30 additions & 28 deletions tools/cosmovisor/args_test.go

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion tools/cosmovisor/cmd/cosmovisor/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"cosmossdk.io/x/upgrade/plan"
)

func NewIntCmd() *cobra.Command {
func NewInitCmd() *cobra.Command {
initCmd := &cobra.Command{
Use: "init <path to executable>",
Short: "Initialize a cosmovisor daemon home directory.",
Expand Down Expand Up @@ -93,6 +93,12 @@ func InitializeCosmovisor(logger log.Logger, args []string) error {
return err
}

// set current working directory to $DAEMON_NAME/cosmosvisor
// to allow current symlink to be relative
if err = os.Chdir(cfg.Root()); err != nil {
return fmt.Errorf("failed to change directory to %s: %w", cfg.Root(), err)
}

logger.Info("checking on the current symlink and creating it if needed")
cur, curErr := cfg.CurrentBin()
if curErr != nil {
Expand Down
109 changes: 71 additions & 38 deletions tools/cosmovisor/cmd/cosmovisor/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ func (s *InitTestSuite) readStdInpFromFile(data []byte) {
}

var (
_ io.Reader = BufferedPipe{}
_ io.Writer = BufferedPipe{}
_ io.Reader = &BufferedPipe{}
_ io.Writer = &BufferedPipe{}
)

// BufferedPipe contains a connected read/write pair of files (a pipe),
Expand Down Expand Up @@ -167,8 +167,8 @@ type BufferedPipe struct {
// NewBufferedPipe creates a new BufferedPipe with the given name.
// Files must be closed once you are done with them (e.g. with .Close()).
// Once ready, buffering must be started using .Start(). See also StartNewBufferedPipe.
func NewBufferedPipe(name string, replicateTo ...io.Writer) (BufferedPipe, error) {
p := BufferedPipe{Name: name}
func NewBufferedPipe(name string, replicateTo ...io.Writer) (*BufferedPipe, error) {
p := &BufferedPipe{Name: name}
p.Reader, p.Writer, p.Error = os.Pipe()
if p.Error != nil {
return p, p.Error
Expand All @@ -184,7 +184,7 @@ func NewBufferedPipe(name string, replicateTo ...io.Writer) (BufferedPipe, error
//
// p, _ := NewBufferedPipe(name, replicateTo...)
// p.Start()
func StartNewBufferedPipe(name string, replicateTo ...io.Writer) (BufferedPipe, error) {
func StartNewBufferedPipe(name string, replicateTo ...io.Writer) (*BufferedPipe, error) {
p, err := NewBufferedPipe(name, replicateTo...)
if err != nil {
return p, err
Expand Down Expand Up @@ -214,6 +214,7 @@ func (p *BufferedPipe) Start() {
if _, p.Error = io.Copy(&b, p.BufferReader); p.Error != nil {
b.WriteString("buffer error: " + p.Error.Error())
}

p.buffer <- b.Bytes()
}()
p.started = true
Expand All @@ -238,6 +239,7 @@ func (p *BufferedPipe) Collect() []byte {
panic("buffered pipe " + p.Name + " has not been started: cannot collect")
}
_ = p.Writer.Close()

if p.buffer == nil {
return []byte{}
}
Expand All @@ -247,12 +249,12 @@ func (p *BufferedPipe) Collect() []byte {
}

// Read implements the io.Reader interface on this BufferedPipe.
func (p BufferedPipe) Read(bz []byte) (n int, err error) {
func (p *BufferedPipe) Read(bz []byte) (n int, err error) {
return p.Reader.Read(bz)
}

// Write implements the io.Writer interface on this BufferedPipe.
func (p BufferedPipe) Write(bz []byte) (n int, err error) {
func (p *BufferedPipe) Write(bz []byte) (n int, err error) {
return p.Writer.Write(bz)
}

Expand All @@ -274,7 +276,7 @@ func (s *InitTestSuite) NewCapturingLogger() (*BufferedPipe, log.Logger) {
bufferedStdOut, err := StartNewBufferedPipe("stdout", os.Stdout)
s.Require().NoError(err, "creating stdout buffered pipe")
logger := log.NewLogger(bufferedStdOut, log.ColorOption(false), log.TimeFormatOption(time.RFC3339Nano)).With(log.ModuleKey, cosmovisorDirName)
return &bufferedStdOut, logger
return bufferedStdOut, logger
}

// CreateHelloWorld creates a shell script that outputs HELLO WORLD.
Expand Down Expand Up @@ -443,15 +445,13 @@ func (s *InitTestSuite) TestInitializeCosmovisorInvalidExisting() {
rootDir := filepath.Join(env.Home, cosmovisorDirName)
require.NoError(t, os.MkdirAll(rootDir, 0o755))
curLn := filepath.Join(rootDir, "current")
genDir := filepath.Join(rootDir, "genesis")
require.NoError(t, copyFile(hwExe, curLn))
expErr := fmt.Sprintf("symlink %s %s: file exists", genDir, curLn)

s.setEnv(t, env)
buffer, logger := s.NewCapturingLogger()
logger.Info(fmt.Sprintf("Calling InitializeCosmovisor: %s", t.Name()))
err := InitializeCosmovisor(logger, []string{hwExe})
require.EqualError(t, err, expErr, "calling InitializeCosmovisor")
require.EqualError(t, err, "symlink genesis current: file exists", "calling InitializeCosmovisor")
bufferBz := buffer.Collect()
bufferStr := string(bufferBz)
assert.Contains(t, bufferStr, "checking on the current symlink and creating it if needed")
Expand Down Expand Up @@ -484,37 +484,43 @@ func (s *InitTestSuite) TestInitializeCosmovisorValid() {
hwExe := s.CreateHelloWorld(0o755)

s.T().Run("starting with blank slate", func(t *testing.T) {
testDir := s.T().TempDir()
env := &cosmovisorInitEnv{
Home: filepath.Join(testDir, "home"),
env := s.prepareConfig(s.T(), cosmovisorInitEnv{
Name: "blank",
}
})

curLn := filepath.Join(env.Home, cosmovisorDirName, "current")
genBinDir := filepath.Join(env.Home, cosmovisorDirName, "genesis", "bin")
genBinExe := filepath.Join(genBinDir, env.Name)

s.setEnv(s.T(), env)
buffer, logger := s.NewCapturingLogger()
logger.Info(fmt.Sprintf("Calling InitializeCosmovisor: %s", t.Name()))
err := InitializeCosmovisor(logger, []string{hwNonExe})
require.NoError(t, err, "calling InitializeCosmovisor")

genDir := filepath.Join(env.Home, cosmovisorDirName, "genesis", "bin")
genBinExe := filepath.Join(genDir, env.Name)

genBinDirEval, err := filepath.EvalSymlinks(genDir)
require.NoError(t, err)

genBinEvalExe := filepath.Join(genBinDirEval, env.Name)

expInLog := []string{
"checking on the genesis/bin directory",
fmt.Sprintf("creating directory (and any parents): %q", genBinDir),
fmt.Sprintf("creating directory (and any parents): %q", genDir),
"checking on the genesis/bin executable",
fmt.Sprintf("copying executable into place: %q", genBinExe),
fmt.Sprintf("making sure %q is executable", genBinExe),
"checking on the current symlink and creating it if needed",
fmt.Sprintf("the current symlink points to: %q", genBinExe),
fmt.Sprintf("the current symlink points to: %q", genBinEvalExe),
fmt.Sprintf("cosmovisor config.toml created at: %s", filepath.Join(env.Home, cosmovisorDirName, cfgFileWithExt)),
}

s.setEnv(s.T(), env)
buffer, logger := s.NewCapturingLogger()
logger.Info(fmt.Sprintf("Calling InitializeCosmovisor: %s", t.Name()))
err := InitializeCosmovisor(logger, []string{hwNonExe})
require.NoError(t, err, "calling InitializeCosmovisor")

_, err = os.Stat(genBinDir)
assert.NoErrorf(t, err, "statting the genesis bin dir: %q", genBinDir)
_, err = os.Stat(genBinDirEval)
assert.NoErrorf(t, err, "statting the genesis bin dir: %q", genBinDirEval)
_, err = os.Stat(curLn)
assert.NoError(t, err, "statting the current link: %q", curLn)
exeInfo, exeErr := os.Stat(genBinExe)
if assert.NoError(t, exeErr, "statting the executable: %q", genBinExe) {
exeInfo, exeErr := os.Stat(genBinEvalExe)
if assert.NoError(t, exeErr, "statting the executable: %q", genBinEvalExe) {
assert.True(t, exeInfo.Mode().IsRegular(), "executable is regular file")
// Check if the world-executable bit is set.
exePermMask := exeInfo.Mode().Perm() & 0o001
Expand All @@ -534,10 +540,18 @@ func (s *InitTestSuite) TestInitializeCosmovisorValid() {
Name: "nocur",
}
rootDir := filepath.Join(env.Home, cosmovisorDirName)

genBinDir := filepath.Join(rootDir, "genesis", "bin")
genBinDirExe := filepath.Join(genBinDir, env.Name)

require.NoError(t, os.MkdirAll(genBinDir, 0o755), "making genesis bin dir")
require.NoError(t, copyFile(hwExe, genBinDirExe), "copying executable to genesis")

genBinDirEval, err := filepath.EvalSymlinks(genBinDir)
require.NoError(t, err)

genBinEvalExe := filepath.Join(genBinDirEval, env.Name)

upgradesDir := filepath.Join(rootDir, "upgrades")
for i := 1; i <= 5; i++ {
upgradeBinDir := filepath.Join(upgradesDir, fmt.Sprintf("upgrade-%02d", i), "bin")
Expand All @@ -552,14 +566,14 @@ func (s *InitTestSuite) TestInitializeCosmovisorValid() {
"checking on the genesis/bin executable",
fmt.Sprintf("the %q file already exists", genBinDirExe),
fmt.Sprintf("making sure %q is executable", genBinDirExe),
fmt.Sprintf("the current symlink points to: %q", genBinDirExe),
fmt.Sprintf("the current symlink points to: %q", genBinEvalExe),
fmt.Sprintf("cosmovisor config.toml created at: %s", filepath.Join(env.Home, cosmovisorDirName, cfgFileWithExt)),
}

s.setEnv(t, env)
buffer, logger := s.NewCapturingLogger()
logger.Info(fmt.Sprintf("Calling InitializeCosmovisor: %s", t.Name()))
err := InitializeCosmovisor(logger, []string{hwExe})
err = InitializeCosmovisor(logger, []string{hwExe})
require.NoError(t, err, "calling InitializeCosmovisor")
bufferBz := buffer.Collect()
bufferStr := string(bufferBz)
Expand All @@ -579,21 +593,27 @@ func (s *InitTestSuite) TestInitializeCosmovisorValid() {
genBinExe := filepath.Join(genBinDir, env.Name)
require.NoError(t, os.MkdirAll(genBinDir, 0o755), "making genesis bin dir")

s.setEnv(t, env)
buffer, logger := s.NewCapturingLogger()
logger.Info(fmt.Sprintf("Calling InitializeCosmovisor: %s", t.Name()))
err := InitializeCosmovisor(logger, []string{hwExe})
require.NoError(t, err, "calling InitializeCosmovisor")

genBinDirEval, err := filepath.EvalSymlinks(genBinDir)
require.NoError(t, err)

genBinEvalExe := filepath.Join(genBinDirEval, env.Name)

expInLog := []string{
"checking on the genesis/bin directory",
fmt.Sprintf("the %q directory already exists", genBinDir),
"checking on the genesis/bin executable",
fmt.Sprintf("copying executable into place: %q", genBinExe),
fmt.Sprintf("making sure %q is executable", genBinExe),
fmt.Sprintf("the current symlink points to: %q", genBinExe),
fmt.Sprintf("the current symlink points to: %q", genBinEvalExe),
fmt.Sprintf("cosmovisor config.toml created at: %s", filepath.Join(env.Home, cosmovisorDirName, cfgFileWithExt)),
}

s.setEnv(t, env)
buffer, logger := s.NewCapturingLogger()
logger.Info(fmt.Sprintf("Calling InitializeCosmovisor: %s", t.Name()))
err := InitializeCosmovisor(logger, []string{hwExe})
require.NoError(t, err, "calling InitializeCosmovisor")
bufferBz := buffer.Collect()
bufferStr := string(bufferBz)
for _, exp := range expInLog {
Expand Down Expand Up @@ -693,7 +713,9 @@ func (s *InitTestSuite) TestInitializeCosmovisorWithOverrideCfg() {
// read the config file
cfgFile, err := os.Open(tc.cfg.DefaultCfgPath())
require.NoError(t, err)
defer cfgFile.Close()
defer func() {
_ = cfgFile.Close()
}()

err = toml.NewDecoder(cfgFile).Decode(cfg)
require.NoError(t, err)
Expand All @@ -708,3 +730,14 @@ func (s *InitTestSuite) TestInitializeCosmovisorWithOverrideCfg() {
})
}
}

func (s *InitTestSuite) prepareConfig(t *testing.T, config cosmovisorInitEnv) *cosmovisorInitEnv {
t.Helper()

config.Home = s.T().TempDir()

err := os.Chdir(config.Home)
require.NoError(t, err)

return &config
}
2 changes: 1 addition & 1 deletion tools/cosmovisor/cmd/cosmovisor/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func NewRootCmd() *cobra.Command {
}

rootCmd.AddCommand(
NewIntCmd(),
NewInitCmd(),
runCmd,
configCmd,
NewVersionCmd(),
Expand Down
7 changes: 7 additions & 0 deletions tools/cosmovisor/cmd/cosmovisor/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"os"
"strings"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -39,6 +40,12 @@ func run(cfgPath string, args []string, options ...RunOption) error {
opt(&runCfg)
}

// set current working directory to $DAEMON_NAME/cosmosvisor
// to allow current symlink to be relative
if err = os.Chdir(cfg.Root()); err != nil {
return err
}

logger := cfg.Logger(runCfg.StdOut)
launcher, err := cosmovisor.NewLauncher(logger, cfg)
if err != nil {
Expand Down
Loading

0 comments on commit f4bb7c1

Please sign in to comment.