feat(cosmovisor): Create Cosmovisor init command. (#12464)

## Description

Closes: #12456 

Creates an `init` command in `cosmovisor` that initializes the `DAEMON_HOME` directory with the initial executable and current link.

---

### Author Checklist

*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*

I have...

- [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] ~~added `!` to the type prefix if API or client breaking change~~ _N/A_
- [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#pr-targeting))
- [x] provided a link to the relevant issue or specification
- [ ] ~~followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/main/docs/building-modules)~~ _N/A_
- [x] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#testing)
- [x] added a changelog entry to `CHANGELOG.md`
- [x] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [x] updated the relevant documentation or specification
- [x] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed

### Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

I have...

- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed 
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
This commit is contained in:
Daniel Wedul 2022-07-06 13:35:39 -06:00 committed by GitHub
parent ba09911eb6
commit e7f78b4480
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 726 additions and 2 deletions

View File

@ -39,6 +39,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
### Features
* [\#12464](https://github.com/cosmos/cosmos-sdk/pull/12464) Create the `cosmovisor init` command.
* [\#12188](https://github.com/cosmos/cosmos-sdk/pull/12188) Add a `DAEMON_RESTART_DELAY` for allowing a node operator to define a delay between the node halt (for upgrade) and backup.
* [\#11823](https://github.com/cosmos/cosmos-sdk/pull/11823) Refactor `cosmovisor` CLI to use `cobra`.
* [\#11731](https://github.com/cosmos/cosmos-sdk/pull/11731) `cosmovisor version -o json` returns the cosmovisor version and the result of `simd --output json --long` in one JSON object.

View File

@ -2,6 +2,22 @@
`cosmovisor` is a small process manager for Cosmos SDK application binaries that monitors the governance module for incoming chain upgrade proposals. If it sees a proposal that gets approved, `cosmovisor` can automatically download the new binary, stop the current binary, switch from the old binary to the new one, and finally restart the node with the new binary.
<!-- TOC -->
- [Design](#design)
- [Contributing](#contributing)
- [Setup](#setup)
- [Installation](#installation)
- [Command Line Arguments And Environment Variables](#command-line-arguments-and-environment-variables)
- [Folder Layout](#folder-layout)
- [Usage](#usage)
- [Initialization](#initialization)
- [Detecting Upgrades](#detecting-upgrades)
- [Auto-Download](#auto-download)
- [Example: SimApp Upgrade](#example-simapp-upgrade)
- [Chain Setup](#chain-setup)
## Design
Cosmovisor is designed to be used as a wrapper for a `Cosmos SDK` app:
@ -115,8 +131,10 @@ The system administrator is responsible for:
* installing the `cosmovisor` binary
* configuring the host's init system (e.g. `systemd`, `launchd`, etc.)
* appropriately setting the environmental variables
* manually installing the `genesis` folder
* manually installing the `upgrades/<name>` folders
* creating the `<DAEMON_HOME>/cosmovisor` directory
* creating the `<DAEMON_HOME>/cosmovisor/genesis/bin` folder
* creating the `<DAEMON_HOME>/cosmovisor/upgrades/<name>/bin` folders
* placing the different versions of the `<DAEMON_NAME>` executable in the appropriate `bin` folders.
`cosmovisor` will set the `current` link to point to `genesis` at first start (i.e. when no `current` link exists) and then handle switching binaries at the correct points in time so that the system administrator can prepare days in advance and relax at upgrade time.
@ -124,6 +142,21 @@ In order to support downloadable binaries, a tarball for each upgrade binary wil
The `DAEMON` specific code and operations (e.g. tendermint config, the application db, syncing blocks, etc.) all work as expected. The application binaries' directives such as command-line flags and environment variables also work as expected.
### Initialization
The `cosmovisor init <path to executable>` command creates the folder structure required for using cosmovisor.
It does the following:
* creates the `<DAEMON_HOME>/cosmovisor` folder if it doesn't yet exist
* creates the `<DAEMON_HOME>/cosmovisor/genesis/bin` folder if it doesn't yet exist
* copies the provided executable file to `<DAEMON_HOME>/cosmovisor/genesis/bin/<DAEMON_NAME>`
* creates the `current` link, pointing to the `genesis` folder
It uses the `DAEMON_HOME` and `DAEMON_NAME` environment variables for folder location and executable name.
The `cosmovisor init` command is specifically for initializing cosmovisor, and should not be confused with a chain's `init` command (e.g. `cosmovisor run init`).
### Detecting Upgrades
`cosmovisor` is polling the `$DAEMON_HOME/data/upgrade-info.json` file for new upgrade instructions. The file is created by the x/upgrade module in `BeginBlocker` when an upgrade is detected and the blockchain reaches the upgrade height.

View File

@ -0,0 +1,136 @@
package main
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/cosmovisor"
cverrors "github.com/cosmos/cosmos-sdk/cosmovisor/errors"
)
func init() {
rootCmd.AddCommand(initCmd)
}
var initCmd = &cobra.Command{
Use: "init <path to executable>",
Short: "Initializes a cosmovisor daemon home directory.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
logger := cmd.Context().Value(cosmovisor.LoggerKey).(*zerolog.Logger)
return InitializeCosmovisor(logger, args)
},
}
// InitializeCosmovisor initializes the cosmovisor directories, current link, and initial executable.
func InitializeCosmovisor(logger *zerolog.Logger, args []string) error {
if len(args) < 1 || len(args[0]) == 0 {
return errors.New("no <path to executable> provided")
}
pathToExe := args[0]
switch exeInfo, err := os.Stat(pathToExe); {
case os.IsNotExist(err):
return fmt.Errorf("executable file not found: %w", err)
case err != nil:
return fmt.Errorf("could not stat executable: %w", err)
case exeInfo.IsDir():
return errors.New("invalid path to executable: must not be a directory")
}
cfg, err := getConfigForInitCmd()
if err != nil {
return err
}
logger.Info().Msg("checking on the genesis/bin directory")
genBinExe := cfg.GenesisBin()
genBinDir, _ := filepath.Split(genBinExe)
genBinDir = filepath.Clean(genBinDir)
switch genBinDirInfo, genBinDirErr := os.Stat(genBinDir); {
case os.IsNotExist(genBinDirErr):
logger.Info().Msgf("creating directory (and any parents): %q", genBinDir)
mkdirErr := os.MkdirAll(genBinDir, 0o755)
if mkdirErr != nil {
return mkdirErr
}
case genBinDirErr != nil:
return fmt.Errorf("error getting info on genesis/bin directory: %w", genBinDirErr)
case !genBinDirInfo.IsDir():
return fmt.Errorf("the path %q already exists but is not a directory", genBinDir)
default:
logger.Info().Msgf("the %q directory already exists", genBinDir)
}
logger.Info().Msg("checking on the genesis/bin executable")
if _, err = os.Stat(genBinExe); os.IsNotExist(err) {
logger.Info().Msgf("copying executable into place: %q", genBinExe)
if cpErr := copyFile(pathToExe, genBinExe); cpErr != nil {
return cpErr
}
} else {
logger.Info().Msgf("the %q file already exists", genBinExe)
}
logger.Info().Msgf("making sure %q is executable", genBinExe)
if err = cosmovisor.MarkExecutable(genBinExe); err != nil {
return err
}
if err = cosmovisor.EnsureBinary(genBinExe); err != nil {
return err
}
logger.Info().Msg("checking on the current symlink and creating it if needed")
cur, curErr := cfg.CurrentBin()
if curErr != nil {
return curErr
}
logger.Info().Msgf("the current symlink points to: %q", cur)
return nil
}
// getConfigForInitCmd gets just the configuration elements needed to initialize cosmovisor.
func getConfigForInitCmd() (*cosmovisor.Config, error) {
var errs []error
// Note: Not using GetConfigFromEnv here because that checks that the directories already exist.
// We also don't care about the rest of the configuration stuff in here.
cfg := &cosmovisor.Config{
Home: os.Getenv(cosmovisor.EnvHome),
Name: os.Getenv(cosmovisor.EnvName),
}
if len(cfg.Name) == 0 {
errs = append(errs, fmt.Errorf("%s is not set", cosmovisor.EnvName))
}
switch {
case len(cfg.Home) == 0:
errs = append(errs, fmt.Errorf("%s is not set", cosmovisor.EnvHome))
case !filepath.IsAbs(cfg.Home):
errs = append(errs, fmt.Errorf("%s must be an absolute path", cosmovisor.EnvHome))
}
if len(errs) > 0 {
return nil, cverrors.FlattenErrors(errs...)
}
return cfg, nil
}
// copyFile copies the file at the given source to the given destination.
func copyFile(source, destination string) error {
// assume we already know that src exists and is a regular file.
src, err := os.Open(source)
if err != nil {
return err
}
defer src.Close()
dst, err := os.Create(destination)
if err != nil {
return err
}
defer dst.Close()
_, err = io.Copy(dst, src)
return err
}

View File

@ -0,0 +1,554 @@
package main
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"testing"
"time"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/cosmos/cosmos-sdk/cosmovisor"
)
type InitTestSuite struct {
suite.Suite
}
func TestInitTestSuite(t *testing.T) {
suite.Run(t, new(InitTestSuite))
}
// cosmovisorInitEnv are some string values of environment variables used to configure Cosmovisor, and used by the init command.
type cosmovisorInitEnv struct {
Home string
Name string
}
// ToMap creates a map of the cosmovisorInitEnv where the keys are the env var names.
func (c cosmovisorInitEnv) ToMap() map[string]string {
return map[string]string{
cosmovisor.EnvHome: c.Home,
cosmovisor.EnvName: c.Name,
}
}
// Set sets the field in this cosmovisorInitEnv corresponding to the provided envVar to the given envVal.
func (c *cosmovisorInitEnv) Set(envVar, envVal string) {
switch envVar {
case cosmovisor.EnvHome:
c.Home = envVal
case cosmovisor.EnvName:
c.Name = envVal
default:
panic(fmt.Errorf("Unknown environment variable [%s]. Cannot set field to [%s]. ", envVar, envVal))
}
}
// clearEnv clears environment variables and returns what they were.
// Designed to be used like this:
// initialEnv := clearEnv()
// defer setEnv(nil, initialEnv)
func (s *InitTestSuite) clearEnv() *cosmovisorInitEnv {
s.T().Logf("Clearing environment variables.")
rv := cosmovisorInitEnv{}
for envVar := range rv.ToMap() {
rv.Set(envVar, os.Getenv(envVar))
s.Require().NoError(os.Unsetenv(envVar))
}
return &rv
}
// setEnv sets environment variables to the values provided.
// If t is not nil, and there's a problem, the test will fail immediately.
// If t is nil, problems will just be logged using s.T().
func (s *InitTestSuite) setEnv(t *testing.T, env *cosmovisorInitEnv) {
if t == nil {
s.T().Logf("Restoring environment variables.")
}
for envVar, envVal := range env.ToMap() {
var err error
var msg string
if len(envVal) != 0 {
err = os.Setenv(envVar, envVal)
msg = fmt.Sprintf("setting %s to %s", envVar, envVal)
} else {
err = os.Unsetenv(envVar)
msg = fmt.Sprintf("unsetting %s", envVar)
}
switch {
case t != nil:
require.NoError(t, err, msg)
case err != nil:
s.T().Logf("error %s: %v", msg, err)
default:
s.T().Logf("done %s", msg)
}
}
}
var _ io.Reader = BufferedPipe{}
var _ io.Writer = BufferedPipe{}
// BufferedPipe contains a connected read/write pair of files (a pipe),
// and a buffer of what goes through it that is populated in the background.
type BufferedPipe struct {
// Name is a string to help humans identify this BufferedPipe.
Name string
// Reader is the reader end of the pipe.
Reader *os.File
// Writer is the writer end of the pipe.
Writer *os.File
// BufferReader is the reader used by this BufferedPipe while buffering.
// If this BufferedPipe is not replicating to anything, it will be the same as the Reader.
// Otherwise, it will be a reader encapsulating all desired replication.
BufferReader io.Reader
// Error is the last error encountered by this BufferedPipe.
Error error
// buffer is the channel used to communicate buffer contents.
buffer chan []byte
// stated is true if this BufferedPipe has been started.
started bool
}
// 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}
p.Reader, p.Writer, p.Error = os.Pipe()
if p.Error != nil {
return p, p.Error
}
p.BufferReader = p.Reader
p.AddReplicationTo(replicateTo...)
return p, nil
}
// StartNewBufferedPipe creates a new BufferedPipe and starts it.
//
// This is functionally equivalent to:
// p, _ := NewBufferedPipe(name, replicateTo...)
// p.Start()
func StartNewBufferedPipe(name string, replicateTo ...io.Writer) (BufferedPipe, error) {
p, err := NewBufferedPipe(name, replicateTo...)
if err != nil {
return p, err
}
p.Start()
return p, nil
}
// AddReplicationTo adds replication of this buffered pipe to the provided writers.
//
// Panics if this BufferedPipe is already started.
func (p *BufferedPipe) AddReplicationTo(writers ...io.Writer) {
p.panicIfStarted("cannot add further replication")
for _, writer := range writers {
p.BufferReader = io.TeeReader(p.BufferReader, writer)
}
}
// Start initiates buffering in a background process.
//
// Panics if this BufferedPipe is already started.
func (p *BufferedPipe) Start() {
p.panicIfStarted("cannot restart")
p.buffer = make(chan []byte)
go func() {
var b bytes.Buffer
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
}
// IsStarted returns true if this BufferedPipe has already been started.
func (p *BufferedPipe) IsStarted() bool {
return p.started
}
// IsBuffering returns true if this BufferedPipe has started buffering and has not yet been collected.
func (p *BufferedPipe) IsBuffering() bool {
return p.buffer != nil
}
// Collect closes this pipe's writer then blocks, returning with the final buffer contents once available.
// If Collect() has previously been called on this BufferedPipe, an empty byte slice is returned.
//
// Panics if this BufferedPipe has not been started.
func (p *BufferedPipe) Collect() []byte {
if !p.started {
panic("buffered pipe " + p.Name + " has not been started: cannot collect")
}
_ = p.Writer.Close()
if p.buffer == nil {
return []byte{}
}
rv := <-p.buffer
p.buffer = nil
return rv
}
// Read implements the io.Reader interface on this BufferedPipe.
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) {
return p.Writer.Write(bz)
}
// Close makes sure the files in this BufferedPipe are closed.
func (p *BufferedPipe) Close() {
_ = p.Reader.Close()
_ = p.Writer.Close()
}
// panicIfStarted panics if this BufferedPipe has been started.
func (p *BufferedPipe) panicIfStarted(msg string) {
if p.started {
panic("buffered pipe " + p.Name + " already started: " + msg)
}
}
// NewCapturingLogger creates a buffered stdout pipe, and a logger that uses it.
func (s *InitTestSuite) NewCapturingLogger() (*BufferedPipe, *zerolog.Logger) {
bufferedStdOut, err := StartNewBufferedPipe("stdout", os.Stdout)
s.Require().NoError(err, "creating stdout buffered pipe")
output := zerolog.ConsoleWriter{Out: bufferedStdOut, TimeFormat: time.RFC3339Nano}
logger := zerolog.New(output).With().Str("module", "cosmovisor").Timestamp().Logger()
return &bufferedStdOut, &logger
}
// CreateHelloWorld creates a shell script that outputs HELLO WORLD.
// It will have the provided filemode and be in a freshly made temp directory.
// The returned string is the full path to the new file.
func (s *InitTestSuite) CreateHelloWorld(filemode os.FileMode) string {
tmpDir := s.T().TempDir()
tmpExe := filepath.Join(tmpDir, "hello-world.sh")
tmpExeBz := []byte(`#!/bin/sh
echo 'HELLO WORLD'
`)
s.Require().NoError(os.WriteFile(tmpExe, tmpExeBz, filemode))
return tmpExe
}
func (s *InitTestSuite) TestInitializeCosmovisorNegativeValidation() {
initEnv := s.clearEnv()
defer s.setEnv(nil, initEnv)
tmpExe := s.CreateHelloWorld(0o755)
tmpDir := s.T().TempDir()
tests := []struct {
name string
env cosmovisorInitEnv
args []string
inErr []string
}{
{
name: "no args",
env: cosmovisorInitEnv{Home: "/example", Name: "foo"},
args: []string{},
inErr: []string{"no <path to executable> provided"},
},
{
name: "one empty arg",
env: cosmovisorInitEnv{Home: "/example", Name: "foo"},
args: []string{""},
inErr: []string{"no <path to executable> provided"},
},
{
name: "exe not found",
env: cosmovisorInitEnv{Home: "/example", Name: "foo"},
args: []string{filepath.Join(tmpDir, "not-gonna-find-me")},
inErr: []string{"executable file not found", "not-gonna-find-me"},
},
{
name: "exe is a dir",
env: cosmovisorInitEnv{Home: "/example", Name: "foo"},
args: []string{tmpDir},
inErr: []string{"invalid path to executable: must not be a directory"},
},
{
name: "no name",
env: cosmovisorInitEnv{Home: "/example", Name: ""},
args: []string{tmpExe},
inErr: []string{cosmovisor.EnvName + " is not set"},
},
{
name: "no home",
env: cosmovisorInitEnv{Home: "", Name: "foo"},
args: []string{tmpExe},
inErr: []string{cosmovisor.EnvHome + " is not set"},
},
{
name: "home is relative",
env: cosmovisorInitEnv{Home: "./home", Name: "foo"},
args: []string{tmpExe},
inErr: []string{cosmovisor.EnvHome + " must be an absolute path"},
},
{
name: "no name and no home",
env: cosmovisorInitEnv{Home: "", Name: ""},
args: []string{tmpExe},
inErr: []string{cosmovisor.EnvName + " is not set", cosmovisor.EnvHome + " is not set"},
},
}
for _, tc := range tests {
s.T().Run(tc.name, func(t *testing.T) {
s.setEnv(t, &tc.env)
buffer, logger := s.NewCapturingLogger()
err := InitializeCosmovisor(logger, tc.args)
require.Error(t, err)
for _, exp := range tc.inErr {
require.ErrorContains(t, err, exp)
}
// And make sure there wasn't any log output.
// Log output indicates that work is being done despite validation errors.
outputBz := buffer.Collect()
outputStr := string(outputBz)
require.Equal(t, "", outputStr, "log output")
})
}
}
func (s *InitTestSuite) TestInitializeCosmovisorInvalidExisting() {
initEnv := s.clearEnv()
defer s.setEnv(nil, initEnv)
hwExe := s.CreateHelloWorld(0o755)
s.T().Run("genesis bin is not a directory", func(t *testing.T) {
testDir := t.TempDir()
env := &cosmovisorInitEnv{
Home: filepath.Join(testDir, "home"),
Name: "pear",
}
genDir := filepath.Join(env.Home, "cosmovisor", "genesis")
genBin := filepath.Join(genDir, "bin")
require.NoError(t, os.MkdirAll(genDir, 0o755), "creating genesis directory")
require.NoError(t, copyFile(hwExe, genBin), "copying exe to genesis/bin")
s.setEnv(t, env)
logger := zerolog.Nop()
expErr := fmt.Sprintf("the path %q already exists but is not a directory", genBin)
err := InitializeCosmovisor(&logger, []string{hwExe})
require.EqualError(t, err, expErr, "invalid path to executable: must not be a directory", "calling InitializeCosmovisor")
})
s.T().Run("the EnsureBinary test fails", func(t *testing.T) {
testDir := t.TempDir()
env := &cosmovisorInitEnv{
Home: filepath.Join(testDir, "home"),
Name: "grapes",
}
// Create the genesis bin executable path fully as a directory (instead of a file).
// That should get through all the other stuff, but error when EnsureBinary is called.
genBinExe := filepath.Join(env.Home, "cosmovisor", "genesis", "bin", env.Name)
require.NoError(t, os.MkdirAll(genBinExe, 0o755))
expErr := fmt.Sprintf("%s is not a regular file", env.Name)
// Check the log messages just to make sure it's erroring where expecting.
expInLog := []string{
"checking on the genesis/bin directory",
"checking on the genesis/bin executable",
fmt.Sprintf("the %q file already exists", genBinExe),
fmt.Sprintf("making sure %q is executable", genBinExe),
}
expNotInLog := []string{
"checking on the current symlink and creating it if needed",
"the current symlink points to",
}
s.setEnv(t, env)
buffer, logger := s.NewCapturingLogger()
logger.Info().Msgf("Calling InitializeCosmovisor: %s", t.Name())
err := InitializeCosmovisor(logger, []string{hwExe})
require.EqualError(t, err, expErr, "calling InitializeCosmovisor")
bufferBz := buffer.Collect()
bufferStr := string(bufferBz)
for _, exp := range expInLog {
assert.Contains(t, bufferStr, exp, "expected log statement")
}
for _, notExp := range expNotInLog {
assert.NotContains(t, bufferStr, notExp, "unexpected log statement")
}
})
s.T().Run("current already exists as a file", func(t *testing.T) {
testDir := t.TempDir()
env := &cosmovisorInitEnv{
Home: filepath.Join(testDir, "home"),
Name: "orange",
}
rootDir := filepath.Join(env.Home, "cosmovisor")
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().Msgf("Calling InitializeCosmovisor: %s", t.Name())
err := InitializeCosmovisor(logger, []string{hwExe})
require.EqualError(t, err, expErr, "calling InitializeCosmovisor")
bufferBz := buffer.Collect()
bufferStr := string(bufferBz)
assert.Contains(t, bufferStr, "checking on the current symlink and creating it if needed")
})
// Failure cases not tested:
// Cannot create genesis bin directory
// I had a test for this that created the `genesis` directory with permissions 0o555.
// I also tried it where it would create the directory at the root of the file system.
// In both cases, the test worked as expected locally, but not on the github runners. So it was removed.
// Given executable is not readable
// I had a test for this that created the executable with permissions 0o311.
// The test worked as expected locally, but not on the github runners. So it was removed.
// Cannot get info on the genesis bin directory.
// Not sure how to create a thing that will return
// an error other than a NotExists error when stat is called on it.
// Cannot write to genesis bin dir
// I had a test for this that created the bin dir with permissions 0o555.
// The test worked as expected locally, but not on the github runners. So it was removed.
// Cannot make the copied file executable.
// Probably need another user for this.
// Create the genesis bin file first, using the other user, and set permissions to 600.
}
func (s *InitTestSuite) TestInitializeCosmovisorValid() {
initEnv := s.clearEnv()
defer s.setEnv(nil, initEnv)
hwNonExe := s.CreateHelloWorld(0o644)
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"),
Name: "blank",
}
curLn := filepath.Join(env.Home, "cosmovisor", "current")
genBinDir := filepath.Join(env.Home, "cosmovisor", "genesis", "bin")
genBinExe := filepath.Join(genBinDir, env.Name)
expInLog := []string{
"checking on the genesis/bin directory",
fmt.Sprintf("creating directory (and any parents): %q", genBinDir),
"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),
}
s.setEnv(s.T(), env)
buffer, logger := s.NewCapturingLogger()
logger.Info().Msgf("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(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) {
assert.True(t, exeInfo.Mode().IsRegular(), "executable is regular file")
// Check if the world-executable bit is set.
exePermMask := exeInfo.Mode().Perm() & 0o001
assert.NotEqual(t, 0, exePermMask, "executable mask")
}
bufferBz := buffer.Collect()
bufferStr := string(bufferBz)
for _, exp := range expInLog {
assert.Contains(t, bufferStr, exp)
}
})
s.T().Run("genesis and upgrades exist but no current", func(t *testing.T) {
testDir := s.T().TempDir()
env := &cosmovisorInitEnv{
Home: filepath.Join(testDir, "home"),
Name: "nocur",
}
rootDir := filepath.Join(env.Home, "cosmovisor")
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")
upgradesDir := filepath.Join(rootDir, "upgrades")
for i := 1; i <= 5; i++ {
upgradeBinDir := filepath.Join(upgradesDir, fmt.Sprintf("upgrade-%02d", i), "bin")
upgradeBinDirExe := filepath.Join(upgradeBinDir, env.Name)
require.NoErrorf(t, os.MkdirAll(upgradeBinDir, 0o755), "Making upgrade %d bin dir", i)
require.NoErrorf(t, copyFile(hwExe, upgradeBinDirExe), "copying executable to upgrade %d", i)
}
expInLog := []string{
"checking on the genesis/bin directory",
fmt.Sprintf("the %q directory already exists", genBinDir),
"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),
}
s.setEnv(t, env)
buffer, logger := s.NewCapturingLogger()
logger.Info().Msgf("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 {
assert.Contains(t, bufferStr, exp)
}
})
s.T().Run("genesis bin dir exists empty", func(t *testing.T) {
testDir := s.T().TempDir()
env := &cosmovisorInitEnv{
Home: filepath.Join(testDir, "home"),
Name: "emptygen",
}
rootDir := filepath.Join(env.Home, "cosmovisor")
genBinDir := filepath.Join(rootDir, "genesis", "bin")
genBinExe := filepath.Join(genBinDir, env.Name)
require.NoError(t, os.MkdirAll(genBinDir, 0o755), "making genesis bin dir")
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),
}
s.setEnv(t, env)
buffer, logger := s.NewCapturingLogger()
logger.Info().Msgf("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 {
assert.Contains(t, bufferStr, exp)
}
})
}