diff --git a/Makefile b/Makefile index de5c5f8..74073dc 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ install: build mv ${BINARY} ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME}/${VERSION}/${OS_ARCH} testacc: - TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 120m -parallel 1 + TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 120m -parallel 3 -count=1 clean: rm -rf ${BINARY} diff --git a/docs/index.md b/docs/index.md index b5ee4a7..891cca6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,6 +35,6 @@ provider "statusflare" { ### Optional - **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. +- **api_url** (String) Statusflare API URL. This can also be specified with the `SF_API_URL` env. variable. - **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 index d219bc7..7f3b8f8 100644 --- a/docs/resources/integration.md +++ b/docs/resources/integration.md @@ -25,12 +25,12 @@ resource "statusflare_integration" "example" { ### Required -- **name** (String) The name of the integration -- **secret** (String, Sensitive) The secret of the integration, e.g. webhook URL +- **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 +- **type** (String) Type of the integration, e.g. webhook. diff --git a/docs/resources/monitor.md b/docs/resources/monitor.md index 66558b3..58344ff 100644 --- a/docs/resources/monitor.md +++ b/docs/resources/monitor.md @@ -28,17 +28,17 @@ resource "statusflare_monitor" "example" { ### Required -- **name** (String) The name of the monitor. Must be unique -- **url** (String) URL Address but without schema. It might be www.example.com +- **name** (String) The name of the monitor. Must be unique. +- **url** (String) URL Address but without schema. It might be www.example.com. ### Optional - **expect_status** (Number) The expected HTTP status code. The default is 200. - **id** (String) The ID of this resource. - **integrations** (List of String) IDs of integrations attached to this monitor. -- **method** (String) The HTTP method. The default is 'GET' +- **method** (String) The HTTP method. The default is 'GET'. - **retries** (Number) Retries or also 'notify_after' field in API. The default is 1. - **scheme** (String) The scheme might be http or https. The default value is https. -- **worker** (String) Don't know purpose of this field but default value is 'managed' +- **worker** (String) ID of the worker to perform checks from. The default is 'managed'. diff --git a/docs/resources/status_page.md b/docs/resources/status_page.md new file mode 100644 index 0000000..bd75ce6 --- /dev/null +++ b/docs/resources/status_page.md @@ -0,0 +1,60 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "statusflare_status_page Resource - terraform-provider-statusflare" +subcategory: "" +description: |- + +--- + +# statusflare_status_page (Resource) + + + +## Example Usage + +```terraform +resource "statusflare_monitor" "example" { + name = "Example monitor" + url = "www.example.com" +} + +resource "statusflare_status_page" "example" { + name = "My example status page" + monitors = [statusflare_monitor.example.id] + + // following config options are defaults you can override + config = { + title = "Status Page", + logo_url = "statusflare.png", + favicon_url = "favicon.ico", + all_monitors_operational = "All Monitors Operational", + not_all_monitors_operational = "Not All Monitors Operational", + monitor_operational_label = "Operational", + monitor_not_operational_label = "Not Operational", + monitor_no_data_label = "No data", + histogram_no_data = "No data", + histogram_no_incidents = "All good", + histogram_some_incidents = "incident(s)", + } +} +``` + + +## Schema + +### Required + +- **name** (String) The name of the status page. + +### Optional + +- **config** (Map of String) Additional configuration of the status page. See example for list of options. +- **custom_domain** (String) The custom domain attached to your status page. +- **custom_domain_path** (String) The path for your custom domain. The default is '/'. +- **hide_monitor_details** (Boolean) Hide monitor details (URL, scheme, ..) on the status page. The default is false. +- **hide_statusflare** (Boolean) Hide statusflare branding/links on the status page. The default is false. +- **histogram_days** (Number) Number of days to render on status page for each monitor. The default is 90. +- **id** (String) The ID of this resource. +- **monitors** (List of String) IDs of monitors attached to this status page. + + diff --git a/examples/resources/statusflare_status_page/resource.tf b/examples/resources/statusflare_status_page/resource.tf new file mode 100644 index 0000000..522741f --- /dev/null +++ b/examples/resources/statusflare_status_page/resource.tf @@ -0,0 +1,24 @@ +resource "statusflare_monitor" "example" { + name = "Example monitor" + url = "www.example.com" +} + +resource "statusflare_status_page" "example" { + name = "My example status page" + monitors = [statusflare_monitor.example.id] + + // following config options are defaults you can override + config = { + title = "Status Page", + logo_url = "statusflare.png", + favicon_url = "favicon.ico", + all_monitors_operational = "All Monitors Operational", + not_all_monitors_operational = "Not All Monitors Operational", + monitor_operational_label = "Operational", + monitor_not_operational_label = "Not Operational", + monitor_no_data_label = "No data", + histogram_no_data = "No data", + histogram_no_incidents = "All good", + histogram_some_incidents = "incident(s)", + } +} \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 27e8f82..7d6feef 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -16,7 +16,7 @@ func New(version string) *schema.Provider { Type: schema.TypeString, Required: true, DefaultFunc: schema.EnvDefaultFunc("SF_API_URL", "https://api.statusflare.com"), - Description: "Statusflare API URL.", + Description: "Statusflare API URL. This can also be specified with the `SF_API_URL` env. variable.", }, "account_id": { Type: schema.TypeString, @@ -42,6 +42,7 @@ func New(version string) *schema.Provider { ResourcesMap: map[string]*schema.Resource{ "statusflare_monitor": resourceMonitor(), "statusflare_integration": resourceIntegration(), + "statusflare_status_page": resourceStatusPage(), }, DataSourcesMap: map[string]*schema.Resource{ "statusflare_integration": dataSourceIntegration(), diff --git a/internal/provider/resource_integration.go b/internal/provider/resource_integration.go index 63c51a0..ab2d85c 100644 --- a/internal/provider/resource_integration.go +++ b/internal/provider/resource_integration.go @@ -14,20 +14,20 @@ func resourceIntegration() *schema.Resource { "name": { Type: schema.TypeString, Required: true, - Description: "The name of the integration", + Description: "The name of the integration.", }, "type": { Type: schema.TypeString, Optional: true, Default: "webhook", - Description: "Type of the integration, e.g. 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", + Description: "The secret of the integration, e.g. webhook URL.", }, } diff --git a/internal/provider/resource_monitor.go b/internal/provider/resource_monitor.go index a88e823..240356d 100644 --- a/internal/provider/resource_monitor.go +++ b/internal/provider/resource_monitor.go @@ -14,12 +14,12 @@ func resourceMonitor() *schema.Resource { "name": { Type: schema.TypeString, Required: true, - Description: "The name of the monitor. Must be unique", + Description: "The name of the monitor. Must be unique.", }, "url": { Type: schema.TypeString, Required: true, - Description: "URL Address but without schema. It might be www.example.com", + Description: "URL Address but without schema. It might be www.example.com.", }, "scheme": { Type: schema.TypeString, @@ -31,7 +31,7 @@ func resourceMonitor() *schema.Resource { Type: schema.TypeString, Optional: true, Default: "GET", - Description: "The HTTP method. The default is 'GET'", + Description: "The HTTP method. The default is 'GET'.", }, "expect_status": { Type: schema.TypeInt, @@ -49,7 +49,7 @@ func resourceMonitor() *schema.Resource { Type: schema.TypeString, Optional: true, Default: "managed", - Description: "Don't know purpose of this field but default value is 'managed'", + Description: "ID of the worker to perform checks from. The default is 'managed'.", }, "integrations": { Type: schema.TypeList, diff --git a/internal/provider/resource_status_page.go b/internal/provider/resource_status_page.go new file mode 100644 index 0000000..658b46a --- /dev/null +++ b/internal/provider/resource_status_page.go @@ -0,0 +1,243 @@ +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 resourceStatusPage() *schema.Resource { + + fields := map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the status page.", + }, + "monitors": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "IDs of monitors attached to this status page.", + }, + "custom_domain": { + Type: schema.TypeString, + Optional: true, + Description: "The custom domain attached to your status page.", + }, + "custom_domain_path": { + Type: schema.TypeString, + Optional: true, + Description: "The path for your custom domain. The default is '/'.", + }, + "hide_monitor_details": { + Type: schema.TypeBool, + Optional: true, + Description: "Hide monitor details (URL, scheme, ..) on the status page. The default is false.", + }, + "hide_statusflare": { + Type: schema.TypeBool, + Optional: true, + Description: "Hide statusflare branding/links on the status page. The default is false.", + }, + "histogram_days": { + Type: schema.TypeInt, + Optional: true, + Description: "Number of days to render on status page for each monitor. The default is 90.", + }, + "config": { + Type: schema.TypeMap, + Optional: true, + Description: "Additional configuration of the status page. See example for list of options.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + //Elem: &schema.Resource{ + // Schema: map[string]*schema.Schema{ + // "title": { + // Type: schema.TypeString, + // Optional: true, + // Description: "Title of the status page.", + // }, + // "histogram_days": { + // Type: schema.TypeInt, + // Optional: true, + // Description: "Number of days to render on status page for each monitor. The default is 90.", + // }, + // "logo_url": { + // Type: schema.TypeString, + // Optional: true, + // Description: "Logo URL for the status page.", + // }, + // "favicon_url": { + // Type: schema.TypeString, + // Optional: true, + // Description: "Favicon URL for the status page.", + // }, + // "all_monitors_operational": { + // Type: schema.TypeString, + // Optional: true, + // Description: "Customize the message that shows no issues. Default is 'All Monitors Operational'", + // }, + // "not_all_monitors_operational": { + // Type: schema.TypeString, + // Optional: true, + // Description: "Customize the message that shows an incident. Default is 'Not All Monitors Operational'", + // }, + // "monitor_operational_label": { + // Type: schema.TypeString, + // Optional: true, + // Description: "Customize the status message that shows a monitor is working fine. Default is 'Operational'", + // }, + // "monitor_not_operational_label": { + // Type: schema.TypeString, + // Optional: true, + // Description: "Customize the status message that shows a monitor is not operational. Default is 'Not Operational'", + // }, + // "monitor_no_data_label": { + // Type: schema.TypeString, + // Optional: true, + // Description: "Customize the status message that shows a monitor has no data. Default is 'No data'", + // }, + // "histogram_no_data": { + // Type: schema.TypeString, + // Optional: true, + // Description: "Customize the message showing days that do not have any data yet. Default is 'No data'", + // }, + // "histogram_no_incidents": { + // Type: schema.TypeString, + // Optional: true, + // Description: "Customize the message showing days that do not have any incidents. Default is 'All good'", + // }, + // "histogram_some_incidents": { + // Type: schema.TypeString, + // Optional: true, + // Description: "Customize the message suffix showing days that do have incidents. Default is 'incident(s)'", + // }, + // }, + //}, + }, + } + + return &schema.Resource{ + CreateContext: resourceStatusPageCreate, + ReadContext: resourceStatusPageRead, + UpdateContext: resourceStatusPageUpdate, + DeleteContext: resourceStatusPageDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: fields, + } +} + +func resourceStatusPageRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + var client *statusflare.Client = meta.(*statusflare.Client) + + statusPage, err := client.GetStatusPage(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + statusPageToData(statusPage, d) + return diags +} + +func resourceStatusPageCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + var client *statusflare.Client = meta.(*statusflare.Client) + + statusPage := statusflare.StatusPage{} + dataToStatusPage(d, &statusPage) + + err := client.CreateStatusPage(&statusPage) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(statusPage.Id) + resourceStatusPageRead(ctx, d, meta) + return diags +} + +func resourceStatusPageUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var client *statusflare.Client = meta.(*statusflare.Client) + + statusPage, err := client.GetStatusPage(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + dataToStatusPage(d, statusPage) + err = client.SaveStatusPage(statusPage) + if err != nil { + return diag.FromErr(err) + } + + return resourceStatusPageRead(ctx, d, meta) +} + +func resourceStatusPageDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var ( + err error + diags diag.Diagnostics + client *statusflare.Client = m.(*statusflare.Client) + ) + + err = client.DeleteStatusPage(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + return diags +} + +func dataToStatusPage(src *schema.ResourceData, dst *statusflare.StatusPage) { + dst.Name = src.Get("name").(string) + dst.Monitors = toStrArray(src.Get("monitors").([]interface{})) + dst.CustomDomain = src.Get("custom_domain").(string) + dst.CustomDomainPath = src.Get("custom_domain_path").(string) + dst.HideMonitorDetails = src.Get("hide_monitor_details").(bool) + dst.HideStatusflare = src.Get("hide_statusflare").(bool) + + dst.Config.Title = src.Get("config.title").(string) + dst.Config.HistogramDays = src.Get("histogram_days").(int) + dst.Config.LogoUrl = src.Get("config.logo_url").(string) + dst.Config.FaviconUrl = src.Get("config.favicon_url").(string) + dst.Config.AllMonitorsOperational = src.Get("config.all_monitors_operational").(string) + dst.Config.NotAllMonitorsOperational = src.Get("config.not_all_monitors_operational").(string) + dst.Config.MonitorOperationalLabel = src.Get("config.monitor_operational_label").(string) + dst.Config.MonitorNotOperationalLabel = src.Get("config.monitor_not_operational_label").(string) + dst.Config.MonitorNoDataLabel = src.Get("config.monitor_no_data_label").(string) + dst.Config.HistogramNoData = src.Get("config.histogram_no_data").(string) + dst.Config.HistogramNoIncidents = src.Get("config.histogram_no_incidents").(string) + dst.Config.HistogramSomeIncidents = src.Get("config.histogram_some_incidents").(string) +} + +func statusPageToData(src *statusflare.StatusPage, dst *schema.ResourceData) { + dst.Set("name", src.Name) + dst.Set("monitors", src.Monitors) + dst.Set("custom_domain", src.CustomDomain) + dst.Set("custom_domain_path", src.CustomDomainPath) + dst.Set("hide_monitor_details", src.HideMonitorDetails) + dst.Set("hide_statusflare", src.HideStatusflare) + + dst.Set("config.title", src.Config.Title) + dst.Set("histogram_days", src.Config.HistogramDays) + dst.Set("config.logo_url", src.Config.LogoUrl) + dst.Set("config.favicon_url", src.Config.FaviconUrl) + dst.Set("config.all_monitors_operational", src.Config.AllMonitorsOperational) + dst.Set("config.not_all_monitors_operational", src.Config.NotAllMonitorsOperational) + dst.Set("config.monitor_operational_label", src.Config.MonitorOperationalLabel) + dst.Set("config.monitor_not_operational_label", src.Config.MonitorNotOperationalLabel) + dst.Set("config.monitor_no_data_label", src.Config.MonitorNoDataLabel) + dst.Set("config.histogram_no_data", src.Config.HistogramNoData) + dst.Set("config.histogram_no_incidents", src.Config.HistogramNoIncidents) + dst.Set("config.histogram_some_incidents", src.Config.HistogramSomeIncidents) +} diff --git a/statusflare/monitor_test.go b/statusflare/monitor_test.go index 3069dc6..0ad98a9 100644 --- a/statusflare/monitor_test.go +++ b/statusflare/monitor_test.go @@ -6,7 +6,7 @@ import ( . "github.com/smartystreets/goconvey/convey" ) -func Test_MonitorIntegration(t *testing.T) { +func Test_Monitor(t *testing.T) { var m *Monitor @@ -25,7 +25,7 @@ func Test_MonitorIntegration(t *testing.T) { Method: "GET", ExpectStatus: 200, Worker: "managed", - Integrations: []string{""}, + Integrations: []string{}, } err := client.CreateMonitor(m) diff --git a/statusflare/status_page.go b/statusflare/status_page.go new file mode 100644 index 0000000..d8cc418 --- /dev/null +++ b/statusflare/status_page.go @@ -0,0 +1,123 @@ +package statusflare + +import ( + "encoding/json" + "fmt" +) + +type StatusPage struct { + Id string `json:"id"` + Name string `json:"name"` + Monitors []string `json:"monitors"` + CustomDomain string `json:"custom_domain,omitempty"` // not sure if it's typo in API, but I'm using Scheme here + CustomDomainPath string `json:"custom_domain_path,omitempty"` + HideMonitorDetails bool `json:"hide_monitor_details"` + HideStatusflare bool `json:"hide_statusflare"` + Config StatusPageConfig `json:"config,omitempty"` +} + +type StatusPageConfig struct { + Title string `json:"title,omitempty"` + HistogramDays int `json:"histogram_days,omitempty"` + LogoUrl string `json:"logo_url,omitempty"` + FaviconUrl string `json:"favicon_url,omitempty"` + AllMonitorsOperational string `json:"all_monitors_operational,omitempty"` + NotAllMonitorsOperational string `json:"not_all_monitors_operational,omitempty"` + MonitorOperationalLabel string `json:"monitor_operational_label,omitempty"` + MonitorNotOperationalLabel string `json:"monitor_not_operational_label,omitempty"` + MonitorNoDataLabel string `json:"monitor_no_data_label,omitempty"` + HistogramNoData string `json:"histogram_no_data,omitempty"` + HistogramNoIncidents string `json:"histogram_no_incidents,omitempty"` + HistogramSomeIncidents string `json:"histogram_some_incidents,omitempty"` +} + +// create new status page +// +// When create process is successful, the +// function populate given 's' by new values like ID +// etc. +// +// Be aware you're usig API token wih read & write +// permissions. +func (c *Client) CreateStatusPage(s *StatusPage) error { + var err error + + body, err := json.Marshal(s) + if err != nil { + return err + } + + url := fmt.Sprintf("/status-pages/%s", c.accountId) + resp, err := c.makeAPICall("POST", url, body) + if err != nil { + return err + } + + return unmarshallResp(resp, s) +} + +// returns list of all status pages +// the pagination is currently questionable. Let's +// assume function gives you all status pages +func (c *Client) AllStatusPages() ([]*StatusPage, error) { + url := fmt.Sprintf("/status-pages/%s", c.accountId) + resp, err := c.makeAPICall("GET", url, nil) + if err != nil { + return []*StatusPage{}, err + } + + statusPages := []*StatusPage{} + err = unmarshallResp(resp, &statusPages) + + return statusPages, err +} + +// get status page for given ID. +// If there is no status page for ID, the error is returned. +func (c *Client) GetStatusPage(id string) (*StatusPage, error) { + var statusPage StatusPage + + url := fmt.Sprintf("/status-pages/%s/%s", c.accountId, id) + resp, err := c.makeAPICall("GET", url, nil) + if err != nil { + return &statusPage, err + } + + err = unmarshallResp(resp, &statusPage) + return &statusPage, err +} + +// update the existing status page in statusflare. +// +// This function require presense of values in 's' +// like accountID or ID. You can use status page returned +// by GetStatusPAge or AllStatusPages +// +// Also 's' will be populated by values the update returns +func (c *Client) SaveStatusPage(s *StatusPage) error { + var err error + + body, err := json.Marshal(s) + if err != nil { + return err + } + + url := fmt.Sprintf("/status-pages/%s/%s", c.accountId, s.Id) + resp, err := c.makeAPICall("PUT", url, body) + if err != nil { + return err + } + + return unmarshallResp(resp, s) +} + +// function delete the status page in statusflare. +func (c *Client) DeleteStatusPage(id string) error { + url := fmt.Sprintf("/status-pages/%s/%s", c.accountId, id) + _, err := c.makeAPICall("DELETE", url, nil) + if err != nil { + return err + } + + return nil +} diff --git a/statusflare/status_page_test.go b/statusflare/status_page_test.go new file mode 100644 index 0000000..12f25e6 --- /dev/null +++ b/statusflare/status_page_test.go @@ -0,0 +1,69 @@ +package statusflare + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func Test_StatusPage(t *testing.T) { + + var s *StatusPage + + client, err := DefaultClient() + if err != nil { + t.Fatal(err) + } + + // scenario: create new status page + Convey("When we create a new status page", t, func() { + + s = &StatusPage{ + Name: "Go test status page", + Monitors: []string{}, + } + + err := client.CreateStatusPage(s) + if err != nil { + t.Fatalf("%v", err) + } + + Convey("Then we cat get this status page by its ID", func() { + _, err := client.GetStatusPage(s.Id) + if err != nil { + t.Fatalf("%v", err) + } + }) + }) + + // scenario: change the status page + Convey("When we save the changed status page", t, func() { + s.Name = "Go changed test status page" + err := client.SaveStatusPage(s) + if err != nil { + t.Fatalf("cannot update status page: %v", err) + } + + Convey("Then the status pages's name is changed", func() { + changedm, _ := client.GetStatusPage(s.Id) + if changedm.Name != "Go changed test status page" { + t.Fatalf("The name of the status page is unchanged") + } + }) + }) + + // scenario: delete the status page + Convey("When we delete the status page", t, func() { + err = client.DeleteStatusPage(s.Id) + if err != nil { + t.Fatalf("error in delete of status page: %v", err) + } + + Convey("Then status page is no more available in Statusflare", func() { + res, err := client.GetMonitor(s.Id) + if err == nil && res.Id != "" { + t.Fatalf("The status page still exist, even we delete it (%s)", s.Id) + } + }) + }) +}