diff --git a/.goreleaser.yaml b/.goreleaser.yaml index c0c9f8e..e17d5e7 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -13,7 +13,6 @@ before: # You may remove this if you don't use go modules. - go mod tidy - builds: - env: @@ -28,6 +27,8 @@ builds: archives: + - files: + - docs/* - format: tar.gz # this name template makes the OS and Arch compatible with the results of `uname`. name_template: >- @@ -64,6 +65,8 @@ brews: description: "OpenStatus CLI" test: | system "#{bin}/openstatus --help" + extra_install: | + man1.install "docs/openstatus-docs" repository: # Repository owner. # diff --git a/cmd/openstatus/main.go b/cmd/openstatus/main.go index 2c606ad..0f675ae 100644 --- a/cmd/openstatus/main.go +++ b/cmd/openstatus/main.go @@ -15,6 +15,7 @@ func main() { app := &cli.Command{ Name: "openstatus", Usage: "This is OpenStatus Command Line Interface", + Description: "OpenStatus is a command line interface for managing your monitors and triggering your synthetics tests. \n\nPlease report any issues at https://github.com/openstatusHQ/cli/issues/new", Version: "v0.0.3", Commands: []*cli.Command{ monitors.MonitorsCmd(), diff --git a/docs/openstatus-docs b/docs/openstatus-docs new file mode 100644 index 0000000..96662f3 --- /dev/null +++ b/docs/openstatus-docs @@ -0,0 +1,87 @@ +.\" Automatically generated by Pandoc 3.7.0.2 +.\" +.TH "" "" "" "" +.SH CLI +.SH NAME +openstatus \- This is OpenStatus Command Line Interface +.SH SYNOPSIS +openstatus +.SH DESCRIPTION +OpenStatus is a command line interface for managing your monitors and +triggering your synthetics tests. +.PP +Please report any issues at +https://github.com/openstatusHQ/cli/issues/new +.PP +\f[B]Usage\f[R]: +.IP +.EX +openstatus [GLOBAL OPTIONS] [command [COMMAND OPTIONS]] [ARGUMENTS...] +.EE +.SH COMMANDS +.SS monitors +Manage your monitors +.SS create +Create monitors (beta) +.RS +.PP +openstatus monitors create [options] +.RE +.PP +\f[B]\(enaccess\-token, \-t\f[R]=\(lq\(lq: OpenStatus API Access Token +.PP +\f[B]\(enauto\-accept, \-y\f[R]: Automatically accept the prompt +.PP +\f[B]\(enconfig\f[R]=\(lq\(lq: The configuration file containing monitor +information (default: openstatus.yaml) +.SS delete +Delete a monitor +.RS +.PP +openstatus monitors delete [MonitorID] [options] +.RE +.PP +\f[B]\(enaccess\-token, \-t\f[R]=\(lq\(lq: OpenStatus API Access Token +.PP +\f[B]\(enauto\-accept, \-y\f[R]: Automatically accept the prompt +.SS info +Get monitor information +.PP +\f[B]\(enaccess\-token, \-t\f[R]=\(lq\(lq: OpenStatus API Access Token +.SS list +List all monitors +.RS +.PP +openstatus monitors list [options] +.RE +.PP +\f[B]\(enaccess\-token, \-t\f[R]=\(lq\(lq: OpenStatus API Access Token +.PP +\f[B]\(enall\f[R]: List all monitors including inactive ones +.SS trigger +Trigger a monitor test +.RS +.PP +openstatus monitors trigger [MonitorId] [options] +.RE +.PP +\f[B]\(enaccess\-token, \-t\f[R]=\(lq\(lq: OpenStatus API Access Token +.SS run, r +Run your synthetics tests +.RS +.PP +openstatus run[options] +.RE +.PP +\f[B]\(enaccess\-token, \-t\f[R]=\(lq\(lq: OpenStatus API Access Token +.PP +\f[B]\(enconfig\f[R]=\(lq\(lq: The configuration file (default: +config.openstatus.yaml) +.SS whoami, w +Get your current workspace information +.RS +.PP +openstatus whoami [options] +.RE +.PP +\f[B]\(enaccess\-token, \-t\f[R]=\(lq\(lq: OpenStatus API Access Token diff --git a/docs/openstatus-docs.md b/docs/openstatus-docs.md new file mode 100644 index 0000000..0d41661 --- /dev/null +++ b/docs/openstatus-docs.md @@ -0,0 +1,91 @@ +# CLI + +# NAME + +openstatus - This is OpenStatus Command Line Interface + +# SYNOPSIS + +openstatus + +# DESCRIPTION + +OpenStatus is a command line interface for managing your monitors and triggering your synthetics tests. + +Please report any issues at https://github.com/openstatusHQ/cli/issues/new + +**Usage**: + +``` +openstatus [GLOBAL OPTIONS] [command [COMMAND OPTIONS]] [ARGUMENTS...] +``` + +# COMMANDS + +## monitors + +Manage your monitors + +### create + +Create monitors (beta) + +>openstatus monitors create [options] + +**--access-token, -t**="": OpenStatus API Access Token + +**--auto-accept, -y**: Automatically accept the prompt + +**--config**="": The configuration file containing monitor information (default: openstatus.yaml) + +### delete + +Delete a monitor + +>openstatus monitors delete [MonitorID] [options] + +**--access-token, -t**="": OpenStatus API Access Token + +**--auto-accept, -y**: Automatically accept the prompt + +### info + +Get monitor information + +**--access-token, -t**="": OpenStatus API Access Token + +### list + +List all monitors + +>openstatus monitors list [options] + +**--access-token, -t**="": OpenStatus API Access Token + +**--all**: List all monitors including inactive ones + +### trigger + +Trigger a monitor test + +>openstatus monitors trigger [MonitorId] [options] + +**--access-token, -t**="": OpenStatus API Access Token + +## run, r + +Run your synthetics tests + +>openstatus run[options] + +**--access-token, -t**="": OpenStatus API Access Token + +**--config**="": The configuration file (default: config.openstatus.yaml) + +## whoami, w + +Get your current workspace information + +>openstatus whoami [options] + +**--access-token, -t**="": OpenStatus API Access Token diff --git a/go.mod b/go.mod index 7e0eeee..9e298ef 100644 --- a/go.mod +++ b/go.mod @@ -9,17 +9,26 @@ require ( github.com/knadh/koanf/parsers/yaml v0.1.0 github.com/knadh/koanf/providers/file v1.1.2 github.com/knadh/koanf/v2 v2.1.1 + github.com/logrusorgru/aurora/v4 v4.0.0 + github.com/olekukonko/tablewriter v1.0.7 github.com/rodaine/table v1.3.0 ) require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect - github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/knadh/koanf/maps v0.1.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect + github.com/olekukonko/ll v0.0.8 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/urfave/cli-docs/v3 v3.0.0-alpha6 // indirect golang.org/x/sys v0.26.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 142b34b..c47c19b 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -9,14 +11,16 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= -github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= +github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w= github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= +github.com/logrusorgru/aurora/v4 v4.0.0 h1:sRjfPpun/63iADiSvGGjgA1cAYegEWMPCJdUpJYn9JA= +github.com/logrusorgru/aurora/v4 v4.0.0/go.mod h1:lP0iIa2nrnT/qoFXcOZSrZQpJ1o6n2CUf/hyHi2Q4ZQ= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -28,12 +32,20 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo= +github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc= +github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= +github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw= +github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rodaine/table v1.3.0 h1:4/3S3SVkHnVZX91EHFvAMV7K42AnJ0XuymRR2C5HlGE= github.com/rodaine/table v1.3.0/go.mod h1:47zRsHar4zw0jgxGxL9YtFfs7EGN6B/TaS+/Dmk4WxU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -43,6 +55,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli-docs/v3 v3.0.0-alpha6 h1:w/l/N0xw1rO/aHRIGXJ0lDwwYFOzilup1qGvIytP3BI= +github.com/urfave/cli-docs/v3 v3.0.0-alpha6/go.mod h1:p7Z4lg8FSTrPB9GTaNyTrK3ygffHZcK3w0cU2VE+mzU= github.com/urfave/cli/v3 v3.0.0-alpha9.2 h1:CL8llQj3dGRLVQQzHxS+ZYRLanOuhyK1fXgLKD+qV+Y= github.com/urfave/cli/v3 v3.0.0-alpha9.2/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/cli/confirmation.go b/internal/cli/confirmation.go new file mode 100644 index 0000000..cbf74a0 --- /dev/null +++ b/internal/cli/confirmation.go @@ -0,0 +1,22 @@ +package cli + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +func AskForConfirmation(s string) bool { + reader := bufio.NewReader(os.Stdin) + fmt.Printf("%s [y/N]: ", s) + response, err := reader.ReadString('\n') + if err != nil { + panic(err) + } + response = strings.ToLower(strings.TrimSpace(response)) + if response == "y" || response == "yes" { + return true + } + return false +} diff --git a/internal/config/monitor.go b/internal/config/monitor.go new file mode 100644 index 0000000..8e0a470 --- /dev/null +++ b/internal/config/monitor.go @@ -0,0 +1,153 @@ +package config + +type Monitor struct { + // Whether the monitor is active + Active bool `json:"active,omitempty"` + // Assertions to run on the response + Assertions []Assertion `json:"assertions,omitempty"` + // Time in milliseconds to wait before marking the request as degraded + DegradedAfter int64 `json:"degradedAfter,omitempty"` + Description string `json:"description,omitempty"` + Frequency Frequency `json:"frequency"` + Kind CoordinateKind `json:"kind"` + // Name of the monitor + Name string `json:"name"` + // Whether the monitor is public + Public bool `json:"public,omitempty"` + // Regions to run the request in + Regions []Region `json:"regions"` + // The HTTP Request we are sending + Request Request `json:"request"` + // Number of retries to attempt + Retry int64 `json:"retry,omitempty"` + // Time in milliseconds to wait before marking the request as timed out + Timeout int64 `json:"timeout,omitempty"` +} + +type Assertion struct { + // Comparison operator + Compare Compare `json:"compare"` + Kind AssertionKind `json:"kind"` + // Status code to assert + // + // Header value to assert + // + // Text body to assert + Target any `json:"target"` + // Header key to assert + Key string `json:"key,omitempty"` +} + +// The HTTP Request we are sending +type Request struct { + // Body to send with the request + Body string `json:"body,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + Method Method `json:"method,omitempty"` + // URL to request + URL string `json:"url,omitempty"` + // Host to connect to + Host string `json:"host,omitempty"` + // Port to connect to + Port float64 `json:"port,omitempty"` +} + +// Comparison operator +type Compare string + +const ( + Contains Compare = "contains" + Empty Compare = "empty" + Eq Compare = "eq" + Gt Compare = "gt" + Gte Compare = "gte" + LTE Compare = "lte" + Lt Compare = "lt" + NotContains Compare = "not_contains" + NotEmpty Compare = "not_empty" + NotEq Compare = "not_eq" +) + +type AssertionKind string + +const ( + Header AssertionKind = "header" + StatusCode AssertionKind = "statusCode" + TextBody AssertionKind = "textBody" +) + +type Frequency string + +const ( + The10M Frequency = "10m" + The1H Frequency = "1h" + The1M Frequency = "1m" + The30M Frequency = "30m" + The30S Frequency = "30s" + The5M Frequency = "5m" +) + +type CoordinateKind string + +const ( + HTTP CoordinateKind = "http" + TCP CoordinateKind = "tcp" +) + +type Region string + +const ( + Ams Region = "ams" + Arn Region = "arn" + Atl Region = "atl" + BOM Region = "bom" + Bog Region = "bog" + Bos Region = "bos" + Cdg Region = "cdg" + Den Region = "den" + Dfw Region = "dfw" + Ewr Region = "ewr" + Eze Region = "eze" + Fra Region = "fra" + Gdl Region = "gdl" + Gig Region = "gig" + Gru Region = "gru" + Hkg Region = "hkg" + Iad Region = "iad" + Jnb Region = "jnb" + Lax Region = "lax" + Lhr Region = "lhr" + Mad Region = "mad" + Mia Region = "mia" + Nrt Region = "nrt" + Ord Region = "ord" + Otp Region = "otp" + Phx Region = "phx" + Private Region = "private" + Qro Region = "qro" + Scl Region = "scl" + Sea Region = "sea" + Sin Region = "sin" + Sjc Region = "sjc" + Syd Region = "syd" + Waw Region = "waw" + Yul Region = "yul" + Yyz Region = "yyz" +) + +type Method string + +const ( + Delete Method = "DELETE" + Get Method = "GET" + Head Method = "HEAD" + Options Method = "OPTIONS" + Patch Method = "PATCH" + Post Method = "POST" + Put Method = "PUT" +) + +type Target struct { + Int *int64 + String *string +} diff --git a/internal/config/openstatus.go b/internal/config/openstatus.go new file mode 100644 index 0000000..558e8a3 --- /dev/null +++ b/internal/config/openstatus.go @@ -0,0 +1,61 @@ +package config + +import ( + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/file" +) + +type Monitors map[string]Monitor + + + +func ReadOpenStatus(path string) ([]Monitor, error) { + f := file.Provider(path) + + // r, _:= f.ReadBytes() + + // fmt.Printf("%v", string(r)) + // for _, line := range string(r) { + // fmt.Println(line) + // } + err := k.Load(f, yaml.Parser()) + + if err != nil { + return nil, err + } + + var out Monitors + + err = k.Unmarshal("", &out) + + for _, value := range out { + for _, assertion := range value.Assertions { + if assertion.Kind == Header || assertion.Kind == TextBody { + assertion.Target = assertion.Target.(string) + } + if assertion.Kind == StatusCode { + assertion.Target = assertion.Target.(int) + } + } + } + + + var monitor []Monitor + for _, value := range out { + for _, assertion := range value.Assertions { + if assertion.Kind == Header || assertion.Kind == TextBody { + assertion.Target = assertion.Target.(string) + } + if assertion.Kind == StatusCode { + assertion.Target = assertion.Target.(int) + } + } + monitor = append(monitor, value) + } + + if err != nil { + return nil, err + } + + return monitor, nil +} diff --git a/internal/monitors/monitor_create.go b/internal/monitors/monitor_create.go new file mode 100644 index 0000000..a194e07 --- /dev/null +++ b/internal/monitors/monitor_create.go @@ -0,0 +1,112 @@ +package monitors + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + + confirmation "github.com/openstatusHQ/cli/internal/cli" + "github.com/openstatusHQ/cli/internal/config" + "github.com/urfave/cli/v3" +) + +func CreateMonitor(httpClient *http.Client, apiKey string, monitor config.Monitor) error { + + url := fmt.Sprintf("https://api.openstatus.dev/v1/monitor/%s", monitor.Kind) + + payloadBuf := new(bytes.Buffer) + json.NewEncoder(payloadBuf).Encode(monitor) + req, _ := http.NewRequest(http.MethodPost, url, payloadBuf) + + req.Header.Add("x-openstatus-key", apiKey) + req.Header.Add("Content-Type", "application/json") + + res, err := httpClient.Do(req) + if err != nil { + return err + } + + if res.StatusCode != http.StatusOK { + return fmt.Errorf("Failed to create monitor") + } + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + + var monitors Monitor + err = json.Unmarshal(body, &monitors) + if err != nil { + return err + } + + return nil +} + +func GetMonitorCreateCmd() *cli.Command { + monitorInfoCmd := cli.Command{ + Name: "create", + Usage: "Create monitors (beta)", + Description: "Create your monitors from your openstatus.yaml file ", + UsageText: "openstatus monitors create [options]", + + Action: func(ctx context.Context, cmd *cli.Command) error { + + path := cmd.String("config") + + if path != "" { + if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { + return cli.Exit("Config does not exist", 1) + } + } + + accept := cmd.Bool("auto-accept") + + monitors, err := config.ReadOpenStatus(path) + if err != nil { + return cli.Exit("Unable to read config file", 1) + } + + if !accept { + if !confirmation.AskForConfirmation(fmt.Sprintf("You are about to create %d monitors do you want to continue", len(monitors))) { + return nil + } + } + for _, value := range monitors { + err = CreateMonitor(http.DefaultClient, cmd.String("access-token"), value) + if err != nil { + return cli.Exit("Unable to create monitor", 1) + } + } + fmt.Printf("%d monitors created successfully\n", len(monitors)) + return nil + }, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "config", + Usage: "The configuration file containing monitor information", + DefaultText: "openstatus.yaml", + Value: "openstatus.yaml", + }, + &cli.StringFlag{ + Name: "access-token", + Usage: "OpenStatus API Access Token", + Aliases: []string{"t"}, + Sources: cli.EnvVars("OPENSTATUS_API_TOKEN"), + Required: true, + }, + &cli.BoolFlag{ + Name: "auto-accept", + Usage: "Automatically accept the prompt", + Aliases: []string{"y"}, + Required: false, + }, + }, + } + return &monitorInfoCmd +} + +// os_3Za36BLXy7pN9ZY36SvSLnjD diff --git a/internal/monitors/monitor_delete.go b/internal/monitors/monitor_delete.go new file mode 100644 index 0000000..6b637f4 --- /dev/null +++ b/internal/monitors/monitor_delete.go @@ -0,0 +1,88 @@ +package monitors + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + confirmation "github.com/openstatusHQ/cli/internal/cli" + "github.com/urfave/cli/v3" +) + + + +func DeleteMonitor(httpClient *http.Client, apiKey string, monitorId string) error { + + if monitorId == "" { + return fmt.Errorf("Monitor ID is required") + } + + url := fmt.Sprintf("https://api.openstatus.dev/v1/monitor/%s", monitorId) + + req, err := http.NewRequest(http.MethodDelete, url, nil) + if err != nil { + return err + } + req.Header.Add("x-openstatus-key", apiKey) + res, err := httpClient.Do(req) + if err != nil { + return err + } + + if res.StatusCode != http.StatusOK { + return fmt.Errorf("Failed to delete monitor") + } + + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + + var r MonitorTriggerResponse + err = json.Unmarshal(body, &r) + if err != nil { + + return err + } + fmt.Printf("Monitor deleted successfully\n") + return nil +} + +func GetMonitorDeleteCmd() *cli.Command { + monitorsCmd := cli.Command{ + Name: "delete", + Usage: "Delete a monitor", + UsageText: "openstatus monitors delete [MonitorID] [options]", + + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "access-token", + Usage: "OpenStatus API Access Token", + Aliases: []string{"t"}, + Sources: cli.EnvVars("OPENSTATUS_API_TOKEN"), + Required: true, + }, + &cli.BoolFlag{ + Name: "auto-accept", + Usage: "Automatically accept the prompt", + Aliases: []string{"y"}, + Required: false, + }, + }, + Action: func(ctx context.Context, cmd *cli.Command) error { + monitorId := cmd.Args().Get(0) + + if !cmd.Bool("auto-accept") { + if !confirmation.AskForConfirmation(fmt.Sprintf("You are about to delete monitor: %s, do you want to continue", monitorId)) { + return nil + } + } + err := DeleteMonitor(http.DefaultClient, cmd.String("access-token"), monitorId) + if err != nil { + return cli.Exit("Failed to delete monitor", 1) + } + return nil + }, + } + return &monitorsCmd +} diff --git a/internal/monitors/monitor_info.go b/internal/monitors/monitor_info.go index 892ecf2..82d96cf 100644 --- a/internal/monitors/monitor_info.go +++ b/internal/monitors/monitor_info.go @@ -6,7 +6,14 @@ import ( "fmt" "io" "net/http" + "os" + "strings" + // "github.com/logrusorgru/aurora/v4" + "github.com/logrusorgru/aurora/v4" + "github.com/olekukonko/tablewriter" + "github.com/olekukonko/tablewriter/renderer" + "github.com/olekukonko/tablewriter/tw" "github.com/urfave/cli/v3" ) @@ -18,7 +25,7 @@ func GetMonitorInfo(httpClient *http.Client, apiKey string, monitorId string) er url := "https://api.openstatus.dev/v1/monitor/" + monitorId - req, _ := http.NewRequest("GET", url, nil) + req, _ := http.NewRequest(http.MethodGet, url, nil) req.Header.Add("x-openstatus-key", apiKey) @@ -27,19 +34,76 @@ func GetMonitorInfo(httpClient *http.Client, apiKey string, monitorId string) er return err } if res.StatusCode != http.StatusOK { - return fmt.Errorf("Failed to get monitor information") + return fmt.Errorf("You don't have permission to access this monitor") } defer res.Body.Close() body, _ := io.ReadAll(res.Body) - var monitors Monitor - err = json.Unmarshal(body, &monitors) + var monitor Monitor + err = json.Unmarshal(body, &monitor) if err != nil { + fmt.Println(err) return err } - fmt.Println("Monitor") - fmt.Printf("ID: %d\nName: %s\nURL: %s\nPeriodicity: %s\nDescription: %s\nMethod: %s\nActive: %t\nPublic: %t\nTimeout: %d\nDegradedAfter: %d\n", monitors.ID, monitors.Name, monitors.URL, monitors.Periodicity, monitors.Description, monitors.Method, monitors.Active, monitors.Public, monitors.Timeout, monitors.DegradedAfter) + // fmt.Println("Monitor") + + fmt.Println(aurora.Bold("Monitor:")) + table := tablewriter.NewTable(os.Stdout, + tablewriter.WithRenderer(renderer.NewBlueprint()), + tablewriter.WithRendition(tw.Rendition{ + Symbols: tw.NewSymbolCustom("custom").WithColumn("="), + Borders: tw.Border{ + Top: tw.Off, + Left: tw.Off, + Right: tw.Off, + Bottom: tw.Off, + }, + Settings: tw.Settings{ + Lines: tw.Lines{ // Major internal separator lines + ShowHeaderLine: tw.Off, // Line after header + ShowFooterLine: tw.On, // Line before footer (if footer exists) + }, + Separators: tw.Separators{ // General row and column separators + BetweenRows: tw.Off, // Horizontal lines between data rows + BetweenColumns: tw.On, // Vertical lines between columns + }, + }, + }), + tablewriter.WithRowAlignment(tw.AlignLeft), // Common for Markdown + tablewriter.WithHeaderAlignment(tw.AlignLeft), // + ) + + data := [][]string{ + {"ID",fmt.Sprintf("%d", monitor.ID)}, + {"Name",monitor.Name}, + {"Description",monitor.Description}, + {"Endpoint",monitor.URL}, + + } + if monitor.Method != "" { + data = append(data, []string{"Method", monitor.Method}) + } + + data = append(data, []string{"Frequency",monitor.Periodicity}) + data = append(data, []string{"Locations", strings.Join(monitor.Regions, ",")}) + data = append(data, []string{"Active", fmt.Sprintf("%t", monitor.Active)}) + data = append(data, []string{"Public", fmt.Sprintf("%t", monitor.Public)}) + + if monitor.Timeout > 0 { + data = append(data, []string{"Timeout", fmt.Sprintf("%d ms", monitor.Timeout)}) + } + if monitor.DegradedAfter > 0 { + data = append(data, []string{"Degraded After", fmt.Sprintf("%d", monitor.DegradedAfter)}) + } + + if monitor.Body != "" { + s := fmt.Sprintf("%s", monitor.Body) + data = append(data, []string{"Body", s[:40]}) + } + table.Bulk(data) + table.Render() + return nil } @@ -48,11 +112,10 @@ func GetMonitorInfoCmd() *cli.Command { Name: "info", Usage: "Get monitor information", Action: func(ctx context.Context, cmd *cli.Command) error { - fmt.Println("Monitor information") monitorId := cmd.Args().Get(0) err := GetMonitorInfo(http.DefaultClient, cmd.String("access-token"), monitorId) if err != nil { - return cli.Exit("Failed to get monitor information", 1) + return cli.Exit(err.Error(), 1) } return nil }, diff --git a/internal/monitors/monitor_trigger.go b/internal/monitors/monitor_trigger.go index 45ddf84..ddc82bb 100644 --- a/internal/monitors/monitor_trigger.go +++ b/internal/monitors/monitor_trigger.go @@ -24,7 +24,7 @@ func MonitorTrigger(httpClient *http.Client, apiKey string, monitorId string) er url := fmt.Sprintf("https://api.openstatus.dev/v1/monitor/%s/trigger", monitorId) - req, err := http.NewRequest("POST", url, nil) + req, err := http.NewRequest(http.MethodPost, url, nil) if err != nil { return err } @@ -44,10 +44,9 @@ func MonitorTrigger(httpClient *http.Client, apiKey string, monitorId string) er var r MonitorTriggerResponse err = json.Unmarshal(body, &r) if err != nil { - return err } - fmt.Printf("Result ID: %d\n", r.ResultId) + fmt.Printf("Check triggered successfully\n") return nil } @@ -56,6 +55,7 @@ func GetMonitorsTriggerCmd() *cli.Command { monitorsCmd := cli.Command{ Name: "trigger", Usage: "Trigger a monitor test", + UsageText: "openstatus monitors trigger [MonitorId] [options]", Flags: []cli.Flag{ &cli.StringFlag{ Name: "access-token", diff --git a/internal/monitors/monitors.go b/internal/monitors/monitors.go index ee241d0..5fedb6d 100644 --- a/internal/monitors/monitors.go +++ b/internal/monitors/monitors.go @@ -7,16 +7,32 @@ import ( ) type Monitor struct { - ID int `json:"id"` - Name string `json:"name"` - URL string `json:"url"` - Periodicity string `json:"periodicity"` - Description string `json:"description"` - Method string `json:"method"` - Active bool `json:"active"` - Public bool `json:"public"` - Timeout int `json:"timeout"` - DegradedAfter int `json:"degraded_after,omitempty"` + ID int `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Periodicity string `json:"periodicity"` + Description string `json:"description"` + Method string `json:"method"` + Regions []string `json:"regions"` + Active bool `json:"active"` + Public bool `json:"public"` + Timeout int `json:"timeout"` + DegradedAfter int `json:"degraded_after,omitempty"` + Body string `json:"body"` + Headers []Header `json:"headers,omitempty"` + Assertions []Assertion `json:"assertions,omitempty"` +} + +type Header struct { + Key string `json:"key"` + Value string `json:"value"` +} + +type Assertion struct { + Type string `json:"type"` + Compare string `json:"compare"` + Value string `json:"value"` + Target any `json:"target"` } type Timing struct { @@ -77,6 +93,8 @@ func MonitorsCmd() *cli.Command { Usage: "Manage your monitors", Commands: []*cli.Command{ + GetMonitorCreateCmd(), + GetMonitorDeleteCmd(), GetMonitorInfoCmd(), GetMonitorsListCmd(), GetMonitorsTriggerCmd(), diff --git a/internal/monitors/monitors_list.go b/internal/monitors/monitors_list.go index 3003709..4373d64 100644 --- a/internal/monitors/monitors_list.go +++ b/internal/monitors/monitors_list.go @@ -17,7 +17,7 @@ var allMonitor bool func ListMonitors(httpClient *http.Client, apiKey string) error { url := "https://api.openstatus.dev/v1/monitor" - req, _ := http.NewRequest("GET", url, nil) + req, _ := http.NewRequest(http.MethodGet, url, nil) req.Header.Add("x-openstatus-key", apiKey) res, err := httpClient.Do(req) if err != nil { @@ -56,6 +56,7 @@ func GetMonitorsListCmd() *cli.Command { monitorsListCmd := cli.Command{ Name: "list", Usage: "List all monitors", + UsageText: "openstatus monitors list [options]", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "all", diff --git a/internal/run/run.go b/internal/run/run.go index de39e9c..4637830 100644 --- a/internal/run/run.go +++ b/internal/run/run.go @@ -13,6 +13,7 @@ import ( "time" "github.com/fatih/color" + "github.com/logrusorgru/aurora/v4" "github.com/openstatusHQ/cli/internal/config" "github.com/openstatusHQ/cli/internal/monitors" "github.com/rodaine/table" @@ -53,7 +54,7 @@ func MonitorTrigger(httpClient *http.Client, apiKey string, monitorId string) er if err != nil { return err } - fmt.Println("Results for monitor:", monitorId) + fmt.Println(aurora.Bold(fmt.Sprintf("Monitor: %s", monitorId))) headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc() columnFmt := color.New(color.FgYellow).SprintfFunc() @@ -117,7 +118,9 @@ func RunCmd() *cli.Command { runCmd := cli.Command{ Name: "run", Aliases: []string{"r"}, - Usage: "Run your synthetics tests defined in your configuration file", + Usage: "Run your synthetics tests", + UsageText: "openstatus run[options]", + Description: "Run synthetic tests defined your config.openstatus.yaml", Action: func(ctx context.Context, cmd *cli.Command) error { path := cmd.String("config") @@ -134,7 +137,7 @@ func RunCmd() *cli.Command { size := len(conf.Tests.Ids) ch := make(chan error, size) - fmt.Println("Tests are running") + fmt.Print("Tests are running\n\n") var wg sync.WaitGroup @@ -155,7 +158,6 @@ func RunCmd() *cli.Command { if len(ch) > 0 { return cli.Exit("Some tests failed", 1) } - fmt.Println("Test ran succesfully🔥") return nil }, Flags: []cli.Flag{ diff --git a/internal/whoami/whoami.go b/internal/whoami/whoami.go index c4b6f86..b36dcdc 100644 --- a/internal/whoami/whoami.go +++ b/internal/whoami/whoami.go @@ -47,6 +47,7 @@ func WhoamiCmd() *cli.Command { whoamiCmd := cli.Command{ Name: "whoami", Aliases: []string{"w"}, + UsageText: "openstatus whoami [options]", Usage: "Get your current workspace information", Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("Your current workspace information")