diff --git a/README.md b/README.md index dfdd759..f13a40d 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,23 @@ OpenStatus CLI is a command line interface for OpenStatus. ## Installation ```bash -brew tap openstatusHQ/cli -brew install openstatus +brew install openstatusHQ/cli/openstatus --cask ``` +#### Windows +```powershell +iwr instl.sh/openstatushq/cli/windows | iex +``` + +#### macOS +```bash +curl -sSL instl.sh/openstatushq/cli/macos | bash +``` + +#### Linux +```bash +curl -sSL instl.sh/openstatushq/cli/linux | bash +``` ## Development diff --git a/cmd/docs/docs.go b/cmd/docs/docs.go index 46889ac..cbfeeac 100644 --- a/cmd/docs/docs.go +++ b/cmd/docs/docs.go @@ -9,7 +9,7 @@ import ( func main() { app := cmd.NewApp() - md, err := docs.ToTabularMarkdown(app , "openstatus") + md, err := docs.ToTabularMarkdown(app, "openstatus") if err != nil { panic(err) } diff --git a/docs/openstatus-docs.md b/docs/openstatus-docs.md index 138d3de..e616c00 100644 --- a/docs/openstatus-docs.md +++ b/docs/openstatus-docs.md @@ -22,59 +22,41 @@ Usage: $ openstatus [GLOBAL FLAGS] monitors [ARGUMENTS...] ``` -### `monitors create` subcommand +### `monitors apply` subcommand -Create monitors (beta). +Create or update monitors. -> openstatus monitors create [options] +> openstatus monitors apply [options] -Create the monitors defined in the openstatus.yaml file. +Creates or updates monitors according to the OpenStatus configuration file. Usage: ```bash -$ openstatus [GLOBAL FLAGS] monitors create [COMMAND FLAGS] [ARGUMENTS...] +$ openstatus [GLOBAL FLAGS] monitors apply [COMMAND FLAGS] [ARGUMENTS...] ``` The following flags are supported: | Name | Description | Default value | Environment variables | |-----------------------------|-------------------------------------------------------|:-----------------:|:----------------------:| -| `--config="…"` | The configuration file containing monitor information | `openstatus.yaml` | *none* | +| `--config="…"` (`-c`) | The configuration file containing monitor information | `openstatus.yaml` | *none* | | `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | | `--auto-accept` (`-y`) | Automatically accept the prompt | `false` | *none* | -### `monitors delete` subcommand -Delete a monitor. +### `monitors import` subcommand -> openstatus monitors delete [MonitorID] [options] +Import all your monitors. -Usage: - -```bash -$ openstatus [GLOBAL FLAGS] monitors delete [COMMAND FLAGS] [ARGUMENTS...] -``` - -The following flags are supported: - -| Name | Description | Default value | Environment variables | -|-----------------------------|---------------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--auto-accept` (`-y`) | Automatically accept the prompt | `false` | *none* | - -### `monitors export` subcommand - -Export all your monitors. - -> openstatus monitor export [options] +> openstatus monitors import [options] -Export all your monitors to YAML. +Import all your monitors from your workspace to a YAML file; it will also create a lock file to manage your monitors with 'apply'. Usage: ```bash -$ openstatus [GLOBAL FLAGS] monitors export [COMMAND FLAGS] [ARGUMENTS...] +$ openstatus [GLOBAL FLAGS] monitors import [COMMAND FLAGS] [ARGUMENTS...] ``` The following flags are supported: @@ -88,7 +70,7 @@ The following flags are supported: Get a monitor information. -> openstatus monitor info [MonitorID] +> openstatus monitors info [MonitorID] Fetch the monitor information. The monitor information includes details such as name, description, endpoint, method, frequency, locations, active status, public status, timeout, degraded after, and body. The body is truncated to 40 characters. diff --git a/docs/openstatus.1 b/docs/openstatus.1 index 3d167e3..e5e0671 100644 --- a/docs/openstatus.1 +++ b/docs/openstatus.1 @@ -24,19 +24,20 @@ Usage: .EX $ openstatus [GLOBAL FLAGS] monitors [ARGUMENTS...] .EE -.SS \f[CR]monitors create\f[R] subcommand -Create monitors (beta). +.SS \f[CR]monitors apply\f[R] subcommand +Create or update monitors. .RS .PP -openstatus monitors create [options] +openstatus monitors apply [options] .RE .PP -Create the monitors defined in the openstatus.yaml file. +Creates or updates monitors according to the OpenStatus configuration +file. .PP Usage: .IP .EX -$ openstatus [GLOBAL FLAGS] monitors create [COMMAND FLAGS] [ARGUMENTS...] +$ openstatus [GLOBAL FLAGS] monitors apply [COMMAND FLAGS] [ARGUMENTS...] .EE .PP The following flags are supported: @@ -55,7 +56,7 @@ Environment variables T} _ T{ -\f[CR]\-\-config=\(dq\&...\(dq\f[R] +\f[CR]\-\-config=\(dq\&...\(dq\f[R] (\f[CR]\-c\f[R]) T}@T{ The configuration file containing monitor information T}@T{ @@ -81,65 +82,20 @@ T}@T{ \f[I]none\f[R] T} .TE -.SS \f[CR]monitors delete\f[R] subcommand -Delete a monitor. -.RS -.PP -openstatus monitors delete [MonitorID] [options] -.RE -.PP -Usage: -.IP -.EX -$ openstatus [GLOBAL FLAGS] monitors delete [COMMAND FLAGS] [ARGUMENTS...] -.EE -.PP -The following flags are supported: -.PP -.TS -tab(@); -lw(20.1n) lw(22.9n) cw(10.4n) cw(16.6n). -T{ -Name -T}@T{ -Description -T}@T{ -Default value -T}@T{ -Environment variables -T} -_ -T{ -\f[CR]\-\-access\-token=\(dq\&...\(dq\f[R] (\f[CR]\-t\f[R]) -T}@T{ -OpenStatus API Access Token -T}@T{ -T}@T{ -\f[CR]OPENSTATUS_API_TOKEN\f[R] -T} -T{ -\f[CR]\-\-auto\-accept\f[R] (\f[CR]\-y\f[R]) -T}@T{ -Automatically accept the prompt -T}@T{ -\f[CR]false\f[R] -T}@T{ -\f[I]none\f[R] -T} -.TE -.SS \f[CR]monitors export\f[R] subcommand -Export all your monitors. +.SS \f[CR]monitors import\f[R] subcommand +Import all your monitors. .RS .PP -openstatus monitor export [options] +openstatus monitors import [options] .RE .PP -Export all your monitors to YAML. +Import all your monitors from your workspace to a YAML file; it will +also create a lock file to manage your monitors with `apply'. .PP Usage: .IP .EX -$ openstatus [GLOBAL FLAGS] monitors export [COMMAND FLAGS] [ARGUMENTS...] +$ openstatus [GLOBAL FLAGS] monitors import [COMMAND FLAGS] [ARGUMENTS...] .EE .PP The following flags are supported: @@ -179,7 +135,7 @@ T} Get a monitor information. .RS .PP -openstatus monitor info [MonitorID] +openstatus monitors info [MonitorID] .RE .PP Fetch the monitor information. diff --git a/go.mod b/go.mod index bb1556c..3f45a27 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ 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/google/go-cmp v0.7.0 // 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 diff --git a/go.sum b/go.sum index 09ace92..53bae45 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd 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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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= diff --git a/internal/config/lock.go b/internal/config/lock.go new file mode 100644 index 0000000..750fc34 --- /dev/null +++ b/internal/config/lock.go @@ -0,0 +1,52 @@ +package config + +import ( + "errors" + "os" + + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/v2" +) + +type Lock struct { + Monitor Monitor `yaml:"monitor"` + ID int `yaml:"id"` +} + +type MonitorsLock map[string]Lock + +func ReadLockFile(filename string) (MonitorsLock, error) { + + var out MonitorsLock + if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) { + return MonitorsLock{}, nil + } + + file := file.Provider(filename) + var k = koanf.New(".") + + err := k.Load(file, yaml.Parser()) + + if err != nil { + return nil, err + } + err = k.Unmarshal("", &out) + if err != nil { + return nil, err + } + + for _, value := range out { + for _, assertion := range value.Monitor.Assertions { + if assertion.Kind == Header || assertion.Kind == TextBody { + assertion.Target = assertion.Target.(string) + } + if assertion.Kind == StatusCode { + assertion.Target = assertion.Target.(int) + } + } + } + + return out, nil + +} diff --git a/internal/config/lock_test.go b/internal/config/lock_test.go new file mode 100644 index 0000000..94cb736 --- /dev/null +++ b/internal/config/lock_test.go @@ -0,0 +1,105 @@ +package config_test + +import ( + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/openstatusHQ/cli/internal/config" +) + +var lockfile = ` +"test-monitor": + id: 1 + monitor: + active: true + assertions: + - compare: eq + kind: statusCode + target: 200 + description: Uptime monitoring example + frequency: 10m + kind: http + name: Uptime Monitor + regions: + - iad + - ams + - syd + - jnb + - gru + request: + headers: + User-Agent: OpenStatus + method: GET + url: https://openstat.us + retry: 3 +` + +func Test_getMonitorTrigger(t *testing.T) { + t.Run("Read Lock file", func(t *testing.T) { + f, err := os.CreateTemp(".", "openstatus.lock") + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) // clean up + + if _, err := f.Write([]byte(lockfile)); err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } + out, err := config.ReadLockFile(f.Name()) + if err != nil { + t.Fatal(err) + } + + expect := config.MonitorsLock{ + "test-monitor": { + ID: 1, + Monitor: config.Monitor{ + Active: true, + Name: "Uptime Monitor", + Description: "Uptime monitoring example", + Frequency: "10m", + Kind: config.HTTP, + Retry: 3, + Public: false, + Regions: []config.Region{config.Iad, config.Ams, config.Syd, config.Jnb, config.Gru}, + Request: config.Request{ + URL: "https://openstat.us", + Method: config.Get, + Headers: map[string]string{"User-Agent": "OpenStatus"}, + }, + Assertions: []config.Assertion{ + { + Compare: config.Eq, + Kind: config.StatusCode, + Target: 200, + }, + }, + }, + }, + } + + equal := cmp.Equal(expect, out) + if !equal { + t.Errorf("Expected %v, got %v", expect, out) + } + }) + + t.Run("No Lock file", func(t *testing.T) { + + out, err := config.ReadLockFile("doesnotexist") + if err != nil { + t.Fatal(err) + } + + expect := config.MonitorsLock{} + + equal := cmp.Equal(expect, out) + if !equal { + t.Errorf("Expected %v, got %v", expect, out) + } + }) +} diff --git a/internal/config/openstatus.go b/internal/config/openstatus.go index 0a614d8..7974188 100644 --- a/internal/config/openstatus.go +++ b/internal/config/openstatus.go @@ -7,15 +7,9 @@ import ( type Monitors map[string]Monitor -func ReadOpenStatus(path string) ([]Monitor, error) { +func ReadOpenStatus(path string) (Monitors, 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 { @@ -26,6 +20,10 @@ func ReadOpenStatus(path string) ([]Monitor, error) { err = k.Unmarshal("", &out) + if err != nil { + return nil, err + } + for _, value := range out { for _, assertion := range value.Assertions { if assertion.Kind == Header || assertion.Kind == TextBody { @@ -37,8 +35,12 @@ func ReadOpenStatus(path string) ([]Monitor, error) { } } + return out, nil +} + +func ParseConfigMonitorsToMonitor(monitors Monitors) []Monitor { var monitor []Monitor - for _, value := range out { + for _, value := range monitors { for _, assertion := range value.Assertions { if assertion.Kind == Header || assertion.Kind == TextBody { assertion.Target = assertion.Target.(string) @@ -50,9 +52,5 @@ func ReadOpenStatus(path string) ([]Monitor, error) { monitor = append(monitor, value) } - if err != nil { - return nil, err - } - - return monitor, nil + return monitor } diff --git a/internal/monitors/monitor_apply.go b/internal/monitors/monitor_apply.go new file mode 100644 index 0000000..322e0aa --- /dev/null +++ b/internal/monitors/monitor_apply.go @@ -0,0 +1,201 @@ +package monitors + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + + "github.com/google/go-cmp/cmp" + confirmation "github.com/openstatusHQ/cli/internal/cli" + "github.com/openstatusHQ/cli/internal/config" + "github.com/urfave/cli/v3" + "sigs.k8s.io/yaml" +) + +func CompareLockWithConfig(apiKey string, applyChange bool, lock config.MonitorsLock, configData config.Monitors) (config.MonitorsLock, error) { + + var created, updated, deleted int + // Create or update monitors + for v, configValue := range configData { + value, exist := lock[v] + + if !exist { + + if applyChange { + + result, err := CreateMonitor(http.DefaultClient, apiKey, configValue) + if err != nil { + return nil, err + } + lock[v] = config.Lock{ + ID: result.ID, + Monitor: configValue, + } + } + + created++ + + continue + } + if !cmp.Equal(configValue, value.Monitor) { + if applyChange { + + result, err := UpdateMonitor(http.DefaultClient, apiKey, value.ID, configValue) + if err != nil { + return nil, err + } + lock[v] = config.Lock{ + ID: result.ID, + Monitor: configValue, + } + } + updated++ + continue + } + } + + // Delete monitors + for v, value := range lock { + if _, exist := configData[v]; !exist { + if applyChange { + + err := DeleteMonitor(http.DefaultClient, apiKey, fmt.Sprintf("%d", value.ID)) + if err != nil { + fmt.Println(err) + } + delete(lock, v) + } + deleted++ + } + } + + if created == 0 && updated == 0 && deleted == 0 { + fmt.Println("No change founded") + return nil, nil + } + + if applyChange { + fmt.Println("Successfully apply") + // if created > 0 { + // fmt.Println("Monitor Created:", created) + // } + // if updated > 0 { + // fmt.Println("Monitor Updated:", updated) + // } + // if deleted > 0 { + // fmt.Println("Monitor Deleted:", deleted) + // } + + return lock, nil + } + fmt.Println("This will apply the following change:") + if created > 0 { + fmt.Println("Monitor Create:", created) + } + if updated > 0 { + fmt.Println("Monitor Update:", updated) + } + if deleted > 0 { + fmt.Println("Monitor Delete:", deleted) + } + + if !confirmation.AskForConfirmation(("Do you want to continue?")) { + return nil, nil + } + return lock, nil +} + +func GetMonitorsApplyCmd() *cli.Command { + monitorsListCmd := cli.Command{ + Name: "apply", + Usage: "Create or update monitors", + Description: "Creates or updates monitors according to the OpenStatus configuration file", + UsageText: "openstatus monitors apply [options]", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "config", + Usage: "The configuration file containing monitor information", + Aliases: []string{"c"}, + 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, + }, + }, + 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) + } + } + + // Read Config file + // + monitors, err := config.ReadOpenStatus(path) + if err != nil { + return cli.Exit("Unable to read config file", 1) + } + + lock, err := config.ReadLockFile("openstatus.lock") + + if err != nil { + return cli.Exit("Unable to read lock file", 1) + } + + accept := cmd.Bool("auto-accept") + if !accept { + r, err := CompareLockWithConfig(cmd.String("access-token"), false, lock, monitors) + if err != nil { + return cli.Exit("Failed to apply change", 1) + + } + if r == nil { + return nil + } + } + + newLock, err := CompareLockWithConfig(cmd.String("access-token"), true, lock, monitors) + if err != nil { + return cli.Exit("Failed to apply change", 1) + } + if newLock == nil { + fmt.Println("No change founded") + return nil + } + y, err := yaml.Marshal(&newLock) + if err != nil { + return cli.Exit("Failed to apply change", 1) + } + // Write Lock file + file, err := os.OpenFile("openstatus.lock", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return cli.Exit("Failed to apply change", 1) + + } + defer file.Close() + + _, err = file.Write(y) + if err != nil { + return cli.Exit("Failed to apply change", 1) + } + return nil + }, + } + return &monitorsListCmd +} diff --git a/internal/monitors/monitor_create.go b/internal/monitors/monitor_create.go index 80839fa..0ffa069 100644 --- a/internal/monitors/monitor_create.go +++ b/internal/monitors/monitor_create.go @@ -15,7 +15,7 @@ import ( "github.com/urfave/cli/v3" ) -func CreateMonitor(httpClient *http.Client, apiKey string, monitor config.Monitor) error { +func CreateMonitor(httpClient *http.Client, apiKey string, monitor config.Monitor) (Monitor, error) { url := fmt.Sprintf("https://api.openstatus.dev/v1/monitor/%s", monitor.Kind) @@ -28,11 +28,11 @@ func CreateMonitor(httpClient *http.Client, apiKey string, monitor config.Monito res, err := httpClient.Do(req) if err != nil { - return err + return Monitor{}, err } if res.StatusCode != http.StatusOK { - return fmt.Errorf("Failed to create monitor") + return Monitor{}, fmt.Errorf("Failed to create monitor") } defer res.Body.Close() body, _ := io.ReadAll(res.Body) @@ -40,18 +40,21 @@ func CreateMonitor(httpClient *http.Client, apiKey string, monitor config.Monito var monitors Monitor err = json.Unmarshal(body, &monitors) if err != nil { - return err + return Monitor{}, err } - return nil + return monitors, nil } func GetMonitorCreateCmd() *cli.Command { monitorInfoCmd := cli.Command{ - Name: "create", - Usage: "Create monitors (beta)", - Description: "Create the monitors defined in the openstatus.yaml file", - UsageText: "openstatus monitors create [options]", + Name: "create", + Usage: "Create monitors (beta)", + Hidden: true, + HideHelp: true, + HideHelpCommand: true, + Description: "Create the monitors defined in the openstatus.yaml file", + UsageText: "openstatus monitors create [options]", Action: func(ctx context.Context, cmd *cli.Command) error { @@ -76,7 +79,7 @@ func GetMonitorCreateCmd() *cli.Command { } } for _, value := range monitors { - err = CreateMonitor(http.DefaultClient, cmd.String("access-token"), value) + _, err = CreateMonitor(http.DefaultClient, cmd.String("access-token"), value) if err != nil { return cli.Exit("Unable to create monitor", 1) } @@ -88,6 +91,7 @@ func GetMonitorCreateCmd() *cli.Command { &cli.StringFlag{ Name: "config", Usage: "The configuration file containing monitor information", + Aliases: []string{"c"}, DefaultText: "openstatus.yaml", Value: "openstatus.yaml", }, @@ -108,5 +112,3 @@ func GetMonitorCreateCmd() *cli.Command { } return &monitorInfoCmd } - -// os_3Za36BLXy7pN9ZY36SvSLnjD diff --git a/internal/monitors/monitor_delete.go b/internal/monitors/monitor_delete.go index 1cfa4f8..004c177 100644 --- a/internal/monitors/monitor_delete.go +++ b/internal/monitors/monitor_delete.go @@ -42,15 +42,17 @@ func DeleteMonitor(httpClient *http.Client, apiKey string, monitorId string) err 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]", + Name: "delete", + Usage: "Delete a monitor", + Hidden: true, + HideHelpCommand: true, + HideHelp: true, + UsageText: "openstatus monitors delete [MonitorID] [options]", Flags: []cli.Flag{ &cli.StringFlag{ @@ -79,6 +81,7 @@ func GetMonitorDeleteCmd() *cli.Command { if err != nil { return cli.Exit("Failed to delete monitor", 1) } + fmt.Printf("Monitor deleted successfully\n") return nil }, } diff --git a/internal/monitors/monitor_export.go b/internal/monitors/monitor_import.go similarity index 76% rename from internal/monitors/monitor_export.go rename to internal/monitors/monitor_import.go index 7138450..249aada 100644 --- a/internal/monitors/monitor_export.go +++ b/internal/monitors/monitor_import.go @@ -38,6 +38,7 @@ func ExportMonitor(httpClient *http.Client, apiKey string, path string) error { } t := map[string]config.Monitor{} + lock := make(map[string]config.Lock, len(monitors)) file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { @@ -87,7 +88,6 @@ func ExportMonitor(httpClient *http.Client, apiKey string, path string) error { Body: monitor.Body, Headers: headers, } - break case "tcp": uri := strings.Split(monitor.URL, ":") @@ -96,7 +96,7 @@ func ExportMonitor(httpClient *http.Client, apiKey string, path string) error { Host: uri[0], Port: int64(port), } - break + default: return fmt.Errorf("unknown job type: %s", monitor.JobType) } @@ -108,15 +108,13 @@ func ExportMonitor(httpClient *http.Client, apiKey string, path string) error { 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, + 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 @@ -131,22 +129,49 @@ func ExportMonitor(httpClient *http.Client, apiKey string, path string) error { if err != nil { return err } + + // + for id, monitor := range t { + i, _ := strconv.Atoi(id) + lock[id] = config.Lock{ + ID: i, + Monitor: monitor, + } + } + + lockFile, err := os.OpenFile("openstatus.lock", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return cli.Exit("Failed to apply change", 1) + + } + defer lockFile.Close() + + y, err = yaml.Marshal(&lock) + if err != nil { + return cli.Exit("Failed to apply change", 1) + } + + _, err = lockFile.Write(y) + if err != nil { + return cli.Exit("Failed to apply change", 1) + } + return nil } -func GetMonitorExportCmd() *cli.Command { +func GetMonitorImportCmd() *cli.Command { monitorInfoCmd := cli.Command{ - Name: "export", - Usage: "Export all your monitors", - UsageText: "openstatus monitor export [options]", - Description: "Export all your monitors to YAML", + Name: "import", + Usage: "Import all your monitors", + UsageText: "openstatus monitors import [options]", + Description: "Import all your monitors from your workspace to a YAML file; it will also create a lock file to manage your monitors with 'apply'.", 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")) + fmt.Printf("Monitors successfully imported to: %s", cmd.String("output")) return nil }, Flags: []cli.Flag{ diff --git a/internal/monitors/monitor_info.go b/internal/monitors/monitor_info.go index af190e7..bf0c4b6 100644 --- a/internal/monitors/monitor_info.go +++ b/internal/monitors/monitor_info.go @@ -46,8 +46,6 @@ func GetMonitorInfo(httpClient *http.Client, apiKey string, monitorId string) er return err } - // fmt.Println("Monitor") - fmt.Println(aurora.Bold("Monitor:")) table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint()), @@ -110,7 +108,7 @@ func GetMonitorInfoCmd() *cli.Command { monitorInfoCmd := cli.Command{ Name: "info", Usage: "Get a monitor information", - UsageText: "openstatus monitor info [MonitorID]", + UsageText: "openstatus monitors info [MonitorID]", Description: "Fetch the monitor information. The monitor information includes details such as name, description, endpoint, method, frequency, locations, active status, public status, timeout, degraded after, and body. The body is truncated to 40 characters.", Action: func(ctx context.Context, cmd *cli.Command) error { monitorId := cmd.Args().Get(0) diff --git a/internal/monitors/monitor_update.go b/internal/monitors/monitor_update.go new file mode 100644 index 0000000..849e617 --- /dev/null +++ b/internal/monitors/monitor_update.go @@ -0,0 +1,43 @@ +package monitors + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/openstatusHQ/cli/internal/config" +) + +func UpdateMonitor(httpClient *http.Client, apiKey string, id int, monitor config.Monitor) (Monitor, error) { + + url := fmt.Sprintf("https://api.openstatus.dev/v1/monitor/%s/%d", monitor.Kind, id) + + payloadBuf := new(bytes.Buffer) + json.NewEncoder(payloadBuf).Encode(monitor) + req, _ := http.NewRequest(http.MethodPut, 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 Monitor{}, err + } + + if res.StatusCode != http.StatusOK { + return Monitor{}, fmt.Errorf("Failed to Update monitor") + } + + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + + var monitors Monitor + err = json.Unmarshal(body, &monitors) + if err != nil { + return Monitor{}, err + } + + return monitors, nil +} diff --git a/internal/monitors/monitors.go b/internal/monitors/monitors.go index d607433..152efba 100644 --- a/internal/monitors/monitors.go +++ b/internal/monitors/monitors.go @@ -95,9 +95,10 @@ func MonitorsCmd() *cli.Command { Usage: "Manage your monitors", Commands: []*cli.Command{ + GetMonitorsApplyCmd(), GetMonitorCreateCmd(), GetMonitorDeleteCmd(), - GetMonitorExportCmd(), + GetMonitorImportCmd(), GetMonitorInfoCmd(), GetMonitorsListCmd(), GetMonitorsTriggerCmd(), diff --git a/internal/run/run.go b/internal/run/run.go index 8752072..48af7bc 100644 --- a/internal/run/run.go +++ b/internal/run/run.go @@ -120,7 +120,15 @@ func RunCmd() *cli.Command { Aliases: []string{"r"}, Usage: "Run your synthetics tests", UsageText: "openstatus run [options]", - Description: "Run the synthetic tests defined in the config.openstatus.yaml", + Description: `Run the synthetic tests defined in the config.openstatus.yaml. +The config file should be in the following format: + +tests: + ids: + - monitor-id-1 + - monitor-id-2 + + `, Action: func(ctx context.Context, cmd *cli.Command) error { path := cmd.String("config")