diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 146dcc5..2bfc055 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,9 +1,9 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/goreleaser/goreleaser/v2.10.2/www/docs/static/schema.json # This is an example .goreleaser.yml file with some sensible defaults. # Make sure to check the documentation at https://goreleaser.com # The lines below are called `modelines`. See `:help modeline` # Feel free to remove those if you don't want/need to use them. -# yaml-language-server: $schema=https://goreleaser.com/static/schema.json # vim: set ts=2 sw=2 tw=0 fo=cnqoj version: 2 @@ -55,21 +55,17 @@ changelog: - "^docs:" - "^test:" -brews: +homebrew_casks: - name: openstatus - directory: Formula + binary: openstatus homepage: https://www.openstatus.dev skip_upload: auto description: "OpenStatus CLI" - test: | - system "#{bin}/openstatus --help" - extra_install: | - man1.install "openstatus-docs.1" + directory: Formula + manpage: openstatus-docs.1 repository: - # Repository owner. - # - # Templates: allowed. + owner: openstatusHQ # Repository name. @@ -80,3 +76,28 @@ brews: branch: main token: "{{ .Env.TAP_GITHUB_TOKEN }}" + +# brews: +# - +# name: openstatus +# directory: Formula +# homepage: https://www.openstatus.dev +# skip_upload: auto +# test: | +# system "#{bin}/openstatus --help" +# extra_install: | +# man1.install "openstatus-docs.1" +# repository: +# # Repository owner. +# # +# # Templates: allowed. +# owner: openstatusHQ + +# # Repository name. +# # +# # Templates: allowed. +# name: homebrew-cli + + +# branch: main +# token: "{{ .Env.TAP_GITHUB_TOKEN }}" diff --git a/cmd/openstatus/main.go b/cmd/openstatus/main.go index 2335c47..dd92b64 100644 --- a/cmd/openstatus/main.go +++ b/cmd/openstatus/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "log" "os" @@ -14,14 +15,23 @@ import ( func main() { app := &cli.Command{ Name: "openstatus", + Suggest: true, 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", + UsageText: "openstatus [command] [flags]", + Description: "OpenStatus is a command line interface for managing your monitors and triggering your synthetics tests.", Version: "v0.0.4", Commands: []*cli.Command{ monitors.MonitorsCmd(), run.RunCmd(), whoami.WhoamiCmd(), }, + EnableShellCompletion: true, + + Action: func(ctx context.Context, cmd *cli.Command) error { + cli.ShowAppHelp(cmd) + fmt.Println("\n\nPlease report any issues at https://github.com/openstatusHQ/cli/issues/new") + return nil + }, } if err := app.Run(context.Background(), os.Args); err != nil { diff --git a/go.mod b/go.mod index 9e298ef..dfdfb91 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,6 @@ require ( ) 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.2 // indirect @@ -27,8 +26,7 @@ require ( 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 + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index c47c19b..30733a7 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -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,6 +7,7 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= @@ -44,8 +43,6 @@ 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= @@ -55,8 +52,6 @@ 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= @@ -68,3 +63,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/config/monitor.go b/internal/config/monitor.go index 8e0a470..7db5eb9 100644 --- a/internal/config/monitor.go +++ b/internal/config/monitor.go @@ -1,55 +1,55 @@ 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"` + Name string `json:"name" ,yaml:"name"` + Description string `json:"description,omitempty" ,yaml:"description,omitempty"` + Frequency Frequency `json:"frequency" ,yaml:"frequency"` // Regions to run the request in - Regions []Region `json:"regions"` - // The HTTP Request we are sending - Request Request `json:"request"` + Regions []Region `json:"regions" ,yaml:"regions"` + // Whether the monitor is active + Active bool `json:"active"` + Kind CoordinateKind `json:"kind" ,yaml:"kind"` // Number of retries to attempt - Retry int64 `json:"retry,omitempty"` + Retry int64 `json:"retry,omitempty" ,yaml:"retry,omitempty"` + // Whether the monitor is public + Public bool `json:"public,omitempty" ,yaml:"public,omitempty"` + // The HTTP Request we are sending + Request Request `json:"request" ,yaml:"request"` + // Time in milliseconds to wait before marking the request as degraded + DegradedAfter int64 `json:"degradedAfter,omitempty" ,yaml:"degradedAfter,omitempty"` // Time in milliseconds to wait before marking the request as timed out - Timeout int64 `json:"timeout,omitempty"` + Timeout int64 `json:"timeout,omitempty" ,yaml:"timeout,omitempty"` + // Assertions to run on the response + Assertions []Assertion `json:"assertions,omitempty" ,yaml:"assertions,omitempty"` } type Assertion struct { // Comparison operator - Compare Compare `json:"compare"` - Kind AssertionKind `json:"kind"` + Compare Compare `json:"compare" ,yaml:"compare"` + Kind AssertionKind `json:"kind" ,yaml:"kind"` // Status code to assert // // Header value to assert // // Text body to assert - Target any `json:"target"` + Target any `json:"target" ,yaml:"target"` // Header key to assert - Key string `json:"key,omitempty"` + Key string `json:"key,omitempty" ,yaml:"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"` + Body string `json:"body,omitempty" ,yaml:"body,omitempty"` + Headers map[string]string `json:"headers,omitempty" ,yaml:"headers,omitempty"` + Method Method `json:"method,omitempty" ,yaml:"method,omitempty"` // URL to request - URL string `json:"url,omitempty"` + URL string `json:"url,omitempty" ,yaml:"url,omitempty"` // Host to connect to - Host string `json:"host,omitempty"` + Host string `json:"host,omitempty" ,yaml:"host,omitempty"` // Port to connect to - Port float64 `json:"port,omitempty"` + Port int64 `json:"port,omitempty" ,yaml:"port,omitempty"` } // Comparison operator diff --git a/internal/monitors/monitor_export.go b/internal/monitors/monitor_export.go new file mode 100644 index 0000000..ff74257 --- /dev/null +++ b/internal/monitors/monitor_export.go @@ -0,0 +1,169 @@ +package monitors + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strconv" + "strings" + + "github.com/openstatusHQ/cli/internal/config" + "github.com/urfave/cli/v3" + "sigs.k8s.io/yaml" +) + +func ExportMonitor(httpClient *http.Client, apiKey string, path string ) error { + url := "https://api.openstatus.dev/v1/monitor" + + req, _ := http.NewRequest(http.MethodGet, url, nil) + 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 Get all monitors monitors") + } + + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + var monitors []Monitor + err = json.Unmarshal(body, &monitors) + if err != nil { + return err + } + + t := map[string]config.Monitor{} + + file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer file.Close() + + for _, monitor := range monitors { + + var regions []config.Region + + for _, region := range monitor.Regions { + regions = append(regions, config.Region(region)) + } + var request config.Request + var assertions []config.Assertion + switch monitor.JobType { + case "http": + var headers = make(map[string]string) + for _, header := range monitor.Headers { + // Our API should not allow empty headers + if header.Key == "" { + continue + } + headers[header.Key] = header.Value + } + + for _, assertion := range monitor.Assertions { + + kind := config.AssertionKind(assertion.Type) + // Our api return status instead of Status Code + if kind == "status" { + kind = config.StatusCode + } + + assertions = append(assertions, config.Assertion{ + Kind: kind, + Target: assertion.Target, + Compare: config.Compare(assertion.Compare), + Key: assertion.Key, + }) + } + + request = config.Request{ + URL: monitor.URL, + Method: config.Method(monitor.Method), + Body: monitor.Body, + Headers: headers, + } + break + case "tcp": + uri := strings.Split(monitor.URL, ":") + + port, _ := strconv.Atoi(uri[1]) + request = config.Request{ + Host: uri[0], + Port: int64(port), + } + break + default: + return fmt.Errorf("unknown job type: %s", monitor.JobType) + } + + t[fmt.Sprint(monitor.ID)] = config.Monitor{ + Name: monitor.Name, + Active: monitor.Active, + Public: monitor.Public, + Description: monitor.Description, + DegradedAfter: int64(monitor.DegradedAfter), + Frequency: config.Frequency(monitor.Periodicity), + // Regions: monitor.Regions, + Request: request, + Kind: config.CoordinateKind(monitor.JobType), + Retry: int64(monitor.Retry), + Regions: regions, + Assertions: assertions, + } + } + // defer file.Close() + y, err := yaml.Marshal(&t) + if err != nil { + return err + } + + // file.WriteString("# yaml-language-server: $schema=https://raw.githubusercontent.com/openstatusHQ/json-schema/refs/heads/improve-schema/1.0.1.json\n\n") + _, err = file.WriteString("# yaml-language-server: $schema=https://www.openstatus.dev/schema.json\n\n") + if err != nil { + return err + } + _, err = file.Write(y) + if err != nil { + return err + } + return nil +} + +func GetMonitorExportCmd() *cli.Command { + monitorInfoCmd := cli.Command{ + Name: "export", + Usage: "Export all your monitors", + Description: "Export your monitors to YAML", + Action: func(ctx context.Context, cmd *cli.Command) error { + // monitorId := cmd.Args().Get(0) + err := ExportMonitor(http.DefaultClient, cmd.String("access-token"), cmd.String("output")) + if err != nil { + return cli.Exit(err.Error(), 1) + } + fmt.Printf("Monitors successfully exported to: %s", cmd.String("output")) + return nil + }, + 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.StringFlag{ + Name: "output", + Usage: "The output file name ", + DefaultText: "openstatus.yaml", + Value: "openstatus.yaml", + Aliases: []string{"o"}, + }, + }, + } + return &monitorInfoCmd +} diff --git a/internal/monitors/monitors.go b/internal/monitors/monitors.go index 5fedb6d..95b00cd 100644 --- a/internal/monitors/monitors.go +++ b/internal/monitors/monitors.go @@ -21,6 +21,8 @@ type Monitor struct { Body string `json:"body"` Headers []Header `json:"headers,omitempty"` Assertions []Assertion `json:"assertions,omitempty"` + Retry int `json:"retry"` + JobType string `json:"jobType"` } type Header struct { @@ -31,7 +33,7 @@ type Header struct { type Assertion struct { Type string `json:"type"` Compare string `json:"compare"` - Value string `json:"value"` + Key string `json:"key"` Target any `json:"target"` } @@ -95,6 +97,7 @@ func MonitorsCmd() *cli.Command { Commands: []*cli.Command{ GetMonitorCreateCmd(), GetMonitorDeleteCmd(), + GetMonitorExportCmd(), GetMonitorInfoCmd(), GetMonitorsListCmd(), GetMonitorsTriggerCmd(),