diff --git a/docs/data-sources/integration.md b/docs/data-sources/integration.md index f943f97..f61735c 100644 --- a/docs/data-sources/integration.md +++ b/docs/data-sources/integration.md @@ -6,17 +6,11 @@ description: |- --- -# Data Source statusflare_integration +# statusflare_integration (Data Source) + -Use this data source to look up Integration in Statusflare.. -## Example Usage -```terraform -data "statusflare_integration" "slack" { - name = "some-slack-integration" -} -``` ## Schema diff --git a/docs/index.md b/docs/index.md index ed96dcf..95974d8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,9 +6,8 @@ description: |- --- -# Statusflare Provider +# statusflare Provider -The Statusflare provider is used to interact with Statusflare monitor & integration resources. ## Example Usage @@ -35,6 +34,7 @@ provider "statusflare" { ### Optional -- **account_id** (String) Your Statusflare Account ID. This can also be specified with the `STATUSFLARE_ACCOUNT_ID` env. variable. -- **key_id** (String) Your token's key ID. This can also be specified with the `STATUSFLARE_KEY_ID` env. variable. -- **token** (String) Token's secret part. This can also be specified with the `STATUSFLARE_TOKEN` env. variable. +- **account_id** (String) Your Statusflare Account ID. This can also be specified with the `SF_ACCOUNT_ID` env. variable. +- **api_url** (String) Statusflare API URL. +- **key_id** (String) Your token's key ID. This can also be specified with the `SF_KEY_ID` env. variable. +- **token** (String) Token's secret part. This can also be specified with the `SF_TOKEN` env. variable. diff --git a/docs/resources/integration.md b/docs/resources/integration.md new file mode 100644 index 0000000..68bfcb1 --- /dev/null +++ b/docs/resources/integration.md @@ -0,0 +1,28 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "statusflare_integration Resource - terraform-provider-statusflare" +subcategory: "" +description: |- + +--- + +# statusflare_integration (Resource) + + + + + + +## Schema + +### Required + +- **name** (String) The name of the integration +- **secret** (String, Sensitive) The secret of the integration, e.g. webhook URL + +### Optional + +- **id** (String) The ID of this resource. +- **type** (String) Type of the integration, e.g. webhook + + diff --git a/docs/resources/monitor.md b/docs/resources/monitor.md index fded19a..aa90bb1 100644 --- a/docs/resources/monitor.md +++ b/docs/resources/monitor.md @@ -6,32 +6,10 @@ description: |- --- -# Resource statusflare_monitor - -Provides a Monitor resource for Statusflare. - -## Example Usage - -```terraform -// example of simple monitor -resource "statusflare_monitor" "first" { - name = "hello-world" - url = "www.helloworld.com" -} - -// example of simple monitor where incidents are forwarded to integration -data "statusflare_integration" "slack" { - name = "some-slack-integration" -} - -resource "statusflare_monitor" "first" { - name = "hello-world" - url = "www.helloworld.com" - integrations = [ - data.statusflare_integration.slack.id - ] -} -``` +# statusflare_monitor (Resource) + + + diff --git a/internal/provider/provider.go b/internal/provider/provider.go index f3aefd0..27e8f82 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -14,25 +14,25 @@ func New(version string) *schema.Provider { configFields := map[string]*schema.Schema{ "api_url": { Type: schema.TypeString, - Optional: true, + Required: true, DefaultFunc: schema.EnvDefaultFunc("SF_API_URL", "https://api.statusflare.com"), Description: "Statusflare API URL.", }, "account_id": { Type: schema.TypeString, - Optional: true, + Required: true, DefaultFunc: schema.EnvDefaultFunc("SF_ACCOUNT_ID", nil), Description: "Your Statusflare Account ID. This can also be specified with the `SF_ACCOUNT_ID` env. variable.", }, "key_id": { Type: schema.TypeString, - Optional: true, + Required: true, DefaultFunc: schema.EnvDefaultFunc("SF_KEY_ID", nil), Description: "Your token's key ID. This can also be specified with the `SF_KEY_ID` env. variable.", }, "token": { Type: schema.TypeString, - Optional: true, + Required: true, DefaultFunc: schema.EnvDefaultFunc("SF_TOKEN", nil), Description: "Token's secret part. This can also be specified with the `SF_TOKEN` env. variable.", }, @@ -40,7 +40,8 @@ func New(version string) *schema.Provider { return &schema.Provider{ ResourcesMap: map[string]*schema.Resource{ - "statusflare_monitor": resourceMonitor(), + "statusflare_monitor": resourceMonitor(), + "statusflare_integration": resourceIntegration(), }, DataSourcesMap: map[string]*schema.Resource{ "statusflare_integration": dataSourceIntegration(), diff --git a/internal/provider/resource_integration.go b/internal/provider/resource_integration.go new file mode 100644 index 0000000..63c51a0 --- /dev/null +++ b/internal/provider/resource_integration.go @@ -0,0 +1,120 @@ +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/statusflare-com/terraform-provider-statusflare/statusflare" +) + +func resourceIntegration() *schema.Resource { + + fields := map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the integration", + }, + "type": { + Type: schema.TypeString, + Optional: true, + Default: "webhook", + Description: "Type of the integration, e.g. webhook", + }, + "secret": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Sensitive: true, + Description: "The secret of the integration, e.g. webhook URL", + }, + } + + return &schema.Resource{ + CreateContext: resourceIntegrationCreate, + ReadContext: resourceIntegrationRead, + UpdateContext: resourceIntegrationUpdate, + DeleteContext: resourceIntegrationDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: fields, + } +} + +func resourceIntegrationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + var client *statusflare.Client = meta.(*statusflare.Client) + + integration, err := client.GetIntegration(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + integrationToData(integration, d) + return diags +} + +func resourceIntegrationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + var client *statusflare.Client = meta.(*statusflare.Client) + + integration := statusflare.Integration{} + dataToIntegration(d, &integration) + + err := client.CreateIntegration(&integration) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(integration.Id) + d.Set("secret", integration.Secret) + + resourceIntegrationRead(ctx, d, meta) + return diags +} + +func resourceIntegrationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var client *statusflare.Client = meta.(*statusflare.Client) + + integration, err := client.GetIntegration(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + dataToIntegration(d, integration) + err = client.SaveIntegration(integration) + if err != nil { + return diag.FromErr(err) + } + + return resourceIntegrationRead(ctx, d, meta) +} + +func resourceIntegrationDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var ( + err error + diags diag.Diagnostics + client *statusflare.Client = m.(*statusflare.Client) + ) + + err = client.DeleteIntegration(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + return diags +} + +func dataToIntegration(src *schema.ResourceData, dst *statusflare.Integration) { + dst.Name = src.Get("name").(string) + dst.Type = src.Get("type").(string) + dst.Secret = src.Get("secret").(string) +} + +func integrationToData(src *statusflare.Integration, dst *schema.ResourceData) { + dst.Set("name", src.Name) + dst.Set("type", src.Type) +} diff --git a/internal/provider/resource_monitor.go b/internal/provider/resource_monitor.go index 75c7648..a88e823 100644 --- a/internal/provider/resource_monitor.go +++ b/internal/provider/resource_monitor.go @@ -98,7 +98,7 @@ func resourceMonitorCreate(ctx context.Context, d *schema.ResourceData, meta int return diag.FromErr(err) } - d.SetId(monitor.ID) + d.SetId(monitor.Id) resourceMonitorRead(ctx, d, meta) return diags } diff --git a/statusflare/client.go b/statusflare/client.go index 2f03135..1f6ab85 100644 --- a/statusflare/client.go +++ b/statusflare/client.go @@ -14,8 +14,8 @@ import ( // represent the session to statusflare type Client struct { apiUrl string - accountID string - keyID string + accountId string + keyId string token string http *http.Client } @@ -54,8 +54,8 @@ func DefaultClient() (*Client, error) { client := &Client{ apiUrl: os.Getenv("SF_API_URL"), - accountID: os.Getenv("SF_ACCOUNT_ID"), - keyID: os.Getenv("SF_KEY_ID"), + accountId: os.Getenv("SF_ACCOUNT_ID"), + keyId: os.Getenv("SF_KEY_ID"), token: os.Getenv("SF_TOKEN"), http: &http.Client{}, } @@ -69,11 +69,11 @@ func DefaultClient() (*Client, error) { // // The account ID identify the whole account. Account might // have multiple key IDs with tokens. -func NewClient(apiUrl string, accountID string, keyID string, token string) *Client { +func NewClient(apiUrl string, accountId string, keyId string, token string) *Client { return &Client{ apiUrl: apiUrl, - accountID: accountID, - keyID: keyID, + accountId: accountId, + keyId: keyId, token: token, http: &http.Client{}, } @@ -93,7 +93,7 @@ func (c *Client) makeAPICall(method string, endpoint string, body []byte) (*http req, _ := http.NewRequest(method, url, reader) req.Header = map[string][]string{ "X-Statusflare-Token": {c.token}, - "X-Statusflare-Token-Key-Id": {c.keyID}, + "X-Statusflare-Token-Key-Id": {c.keyId}, } resp, err := c.http.Do(req) diff --git a/statusflare/integration.go b/statusflare/integration.go index 52e67ae..ac3494b 100644 --- a/statusflare/integration.go +++ b/statusflare/integration.go @@ -1,16 +1,47 @@ package statusflare -import "fmt" +import ( + "encoding/json" + "fmt" +) type Integration struct { Id string `json:"id"` - Type string `json:"type"` Name string `json:"name"` - Secret string `json:"secret"` + Type string `json:"type"` + Secret string `json:"secret,omitempty"` +} + +// create new integration +// +// When create process is successful, the +// function populate given 'i' by new values like ID +// etc. +// +// Be aware you're usig API token wih read & write +// permissions. +func (c *Client) CreateIntegration(i *Integration) error { + var err error + + body, err := json.Marshal(i) + if err != nil { + return err + } + + url := fmt.Sprintf("/integrations/%s", c.accountId) + resp, err := c.makeAPICall("POST", url, body) + if err != nil { + return err + } + + return unmarshallResp(resp, i) } +// returns list of all integrations +// the pagination is currently questionable. Let's +// assume function gives you all integrations func (c *Client) AllIntegrations() ([]*Integration, error) { - url := fmt.Sprintf("/integrations/%s", c.accountID) + url := fmt.Sprintf("/integrations/%s", c.accountId) resp, err := c.makeAPICall("GET", url, nil) if err != nil { return []*Integration{}, err @@ -21,3 +52,53 @@ func (c *Client) AllIntegrations() ([]*Integration, error) { return integrations, err } + +// get integration for given ID. +// If there is no integration for ID, the error is returned. +func (c *Client) GetIntegration(id string) (*Integration, error) { + var integration Integration + + url := fmt.Sprintf("/integrations/%s/%s", c.accountId, id) + resp, err := c.makeAPICall("GET", url, nil) + if err != nil { + return &integration, err + } + + err = unmarshallResp(resp, &integration) + return &integration, err +} + +// update the existing integration in statusflare. +// +// This function require presense of values in 'i' +// like accountID or ID. You can use integration returned +// by GetIntegration or AllIntegrations +// +// Also 'i' will be populated by values the update returns +func (c *Client) SaveIntegration(i *Integration) error { + var err error + + body, err := json.Marshal(i) + if err != nil { + return err + } + + url := fmt.Sprintf("/integrations/%s/%s", c.accountId, i.Id) + resp, err := c.makeAPICall("PUT", url, body) + if err != nil { + return err + } + + return unmarshallResp(resp, i) +} + +// function delete the monitor in statusflare. +func (c *Client) DeleteIntegration(id string) error { + url := fmt.Sprintf("/integrations/%s/%s", c.accountId, id) + _, err := c.makeAPICall("DELETE", url, nil) + if err != nil { + return err + } + + return nil +} diff --git a/statusflare/integration_test.go b/statusflare/integration_test.go index 7e9aff3..86555c4 100644 --- a/statusflare/integration_test.go +++ b/statusflare/integration_test.go @@ -12,6 +12,30 @@ func Test_Integrations(t *testing.T) { t.Fatal(err) } + var i *Integration + + // scenario: create new integration + Convey("When we create a new integration", t, func() { + + i = &Integration{ + Name: "Go test integration", + Type: "webhook", + Secret: "some-secret-webhook", + } + + err := client.CreateIntegration(i) + if err != nil { + t.Fatalf("%v", err) + } + + Convey("Then we can get this integration by its ID", func() { + _, err := client.GetIntegration(i.Id) + if err != nil { + t.Fatalf("%v", err) + } + }) + }) + Convey("When we get all integrations", t, func() { integrations, err := client.AllIntegrations() if err != nil { @@ -24,4 +48,35 @@ func Test_Integrations(t *testing.T) { } }) }) + + // scenario: change the integration + Convey("When we change the integration name", t, func() { + i.Name = "Go changed test integration" + err := client.SaveIntegration(i) + if err != nil { + t.Fatalf("cannot update integration: %v", err) + } + + Convey("Then the integration's name is changed", func() { + changedm, _ := client.GetIntegration(i.Id) + if changedm.Name != "Go changed test integration" { + t.Fatalf("The name of the integration is unchanged") + } + }) + }) + + // scenario: delete the integration + Convey("When we delete the integration", t, func() { + err = client.DeleteIntegration(i.Id) + if err != nil { + t.Fatalf("error in delete of integration: %v", err) + } + + Convey("Then integration is no more available in Statusflare", func() { + res, err := client.GetIntegration(i.Id) + if err == nil && res.Id != "" { + t.Fatalf("The integration still exist (%s)", i.Id) + } + }) + }) } diff --git a/statusflare/monitor.go b/statusflare/monitor.go index f8d06ac..5387fc9 100644 --- a/statusflare/monitor.go +++ b/statusflare/monitor.go @@ -6,7 +6,7 @@ import ( ) type Monitor struct { - ID string `json:"id"` + Id string `json:"id"` Name string `json:"name"` URL string `json:"url"` Scheme string `json:"schema"` // not sure if it's typo in API, but I'm using Scheme here @@ -33,24 +33,20 @@ func (c *Client) CreateMonitor(m *Monitor) error { return err } - url := fmt.Sprintf("/uptime/%s", c.accountID) + url := fmt.Sprintf("/uptime/%s", c.accountId) resp, err := c.makeAPICall("POST", url, body) if err != nil { return err } - if resp.StatusCode != 200 { - return fmt.Errorf("the server respond %d", resp.StatusCode) - } - return unmarshallResp(resp, m) } // returns list of all monitors // the pagination is currently questionable. Let's -// assume function gives you all moninots +// assume function gives you all monitors func (c *Client) AllMonitors() ([]*Monitor, error) { - url := fmt.Sprintf("/uptime/%s", c.accountID) + url := fmt.Sprintf("/uptime/%s", c.accountId) resp, err := c.makeAPICall("GET", url, nil) if err != nil { return []*Monitor{}, err @@ -67,7 +63,7 @@ func (c *Client) AllMonitors() ([]*Monitor, error) { func (c *Client) GetMonitor(id string) (*Monitor, error) { var monitor Monitor - url := fmt.Sprintf("/uptime/%s/%s", c.accountID, id) + url := fmt.Sprintf("/uptime/%s/%s", c.accountId, id) resp, err := c.makeAPICall("GET", url, nil) if err != nil { return &monitor, err @@ -92,30 +88,22 @@ func (c *Client) SaveMonitor(m *Monitor) error { return err } - url := fmt.Sprintf("/uptime/%s/%s", c.accountID, m.ID) + url := fmt.Sprintf("/uptime/%s/%s", c.accountId, m.Id) resp, err := c.makeAPICall("PUT", url, body) if err != nil { return err } - if resp.StatusCode != 200 { - return fmt.Errorf("the server respond %d", resp.StatusCode) - } - return unmarshallResp(resp, m) } // function delete the monitor in statusflare. func (c *Client) DeleteMonitor(id string) error { - url := fmt.Sprintf("/uptime/%s/%s", c.accountID, id) - resp, err := c.makeAPICall("DELETE", url, nil) + url := fmt.Sprintf("/uptime/%s/%s", c.accountId, id) + _, err := c.makeAPICall("DELETE", url, nil) if err != nil { return err } - if resp.StatusCode != 200 { - return fmt.Errorf("the server respond %d", resp.StatusCode) - } - return nil } diff --git a/statusflare/monitor_test.go b/statusflare/monitor_test.go index 16c8330..3069dc6 100644 --- a/statusflare/monitor_test.go +++ b/statusflare/monitor_test.go @@ -19,12 +19,13 @@ func Test_MonitorIntegration(t *testing.T) { Convey("When we create a new monitor", t, func() { m = &Monitor{ - Name: "test-1", - URL: "sme.sk", + Name: "Go test monitor", + URL: "www.statusflare.com", Scheme: "https", Method: "GET", ExpectStatus: 200, Worker: "managed", + Integrations: []string{""}, } err := client.CreateMonitor(m) @@ -32,8 +33,8 @@ func Test_MonitorIntegration(t *testing.T) { t.Fatalf("%v", err) } - Convey("Then we cat get this monitor by his ID", func() { - _, err := client.GetMonitor(m.ID) + Convey("Then we cat get this monitor by its ID", func() { + _, err := client.GetMonitor(m.Id) if err != nil { t.Fatalf("%v", err) } @@ -42,16 +43,16 @@ func Test_MonitorIntegration(t *testing.T) { // scenario: change the monitor Convey("When we save the changed monitor", t, func() { - m.Name = "test-2" - m.URL = "hnonline.sk" + m.Name = "Go changed test monitor" + m.URL = "dash.statusflare.com" err := client.SaveMonitor(m) if err != nil { t.Fatalf("cannot update monitor: %v", err) } Convey("Then the monitor's name is changed", func() { - changedm, _ := client.GetMonitor(m.ID) - if changedm.Name != "test-zvr-6" { + changedm, _ := client.GetMonitor(m.Id) + if changedm.Name != "Go changed test monitor" { t.Fatalf("The name of the monitor is unchanged") } }) @@ -59,15 +60,15 @@ func Test_MonitorIntegration(t *testing.T) { // scenario: delete the monitor Convey("When we delete the monitor", t, func() { - err = client.DeleteMonitor(m.ID) + err = client.DeleteMonitor(m.Id) if err != nil { t.Fatalf("error in delete of monitor: %v", err) } Convey("Then monitor is no more available in Statusflare", func() { - res, err := client.GetMonitor(m.ID) - if err == nil && res.ID != "" { - t.Fatalf("The monitor still exist, even we delete it (%s)", m.ID) + res, err := client.GetMonitor(m.Id) + if err == nil && res.Id != "" { + t.Fatalf("The monitor still exist, even we delete it (%s)", m.Id) } }) })