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)
This commit is contained in:
Daniel Wedul 2021-11-12 10:44:33 -07:00 committed by GitHub
parent 363b51e58c
commit 181ba0e41a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 970 additions and 1 deletions

View File

@ -49,6 +49,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* [\#9837](https://github.com/cosmos/cosmos-sdk/issues/9837) `--generate-only` flag will accept the keyname now.
* [\#10326](https://github.com/cosmos/cosmos-sdk/pull/10326) `x/authz` add query all grants by granter query.
* [\#10348](https://github.com/cosmos/cosmos-sdk/pull/10348) Add `fee.{payer,granter}` and `tip` fields to StdSignDoc for signing tipped transactions.
* [\#10379](https://github.com/cosmos/cosmos-sdk/pull/10379) Add validation to `x/upgrade` CLI `software-upgrade` command `--plan-info` value.
### Improvements

18
go.mod
View File

@ -23,6 +23,8 @@ require (
github.com/gorilla/mux v1.8.0
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/grpc-ecosystem/grpc-gateway v1.16.0
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
github.com/hashicorp/go-getter v1.4.1
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d
github.com/hdevalence/ed25519consensus v0.0.0-20210204194344-59a8610d2b87
github.com/improbable-eng/grpc-web v0.15.0
@ -56,10 +58,14 @@ require (
)
require (
cloud.google.com/go v0.93.3 // indirect
cloud.google.com/go/storage v1.10.0 // indirect
filippo.io/edwards25519 v1.0.0-beta.2 // indirect
github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d // indirect
github.com/Workiva/go-datastructures v1.0.52 // indirect
github.com/aws/aws-sdk-go v1.27.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
@ -78,16 +84,21 @@ require (
github.com/go-logfmt/logfmt v0.5.0 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.0.0 // indirect
github.com/google/orderedcode v0.0.1 // indirect
github.com/googleapis/gax-go/v2 v2.1.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/gtank/merlin v0.1.1 // indirect
github.com/gtank/ristretto255 v0.1.2 // indirect
github.com/hashicorp/go-immutable-radix v1.0.0 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // indirect
github.com/hashicorp/go-version v1.2.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jmhodges/levigo v1.0.0 // indirect
github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d // indirect
github.com/klauspost/compress v1.12.3 // indirect
@ -96,6 +107,8 @@ require (
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 // indirect
github.com/minio/highwayhash v1.0.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-testing-interface v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.4.2 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
@ -111,12 +124,17 @@ require (
github.com/subosito/gotenv v1.2.0 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect
github.com/ulikunitz/xz v0.5.5 // indirect
github.com/zondax/hid v0.9.0 // indirect
go.etcd.io/bbolt v1.3.5 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f // indirect
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect
golang.org/x/sys v0.0.0-20210903071746-97244b99971b // indirect
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
golang.org/x/text v0.3.6 // indirect
google.golang.org/api v0.56.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/ini.v1 v1.63.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect

31
go.sum
View File

@ -24,6 +24,7 @@ cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAV
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.93.3 h1:wPBktZFzYBcCZVARvwVKqH1uEj+aLXofJEtrb4oOsio=
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
@ -44,6 +45,7 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
collectd.org v0.3.0/go.mod h1:A/8DzQBkF6abtvrT2j/AU/4tiBgJWYyh0y/oB/4MlWE=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
@ -115,6 +117,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM=
github.com/aws/aws-sdk-go v1.27.0 h1:0xphMHGMLBrPMfxR2AmVjZKcMEESEgWF8Kru94BNByk=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/aws/aws-sdk-go-v2 v1.2.0/go.mod h1:zEQs02YRBw1DjK0PoJv3ygDYOFTre1ejlJWl8FwAuQo=
@ -130,6 +134,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
@ -170,6 +176,7 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -359,6 +366,7 @@ github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4er
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@ -416,9 +424,11 @@ github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSN
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa h1:Q75Upo5UN4JbPFURXZ8nLKYUvF85dyFRop/vQ0Rv+64=
github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/orderedcode v0.0.1 h1:UzfcAexk9Vhv8+9pNOgRu41f16lHq725vPwnSeiG/Us=
github.com/google/orderedcode v0.0.1/go.mod h1:iVyU4/qPKHY5h/wSd6rZZCDcLJNxiWO6dvsYES2Sb20=
@ -443,6 +453,7 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0 h1:6DWmvNpomjL1+3liNSZbVns3zsYzzCjm6pRBO1tLeso=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
@ -489,7 +500,10 @@ github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOj
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-getter v1.4.1 h1:3A2Mh8smGFcf5M+gmcv898mZdrxpseik45IpcyISLsA=
github.com/hashicorp/go-getter v1.4.1/go.mod h1:7qxyCd8rBfcShwsvxgIguu4KbS3l8bUCwg2Umn7RjeY=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
@ -499,11 +513,15 @@ github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo=
github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@ -551,8 +569,11 @@ github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jhump/protoreflect v1.10.1 h1:iH+UZfsbRE6vpyZH7asAjTPWJf7RJbpZ9j/N3lDlKs0=
github.com/jhump/protoreflect v1.10.1/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U=
github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ=
@ -643,6 +664,7 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
@ -657,7 +679,9 @@ github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLT
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
@ -943,6 +967,8 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok=
github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
@ -982,6 +1008,7 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@ -1130,6 +1157,7 @@ golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f h1:Qmd2pbz05z7z6lm0DrgQVVPuBm92jqujBKMHMOlOQEw=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -1357,6 +1385,7 @@ google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtuk
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
google.golang.org/api v0.56.0 h1:08F9XVYTLOGeSQb3xI9C0gXMuQanhdGed0cWFhDozbI=
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -1364,6 +1393,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@ -1455,6 +1485,7 @@ gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=

View File

@ -1,6 +1,9 @@
package cli
import (
"os"
"path/filepath"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
@ -8,12 +11,15 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/gov/client/cli"
gov "github.com/cosmos/cosmos-sdk/x/gov/types"
"github.com/cosmos/cosmos-sdk/x/upgrade/plan"
"github.com/cosmos/cosmos-sdk/x/upgrade/types"
)
const (
FlagUpgradeHeight = "upgrade-height"
FlagUpgradeInfo = "upgrade-info"
FlagNoValidate = "no-validate"
FlagDaemonName = "daemon-name"
)
// GetTxCmd returns the transaction commands for this module
@ -45,6 +51,24 @@ func NewCmdSubmitUpgradeProposal() *cobra.Command {
if err != nil {
return err
}
noValidate, err := cmd.Flags().GetBool(FlagNoValidate)
if err != nil {
return err
}
if !noValidate {
prop := content.(*types.SoftwareUpgradeProposal)
var daemonName string
if daemonName, err = cmd.Flags().GetString(FlagDaemonName); err != nil {
return err
}
var planInfo *plan.Info
if planInfo, err = plan.ParseInfo(prop.Plan.Info); err != nil {
return err
}
if err = planInfo.ValidateFull(daemonName); err != nil {
return err
}
}
from := clientCtx.GetFromAddress()
@ -70,7 +94,9 @@ func NewCmdSubmitUpgradeProposal() *cobra.Command {
cmd.Flags().String(cli.FlagDescription, "", "description of proposal")
cmd.Flags().String(cli.FlagDeposit, "", "deposit of proposal")
cmd.Flags().Int64(FlagUpgradeHeight, 0, "The height at which the upgrade must happen")
cmd.Flags().String(FlagUpgradeInfo, "", "Optional info for the planned upgrade such as commit hash, etc.")
cmd.Flags().String(FlagUpgradeInfo, "", "Info for the upgrade plan such as new version download urls, etc.")
cmd.Flags().Bool(FlagNoValidate, false, "Skip validation of the upgrade info")
cmd.Flags().String(FlagDaemonName, getDefaultDaemonName(), "The name of the executable being upgraded (for upgrade-info validation). Default is the DAEMON_NAME env var if set, or else this executable")
return cmd
}
@ -154,3 +180,15 @@ func parseArgsToContent(cmd *cobra.Command, name string) (gov.Content, error) {
content := types.NewSoftwareUpgradeProposal(title, description, plan)
return content, nil
}
// getDefaultDaemonName gets the default name to use for the daemon.
// If a DAEMON_NAME env var is set, that is used.
// Otherwise, the last part of the currently running executable is used.
func getDefaultDaemonName() string {
// DAEMON_NAME is specifically used here to correspond with the Comsovisor setup env vars.
name := os.Getenv("DAEMON_NAME")
if len(name) == 0 {
_, name = filepath.Split(os.Args[0])
}
return name
}

View File

@ -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
}

View File

@ -0,0 +1,299 @@
package plan
import (
"archive/zip"
"crypto/sha256"
"fmt"
"io"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type DownloaderTestSuite struct {
suite.Suite
// Home is a temporary directory for use in these tests.
// It will have a src/ for things to download.
Home string
}
func (s *DownloaderTestSuite) SetupTest() {
s.Home = s.T().TempDir()
s.Assert().NoError(os.MkdirAll(filepath.Join(s.Home, "src"), 0777), "creating src/ dir")
s.T().Logf("Home: [%s]", s.Home)
}
func TestDownloaderTestSuite(t *testing.T) {
suite.Run(t, new(DownloaderTestSuite))
}
// TestFile represents a file that will be used for a test.
type TestFile struct {
// Name is the relative path and name of the file.
Name string
// Contents is the contents of the file.
Contents []byte
}
func NewTestFile(name, contents string) *TestFile {
return &TestFile{
Name: name,
Contents: []byte(contents),
}
}
// SaveIn saves this TestFile in the given path.
// The full path to the file is returned.
func (f TestFile) SaveIn(path string) (string, error) {
name := filepath.Join(path, f.Name)
file, err := os.Create(name)
if err != nil {
return name, err
}
defer file.Close()
_, err = file.Write(f.Contents)
return name, err
}
// TestZip represents a collection of TestFile objects to be zipped into an archive.
type TestZip []*TestFile
func NewTestZip(testFiles ...*TestFile) TestZip {
tz := make([]*TestFile, len(testFiles))
for i, tf := range testFiles {
tz[i] = tf
}
return tz
}
// SaveAs saves this TestZip at the given path.
func (z TestZip) SaveAs(path string) error {
archive, err := os.Create(path)
if err != nil {
return err
}
defer archive.Close()
zipper := zip.NewWriter(archive)
for _, tf := range z {
zfw, zfwerr := zipper.Create(tf.Name)
if zfwerr != nil {
return zfwerr
}
_, err = zfw.Write(tf.Contents)
if err != nil {
return err
}
}
return zipper.Close()
}
// saveTestZip saves a TestZip in this test's Home/src directory with the given name.
// The full path to the saved archive is returned.
func (s DownloaderTestSuite) saveSrcTestZip(name string, z TestZip) string {
fullName := filepath.Join(s.Home, "src", name)
s.Require().NoError(z.SaveAs(fullName), "saving test zip %s", name)
return fullName
}
// saveSrcTestFile saves a TestFile in this test's Home/src directory.
// The full path to the saved file is returned.
func (s DownloaderTestSuite) saveSrcTestFile(f *TestFile) string {
path := filepath.Join(s.Home, "src")
fullName, err := f.SaveIn(path)
s.Require().NoError(err, "saving test file %s", f.Name)
return fullName
}
// requireFileExistsAndIsExecutable requires that the given file exists and is executable.
func requireFileExistsAndIsExecutable(t *testing.T, path string) {
info, err := os.Stat(path)
require.NoError(t, err, "stat error")
perm := info.Mode().Perm()
// Checks if at least one executable bit is set (user, group, or other)
isExe := perm&0111 != 0
require.True(t, isExe, "is executable: permissions = %s", perm)
}
// requireFileEquals requires that the contents of the file at the given path
// is equal to the contents of the given TestFile.
func requireFileEquals(t *testing.T, path string, tf *TestFile) {
file, err := os.ReadFile(path)
require.NoError(t, err, "reading file")
require.Equal(t, string(tf.Contents), string(file), "file contents")
}
// makeFileUrl converts the given path to a URL with the correct checksum query parameter.
func makeFileURL(t *testing.T, path string) string {
f, err := os.Open(path)
require.NoError(t, err, "opening file")
defer f.Close()
hasher := sha256.New()
_, err = io.Copy(hasher, f)
require.NoError(t, err, "copying file to hasher")
return fmt.Sprintf("file://%s?checksum=sha256:%x", path, hasher.Sum(nil))
}
func (s *DownloaderTestSuite) TestDownloadUpgrade() {
justAFile := NewTestFile("just-a-file", "#!/usr/bin\necho 'I am just a file'\n")
someFileName := "some-file"
someFileInBin := NewTestFile("bin"+someFileName, "#!/usr/bin\necho 'I am some file in bin'\n")
anotherFile := NewTestFile("another-file", "#!/usr/bin\necho 'I am just another file'\n")
justAFilePath := s.saveSrcTestFile(justAFile)
justAFileZip := s.saveSrcTestZip(justAFile.Name+".zip", NewTestZip(justAFile))
someFileInBinZip := s.saveSrcTestZip(someFileInBin.Name+".zip", NewTestZip(someFileInBin))
allFilesZip := s.saveSrcTestZip(anotherFile.Name+".zip", NewTestZip(justAFile, someFileInBin, anotherFile))
getDstDir := func(testName string) string {
_, tName := filepath.Split(testName)
return s.Home + "/dst/" + tName
}
s.T().Run("url does not exist", func(t *testing.T) {
dstRoot := getDstDir(t.Name())
url := "file:///never/gonna/be/a/thing.zip?checksum=sha256:2c22e34510bd1d4ad2343cdc54f7165bccf30caef73f39af7dd1db2795a3da48"
err := DownloadUpgrade(dstRoot, url, "nothing")
require.Error(t, err)
assert.Contains(t, err.Error(), "no such file or directory")
})
s.T().Run("url does not have checksum", func(t *testing.T) {
dstRoot := getDstDir(t.Name())
url := "file://" + justAFilePath
err := DownloadUpgrade(dstRoot, url, justAFile.Name)
require.Error(t, err)
require.Contains(t, err.Error(), "missing checksum query parameter")
})
s.T().Run("url has incorrect checksum", func(t *testing.T) {
dstRoot := getDstDir(t.Name())
badChecksum := "2c22e34510bd1d4ad2343cdc54f7165bccf30caef73f39af7dd1db2795a3da48"
url := "file://" + justAFilePath + "?checksum=sha256:" + badChecksum
err := DownloadUpgrade(dstRoot, url, justAFile.Name)
require.Error(t, err)
assert.Contains(t, err.Error(), "Checksums did not match")
assert.Contains(t, err.Error(), "Expected: "+badChecksum)
})
s.T().Run("url returns single file", func(t *testing.T) {
dstRoot := getDstDir(t.Name())
url := makeFileURL(t, justAFilePath)
err := DownloadUpgrade(dstRoot, url, justAFile.Name)
require.NoError(t, err)
expectedFile := filepath.Join(dstRoot, "bin", justAFile.Name)
requireFileExistsAndIsExecutable(t, expectedFile)
requireFileEquals(t, expectedFile, justAFile)
})
s.T().Run("url returns archive with file in bin", func(t *testing.T) {
dstRoot := getDstDir(t.Name())
url := makeFileURL(t, someFileInBinZip)
err := DownloadUpgrade(dstRoot, url, someFileName)
require.NoError(t, err)
expectedFile := filepath.Join(dstRoot, "bin", someFileName)
requireFileExistsAndIsExecutable(t, expectedFile)
requireFileEquals(t, expectedFile, someFileInBin)
})
s.T().Run("url returns archive with just expected file", func(t *testing.T) {
dstRoot := getDstDir(t.Name())
url := makeFileURL(t, justAFileZip)
err := DownloadUpgrade(dstRoot, url, justAFile.Name)
require.NoError(t, err)
expectedFile := filepath.Join(dstRoot, "bin", justAFile.Name)
requireFileExistsAndIsExecutable(t, expectedFile)
requireFileEquals(t, expectedFile, justAFile)
})
s.T().Run("url returns archive without expected file", func(t *testing.T) {
dstRoot := getDstDir(t.Name())
url := makeFileURL(t, allFilesZip)
err := DownloadUpgrade(dstRoot, url, "not-expected")
require.Error(t, err)
require.Contains(t, err.Error(), "result does not contain a bin/not-expected or not-expected file")
})
}
func (s *DownloaderTestSuite) TestEnsureBinary() {
nonExeName := s.saveSrcTestFile(NewTestFile("non-exe.txt", "Not executable"))
s.Require().NoError(os.Chmod(nonExeName, 0600), "chmod error nonExeName")
isExeName := s.saveSrcTestFile(NewTestFile("is-exe.sh", "#!/bin/bash\necho 'executing'\n"))
s.Require().NoError(os.Chmod(isExeName, 0777), "chmod error isExeName")
s.T().Run("file does not exist", func(t *testing.T) {
name := filepath.Join(s.Home, "does-not-exist.txt")
actual := EnsureBinary(name)
require.Error(t, actual)
})
s.T().Run("file is a directory", func(t *testing.T) {
name := filepath.Join(s.Home, "src")
actual := EnsureBinary(name)
require.EqualError(t, actual, fmt.Sprintf("%s is not a regular file", "src"))
})
s.T().Run("file exists and becomes executable", func(t *testing.T) {
name := nonExeName
actual := EnsureBinary(name)
require.NoError(t, actual, "EnsureBinary error")
requireFileExistsAndIsExecutable(t, name)
})
s.T().Run("file is already executable", func(t *testing.T) {
name := isExeName
actual := EnsureBinary(name)
require.NoError(t, actual, "EnsureBinary error")
requireFileExistsAndIsExecutable(t, name)
})
}
func (s *DownloaderTestSuite) TestDownloadURLWithChecksum() {
planContents := `{"binaries":{"xxx/yyy":"url"}}`
planFile := NewTestFile("plan-info.json", planContents)
planPath := s.saveSrcTestFile(planFile)
planChecksum := fmt.Sprintf("%x", sha256.Sum256(planFile.Contents))
emptyFile := NewTestFile("empty-plan-info.json", "")
emptyPlanPath := s.saveSrcTestFile(emptyFile)
emptyChecksum := fmt.Sprintf("%x", sha256.Sum256(emptyFile.Contents))
s.T().Run("url does not exist", func(t *testing.T) {
url := "file:///never-gonna-be-a-thing?checksum=sha256:2c22e34510bd1d4ad2343cdc54f7165bccf30caef73f39af7dd1db2795a3da48"
_, err := DownloadURLWithChecksum(url)
require.Error(t, err)
assert.Contains(t, err.Error(), "could not download url")
})
s.T().Run("without checksum", func(t *testing.T) {
url := "file://" + planPath
_, err := DownloadURLWithChecksum(url)
require.Error(t, err)
assert.Contains(t, err.Error(), "missing checksum query parameter")
})
s.T().Run("with correct checksum", func(t *testing.T) {
url := "file://" + planPath + "?checksum=sha256:" + planChecksum
actual, err := DownloadURLWithChecksum(url)
require.NoError(t, err)
require.Equal(t, planContents, actual)
})
s.T().Run("with incorrect checksum", func(t *testing.T) {
badChecksum := "2c22e34510bd1d4ad2343cdc54f7165bccf30caef73f39af7dd1db2795a3da48"
url := "file://" + planPath + "?checksum=sha256:" + badChecksum
_, err := DownloadURLWithChecksum(url)
require.Error(t, err)
assert.Contains(t, err.Error(), "Checksums did not match")
assert.Contains(t, err.Error(), "Expected: "+badChecksum)
assert.Contains(t, err.Error(), "Got: "+planChecksum)
})
s.T().Run("plan is empty", func(t *testing.T) {
url := "file://" + emptyPlanPath + "?checksum=sha256:" + emptyChecksum
_, err := DownloadURLWithChecksum(url)
require.Error(t, err)
assert.Contains(t, err.Error(), "no content returned")
})
}

106
x/upgrade/plan/info.go Normal file
View File

@ -0,0 +1,106 @@
package plan
import (
"encoding/json"
"errors"
"fmt"
neturl "net/url"
"os"
"path/filepath"
"regexp"
"strings"
)
// Info is the special structure that the Plan.Info string can be (as json).
type Info struct {
Binaries BinaryDownloadURLMap `json:"binaries"`
}
// BinaryDownloadURLMap is a map of os/architecture stings to a URL where the binary can be downloaded.
type BinaryDownloadURLMap map[string]string
// ParseInfo parses an info string into a map of os/arch strings to URL string.
// If the infoStr is a url, an GET request will be made to it, and its response will be parsed instead.
func ParseInfo(infoStr string) (*Info, error) {
infoStr = strings.TrimSpace(infoStr)
if len(infoStr) == 0 {
return nil, errors.New("plan info must not be blank")
}
// If it's a url, download it and treat the result as the real info.
if _, err := neturl.Parse(infoStr); err == nil {
infoStr, err = DownloadURLWithChecksum(infoStr)
if err != nil {
return nil, err
}
}
// Now, try to parse it into the expected structure.
var planInfo Info
if err := json.Unmarshal([]byte(infoStr), &planInfo); err != nil {
return nil, fmt.Errorf("could not parse plan info: %v", err)
}
return &planInfo, nil
}
// ValidateFull does all possible validation of this Info.
// The provided daemonName is the name of the executable file expected in all downloaded directories.
// It checks that:
// * Binaries.ValidateBasic() doesn't return an error
// * Binaries.CheckURLs(daemonName) doesn't return an error.
// Warning: This is an expensive process. See BinaryDownloadURLMap.CheckURLs for more info.
func (m Info) ValidateFull(daemonName string) error {
if err := m.Binaries.ValidateBasic(); err != nil {
return err
}
if err := m.Binaries.CheckURLs(daemonName); err != nil {
return err
}
return nil
}
// ValidateBasic does stateless validation of this BinaryDownloadURLMap.
// It validates that:
// * This has at least one entry.
// * All entry keys have the format "os/arch" or are "any".
// * All entry values are valid URLs.
// * All URLs contain a checksum query parameter.
func (m BinaryDownloadURLMap) ValidateBasic() error {
// Make sure there's at least one.
if len(m) == 0 {
return errors.New("no \"binaries\" entries found")
}
osArchRx := regexp.MustCompile(`[a-zA-Z0-9]+/[a-zA-Z0-9]+`)
for key, val := range m {
if key != "any" && !osArchRx.MatchString(key) {
return fmt.Errorf("invalid os/arch format in key \"%s\"", key)
}
if err := ValidateIsURLWithChecksum(val); err != nil {
return fmt.Errorf("invalid url \"%s\" in binaries[%s]: %v", val, key, err)
}
}
return nil
}
// CheckURLs checks that all entries have valid URLs that return expected data.
// The provided daemonName is the name of the executable file expected in all downloaded directories.
// Warning: This is an expensive process.
// It will make an HTTP GET request to each URL and download the response.
func (m BinaryDownloadURLMap) CheckURLs(daemonName string) error {
tempDir, err := os.MkdirTemp("", "os-arch-downloads")
if err != nil {
return fmt.Errorf("could not create temp directory: %w", err)
}
defer os.RemoveAll(tempDir)
for osArch, url := range m {
dstRoot := filepath.Join(tempDir, strings.ReplaceAll(osArch, "/", "-"))
if err = DownloadUpgrade(dstRoot, url, daemonName); err != nil {
return fmt.Errorf("error downloading binary for os/arch %s: %v", osArch, err)
}
}
return nil
}

335
x/upgrade/plan/info_test.go Normal file
View File

@ -0,0 +1,335 @@
package plan
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type InfoTestSuite struct {
suite.Suite
// Home is a temporary directory for use in these tests.
Home string
}
func (s *InfoTestSuite) SetupTest() {
s.Home = s.T().TempDir()
s.T().Logf("Home: [%s]", s.Home)
}
func TestInfoTestSuite(t *testing.T) {
suite.Run(t, new(InfoTestSuite))
}
// saveSrcTestFile saves a TestFile in this test's Home/src directory.
// The full path to the saved file is returned.
func (s InfoTestSuite) saveTestFile(f *TestFile) string {
fullName, err := f.SaveIn(s.Home)
s.Require().NoError(err, "saving test file %s", f.Name)
return fullName
}
func (s InfoTestSuite) TestParseInfo() {
goodJSON := `{"binaries":{"os1/arch1":"url1","os2/arch2":"url2"}}`
binariesWrongJSON := `{"binaries":["foo","bar"]}`
binariesWrongValueJSON := `{"binaries":{"os1/arch1":1,"os2/arch2":2}}`
goodJSONPath := s.saveTestFile(NewTestFile("good.json", goodJSON))
binariesWrongJSONPath := s.saveTestFile(NewTestFile("binaries-wrong.json", binariesWrongJSON))
binariesWrongValueJSONPath := s.saveTestFile(NewTestFile("binaries-wrong-value.json", binariesWrongValueJSON))
goodJSONAsInfo := &Info{
Binaries: BinaryDownloadURLMap{
"os1/arch1": "url1",
"os2/arch2": "url2",
},
}
makeInfoStrFuncString := func(val string) func(t *testing.T) string {
return func(t *testing.T) string {
return val
}
}
makeInfoStrFuncURL := func(file string) func(t *testing.T) string {
return func(t *testing.T) string {
return makeFileURL(t, file)
}
}
tests := []struct {
name string
infoStrMaker func(t *testing.T) string
expectedInfo *Info
expectedInError []string
}{
{
name: "json good",
infoStrMaker: makeInfoStrFuncString(goodJSON),
expectedInfo: goodJSONAsInfo,
expectedInError: nil,
},
{
name: "blank string",
infoStrMaker: makeInfoStrFuncString(" "),
expectedInfo: nil,
expectedInError: []string{"plan info must not be blank"},
},
{
name: "json binaries is wrong data type",
infoStrMaker: makeInfoStrFuncString(binariesWrongJSON),
expectedInfo: nil,
expectedInError: []string{"could not parse plan info", "cannot unmarshal array into Go struct field Info.binaries"},
},
{
name: "json wrong data type in binaries value",
infoStrMaker: makeInfoStrFuncString(binariesWrongValueJSON),
expectedInfo: nil,
expectedInError: []string{"could not parse plan info", "cannot unmarshal number into Go struct field Info.binaries"},
},
{
name: "url does not exist",
infoStrMaker: makeInfoStrFuncString("file:///this/file/does/not/exist?checksum=sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"),
expectedInfo: nil,
expectedInError: []string{"could not download url", "file:///this/file/does/not/exist"},
},
{
name: "url good",
infoStrMaker: makeInfoStrFuncURL(goodJSONPath),
expectedInfo: goodJSONAsInfo,
expectedInError: nil,
},
{
name: "url binaries is wrong data type",
infoStrMaker: makeInfoStrFuncURL(binariesWrongJSONPath),
expectedInfo: nil,
expectedInError: []string{"could not parse plan info", "cannot unmarshal array into Go struct field Info.binaries"},
},
{
name: "url wrong data type in binaries value",
infoStrMaker: makeInfoStrFuncURL(binariesWrongValueJSONPath),
expectedInfo: nil,
expectedInError: []string{"could not parse plan info", "cannot unmarshal number into Go struct field Info.binaries"},
},
}
for _, tc := range tests {
s.T().Run(tc.name, func(t *testing.T) {
infoStr := tc.infoStrMaker(t)
actualInfo, actualErr := ParseInfo(infoStr)
if len(tc.expectedInError) > 0 {
require.Error(t, actualErr)
for _, expectedErr := range tc.expectedInError {
assert.Contains(t, actualErr.Error(), expectedErr)
}
} else {
require.NoError(t, actualErr)
}
assert.Equal(t, tc.expectedInfo, actualInfo)
})
}
}
func (s InfoTestSuite) TestInfoValidateFull() {
darwinAMD64File := NewTestFile("darwin_amd64", "#!/usr/bin\necho 'darwin/amd64'\n")
linux386File := NewTestFile("linux_386", "#!/usr/bin\necho 'darwin/amd64'\n")
darwinAMD64Path := s.saveTestFile(darwinAMD64File)
linux386Path := s.saveTestFile(linux386File)
darwinAMD64URL := makeFileURL(s.T(), darwinAMD64Path)
linux386URL := makeFileURL(s.T(), linux386Path)
tests := []struct {
name string
planInfo *Info
errs []string
}{
// Positive test case
{
name: "two good entries",
planInfo: &Info{
Binaries: BinaryDownloadURLMap{
"darwin/amd64": darwinAMD64URL,
"linux/386": linux386URL,
},
},
errs: nil,
},
// a failure from BinaryDownloadURLMap.ValidateBasic
{
name: "empty binaries",
planInfo: &Info{Binaries: BinaryDownloadURLMap{}},
errs: []string{"no \"binaries\" entries found"},
},
// a failure from BinaryDownloadURLMap.CheckURLS
{
name: "url does not exist",
planInfo: &Info{
Binaries: BinaryDownloadURLMap{
"darwin/arm64": "file:///no/such/file/exists/hopefully.zip?checksum=sha256:b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259",
},
},
errs: []string{"error downloading binary", "darwin/arm64", "no such file or directory"},
},
}
for _, tc := range tests {
s.T().Run(tc.name, func(t *testing.T) {
actualErr := tc.planInfo.ValidateFull("daemon")
if len(tc.errs) > 0 {
require.Error(t, actualErr)
for _, expectedErr := range tc.errs {
assert.Contains(t, actualErr.Error(), expectedErr)
}
} else {
require.NoError(t, actualErr)
}
})
}
}
func (s InfoTestSuite) TestBinaryDownloadURLMapValidateBasic() {
addDummyChecksum := func(url string) string {
return url + "?checksum=sha256:b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259"
}
tests := []struct {
name string
urlMap BinaryDownloadURLMap
errs []string
}{
{
name: "empty map",
urlMap: BinaryDownloadURLMap{},
errs: []string{"no \"binaries\" entries found"},
},
{
name: "key with empty string",
urlMap: BinaryDownloadURLMap{
"": addDummyChecksum("https://v1.cosmos.network/sdk"),
},
errs: []string{"invalid os/arch", `""`},
},
{
name: "invalid key format",
urlMap: BinaryDownloadURLMap{
"badkey": addDummyChecksum("https://v1.cosmos.network/sdk"),
},
errs: []string{"invalid os/arch", "badkey"},
},
{
name: "any key is valid",
urlMap: BinaryDownloadURLMap{
"any": addDummyChecksum("https://v1.cosmos.network/sdk"),
},
errs: nil,
},
{
name: "os arch key is valid",
urlMap: BinaryDownloadURLMap{
"darwin/amd64": addDummyChecksum("https://v1.cosmos.network/sdk"),
},
errs: nil,
},
{
name: "not a url",
urlMap: BinaryDownloadURLMap{
"isa/url": addDummyChecksum("https://v1.cosmos.network/sdk"),
"nota/url": addDummyChecksum("https://v1.cosmos.network:not-a-port/sdk"),
},
errs: []string{"invalid url", "nota/url", "invalid port"},
},
{
name: "url without checksum",
urlMap: BinaryDownloadURLMap{
"darwin/amd64": "https://v1.cosmos.network/sdk",
},
errs: []string{"invalid url", "darwin/amd64", "missing checksum query parameter"},
},
{
name: "multiple valid entries but one bad url",
urlMap: BinaryDownloadURLMap{
"any": addDummyChecksum("https://v1.cosmos.network/sdk"),
"darwin/amd64": addDummyChecksum("https://v1.cosmos.network/sdk"),
"darwin/arm64": addDummyChecksum("https://v1.cosmos.network/sdk"),
"windows/bad": addDummyChecksum("https://v1.cosmos.network:not-a-port/sdk"),
"linux/386": addDummyChecksum("https://v1.cosmos.network/sdk"),
},
errs: []string{"invalid url", "windows/bad", "invalid port"},
},
{
name: "multiple valid entries but one bad key",
urlMap: BinaryDownloadURLMap{
"any": addDummyChecksum("https://v1.cosmos.network/sdk"),
"darwin/amd64": addDummyChecksum("https://v1.cosmos.network/sdk"),
"badkey": addDummyChecksum("https://v1.cosmos.network/sdk"),
"darwin/arm64": addDummyChecksum("https://v1.cosmos.network/sdk"),
"linux/386": addDummyChecksum("https://v1.cosmos.network/sdk"),
},
errs: []string{"invalid os/arch", "badkey"},
},
}
for _, tc := range tests {
s.T().Run(tc.name, func(t *testing.T) {
actualErr := tc.urlMap.ValidateBasic()
if len(tc.errs) > 0 {
require.Error(t, actualErr)
for _, expectedErr := range tc.errs {
assert.Contains(t, actualErr.Error(), expectedErr)
}
} else {
require.NoError(t, actualErr)
}
})
}
}
func (s InfoTestSuite) TestBinaryDownloadURLMapCheckURLs() {
darwinAMD64File := NewTestFile("darwin_amd64", "#!/usr/bin\necho 'darwin/amd64'\n")
linux386File := NewTestFile("linux_386", "#!/usr/bin\necho 'darwin/amd64'\n")
darwinAMD64Path := s.saveTestFile(darwinAMD64File)
linux386Path := s.saveTestFile(linux386File)
darwinAMD64URL := makeFileURL(s.T(), darwinAMD64Path)
linux386URL := makeFileURL(s.T(), linux386Path)
tests := []struct {
name string
urlMap BinaryDownloadURLMap
errs []string
}{
{
name: "two good entries",
urlMap: BinaryDownloadURLMap{
"darwin/amd64": darwinAMD64URL,
"linux/386": linux386URL,
},
errs: nil,
},
{
name: "url does not exist",
urlMap: BinaryDownloadURLMap{
"darwin/arm64": "file:///no/such/file/exists/hopefully.zip?checksum=sha256:b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259",
},
errs: []string{"error downloading binary", "darwin/arm64", "no such file or directory"},
},
{
name: "bad checksum",
urlMap: BinaryDownloadURLMap{
"darwin/amd64": "file://" + darwinAMD64Path + "?checksum=sha256:b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259",
},
errs: []string{"error downloading binary", "darwin/amd64", "Checksums did not match", "b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259"},
},
}
for _, tc := range tests {
s.T().Run(tc.name, func(t *testing.T) {
actualErr := tc.urlMap.CheckURLs("daemon")
if len(tc.errs) > 0 {
require.Error(t, actualErr)
for _, expectedErr := range tc.errs {
assert.Contains(t, actualErr.Error(), expectedErr)
}
} else {
require.NoError(t, actualErr)
}
})
}
}