-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add upgrade proposal plan validation to CLI (#10379)
<!-- The default pull request template is for types feat, fix, or refactor. For other templates, add one of the following parameters to the url: - template=docs.md - template=other.md --> ## Description Closes: #10286 When submitting a software upgrade proposal (e.g. `$DAEMON tx gov submit-proposal software-upgrade`) * Validate the plan info by default. * Add flag `--no-validate` to allow skipping that validation. * Add flag `--daemon-name` to designate the executable name (needed for validation). * The daemon name comes first from the `--daemon-name` flag. If that's not provided, it looks for a `DAEMON_NAME` environment variable (to match what's used by Cosmovisor). If that's not set, the name of the currently running executable is used. Things that are validated: * The plan info cannot be empty or blank. * If the plan info is a url: * It must have a `checksum` query parameter. * It must return properly formatted plan info JSON. * The `checksum` is correct. * If the plan info is not a url: * It must be propery formatted plan info JSON. * There is at least one entry in the `binaries` field. * The keys of the `binaries` field are either "any" or in the format of "os/arch". * All URLs contain a `checksum` query parameter. * Each URL contains a usable response. * The `checksum` is correct for each URL. Note: With this change, either a valid `--upgrade-info` will need to be provided, or else `--no-validate` must be provided. If no `--upgrade-info` is given, a validation error is returned. --- ### 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/master/CONTRIBUTING.md#pr-targeting)) - [x] provided a link to the relevant issue or specification - [x] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules) - [x] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing) - [x] added a changelog entry to `CHANGELOG.md` - [x] included comments for [documenting Go code](https://blog.golang.org/godoc) - [ ] updated the relevant documentation or specification - [ ] reviewed "Files changed" and left comments if necessary - [x] 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)
- Loading branch information
1 parent
363b51e
commit 181ba0e
Showing
8 changed files
with
970 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
package plan | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
neturl "net/url" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/hashicorp/go-getter" | ||
) | ||
|
||
// DownloadUpgrade downloads the given url into the provided directory and ensures it's valid. | ||
// The provided url must contain a checksum parameter that matches the file being downloaded. | ||
// If this returns nil, the download was successful, and {dstRoot}/bin/{daemonName} is a regular executable file. | ||
// This is an opinionated directory structure that corresponds with Cosmovisor requirements. | ||
// If the url is not an archive, it is downloaded and saved to {dstRoot}/bin/{daemonName}. | ||
// If the url is an archive, it is downloaded and unpacked to {dstRoot}. | ||
// If the archive does not contain a /bin/{daemonName} file, then this will attempt to move /{daemonName} to /bin/{daemonName}. | ||
// If the archive does not contain either /bin/{daemonName} or /{daemonName}, an error is returned. | ||
// Note: Because a checksum is required, this function cannot be used to download non-archive directories. | ||
// If dstRoot already exists, some or all of its contents might be updated. | ||
func DownloadUpgrade(dstRoot, url, daemonName string) error { | ||
if err := ValidateIsURLWithChecksum(url); err != nil { | ||
return err | ||
} | ||
target := filepath.Join(dstRoot, "bin", daemonName) | ||
// First try to download it as a single file. If there's no error, it's okay and we're done. | ||
if err := getter.GetFile(target, url); err != nil { | ||
// If it was a checksum error, no need to try as directory. | ||
if _, ok := err.(*getter.ChecksumError); ok { | ||
return err | ||
} | ||
// File download didn't work, try it as an archive. | ||
if err = downloadUpgradeAsArchive(dstRoot, url, daemonName); err != nil { | ||
// Out of options, send back the error. | ||
return err | ||
} | ||
} | ||
return EnsureBinary(target) | ||
} | ||
|
||
// downloadUpgradeAsArchive tries to download the given url as an archive. | ||
// The archive is unpacked and saved in dstDir. | ||
// If the archive contains /{daemonName} and not /bin/{daemonName}, then /{daemonName} will be moved to /bin/{daemonName}. | ||
// If this returns nil, the download was successful, and {dstDir}/bin/{daemonName} is a regular executable file. | ||
func downloadUpgradeAsArchive(dstDir, url, daemonName string) error { | ||
err := getter.Get(dstDir, url) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// If bin/{daemonName} exists, we're done. | ||
dstDirBinFile := filepath.Join(dstDir, "bin", daemonName) | ||
err = EnsureBinary(dstDirBinFile) | ||
if err == nil { | ||
return nil | ||
} | ||
|
||
// Otherwise, check for a root {daemonName} file and move it to the bin/ directory if found. | ||
dstDirFile := filepath.Join(dstDir, daemonName) | ||
err = EnsureBinary(dstDirFile) | ||
if err == nil { | ||
err = os.Rename(dstDirFile, dstDirBinFile) | ||
if err != nil { | ||
return fmt.Errorf("could not move %s to the bin directory: %w", daemonName, err) | ||
} | ||
return nil | ||
} | ||
|
||
return fmt.Errorf("url \"%s\" result does not contain a bin/%s or %s file", url, daemonName, daemonName) | ||
} | ||
|
||
// EnsureBinary checks that the given file exists as a regular file and is executable. | ||
// An error is returned if: | ||
// - The file does not exist. | ||
// - The path exists, but is one of: Dir, Symlink, NamedPipe, Socket, Device, CharDevice, or Irregular. | ||
// - The file exists, is not executable by all three of User, Group, and Other, and cannot be made executable. | ||
func EnsureBinary(path string) error { | ||
info, err := os.Stat(path) | ||
if err != nil { | ||
return err | ||
} | ||
if !info.Mode().IsRegular() { | ||
_, f := filepath.Split(path) | ||
return fmt.Errorf("%s is not a regular file", f) | ||
} | ||
// Make sure all executable bits are set. | ||
oldMode := info.Mode().Perm() | ||
newMode := oldMode | 0111 // Set the three execute bits to on (a+x). | ||
if oldMode != newMode { | ||
return os.Chmod(path, newMode) | ||
} | ||
return nil | ||
} | ||
|
||
// DownloadURLWithChecksum gets the contents of the given url, ensuring the checksum is correct. | ||
// The provided url must contain a checksum parameter that matches the file being downloaded. | ||
// If there isn't an error, the content returned by the url will be returned as a string. | ||
// Returns an error if: | ||
// - The url is not a URL or does not contain a checksum parameter. | ||
// - Downloading the URL fails. | ||
// - The checksum does not match what is returned by the URL. | ||
// - The URL does not return a regular file. | ||
// - The downloaded file is empty or only whitespace. | ||
func DownloadURLWithChecksum(url string) (string, error) { | ||
if err := ValidateIsURLWithChecksum(url); err != nil { | ||
return "", err | ||
} | ||
tempDir, err := os.MkdirTemp("", "reference") | ||
if err != nil { | ||
return "", fmt.Errorf("could not create temp directory: %w", err) | ||
} | ||
defer os.RemoveAll(tempDir) | ||
tempFile := filepath.Join(tempDir, "content") | ||
if err = getter.GetFile(tempFile, url); err != nil { | ||
return "", fmt.Errorf("could not download url \"%s\": %w", url, err) | ||
} | ||
tempFileBz, rerr := os.ReadFile(tempFile) | ||
if rerr != nil { | ||
return "", fmt.Errorf("could not read downloaded temporary file: %w", rerr) | ||
} | ||
tempFileStr := strings.TrimSpace(string(tempFileBz)) | ||
if len(tempFileStr) == 0 { | ||
return "", fmt.Errorf("no content returned by \"%s\"", url) | ||
} | ||
return tempFileStr, nil | ||
} | ||
|
||
// ValidateIsURLWithChecksum checks that the given string is a url and contains a checksum query parameter. | ||
func ValidateIsURLWithChecksum(urlStr string) error { | ||
url, err := neturl.Parse(urlStr) | ||
if err != nil { | ||
return err | ||
} | ||
if len(url.Query().Get("checksum")) == 0 { | ||
return errors.New("missing checksum query parameter") | ||
} | ||
return nil | ||
} |
Oops, something went wrong.