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:
parent
ba09911eb6
commit
e7f78b4480
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
136
cosmovisor/cmd/cosmovisor/init.go
Normal file
136
cosmovisor/cmd/cosmovisor/init.go
Normal 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
|
||||
}
|
||||
554
cosmovisor/cmd/cosmovisor/init_test.go
Normal file
554
cosmovisor/cmd/cosmovisor/init_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user