-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from teamscanworks/feat/goldenimage
Implement Golden Test Image + Initial API Server & Client
- Loading branch information
Showing
23 changed files
with
737 additions
and
416 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,3 +4,5 @@ breaker-cli | |
config.yaml | ||
|
||
keyring-test/ | ||
|
||
**/*.txt |
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 |
---|---|---|
@@ -1,8 +1,6 @@ | ||
# Breaker | ||
|
||
`breaker` functions as a basic service for the `x/circuit` module, facilitating circuit breaker capabilities. Using a HTTP API payloads can be submitted to an endpoint that can be used to trip or reset circuits. | ||
|
||
Given that `breaker` is limited by the functionality present in `x/circuit`, the ability to gate access to module request urls is limited to an allowed/denied list that applies to all addresses. | ||
`breaker` functions as a basic service for the `x/circuit` module, facilitating circuit breaker capabilities, exposing the ability to trip and reset circuits via a HTTP API. | ||
|
||
# Features | ||
|
||
|
@@ -11,75 +9,25 @@ Given that `breaker` is limited by the functionality present in `x/circuit`, the | |
* Fetch statistical information (disabled commands, etc..) | ||
* JWT authentication | ||
* YAML based configuration | ||
* Basic keyring management for cosmos-sdk | ||
|
||
# Dependency Management | ||
|
||
Until `compass` is publicly released, managing dependencies for `breaker` requires marking the compass repository as a private module, which can be done in on the following ways: | ||
|
||
* running `go env GOPRIVATE=github.com/teamscanworks/compass` | ||
* running `export GOPRIVATE=github.com/teamscanworks/compass` | ||
* Service specific keyring | ||
|
||
# Usage | ||
|
||
## Build CLI | ||
For usage related documentation please consult the (docs folder)[./docs/README.md] | ||
|
||
To build the CLI run `make build`, which outputs an executable `breaker-cli` in the current directory. | ||
|
||
## Populate Configuration File | ||
|
||
To populate the config file with a default configuration suitable for further customization run: | ||
|
||
```shell | ||
$> ./breaker-cli config new | ||
``` | ||
|
||
## Initialize Keyring | ||
|
||
When initializing the configuration file for the first time it is recommended that you create a new mnemonic which will be used to facilitate the actual signing of transactions. | ||
|
||
To do so run the following command, which avoids logging the mnemonic phrase by using `fmt.Println` instead. | ||
|
||
|
||
```shell | ||
$> ./breaker-cli config new-key --create.mnemonic | ||
{"level":"info","ts":1688684424.1259928,"logger":"compass","caller":"[email protected]/client.go:92","msg":"initialized client"} | ||
{"level":"warn","ts":1688684424.1260705,"logger":"breaker.client","caller":"breakerclient/breakerclient.go:94","msg":"no keys found, you should create at least one"} | ||
{"level":"info","ts":1688684424.126079,"caller":"cli/cli.go:216","msg":"creating mnemonic"} | ||
Enter keyring passphrase (attempt 1/3): | ||
Re-enter keyring passphrase: | ||
mnemonic icon concert service unusual wonder observe radar flock other lunch antique patch company snack gravity invest hurt seek card mercy point gadget legal violin | ||
``` | ||
|
||
## Display Active Keypair | ||
|
||
To display the active keypair that is used for signing transactions run the following command, making sure the address has appropriate permissions for using the `x/circuit` module | ||
|
||
```shell | ||
$> ./breaker-cli config list-active-keypair | ||
{"level":"info","ts":1688684300.6084576,"logger":"compass","caller":"[email protected]/client.go:92","msg":"initialized client"} | ||
Enter keyring passphrase (attempt 1/3): | ||
{"level":"info","ts":1688684302.6615007,"logger":"breaker.client","caller":"breakerclient/breakerclient.go:96","msg":"configured from address","from.address":"cosmos10d2kehl8ss0q5yn90hk4rw3fxk8nfug4saczwv"} | ||
{"level":"info","ts":1688684302.6652818,"caller":"cli/cli.go:183","msg":"found active keypair","address":"cosmos10d2kehl8ss0q5yn90hk4rw3fxk8nfug4saczwv"} | ||
``` | ||
|
||
## Running The API Server | ||
## Running Tests | ||
|
||
After populating the configuration file you can start the API server as follows. You will be prompted to enter a password to decrypt the keyring that was previously configured. | ||
Due to the usage of `x/circuit`, a special test environment needs to be prepared in order to accurately run all tests. This can be done by running the following commands anytime you want to start from a fresh golden image: | ||
|
||
```shell | ||
$> ./breaker-cli api start 11:27:31 | ||
{"level":"info","ts":1688668051.7295012,"logger":"compass","caller":"[email protected]/client.go:92","msg":"initialized client"} | ||
Enter keyring passphrase (attempt 1/3): | ||
{"level":"info","ts":1688668053.498679,"logger":"breaker.client","caller":"breakerclient/breakerclient.go:97","msg":"configured from address","from.address":"cosmos18q2gyed58368mmrkz3k30s6kyrx0p4wrykals7"} | ||
$> make reset-simd | ||
$> make start-simd | ||
$> ./scripts/submit_prop.sh | ||
$> ./scripts/submit_votes.sh # this sleeps for about 70 seconds to allow gov proposal to pass | ||
``` | ||
|
||
## Running Tests | ||
|
||
To create a fresh test environment run `make reset-simd`. After this you can run `make start-simd` which will start a simd environment designed for basic testing of `breaker` and running unit tests. | ||
After the above commands have completed you may now now run all unit tests: | ||
|
||
```shell | ||
$> make reset-simd | ||
$> make start-simd | ||
# open new terminal window | ||
$> make test | ||
$> make test | ||
``` |
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,135 @@ | ||
package api | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
"net/http" | ||
|
||
"cosmossdk.io/x/circuit/types" | ||
) | ||
|
||
type APIClient struct { | ||
hc *http.Client | ||
url string | ||
jwt string | ||
} | ||
|
||
// Returns a new client for usage with the breaker api. | ||
// Requires providing a valid JWT that has been issued, which can be done via the cli | ||
// | ||
// NOTE: JWT is not required for the `/status` api calls | ||
// | ||
// TODO: add a way of acquiring/renewing JWT via api | ||
func NewAPIClient(url string, jwt string) APIClient { | ||
return APIClient{ | ||
hc: http.DefaultClient, | ||
url: url, | ||
jwt: jwt, | ||
} | ||
} | ||
|
||
// Returns all commands which have had a circuit tripped | ||
func (ac *APIClient) DisabledCommands() (*types.DisabledListResponse, error) { | ||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/v1/status/list/disabledCommands", ac.url), &bytes.Buffer{}) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to construct http request %s", err) | ||
} | ||
res, err := ac.hc.Do(req) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to send http request %s", err) | ||
} | ||
var resp types.DisabledListResponse | ||
data, err := ioutil.ReadAll(res.Body) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to read http response body %s", err) | ||
} | ||
if err = resp.Unmarshal(data); err != nil { | ||
return nil, fmt.Errorf("failed to deserialize http response body %s", err) | ||
} | ||
return &resp, nil | ||
} | ||
|
||
// Returns all accounts that have been granted some form of permission with the circuit breaker module | ||
func (ac *APIClient) Accounts() (*types.AccountsResponse, error) { | ||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/v1/status/list/accounts", ac.url), &bytes.Buffer{}) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to construct http request %s", err) | ||
} | ||
res, err := ac.hc.Do(req) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to send http request %s", err) | ||
} | ||
var resp types.AccountsResponse | ||
data, err := ioutil.ReadAll(res.Body) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to read http response body %s", err) | ||
} | ||
if err = resp.Unmarshal(data); err != nil { | ||
return nil, fmt.Errorf("failed to deserialize http response body %s", err) | ||
} | ||
return &resp, nil | ||
} | ||
|
||
// Trips a circuit, preventing access to the given urls, emitting the `message` via system logs | ||
func (ac *APIClient) TripCircuit(urls []string, message string) (*Response, error) { | ||
payload := PayloadV1{ | ||
Urls: urls, | ||
Message: message, | ||
Operation: MODE_TRIP, | ||
} | ||
data, err := json.Marshal(&payload) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to serialize payload %s", err) | ||
} | ||
buffer := bytes.NewBuffer(data) | ||
req, err := http.NewRequest("POST", fmt.Sprintf("%s/v1/webhook", ac.url), buffer) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to construct http request %s", err) | ||
} | ||
req.Header.Set("Authorization", fmt.Sprintf("Bearer: %s", ac.jwt)) | ||
res, err := ac.hc.Do(req) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to send http request %s", err) | ||
} | ||
return ac.unmarshalResponse(res.Body) | ||
} | ||
|
||
// Resets a circuit, allowing access to the given urls, emitting the `message` via system logs | ||
func (ac *APIClient) ResetCircuit(urls []string, message string) (*Response, error) { | ||
payload := PayloadV1{ | ||
Urls: urls, | ||
Message: message, | ||
Operation: MODE_RESET, | ||
} | ||
data, err := json.Marshal(&payload) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to serialize payload %s", err) | ||
} | ||
buffer := bytes.NewBuffer(data) | ||
req, err := http.NewRequest("POST", fmt.Sprintf("%s/v1/webhook", ac.url), buffer) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to construct http request %s", err) | ||
} | ||
req.Header.Set("Authorization", fmt.Sprintf("Bearer: %s", ac.jwt)) | ||
res, err := ac.hc.Do(req) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to send http request %s", err) | ||
} | ||
return ac.unmarshalResponse(res.Body) | ||
} | ||
|
||
func (ac *APIClient) unmarshalResponse(body io.ReadCloser) (*Response, error) { | ||
var resp Response | ||
|
||
data, err := ioutil.ReadAll(body) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to read http response body %s", err) | ||
} | ||
if err = json.Unmarshal(data, &resp); err != nil { | ||
return nil, fmt.Errorf("failed to deserialize http response body %s", err) | ||
} | ||
return &resp, nil | ||
} |
Oops, something went wrong.