diff --git a/vkcs/monitoring.go b/vkcs/monitoring.go new file mode 100644 index 00000000..4255eaa8 --- /dev/null +++ b/vkcs/monitoring.go @@ -0,0 +1,108 @@ +package vkcs + +import ( + "net/http" + "time" + + "github.com/gophercloud/gophercloud" +) + +// monitoringClient performs request to cloud monitoring api +type monitoringClient interface { + Get(url string, JSONResponse interface{}, opts *gophercloud.RequestOpts) (*http.Response, error) + Post(url string, JSONBody interface{}, JSONResponse interface{}, opts *gophercloud.RequestOpts) (*http.Response, error) + Patch(url string, JSONBody interface{}, JSONResponse interface{}, opts *gophercloud.RequestOpts) (*http.Response, error) + Delete(url string, opts *gophercloud.RequestOpts) (*http.Response, error) + Head(url string, opts *gophercloud.RequestOpts) (*http.Response, error) + Put(url string, JSONBody interface{}, JSONResponse interface{}, opts *gophercloud.RequestOpts) (*http.Response, error) + ServiceURL(parts ...string) string +} + +type CreateTrigger struct { + Name string `json:"name"` + Status string `json:"status"` + Namespace string `json:"namespace"` + Query string `json:"query"` + Interval int `json:"interval"` + NotificationTitle string `json:"notification_title"` + NotificationChannels []string `json:"notification_channels"` +} + +type TriggerIn struct { + Trigger CreateTrigger `json:"trigger"` +} + +// Map converts opts to a map (for a request body) +func (opts TriggerIn) Map() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +type TriggerData struct { + CreatedAt time.Time `json:"created_at"` + ID string `json:"id"` + Namespace string `json:"namespace"` + Name string `json:"name"` + Interval int `json:"interval"` + NotificationChannels []string `json:"notification_channels"` + NotificationTitle string `json:"notification_title"` + Query string `json:"query"` + Status string `json:"status"` + UpdatedAt time.Time `json:"updated_at"` +} + +type TriggerOut struct { + Trigger TriggerData `json:"trigger"` +} + +type TriggersList struct { + Triggers []TriggerData `json:"triggers"` +} + +type ChannelIn struct { + Name string `json:"name" required:"true"` + ChannelType string `json:"channel_type" required:"true"` + Address string `json:"address" required:"true"` +} + +// Map converts opts to a map (for a request body) +func (opts ChannelIn) Map() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +type ChannelOut struct { + Channel struct { + Address string `json:"address"` + ChannelType string `json:"channel_type"` + CreatedAt time.Time `json:"created_at"` + ID string `json:"id"` + Name string `json:"name"` + UpdatedAt time.Time `json:"updated_at"` + } `json:"channel"` +} + +type ChannelList struct { + Channels []struct { + Address string `json:"address"` + ChannelType string `json:"channel_type"` + CreatedAt time.Time `json:"created_at"` + ID string `json:"id"` + InUse bool `json:"in_use"` + Name string `json:"name"` + UpdatedAt time.Time `json:"updated_at"` + } `json:"channels"` +} + +type TemplateIn struct { + InstanceID string `json:"instance_id"` + Capabilities []string `json:"capabilities"` +} + +// Map converts opts to a map (for a request body) +func (opts TemplateIn) Map() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +type TemplateOut struct { + Script string `json:"script"` + LinkID string `json:"link_id"` +} diff --git a/vkcs/monitoring_channel.go b/vkcs/monitoring_channel.go new file mode 100644 index 00000000..3874e6be --- /dev/null +++ b/vkcs/monitoring_channel.go @@ -0,0 +1,89 @@ +package vkcs + +import ( + "github.com/gophercloud/gophercloud" + "net/http" +) + +func instanceChannelURL(c ContainerClient, pid string, id string) string { + return c.ServiceURL(pid, "notification_channels", id) +} + +func createChannelURL(c ContainerClient, pid string) string { + return c.ServiceURL(pid, "notification_channels") +} + +type commonChannelResult struct { + gophercloud.Result +} + +// extract is used to extract result into short response struct +func (r commonChannelResult) extract() (*ChannelOut, error) { + var c *ChannelOut + if err := r.ExtractInto(&c); err != nil { + return nil, err + } + return c, nil +} + +type deleteChannelResult struct { + gophercloud.Result +} + +func (r deleteChannelResult) extractErr() error { + return r.Err +} + +func channelCreate(client monitoringClient, pid string, opts createOptsBuilder) commonChannelResult { + b, err := opts.Map() + var r commonChannelResult + if err != nil { + r.Err = err + return r + } + var result *http.Response + reqOpts := getMonRequestOpts(http.StatusCreated) + result, r.Err = client.Post(createChannelURL(client, pid), b, &r.Body, reqOpts) + if r.Err == nil { + r.Header = result.Header + } + return r +} + +func channelUpdate(client monitoringClient, pid string, id string, opts createOptsBuilder) commonChannelResult { + b, err := opts.Map() + var r commonChannelResult + if err != nil { + r.Err = err + return r + } + var result *http.Response + reqOpts := getMonRequestOpts(http.StatusCreated) + result, r.Err = client.Patch(instanceChannelURL(client, pid, id), b, &r.Body, reqOpts) + if r.Err == nil { + r.Header = result.Header + } + return r +} + +// triggerGet performs request to get trigger instance +func channelGet(client monitoringClient, pid string, id string) (r commonChannelResult) { + reqOpts := getMonRequestOpts(http.StatusOK) + var result *http.Response + result, r.Err = client.Get(instanceChannelURL(client, pid, id), &r.Body, reqOpts) + if r.Err == nil { + r.Header = result.Header + } + return +} + +// triggerDelete performs request to delete trigger +func channelDelete(client monitoringClient, pid string, id string) (r deleteChannelResult) { + reqOpts := getMonRequestOpts(http.StatusNoContent) + var result *http.Response + result, r.Err = client.Delete(instanceChannelURL(client, pid, id), reqOpts) + if r.Err == nil { + r.Header = result.Header + } + return +} diff --git a/vkcs/monitoring_template.go b/vkcs/monitoring_template.go new file mode 100644 index 00000000..da140e3d --- /dev/null +++ b/vkcs/monitoring_template.go @@ -0,0 +1,39 @@ +package vkcs + +import ( + "github.com/gophercloud/gophercloud" + "net/http" +) + +func createTemplateURL(c ContainerClient, pid string) string { + return c.ServiceURL("project", pid, "link") +} + +type commonTemplateResult struct { + gophercloud.Result +} + +// extract is used to extract result into short response struct +func (r commonTemplateResult) extract() (*TemplateOut, error) { + var t *TemplateOut + if err := r.ExtractInto(&t); err != nil { + return nil, err + } + return t, nil +} + +func templateCreate(client monitoringClient, pid string, opts createOptsBuilder) commonTemplateResult { + b, err := opts.Map() + var r commonTemplateResult + if err != nil { + r.Err = err + return r + } + var result *http.Response + reqOpts := getMonRequestOpts(http.StatusCreated) + result, r.Err = client.Post(createTemplateURL(client, pid), b, &r.Body, reqOpts) + if r.Err == nil { + r.Header = result.Header + } + return r +} diff --git a/vkcs/monitoring_trigger.go b/vkcs/monitoring_trigger.go new file mode 100644 index 00000000..da91b887 --- /dev/null +++ b/vkcs/monitoring_trigger.go @@ -0,0 +1,99 @@ +package vkcs + +import ( + "github.com/gophercloud/gophercloud" + "net/http" +) + +type commonTriggerResult struct { + gophercloud.Result +} + +// extract is used to extract result into short response struct +func (r commonTriggerResult) extract() (*TriggerOut, error) { + var c *TriggerOut + if err := r.ExtractInto(&c); err != nil { + return nil, err + } + return c, nil +} + +type triggerDeleteResult struct { + gophercloud.Result +} + +func (r triggerDeleteResult) extractErr() error { + return r.Err +} + +func getMonRequestOpts(codes ...int) *gophercloud.RequestOpts { + reqOpts := &gophercloud.RequestOpts{ + OkCodes: codes, + } + if len(codes) != 0 { + reqOpts.OkCodes = codes + } + return reqOpts +} + +func instanceTriggerURL(c ContainerClient, pid string, id string) string { + return c.ServiceURL(pid, "triggers", id) +} + +func createTriggerURL(c ContainerClient, pid string) string { + return c.ServiceURL(pid, "triggers") +} + +func triggerCreate(client monitoringClient, pid string, opts createOptsBuilder) commonTriggerResult { + b, err := opts.Map() + var r commonTriggerResult + if err != nil { + r.Err = err + return r + } + var result *http.Response + reqOpts := getMonRequestOpts(http.StatusCreated) + result, r.Err = client.Post(createTriggerURL(client, pid), b, &r.Body, reqOpts) + if r.Err == nil { + r.Header = result.Header + } + return r +} + +func triggerUpdate(client monitoringClient, pid string, id string, opts createOptsBuilder) commonTriggerResult { + b, err := opts.Map() + var r commonTriggerResult + if err != nil { + r.Err = err + return r + } + var result *http.Response + reqOpts := getMonRequestOpts(http.StatusOK) + result, r.Err = client.Patch(instanceTriggerURL(client, pid, id), b, &r.Body, reqOpts) + if r.Err == nil { + r.Header = result.Header + } + return r +} + +// triggerGet performs request to get trigger instance +func triggerGet(client monitoringClient, pid string, id string) (r commonTriggerResult) { + reqOpts := getMonRequestOpts(http.StatusOK) + var result *http.Response + result, r.Err = client.Get(instanceTriggerURL(client, pid, id), &r.Body, reqOpts) + if r.Err == nil { + r.Header = result.Header + } + return +} + +// triggerDelete performs request to delete trigger +func triggerDelete(client monitoringClient, pid string, id string) (r triggerDeleteResult) { + reqOpts := getMonRequestOpts(http.StatusNoContent) + var result *http.Response + result, r.Err = client.Delete(instanceTriggerURL(client, pid, id), reqOpts) + if r.Err == nil { + r.Header = result.Header + } + return +} diff --git a/vkcs/provider.go b/vkcs/provider.go index a8e60d74..295ba710 100755 --- a/vkcs/provider.go +++ b/vkcs/provider.go @@ -3,6 +3,7 @@ package vkcs import ( "context" "net/http" + "strings" "time" "github.com/gophercloud/gophercloud" @@ -26,7 +27,10 @@ const ( type configer interface { LoadAndValidate() error GetRegion() string + GetTenantID() string ComputeV2Client(region string) (*gophercloud.ServiceClient, error) + MonitoringV1Client(region string) (*gophercloud.ServiceClient, error) + MonitoringTemplaterV2Client(region string) (*gophercloud.ServiceClient, error) ImageV2Client(region string) (*gophercloud.ServiceClient, error) NetworkingV2Client(region string, sdn string) (*gophercloud.ServiceClient, error) BlockStorageV3Client(region string) (*gophercloud.ServiceClient, error) @@ -49,6 +53,55 @@ func (c *config) GetRegion() string { return c.Region } +// GetTenantID is implementation of getTenantID method +func (c *config) GetTenantID() string { + return c.TenantID +} + +func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts, clientType string) (*gophercloud.ServiceClient, error) { + sc := new(gophercloud.ServiceClient) + eo.ApplyDefaults(clientType) + url, err := client.EndpointLocator(eo) + if err != nil { + return sc, err + } + sc.ProviderClient = client + sc.Endpoint = url + sc.Type = clientType + return sc, nil +} + +func JoinPath(base, add string) string { + base = strings.TrimSuffix(base, "/") + add = strings.TrimPrefix(add, "/") + return base + "/" + add +} + +func NewMonitoringV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "cloudalerting") + if err != nil { + return sc, err + } + sc.Endpoint = JoinPath(sc.Endpoint, "v1") + return sc, err +} +func NewMonitoringTemplaterV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "cloudmonitoring-templater") + if err != nil { + return sc, err + } + sc.Endpoint = JoinPath(sc.Endpoint, "v2") + return sc, err +} + +func (c *config) MonitoringTemplaterV2Client(region string) (*gophercloud.ServiceClient, error) { + return c.CommonServiceClientInit(NewMonitoringTemplaterV2, region, "cloudmonitoring-templater") +} + +func (c *config) MonitoringV1Client(region string) (*gophercloud.ServiceClient, error) { + return c.CommonServiceClientInit(NewMonitoringV1, region, "cloudalerting") +} + func (c *config) ComputeV2Client(region string) (*gophercloud.ServiceClient, error) { return c.Config.ComputeV2Client(region) } @@ -273,6 +326,9 @@ func Provider() *schema.Provider { "vkcs_db_config_group": resourceDatabaseConfigGroup(), "vkcs_kubernetes_cluster": resourceKubernetesCluster(), "vkcs_kubernetes_node_group": resourceKubernetesNodeGroup(), + "vkcs_monitoring_channel": resourceMonitoringChannel(), + "vkcs_monitoring_trigger": resourceMonitoringTrigger(), + "vkcs_monitoring_template": resourceMonitoringTemplater(), }, } diff --git a/vkcs/resource_vkcs_monitoring_channel.go b/vkcs/resource_vkcs_monitoring_channel.go new file mode 100644 index 00000000..f19f7747 --- /dev/null +++ b/vkcs/resource_vkcs_monitoring_channel.go @@ -0,0 +1,118 @@ +package vkcs + +import ( + "context" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "time" +) + +func resourceMonitoringChannel() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceChannelCreate, + ReadContext: resourceChannelRead, + UpdateContext: resourceChannelUpdate, + DeleteContext: resourceChannelDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Update: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Human-readable name for the channel.", + }, + "channel_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Type of channel: email or sms.", + }, + "address": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Address for channel email or phone.", + }, + }, + Description: "Manages monitoring notification channels within VKCS.", + } +} + +func resourceChannelCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + + config := meta.(configer) + MonitoringV1Client, err := config.MonitoringV1Client(getRegion(d, config)) + if err != nil { + return diag.Errorf("Error creating VKCS monitoring client: %s", err) + } + + chn := ChannelIn{ + Name: d.Get("name").(string), + ChannelType: d.Get("channel_type").(string), + Address: d.Get("address").(string), + } + + ch, err := channelCreate(MonitoringV1Client, config.GetTenantID(), &chn).extract() + if err != nil { + return diag.Errorf("Error creating VKCS monitoring channel: %s", err) + } + d.SetId(ch.Channel.ID) + + return nil +} + +func resourceChannelRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(configer) + MonitoringV1Client, err := config.MonitoringV1Client(getRegion(d, config)) + if err != nil { + return diag.Errorf("Error creating VKCS monitoring client: %s", err) + } + ch, err := channelGet(MonitoringV1Client, config.GetTenantID(), d.Id()).extract() + if err != nil { + return diag.Errorf("Error get VKCS monitoring channel(%s): %s", d.Id(), err) + } + d.Set("name", ch.Channel.Name) + d.Set("channel_type", ch.Channel.ChannelType) + d.Set("address", ch.Channel.Address) + return nil +} + +func resourceChannelUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(configer) + MonitoringV1Client, err := config.MonitoringV1Client(getRegion(d, config)) + if err != nil { + return diag.Errorf("Error creating VKCS monitoring client: %s", err) + } + + chn := ChannelIn{ + Name: d.Get("name").(string), + ChannelType: d.Get("channel_type").(string), + Address: d.Get("address").(string), + } + _, err = channelUpdate(MonitoringV1Client, config.GetTenantID(), d.Id(), &chn).extract() + if err != nil { + return diag.Errorf("Error update VKCS monitoring channel: %s", err) + } + + return nil +} + +func resourceChannelDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(configer) + MonitoringV1Client, err := config.MonitoringV1Client(getRegion(d, config)) + if err != nil { + return diag.Errorf("Error creating VKCS monitoring client: %s", err) + } + + err = channelDelete(MonitoringV1Client, config.GetTenantID(), d.Id()).extractErr() + if err != nil { + return diag.Errorf("Error de VKCS monitoring channel: %s", err) + } + + return nil +} diff --git a/vkcs/resource_vkcs_monitoring_channel_test.go b/vkcs/resource_vkcs_monitoring_channel_test.go new file mode 100644 index 00000000..dc17109c --- /dev/null +++ b/vkcs/resource_vkcs_monitoring_channel_test.go @@ -0,0 +1,86 @@ +package vkcs + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const testAccMonitoringChannelBasic = ` + resource "vkcs_monitoring_channel" "basic" { + name = "basic_test" + channel_type = "email" + address = "foo@example.com" + } +` +const testAccMonitoringChannelUpdate = ` + resource "vkcs_monitoring_channel" "basic" { + name = "basic_test" + channel_type = "email" + address = "bar@example.com" + } +` + +func testAccCheckMonChannelExists(n string, ch *ChannelOut) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no id is set") + } + + config := testAccProvider.Meta().(configer) + MonClient, err := config.MonitoringV1Client(osRegionName) + if err != nil { + return fmt.Errorf("Error creating VKCS compute client: %s", err) + } + + found, err := channelGet(MonClient, config.GetTenantID(), rs.Primary.ID).extract() + if err != nil { + return err + } + + if found.Channel.ID != rs.Primary.ID { + return fmt.Errorf("channel not found") + } + + *ch = *found + + return nil + } +} + +func TestAccMonitoringChannel_basic(t *testing.T) { + var ch ChannelOut + + resource.Test(t, resource.TestCase{ + PreCheck: func() {}, + ProviderFactories: testAccProviders, + CheckDestroy: func(*terraform.State) error { return nil }, + Steps: []resource.TestStep{ + { + Config: testAccRenderConfig(testAccMonitoringChannelBasic), + Check: resource.ComposeTestCheckFunc( + testAccCheckMonChannelExists( + "vkcs_monitoring_channel.basic", &ch), + resource.TestCheckResourceAttrPtr( + "vkcs_monitoring_channel.basic", "name", &ch.Channel.Name), + ), + }, + { + Config: testAccRenderConfig(testAccMonitoringChannelUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckMonChannelExists( + "vkcs_monitoring_channel.basic", &ch), + resource.TestCheckResourceAttr( + "vkcs_monitoring_channel.basic", "address", "bar@example.com"), + ), + }, + }, + }) +} diff --git a/vkcs/resource_vkcs_monitoring_templater.go b/vkcs/resource_vkcs_monitoring_templater.go new file mode 100644 index 00000000..b8397859 --- /dev/null +++ b/vkcs/resource_vkcs_monitoring_templater.go @@ -0,0 +1,72 @@ +package vkcs + +import ( + "context" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "time" +) + +func resourceMonitoringTemplater() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceTemplaterCreate, + ReadContext: resourceTemplaterRead, + UpdateContext: resourceTemplaterUpdate, + DeleteContext: resourceTemplaterDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Update: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "instance_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Compute instance id.", + }, + "script": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Script for vm. It's contains script for agent installation.", + }, + }, + Description: "Manages monitoring template within (for compute instances) VKCS.", + } +} + +func resourceTemplaterCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(configer) + MonitoringTemplaterV2Client, err := config.MonitoringTemplaterV2Client(getRegion(d, config)) + if err != nil { + return diag.Errorf("Error creating VKCS monitoring client: %s", err) + } + + tmp := TemplateIn{ + InstanceID: d.Get("instance_id").(string), + Capabilities: []string{"telegraf"}, + } + + t, err := templateCreate(MonitoringTemplaterV2Client, config.GetTenantID(), &tmp).extract() + if err != nil { + return diag.Errorf("Error creating VKCS monitoring template: %s", err) + } + d.SetId(t.LinkID) + d.Set("script", t.Script) + return nil +} + +func resourceTemplaterRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return nil +} + +func resourceTemplaterUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return nil +} + +func resourceTemplaterDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return nil +} diff --git a/vkcs/resource_vkcs_monitoring_templater_test.go b/vkcs/resource_vkcs_monitoring_templater_test.go new file mode 100644 index 00000000..1769d46b --- /dev/null +++ b/vkcs/resource_vkcs_monitoring_templater_test.go @@ -0,0 +1,65 @@ +package vkcs + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const testAccMonitoringTemplateBasic = ` +{{.BaseNetwork}} +{{.BaseImage}} +{{.BaseFlavor}} + +resource "vkcs_compute_instance" "instance_1" { + depends_on = ["vkcs_networking_subnet.base"] + name = "instance_1" + availability_zone = "{{.AvailabilityZone}}" + security_groups = ["default"] + metadata = { + foo = "bar" + } + network { + uuid = vkcs_networking_network.base.id + } + block_device { + uuid = data.vkcs_images_image.base.id + source_type = "image" + destination_type = "volume" + volume_type = "high-iops" + volume_size = 20 + boot_index = 0 + delete_on_termination = true + } + + flavor_id = data.vkcs_compute_flavor.base.id +} + + resource "vkcs_monitoring_template" "basic" { + instance_id = vkcs_compute_instance.instance_1.id + } +` + +func TestAccMonitoringTemplate_basic(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckComputeInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccRenderConfig(testAccMonitoringTemplateBasic), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrWith("vkcs_monitoring_template.basic", "script", func(value string) error { + if !strings.Contains(value, "sudo bash") { + return fmt.Errorf("should be script") + } + return nil + }), + ), + }, + }, + }) +} diff --git a/vkcs/resource_vkcs_monitoring_trigger.go b/vkcs/resource_vkcs_monitoring_trigger.go new file mode 100644 index 00000000..039d1e11 --- /dev/null +++ b/vkcs/resource_vkcs_monitoring_trigger.go @@ -0,0 +1,163 @@ +package vkcs + +import ( + "context" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "time" +) + +func resourceMonitoringTrigger() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceTriggerCreate, + ReadContext: resourceTriggerRead, + UpdateContext: resourceTriggerUpdate, + DeleteContext: resourceTriggerDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Update: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Human-readable name for the trigger.", + }, + "status": { + Type: schema.TypeString, + Optional: true, + Default: "on", + Description: "Human-readable status for the trigger.", + }, + "namespace": { + Type: schema.TypeString, + Optional: true, + Default: "mcs/vm", + Description: "Namespace for metrics tenant. For vm mcs/vm", + }, + "query": { + Type: schema.TypeString, + Required: true, + Description: "Promql query for triggers like: cpu_usage_guest{vm_uuid=\"03e042d3-6b68-47eb-ac35-8b4f6c2f77e2\"} > 50", + }, + "interval": { + Type: schema.TypeInt, + Required: true, + Description: "Run interval in seconds.", + }, + "notification_title": { + Type: schema.TypeString, + Required: true, + Description: "Template for notification messages. Like: {{ $labels.host }} {{$labels.__name__}} = {{ $value }}", + }, + "notification_channels": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: "List of channel uuids.", + }, + }, + Description: "Manages monitoring triggers within VKCS.", + } +} + +func resourceTriggerCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(configer) + MonitoringV1Client, err := config.MonitoringV1Client(getRegion(d, config)) + if err != nil { + return diag.Errorf("Error creating VKCS monitoring client: %s", err) + } + + var channels []string + for _, ic := range d.Get("notification_channels").([]interface{}) { + channels = append(channels, ic.(string)) + } + + tr := TriggerIn{ + Trigger: CreateTrigger{ + Name: d.Get("name").(string), + Status: d.Get("status").(string), + Namespace: d.Get("namespace").(string), + Query: d.Get("query").(string), + Interval: d.Get("interval").(int), + NotificationTitle: d.Get("notification_title").(string), + NotificationChannels: channels, + }, + } + + ch, err := triggerCreate(MonitoringV1Client, config.GetTenantID(), &tr).extract() + if err != nil { + return diag.Errorf("Error creating VKCS monitoring trigger: %s", err) + } + d.SetId(ch.Trigger.ID) + + return nil + +} + +func resourceTriggerRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(configer) + MonitoringV1Client, err := config.MonitoringV1Client(getRegion(d, config)) + if err != nil { + return diag.Errorf("Error creating VKCS monitoring client: %s", err) + } + ch, err := triggerGet(MonitoringV1Client, config.GetTenantID(), d.Id()).extract() + if err != nil { + return diag.Errorf("Error get VKCS monitoring trigger(%s): %s", d.Id(), err) + } + d.Set("name", ch.Trigger.Name) + d.Set("status", ch.Trigger.Status) + d.Set("query", ch.Trigger.Query) + d.Set("interval", ch.Trigger.Interval) + d.Set("notification_title", ch.Trigger.NotificationTitle) + d.Set("notification_channels", ch.Trigger.NotificationChannels) + return nil +} + +func resourceTriggerUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(configer) + MonitoringV1Client, err := config.MonitoringV1Client(getRegion(d, config)) + if err != nil { + return diag.Errorf("Error creating VKCS monitoring client: %s", err) + } + + var channels []string + for _, ic := range d.Get("notification_channels").([]interface{}) { + channels = append(channels, ic.(string)) + } + + tr := TriggerIn{ + Trigger: CreateTrigger{ + Name: d.Get("name").(string), + Status: d.Get("status").(string), + Namespace: d.Get("namespace").(string), + Query: d.Get("query").(string), + Interval: d.Get("interval").(int), + NotificationTitle: d.Get("notification_title").(string), + NotificationChannels: channels, + }, + } + _, err = triggerUpdate(MonitoringV1Client, config.GetTenantID(), d.Id(), &tr).extract() + if err != nil { + return diag.Errorf("Error update VKCS monitoring trigger: %s", err) + } + + return nil +} + +func resourceTriggerDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(configer) + MonitoringV1Client, err := config.MonitoringV1Client(getRegion(d, config)) + if err != nil { + return diag.Errorf("Error creating VKCS monitoring client: %s", err) + } + + err = triggerDelete(MonitoringV1Client, config.GetTenantID(), d.Id()).extractErr() + if err != nil { + return diag.Errorf("Error de VKCS monitoring trigger: %s", err) + } + return nil +} diff --git a/vkcs/resource_vkcs_monitoring_trigger_test.go b/vkcs/resource_vkcs_monitoring_trigger_test.go new file mode 100644 index 00000000..2636e453 --- /dev/null +++ b/vkcs/resource_vkcs_monitoring_trigger_test.go @@ -0,0 +1,105 @@ +package vkcs + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "testing" +) + +const testAccMonitoringTriggerBasic = ` + resource "vkcs_monitoring_channel" "basic" { + name = "basic_test" + channel_type = "email" + address = "bar@example.com" + } + + resource "vkcs_monitoring_trigger" "basic" { + name = "test" + namespace = "mcs/test" + query = "test{} > 10" + interval = 60 + notification_title = "123" + notification_channels = [vkcs_monitoring_channel.basic.id] + } +` +const testAccMonitoringTriggerUpdate = ` + resource "vkcs_monitoring_channel" "basic" { + name = "basic_test" + channel_type = "email" + address = "bar@example.com" + } + + resource "vkcs_monitoring_trigger" "basic" { + name = "test" + namespace = "mcs/test" + query = "test{} > 10" + interval = 120 + notification_title = "123" + notification_channels = [vkcs_monitoring_channel.basic.id] + } +` + +func testAccCheckMonTriggerExists(n string, ch *TriggerOut) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no id is set") + } + + config := testAccProvider.Meta().(configer) + MonClient, err := config.MonitoringV1Client(osRegionName) + if err != nil { + return fmt.Errorf("Error creating VKCS compute client: %s", err) + } + + found, err := triggerGet(MonClient, config.GetTenantID(), rs.Primary.ID).extract() + if err != nil { + return err + } + + if found.Trigger.ID != rs.Primary.ID { + return fmt.Errorf("trigger not found") + } + + *ch = *found + + return nil + } +} + +func TestAccMonitoringTrigger_basic(t *testing.T) { + var tr TriggerOut + + resource.Test(t, resource.TestCase{ + PreCheck: func() {}, + ProviderFactories: testAccProviders, + CheckDestroy: func(*terraform.State) error { return nil }, + Steps: []resource.TestStep{ + { + Config: testAccRenderConfig(testAccMonitoringTriggerBasic), + Check: resource.ComposeTestCheckFunc( + testAccCheckMonTriggerExists( + "vkcs_monitoring_trigger.basic", &tr), + resource.TestCheckResourceAttrPtr( + "vkcs_monitoring_trigger.basic", "name", &tr.Trigger.Name), + resource.TestCheckResourceAttr( + "vkcs_monitoring_trigger.basic", "interval", "60"), + ), + }, + { + Config: testAccRenderConfig(testAccMonitoringTriggerUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckMonTriggerExists( + "vkcs_monitoring_trigger.basic", &tr), + resource.TestCheckResourceAttr( + "vkcs_monitoring_trigger.basic", "interval", "120"), + ), + }, + }, + }) +} diff --git a/vkcs/test_suite.go b/vkcs/test_suite.go index 119a4e3e..b2eee673 100755 --- a/vkcs/test_suite.go +++ b/vkcs/test_suite.go @@ -73,6 +73,14 @@ func (d *dummyConfig) NetworkingV2Client(region string, sdn string) (*gopherclou return nil, nil } +func (d *dummyConfig) MonitoringV1Client(region string) (*gophercloud.ServiceClient, error) { + return nil, nil +} + +func (d *dummyConfig) MonitoringTemplaterV2Client(region string) (*gophercloud.ServiceClient, error) { + return nil, nil +} + func (d *dummyConfig) GetMutex() *mutexkv.MutexKV { return nil } @@ -83,6 +91,11 @@ func (d *dummyConfig) GetRegion() string { return args.String(0) } +func (d *dummyConfig) GetTenantID() string { + args := d.Called() + return args.String(0) +} + // ContainerClientFixture ... type ContainerClientFixture struct { mock.Mock