├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── release.yml │ └── test.yml ├── examples ├── resources │ ├── netdata_space │ │ ├── import.sh │ │ └── resource.tf │ ├── netdata_room │ │ ├── import.sh │ │ └── resource.tf │ ├── netdata_space_member │ │ ├── import.sh │ │ └── resource.tf │ ├── netdata_node_room_member │ │ ├── import.sh │ │ └── resource.tf │ ├── netdata_room_member │ │ ├── import.sh │ │ └── resource.tf │ ├── netdata_notification_slack_channel │ │ ├── import.sh │ │ └── resource.tf │ ├── netdata_notification_discord_channel │ │ ├── import.sh │ │ └── resource.tf │ └── netdata_notification_pagerduty_channel │ │ ├── import.sh │ │ └── resource.tf ├── data-sources │ ├── netdata_space │ │ └── data-source.tf │ └── netdata_room │ │ └── data-source.tf ├── provider │ └── provider.tf └── complete │ └── main.tf ├── terraform-registry-manifest.json ├── tools └── tools.go ├── internal ├── provider │ ├── provider_test.go │ ├── utils.go │ ├── space_resource_test.go │ ├── room_resource_test.go │ ├── space_data_source_test.go │ ├── room_data_source_test.go │ ├── space_member_resource_test.go │ ├── sweeper_test.go │ ├── room_member_resource_test.go │ ├── notifications.go │ ├── room_data_source.go │ ├── provider.go │ ├── space_data_source.go │ ├── node_room_member_test.go │ ├── notification_slack_channel_resource_test.go │ ├── notification_pagerduty_channel_resource_test.go │ ├── notification_discord_channel_resource_test.go │ ├── room_resource.go │ ├── room_member_resource.go │ ├── space_resource.go │ ├── space_member_resource.go │ ├── notification_slack_channel_resource.go │ ├── notification_pagerduty_channel_resource.go │ └── notification_discord_channel_resource.go └── client │ ├── invitations.go │ ├── client.go │ ├── room_member.go │ ├── rooms.go │ ├── notification_slack.go │ ├── space_member.go │ ├── notification_discord.go │ ├── notification_pagerduty.go │ ├── spaces.go │ ├── models.go │ ├── notifications.go │ └── node_room_member.go ├── GNUmakefile ├── .golangci.yml ├── .gitignore ├── main.go ├── docs ├── data-sources │ ├── space.md │ └── room.md ├── index.md └── resources │ ├── space.md │ ├── room.md │ ├── space_member.md │ ├── room_member.md │ ├── notification_slack_channel.md │ ├── notification_pagerduty_channel.md │ ├── notification_discord_channel.md │ └── node_room_member.md ├── README.md ├── CHANGELOG.md ├── .goreleaser.yml ├── go.mod └── LICENSE /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @netdata/cloud-sre 2 | -------------------------------------------------------------------------------- /examples/resources/netdata_space/import.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | terraform import netdata_space.test space_id 4 | -------------------------------------------------------------------------------- /examples/data-sources/netdata_space/data-source.tf: -------------------------------------------------------------------------------- 1 | data "netdata_space" "test" { 2 | id = "" 3 | } 4 | -------------------------------------------------------------------------------- /examples/resources/netdata_room/import.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | terraform import netdata_room.test space_id,room_id 4 | -------------------------------------------------------------------------------- /examples/resources/netdata_space_member/import.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | terraform import netdata_space_member.test space_id,id 4 | -------------------------------------------------------------------------------- /examples/provider/provider.tf: -------------------------------------------------------------------------------- 1 | provider "netdata" { 2 | url = "https://app.netdata.cloud" 3 | auth_token = "" 4 | } 5 | -------------------------------------------------------------------------------- /examples/resources/netdata_node_room_member/import.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | terraform import netdata_node_room_member.test space_id,room_id 4 | -------------------------------------------------------------------------------- /terraform-registry-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "metadata": { 4 | "protocol_versions": ["6.0"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/data-sources/netdata_room/data-source.tf: -------------------------------------------------------------------------------- 1 | data "netdata_room" "test" { 2 | space_id = "" 3 | id = "" 4 | } 5 | -------------------------------------------------------------------------------- /examples/resources/netdata_room_member/import.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | terraform import netdata_room_member.test space_id,room_id,space_member_id 4 | -------------------------------------------------------------------------------- /examples/resources/netdata_notification_slack_channel/import.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | terraform import netdata_notification_slack_channel.test space_id,channel_id 4 | -------------------------------------------------------------------------------- /examples/resources/netdata_notification_discord_channel/import.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | terraform import netdata_notification_discord_channel.test space_id,channel_id 4 | -------------------------------------------------------------------------------- /examples/resources/netdata_space/resource.tf: -------------------------------------------------------------------------------- 1 | resource "netdata_space" "test" { 2 | name = "MyTestingSpace" 3 | description = "Created by Terraform" 4 | } 5 | -------------------------------------------------------------------------------- /examples/resources/netdata_notification_pagerduty_channel/import.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | terraform import netdata_notification_pagerduty_channel.test space_id,channel_id 4 | -------------------------------------------------------------------------------- /examples/resources/netdata_space_member/resource.tf: -------------------------------------------------------------------------------- 1 | resource "netdata_space_member" "test" { 2 | email = "" 3 | space_id = "" 4 | role = "admin" 5 | } 6 | -------------------------------------------------------------------------------- /examples/resources/netdata_room/resource.tf: -------------------------------------------------------------------------------- 1 | resource "netdata_room" "test" { 2 | space_id = "" 3 | name = "MyTestingSpace" 4 | description = "Created by Terraform" 5 | } 6 | -------------------------------------------------------------------------------- /examples/resources/netdata_room_member/resource.tf: -------------------------------------------------------------------------------- 1 | resource "netdata_room_member" "test" { 2 | room_id = "" 3 | space_id = "" 4 | space_member_id = "" 5 | } 6 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build tools 5 | 6 | package tools 7 | 8 | import ( 9 | // Documentation generation 10 | _ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs" 11 | ) 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # See GitHub's documentation for more information on this file: 2 | # https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates 3 | version: 2 4 | updates: 5 | - package-ecosystem: "gomod" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | -------------------------------------------------------------------------------- /internal/provider/provider_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "github.com/hashicorp/terraform-plugin-framework/providerserver" 5 | "github.com/hashicorp/terraform-plugin-go/tfprotov6" 6 | ) 7 | 8 | var ( 9 | testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ 10 | "netdata": providerserver.NewProtocol6WithError(New("test")()), 11 | } 12 | ) 13 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | default: testacc 2 | 3 | # Run acceptance tests 4 | .PHONY: testacc 5 | testacc: 6 | TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 120m 7 | go test ./... -sweep empty 8 | 9 | # Build locally 10 | .PHONY: local-build 11 | local-build: 12 | go install . 13 | 14 | # Generate docs 15 | .PHONY: docs 16 | docs: 17 | @which tfplugindocs &>/dev/null || (echo "install tfplugindocs (https://github.com/hashicorp/terraform-plugin-docs)"; exit 1) 18 | tfplugindocs generate . 19 | -------------------------------------------------------------------------------- /examples/resources/netdata_notification_slack_channel/resource.tf: -------------------------------------------------------------------------------- 1 | resource "netdata_notification_slack_channel" "test" { 2 | name = "slack notifications" 3 | 4 | enabled = true 5 | space_id = "" 6 | rooms_id = [""] 7 | repeat_notification_min = 30 8 | webhook_url = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" 9 | notifications = ["CRITICAL", "WARNING", "CLEAR"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/resources/netdata_notification_discord_channel/resource.tf: -------------------------------------------------------------------------------- 1 | resource "netdata_notification_discord_channel" "test" { 2 | name = "discord notifications" 3 | 4 | enabled = true 5 | space_id = "" 6 | rooms_id = [""] 7 | repeat_notification_min = 30 8 | webhook_url = "https://discord.com/api/webhooks/0000000000000/XXXXXXXXXXXXXXXXXXXXXXXX" 9 | notifications = ["CRITICAL", "WARNING", "CLEAR"] 10 | channel_type = "text" 11 | } 12 | -------------------------------------------------------------------------------- /examples/resources/netdata_notification_pagerduty_channel/resource.tf: -------------------------------------------------------------------------------- 1 | resource "netdata_notification_pagerduty_channel" "test" { 2 | name = "pagerduty notifications" 3 | 4 | enabled = true 5 | space_id = netdata_space.test.id 6 | rooms_id = [""] 7 | notifications = ["CRITICAL", "WARNING", "CLEAR"] 8 | repeat_notification_min = 30 9 | alert_events_url = "https://events.pagerduty.com/v2/enqueue" 10 | integration_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 11 | } 12 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Visit https://golangci-lint.run/ for usage documentation 2 | # and information on other useful linters 3 | issues: 4 | max-per-linter: 0 5 | max-same-issues: 0 6 | 7 | linters: 8 | disable-all: true 9 | enable: 10 | - durationcheck 11 | - errcheck 12 | - forcetypeassert 13 | - godot 14 | - gofmt 15 | - gosimple 16 | - ineffassign 17 | - makezero 18 | - misspell 19 | - nilerr 20 | - predeclared 21 | - staticcheck 22 | - tenv 23 | - unconvert 24 | - unparam 25 | - unused 26 | - vet 27 | -------------------------------------------------------------------------------- /internal/provider/utils.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | const ( 9 | nonCommunitySpaceIDEnv = "SPACE_ID_NON_COMMUNITY" 10 | ) 11 | 12 | func testAccPreCheck(t *testing.T) { 13 | if getNonCommunitySpaceIDEnv() == "" { 14 | t.Fatalf("%s must be set", nonCommunitySpaceIDEnv) 15 | } 16 | } 17 | 18 | func getNonCommunitySpaceIDEnv() string { 19 | return os.Getenv(nonCommunitySpaceIDEnv) 20 | } 21 | 22 | func getNetdataCloudURL() string { 23 | url, ok := os.LookupEnv("NETDATA_CLOUD_URL") 24 | if !ok { 25 | return NetdataCloudURL 26 | } 27 | return url 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.dll 2 | *.exe 3 | .DS_Store 4 | example.tf 5 | terraform.tfplan 6 | terraform.tfstate 7 | bin/ 8 | dist/ 9 | modules-dev/ 10 | /pkg/ 11 | website/.vagrant 12 | website/.bundle 13 | website/build 14 | website/node_modules 15 | .vagrant/ 16 | *.backup 17 | ./*.tfstate 18 | .terraform/ 19 | *.log 20 | *.bak 21 | *~ 22 | .*.swp 23 | .idea 24 | *.iml 25 | *.test 26 | *.iml 27 | 28 | website/vendor 29 | 30 | # Test exclusions 31 | !command/test-fixtures/**/*.tfstate 32 | !command/test-fixtures/**/.terraform/ 33 | 34 | # Keep windows files with windows line endings 35 | *.winfile eol=crlf 36 | devbox.* 37 | .envrc 38 | -------------------------------------------------------------------------------- /examples/resources/netdata_node_room_member/resource.tf: -------------------------------------------------------------------------------- 1 | resource "netdata_node_room_member" "test" { 2 | space_id = "" 3 | room_id = "" 4 | node_names = [ 5 | "node1", 6 | "node2" 7 | ] 8 | rule { 9 | action = "INCLUDE" 10 | description = "Description of the rule" 11 | clause { 12 | label = "role" 13 | operator = "equals" 14 | value = "parent" 15 | negate = false 16 | } 17 | clause { 18 | label = "environment" 19 | operator = "equals" 20 | value = "production" 21 | negate = false 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "log" 7 | 8 | "github.com/hashicorp/terraform-plugin-framework/providerserver" 9 | "github.com/netdata/terraform-provider-netdata/internal/provider" 10 | ) 11 | 12 | var ( 13 | version string = "0.1.0" 14 | ) 15 | 16 | func main() { 17 | var debug bool 18 | 19 | flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") 20 | flag.Parse() 21 | 22 | opts := providerserver.ServeOpts{ 23 | Address: "registry.terraform.io/netdata/netdata", 24 | Debug: debug, 25 | } 26 | 27 | err := providerserver.Serve(context.Background(), provider.New(version), opts) 28 | 29 | if err != nil { 30 | log.Fatal(err.Error()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/provider/space_resource_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 8 | ) 9 | 10 | func TestAccSpaceResource(t *testing.T) { 11 | resource.Test(t, resource.TestCase{ 12 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 13 | Steps: []resource.TestStep{ 14 | { 15 | Config: `resource "netdata_space" "test" { name = "testAcc" }`, 16 | Check: resource.ComposeAggregateTestCheckFunc( 17 | resource.TestCheckResourceAttr("netdata_space.test", "name", "testAcc"), 18 | resource.TestCheckResourceAttr("netdata_space.test", "description", ""), 19 | resource.TestMatchResourceAttr("netdata_space.test", "claim_token", regexp.MustCompile(`^.{135}$`)), 20 | ), 21 | }, 22 | }, 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /internal/provider/room_resource_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 9 | ) 10 | 11 | func TestAccRoomResource(t *testing.T) { 12 | resource.Test(t, resource.TestCase{ 13 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 14 | Steps: []resource.TestStep{ 15 | { 16 | Config: fmt.Sprintf(` 17 | resource "netdata_room" "test" { 18 | space_id = "%s" 19 | name = "testAcc" 20 | } 21 | `, os.Getenv("SPACE_ID_NON_COMMUNITY")), 22 | Check: resource.ComposeAggregateTestCheckFunc( 23 | resource.TestCheckResourceAttr("netdata_room.test", "name", "testAcc"), 24 | resource.TestCheckResourceAttr("netdata_room.test", "description", ""), 25 | ), 26 | }, 27 | }, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /internal/provider/space_data_source_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 9 | ) 10 | 11 | func TestAccSpaceDataSource(t *testing.T) { 12 | resource.Test(t, resource.TestCase{ 13 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 14 | Steps: []resource.TestStep{ 15 | { 16 | Config: fmt.Sprintf(` 17 | data "netdata_space" "test" { 18 | id = "%s" 19 | } 20 | `, getNonCommunitySpaceIDEnv()), 21 | Check: resource.ComposeAggregateTestCheckFunc( 22 | resource.TestCheckResourceAttrSet("data.netdata_space.test", "name"), 23 | resource.TestMatchResourceAttr("data.netdata_space.test", "claim_token", regexp.MustCompile(`^.{135}$`)), 24 | ), 25 | }, 26 | }, 27 | }, 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /docs/data-sources/space.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "netdata_space Data Source - terraform-provider-netdata" 4 | subcategory: "" 5 | description: |- 6 | Use this data source to get information about a Netdata Cloud Space. 7 | --- 8 | 9 | # netdata_space (Data Source) 10 | 11 | Use this data source to get information about a Netdata Cloud Space. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | data "netdata_space" "test" { 17 | id = "" 18 | } 19 | ``` 20 | 21 | 22 | ## Schema 23 | 24 | ### Required 25 | 26 | - `id` (String) The ID of the space 27 | 28 | ### Read-Only 29 | 30 | - `claim_token` (String) The claim token of the space 31 | - `description` (String) The description of the space 32 | - `name` (String) The name of the space 33 | -------------------------------------------------------------------------------- /docs/data-sources/room.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "netdata_room Data Source - terraform-provider-netdata" 4 | subcategory: "" 5 | description: |- 6 | Use this data source to get information about a Netdata Cloud Room. 7 | --- 8 | 9 | # netdata_room (Data Source) 10 | 11 | Use this data source to get information about a Netdata Cloud Room. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | data "netdata_room" "test" { 17 | space_id = "" 18 | id = "" 19 | } 20 | ``` 21 | 22 | 23 | ## Schema 24 | 25 | ### Required 26 | 27 | - `id` (String) The ID of the room 28 | - `space_id` (String) The ID of the space 29 | 30 | ### Read-Only 31 | 32 | - `description` (String) The description of the room 33 | - `name` (String) The name of the room 34 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "netdata Provider" 4 | description: |- 5 | The Netdata Provider allows you to manage Netdata Cloud resources. 6 | --- 7 | 8 | # netdata Provider 9 | 10 | The Netdata Provider allows you to manage Netdata Cloud resources. 11 | 12 | ## Example Usage 13 | 14 | ```terraform 15 | provider "netdata" { 16 | url = "https://app.netdata.cloud" 17 | auth_token = "" 18 | } 19 | ``` 20 | 21 | 22 | ## Schema 23 | 24 | ### Optional 25 | 26 | - `auth_token` (String, Sensitive) Netdata Cloud Authentication Token with `scope:all`, more [info](https://learn.netdata.cloud/docs/netdata-cloud/api-tokens). Can be also set as environment variable `NETDATA_CLOUD_AUTH_TOKEN` 27 | - `url` (String) Netdata Cloud URL Address by default is https://app.netdata.cloud. Can be also set as environment variable `NETDATA_CLOUD_URL` 28 | -------------------------------------------------------------------------------- /internal/provider/room_data_source_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 8 | ) 9 | 10 | func TestAccRoomDataSource(t *testing.T) { 11 | resource.Test(t, resource.TestCase{ 12 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 13 | PreCheck: func() { testAccPreCheck(t) }, 14 | Steps: []resource.TestStep{ 15 | { 16 | Config: fmt.Sprintf(` 17 | resource "netdata_room" "test" { 18 | space_id = "%s" 19 | name = "testAcc" 20 | } 21 | data "netdata_room" "test" { 22 | space_id = "%s" 23 | id = netdata_room.test.id 24 | } 25 | `, getNonCommunitySpaceIDEnv(), getNonCommunitySpaceIDEnv()), 26 | Check: resource.ComposeAggregateTestCheckFunc( 27 | resource.TestCheckResourceAttr("data.netdata_room.test", "name", "testAcc"), 28 | ), 29 | }, 30 | }, 31 | }, 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /internal/provider/space_member_resource_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 8 | ) 9 | 10 | func TestAccSpaceMemberResource(t *testing.T) { 11 | resource.Test(t, resource.TestCase{ 12 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 13 | Steps: []resource.TestStep{ 14 | { 15 | Config: fmt.Sprintf(` 16 | resource "netdata_space_member" "test" { 17 | email = "space@member.local" 18 | space_id = "%s" 19 | role = "admin" 20 | } 21 | `, getNonCommunitySpaceIDEnv()), 22 | Check: resource.ComposeAggregateTestCheckFunc( 23 | resource.TestCheckResourceAttrSet("netdata_space_member.test", "id"), 24 | resource.TestCheckResourceAttr("netdata_space_member.test", "email", "space@member.local"), 25 | resource.TestCheckResourceAttr("netdata_space_member.test", "role", "admin"), 26 | resource.TestCheckResourceAttrSet("netdata_space_member.test", "space_id"), 27 | ), 28 | }, 29 | }, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /internal/provider/sweeper_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 9 | "github.com/netdata/terraform-provider-netdata/internal/client" 10 | ) 11 | 12 | func TestMain(m *testing.M) { 13 | resource.TestMain(m) 14 | } 15 | 16 | func init() { 17 | resource.AddTestSweepers("invitations_sweeper", &resource.Sweeper{ 18 | Name: "invitations sweeper", 19 | F: func(r string) error { 20 | url := os.Getenv("NETDATA_CLOUD_URL") 21 | auth_token := os.Getenv("NETDATA_CLOUD_AUTH_TOKEN") 22 | 23 | if url == "" { 24 | url = NetdataCloudURL 25 | } 26 | 27 | if auth_token == "" { 28 | return fmt.Errorf("auth_token must be set") 29 | } 30 | 31 | spaceID := getNonCommunitySpaceIDEnv() 32 | if spaceID == "" { 33 | return fmt.Errorf("%s must be set", nonCommunitySpaceIDEnv) 34 | } 35 | 36 | client := client.NewClient(url, auth_token) 37 | invitations, err := client.GetInvitations(spaceID) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | err = client.DeleteInvitations(spaceID, invitations) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | return nil 48 | }, 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform Provider for Netdata Cloud 2 | 3 | This is the [Terraform](https://www.terraform.io/) provider for the [Netdata Cloud](https://www.netdata.cloud/). 4 | 5 | This provider allows you to install and manage Netdata Cloud resources using Terraform. 6 | 7 | ## Contents 8 | 9 | - [Terraform Provider for Netdata Cloud](#terraform-provider-for-netdata-cloud) 10 | - [Contents](#contents) 11 | - [Requirements](#requirements) 12 | - [Getting Started](#getting-started) 13 | 14 | ## Requirements 15 | 16 | * [Terraform](https://www.terraform.io/downloads.html) v1.1.0 or later 17 | * [Go](https://golang.org/doc/install) v1.20 or later (to build the provider plugin) 18 | 19 | ## Getting Started 20 | 21 | * from terraform registry 22 | 23 | * from source code 24 | 25 | * setup your [CLI configuration](https://developer.hashicorp.com/terraform/cli/config/config-file#development-overrides-for-provider-developers) 26 | 27 | ```console 28 | $ cat ~/.terraformrc 29 | provider_installation { 30 | dev_overrides { 31 | "netdata/netdata" = "" 32 | } 33 | direct {} 34 | } 35 | ``` 36 | 37 | * build the provider 38 | 39 | ```console 40 | $ make local-build 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/resources/space.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "netdata_space Resource - terraform-provider-netdata" 4 | subcategory: "" 5 | description: |- 6 | Provides a Netdata Cloud Space resource. Use this resource to manage spaces. 7 | --- 8 | 9 | # netdata_space (Resource) 10 | 11 | Provides a Netdata Cloud Space resource. Use this resource to manage spaces. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "netdata_space" "test" { 17 | name = "MyTestingSpace" 18 | description = "Created by Terraform" 19 | } 20 | ``` 21 | 22 | 23 | ## Schema 24 | 25 | ### Required 26 | 27 | - `name` (String) The name of the space 28 | 29 | ### Optional 30 | 31 | - `description` (String) The description of the space 32 | 33 | ### Read-Only 34 | 35 | - `claim_token` (String) The claim token of the space 36 | - `id` (String) The ID of the space 37 | 38 | ## Import 39 | 40 | Import is supported using the following syntax: 41 | 42 | The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: 43 | 44 | ```shell 45 | #!/bin/sh 46 | 47 | terraform import netdata_space.test space_id 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/resources/room.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "netdata_room Resource - terraform-provider-netdata" 4 | subcategory: "" 5 | description: |- 6 | Provides a Netdata Cloud Room resource. Use this resource to manage rooms in the selected space. 7 | --- 8 | 9 | # netdata_room (Resource) 10 | 11 | Provides a Netdata Cloud Room resource. Use this resource to manage rooms in the selected space. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "netdata_room" "test" { 17 | space_id = "" 18 | name = "MyTestingSpace" 19 | description = "Created by Terraform" 20 | } 21 | ``` 22 | 23 | 24 | ## Schema 25 | 26 | ### Required 27 | 28 | - `name` (String) The name of the room 29 | - `space_id` (String) The ID of the space 30 | 31 | ### Optional 32 | 33 | - `description` (String) The description of the room 34 | 35 | ### Read-Only 36 | 37 | - `id` (String) The ID of the room 38 | 39 | ## Import 40 | 41 | Import is supported using the following syntax: 42 | 43 | The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: 44 | 45 | ```shell 46 | #!/bin/sh 47 | 48 | terraform import netdata_room.test space_id,room_id 49 | ``` 50 | -------------------------------------------------------------------------------- /internal/client/invitations.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | func (c *Client) GetInvitations(spaceID string) (*[]Invitation, error) { 10 | if spaceID == "" { 11 | return nil, ErrSpaceIDRequired 12 | } 13 | req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v2/spaces/%s/invitations", c.HostURL, spaceID), nil) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | var invitations []Invitation 19 | 20 | err = c.doRequestUnmarshal(req, &invitations) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return &invitations, nil 26 | } 27 | 28 | func (c *Client) DeleteInvitations(spaceID string, invitations *[]Invitation) error { 29 | if spaceID == "" { 30 | return ErrSpaceIDRequired 31 | } 32 | 33 | if len(*invitations) == 0 { 34 | return nil 35 | } 36 | 37 | var invitationIDs []string 38 | for _, invitation := range *invitations { 39 | invitationIDs = append(invitationIDs, invitation.ID) 40 | } 41 | req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/v1/spaces/%s/invitations?invitation_ids=%s", c.HostURL, spaceID, strings.Join(invitationIDs, ",")), nil) 42 | if err != nil { 43 | return err 44 | } 45 | _, err = c.doRequest(req) 46 | if err != nil { 47 | return err 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /internal/provider/room_member_resource_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 8 | ) 9 | 10 | func TestAccRoomMemberResource(t *testing.T) { 11 | resource.Test(t, resource.TestCase{ 12 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 13 | Steps: []resource.TestStep{ 14 | { 15 | Config: fmt.Sprintf(` 16 | resource "netdata_space_member" "test" { 17 | email = "room@member.local" 18 | space_id = "%s" 19 | role = "admin" 20 | } 21 | resource "netdata_room" "test" { 22 | space_id = "%s" 23 | name = "testAcc" 24 | } 25 | resource "netdata_room_member" "test" { 26 | room_id = netdata_room.test.id 27 | space_id = "%s" 28 | space_member_id = netdata_space_member.test.id 29 | } 30 | `, getNonCommunitySpaceIDEnv(), getNonCommunitySpaceIDEnv(), getNonCommunitySpaceIDEnv()), 31 | Check: resource.ComposeAggregateTestCheckFunc( 32 | resource.TestCheckResourceAttrSet("netdata_room_member.test", "room_id"), 33 | resource.TestCheckResourceAttrSet("netdata_room_member.test", "space_id"), 34 | resource.TestCheckResourceAttrSet("netdata_room_member.test", "space_member_id"), 35 | ), 36 | }, 37 | }, 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /docs/resources/space_member.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "netdata_space_member Resource - terraform-provider-netdata" 4 | subcategory: "" 5 | description: |- 6 | Provides a Netdata Cloud Space Member resource. Use this resource to manage user membership to the space. 7 | --- 8 | 9 | # netdata_space_member (Resource) 10 | 11 | Provides a Netdata Cloud Space Member resource. Use this resource to manage user membership to the space. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "netdata_space_member" "test" { 17 | email = "" 18 | space_id = "" 19 | role = "admin" 20 | } 21 | ``` 22 | 23 | 24 | ## Schema 25 | 26 | ### Required 27 | 28 | - `email` (String) Email of the member 29 | - `role` (String) Role of the member. The community plan can only set the role to `admin` 30 | - `space_id` (String) Space ID of the member 31 | 32 | ### Read-Only 33 | 34 | - `id` (String) The Member ID of the space 35 | 36 | ## Import 37 | 38 | Import is supported using the following syntax: 39 | 40 | The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: 41 | 42 | ```shell 43 | #!/bin/sh 44 | 45 | terraform import netdata_space_member.test space_id,id 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/resources/room_member.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "netdata_room_member Resource - terraform-provider-netdata" 4 | subcategory: "" 5 | description: |- 6 | Provides a Netdata Cloud Room Member resource. Use this resource to manage user membership to the room in the selected space. It is referring to the user created at the space level. 7 | --- 8 | 9 | # netdata_room_member (Resource) 10 | 11 | Provides a Netdata Cloud Room Member resource. Use this resource to manage user membership to the room in the selected space. It is referring to the user created at the space level. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "netdata_room_member" "test" { 17 | room_id = "" 18 | space_id = "" 19 | space_member_id = "" 20 | } 21 | ``` 22 | 23 | 24 | ## Schema 25 | 26 | ### Required 27 | 28 | - `room_id` (String) The Room ID of the space 29 | - `space_id` (String) Space ID of the member 30 | - `space_member_id` (String) The Space Member ID of the space 31 | 32 | ## Import 33 | 34 | Import is supported using the following syntax: 35 | 36 | The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: 37 | 38 | ```shell 39 | #!/bin/sh 40 | 41 | terraform import netdata_room_member.test space_id,room_id,space_member_id 42 | ``` 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Terraform Provider release workflow. 2 | name: Release 3 | 4 | # This GitHub action creates a release when a tag that matches the pattern 5 | # "v*" (e.g. v0.1.0) is created. 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | # Releases need permissions to read and write the repository contents. 12 | # GitHub considers creating releases and uploading assets as writing contents. 13 | permissions: 14 | contents: write 15 | 16 | jobs: 17 | goreleaser: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 21 | with: 22 | # Allow goreleaser to access older tag information. 23 | fetch-depth: 0 24 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 25 | with: 26 | go-version-file: 'go.mod' 27 | cache: true 28 | - name: Import GPG key 29 | uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 30 | id: import_gpg 31 | with: 32 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 33 | passphrase: ${{ secrets.PASSPHRASE }} 34 | - name: Run GoReleaser 35 | uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5.0.0 36 | with: 37 | args: release --clean 38 | version: '~> v2' 39 | env: 40 | # GitHub sets the GITHUB_TOKEN secret automatically. 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.4.1 2 | 3 | BUGFIXES: 4 | 5 | - fix examples for notifications channels 6 | 7 | ## 0.4.0 8 | 9 | BREAKING CHANGES: 10 | 11 | - resource/netdata_notification_discord_channel: renamed attribute from `alarms` to `notifications` with new values 12 | - resource/netdata_notification_pagerduty_channel: renamed attribute from `alarms` to `notifications` with new values 13 | - resource/netdata_notification_slack_channel: renamed attribute from `alarms` to `notifications` with new values 14 | 15 | ## 0.3.0 16 | 17 | FEATURES: 18 | 19 | - add Node Rule-Based Room Assignment option to the `netdata_node_room_member` resource 20 | 21 | ## 0.2.2 22 | 23 | FEATURES: 24 | 25 | - option to specify notification repeat interval `repeat_notification_min` for the paid notification channels 26 | 27 | ## 0.2.1 28 | 29 | BUGFIXES: 30 | 31 | - the `integration_id` attribute is being removed because it is internally used for the create resource only and 32 | doesn't bring much value to store it 33 | 34 | ## 0.2.0 35 | 36 | FEATURES: 37 | 38 | - add `netdata_node_room_member` resource 39 | 40 | ## 0.1.3 41 | 42 | FEATURES: 43 | 44 | - more detailed resource descriptions 45 | 46 | ## 0.1.2 47 | 48 | BUGFIXES: 49 | 50 | - fix bug with empty claim token 51 | 52 | ## 0.1.1 53 | 54 | BUGFIXES: 55 | 56 | - empty claim token when importing space 57 | 58 | ## 0.1.0 59 | 60 | Initial version. 61 | 62 | FEATURES: 63 | 64 | - support for the Netdata Cloud Spaces 65 | - support for the Netdata Cloud Rooms 66 | - support for the Netdata Space and Room Membership 67 | - support for the Netdata Notifications: Discord, Slack, Pagerduty 68 | -------------------------------------------------------------------------------- /internal/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | var ( 13 | ErrNotFound = errors.New("not found") 14 | ErrSpaceIDRequired = errors.New("spaceID is required") 15 | ErrChannelIDRequired = errors.New("channelID is required") 16 | ErrRoomIDRequired = errors.New("roomID is required") 17 | ErrMemberIDRequired = errors.New("memberID is required") 18 | ErrNodeID = errors.New("nodeID is required") 19 | ErrNodeMembershipIDRequired = errors.New("nodeMembershipID is required") 20 | ErrNodeMembershipActionRequired = errors.New("nodeMembershipAction is required") 21 | ) 22 | 23 | type Client struct { 24 | HostURL string 25 | HTTPClient *http.Client 26 | AuthToken string 27 | } 28 | 29 | func NewClient(url, auth_token string) *Client { 30 | c := Client{ 31 | HostURL: url, 32 | AuthToken: "Bearer " + auth_token, 33 | HTTPClient: &http.Client{Timeout: 10 * time.Second}, 34 | } 35 | 36 | return &c 37 | } 38 | 39 | func (c *Client) doRequest(req *http.Request) ([]byte, error) { 40 | req.Header.Set("Authorization", c.AuthToken) 41 | req.Header.Set("Accept", "application/json") 42 | 43 | res, err := c.HTTPClient.Do(req) 44 | if err != nil { 45 | return nil, err 46 | } 47 | defer res.Body.Close() 48 | 49 | body, err := io.ReadAll(res.Body) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | statusOK := res.StatusCode >= 200 && res.StatusCode < 300 55 | if !statusOK { 56 | return nil, fmt.Errorf("uri: %s, method: %s, status: %d, body: %s", req.URL.RequestURI(), req.Method, res.StatusCode, body) 57 | } 58 | 59 | return body, err 60 | } 61 | 62 | func (c *Client) doRequestUnmarshal(req *http.Request, out any) error { 63 | body, err := c.doRequest(req) 64 | if err != nil { 65 | return err 66 | } 67 | return json.Unmarshal(body, &out) 68 | } 69 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Visit https://goreleaser.com for documentation on how to customize this 2 | # behavior. 3 | version: 2 4 | before: 5 | hooks: 6 | # this is just an example and not a requirement for provider building/publishing 7 | - go mod tidy 8 | builds: 9 | - env: 10 | # goreleaser does not work with CGO, it could also complicate 11 | # usage by users in CI/CD systems like Terraform Cloud where 12 | # they are unable to install libraries. 13 | - CGO_ENABLED=0 14 | mod_timestamp: '{{ .CommitTimestamp }}' 15 | flags: 16 | - -trimpath 17 | ldflags: 18 | - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' 19 | goos: 20 | - freebsd 21 | - windows 22 | - linux 23 | - darwin 24 | goarch: 25 | - amd64 26 | - '386' 27 | - arm 28 | - arm64 29 | ignore: 30 | - goos: darwin 31 | goarch: '386' 32 | binary: '{{ .ProjectName }}_v{{ .Version }}' 33 | archives: 34 | - format: zip 35 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 36 | checksum: 37 | extra_files: 38 | - glob: 'terraform-registry-manifest.json' 39 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 40 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 41 | algorithm: sha256 42 | signs: 43 | - artifacts: checksum 44 | args: 45 | # if you are using this in a GitHub action or some other automated pipeline, you 46 | # need to pass the batch flag to indicate its not interactive. 47 | - "--batch" 48 | - "--local-user" 49 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 50 | - "--output" 51 | - "${signature}" 52 | - "--detach-sign" 53 | - "${artifact}" 54 | release: 55 | extra_files: 56 | - glob: 'terraform-registry-manifest.json' 57 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 58 | # If you want to manually examine the release before its live, uncomment this line: 59 | # draft: true 60 | changelog: 61 | disable: true 62 | -------------------------------------------------------------------------------- /docs/resources/notification_slack_channel.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "netdata_notification_slack_channel Resource - terraform-provider-netdata" 4 | subcategory: "" 5 | description: |- 6 | Resource for managing centralized notifications for Slack. Available only in paid plans. 7 | --- 8 | 9 | # netdata_notification_slack_channel (Resource) 10 | 11 | Resource for managing centralized notifications for Slack. Available only in paid plans. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "netdata_notification_slack_channel" "test" { 17 | name = "slack notifications" 18 | 19 | enabled = true 20 | space_id = "" 21 | rooms_id = [""] 22 | repeat_notification_min = 30 23 | webhook_url = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" 24 | notifications = ["CRITICAL", "WARNING", "CLEAR"] 25 | } 26 | ``` 27 | 28 | 29 | ## Schema 30 | 31 | ### Required 32 | 33 | - `enabled` (Boolean) The enabled status of the Slack notification 34 | - `name` (String) The name of the Slack notification 35 | - `notifications` (List of String) The notification options for the Slack. Valid values are: `CRITICAL`, `WARNING`, `CLEAR`, `REACHABLE`, `UNREACHABLE` 36 | - `space_id` (String) The ID of the space for the Slack notification 37 | - `webhook_url` (String, Sensitive) Slack webhook URL 38 | 39 | ### Optional 40 | 41 | - `repeat_notification_min` (Number) The time interval for the Slack notification to be repeated. The interval is presented in minutes and should be between 30 and 1440, or 0 to avoid repetition, which is the default. 42 | - `rooms_id` (List of String) The list of room IDs to set the Slack notification. If the rooms list is null, the Slack notification will be applied to `All rooms` 43 | 44 | ### Read-Only 45 | 46 | - `id` (String) The ID of the Slack notification 47 | 48 | ## Import 49 | 50 | Import is supported using the following syntax: 51 | 52 | The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: 53 | 54 | ```shell 55 | #!/bin/sh 56 | 57 | terraform import netdata_notification_slack_channel.test space_id,channel_id 58 | ``` 59 | -------------------------------------------------------------------------------- /internal/client/room_member.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func (c *Client) GetRoomMembers(spaceID, roomID string) (*[]RoomMember, error) { 11 | if spaceID == "" { 12 | return nil, ErrSpaceIDRequired 13 | } 14 | if roomID == "" { 15 | return nil, ErrRoomIDRequired 16 | } 17 | req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v2/spaces/%s/rooms/%s/members", c.HostURL, spaceID, roomID), nil) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | var roomMembers []RoomMember 23 | 24 | err = c.doRequestUnmarshal(req, &roomMembers) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return &roomMembers, nil 30 | } 31 | 32 | func (c *Client) GetRoomMemberID(spaceID, roomID, spaceMemberID string) (*RoomMember, error) { 33 | roomMembers, err := c.GetRoomMembers(spaceID, roomID) 34 | if err != nil { 35 | return nil, err 36 | } 37 | for _, roomMember := range *roomMembers { 38 | if roomMember.SpaceMemberID == spaceMemberID { 39 | return &roomMember, nil 40 | } 41 | } 42 | return nil, ErrNotFound 43 | } 44 | 45 | func (c *Client) CreateRoomMember(spaceID, roomID, spaceMemberID string) error { 46 | if spaceID == "" { 47 | return ErrSpaceIDRequired 48 | } 49 | if roomID == "" { 50 | return ErrRoomIDRequired 51 | } 52 | if spaceMemberID == "" { 53 | return ErrMemberIDRequired 54 | } 55 | reqBody, err := json.Marshal([]string{spaceMemberID}) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v2/spaces/%s/rooms/%s/members", c.HostURL, spaceID, roomID), bytes.NewReader(reqBody)) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | _, err = c.doRequest(req) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | return nil 71 | 72 | } 73 | 74 | func (c *Client) DeleteRoomMember(spaceID, roomID, spaceMemberID string) error { 75 | if spaceID == "" { 76 | return ErrSpaceIDRequired 77 | } 78 | if roomID == "" { 79 | return ErrRoomIDRequired 80 | } 81 | if spaceMemberID == "" { 82 | return ErrMemberIDRequired 83 | } 84 | req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/v2/spaces/%s/rooms/%s/members?member_ids=%s", c.HostURL, spaceID, roomID, spaceMemberID), nil) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | _, err = c.doRequest(req) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /docs/resources/notification_pagerduty_channel.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "netdata_notification_pagerduty_channel Resource - terraform-provider-netdata" 4 | subcategory: "" 5 | description: |- 6 | Resource for managing centralized notifications for Pagerduty. Available only in paid plans. 7 | --- 8 | 9 | # netdata_notification_pagerduty_channel (Resource) 10 | 11 | Resource for managing centralized notifications for Pagerduty. Available only in paid plans. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "netdata_notification_pagerduty_channel" "test" { 17 | name = "pagerduty notifications" 18 | 19 | enabled = true 20 | space_id = netdata_space.test.id 21 | rooms_id = [""] 22 | notifications = ["CRITICAL", "WARNING", "CLEAR"] 23 | repeat_notification_min = 30 24 | alert_events_url = "https://events.pagerduty.com/v2/enqueue" 25 | integration_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 26 | } 27 | ``` 28 | 29 | 30 | ## Schema 31 | 32 | ### Required 33 | 34 | - `alert_events_url` (String) URL for alert events 35 | - `enabled` (Boolean) The enabled status of the Pagerduty notification 36 | - `integration_key` (String, Sensitive) Integration key 37 | - `name` (String) The name of the Pagerduty notification 38 | - `notifications` (List of String) The notification options for the Pagerduty. Valid values are: `CRITICAL`, `WARNING`, `CLEAR`, `REACHABLE`, `UNREACHABLE` 39 | - `space_id` (String) The ID of the space for the Pagerduty notification 40 | 41 | ### Optional 42 | 43 | - `repeat_notification_min` (Number) The time interval for the Pagerduty notification to be repeated. The interval is presented in minutes and should be between 30 and 1440, or 0 to avoid repetition, which is the default. 44 | - `rooms_id` (List of String) The list of room IDs to set the Pagerduty notification. If the rooms list is null, the Pagerduty notification will be applied to `All rooms` 45 | 46 | ### Read-Only 47 | 48 | - `id` (String) The ID of the Pagerduty notification 49 | 50 | ## Import 51 | 52 | Import is supported using the following syntax: 53 | 54 | The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: 55 | 56 | ```shell 57 | #!/bin/sh 58 | 59 | terraform import netdata_notification_pagerduty_channel.test space_id,channel_id 60 | ``` 61 | -------------------------------------------------------------------------------- /docs/resources/notification_discord_channel.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "netdata_notification_discord_channel Resource - terraform-provider-netdata" 4 | subcategory: "" 5 | description: |- 6 | Resource for managing centralized notifications for Discord. Available only in paid plans. 7 | --- 8 | 9 | # netdata_notification_discord_channel (Resource) 10 | 11 | Resource for managing centralized notifications for Discord. Available only in paid plans. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "netdata_notification_discord_channel" "test" { 17 | name = "discord notifications" 18 | 19 | enabled = true 20 | space_id = "" 21 | rooms_id = [""] 22 | repeat_notification_min = 30 23 | webhook_url = "https://discord.com/api/webhooks/0000000000000/XXXXXXXXXXXXXXXXXXXXXXXX" 24 | notifications = ["CRITICAL", "WARNING", "CLEAR"] 25 | channel_type = "text" 26 | } 27 | ``` 28 | 29 | 30 | ## Schema 31 | 32 | ### Required 33 | 34 | - `channel_type` (String) Discord channel type. Valid values are: `text`, `forum` 35 | - `enabled` (Boolean) The enabled status of the Discord notification 36 | - `name` (String) The name of the Discord notification 37 | - `notifications` (List of String) The notification options for the Discord. Valid values are: `CRITICAL`, `WARNING`, `CLEAR`, `REACHABLE`, `UNREACHABLE` 38 | - `space_id` (String) The ID of the space for the Discord notification 39 | - `webhook_url` (String, Sensitive) Discord webhook URL 40 | 41 | ### Optional 42 | 43 | - `channel_thread` (String) Discord channel thread name required if channel type is `forum` 44 | - `repeat_notification_min` (Number) The time interval for the Discord notification to be repeated. The interval is presented in minutes and should be between 30 and 1440, or 0 to avoid repetition, which is the default. 45 | - `rooms_id` (List of String) The list of room IDs to set the Discord notification. If the rooms list is null, the Discord notification will be applied to `All rooms` 46 | 47 | ### Read-Only 48 | 49 | - `id` (String) The ID of the Discord notification 50 | 51 | ## Import 52 | 53 | Import is supported using the following syntax: 54 | 55 | The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: 56 | 57 | ```shell 58 | #!/bin/sh 59 | 60 | terraform import netdata_notification_discord_channel.test space_id,channel_id 61 | ``` 62 | -------------------------------------------------------------------------------- /internal/client/rooms.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func (c *Client) GetRooms(spaceID string) (*[]RoomInfo, error) { 11 | if spaceID == "" { 12 | return nil, ErrSpaceIDRequired 13 | } 14 | req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v2/spaces/%s/rooms", c.HostURL, spaceID), nil) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | var rooms []RoomInfo 20 | 21 | err = c.doRequestUnmarshal(req, &rooms) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | return &rooms, nil 27 | } 28 | 29 | func (c *Client) GetRoomByID(id, spaceID string) (*RoomInfo, error) { 30 | rooms, err := c.GetRooms(spaceID) 31 | if err != nil { 32 | return nil, err 33 | } 34 | for _, room := range *rooms { 35 | if room.ID == id { 36 | return &room, nil 37 | } 38 | } 39 | return nil, ErrNotFound 40 | } 41 | 42 | func (c *Client) CreateRoom(spaceID, name, description string) (*RoomInfo, error) { 43 | if spaceID == "" { 44 | return nil, ErrSpaceIDRequired 45 | } 46 | reqBody, err := json.Marshal(map[string]string{ 47 | "name": name, 48 | "description": description, 49 | }) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/spaces/%s/rooms", c.HostURL, spaceID), bytes.NewReader(reqBody)) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | var room RoomInfo 60 | 61 | err = c.doRequestUnmarshal(req, &room) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | room.Name = name 67 | room.Description = description 68 | 69 | return &room, nil 70 | } 71 | 72 | func (c *Client) UpdateRoomByID(id, spaceID, name, description string) error { 73 | if id == "" { 74 | return fmt.Errorf("id is empty") 75 | } 76 | if spaceID == "" { 77 | return ErrSpaceIDRequired 78 | } 79 | reqBody, err := json.Marshal(map[string]string{ 80 | "name": name, 81 | "description": description, 82 | }) 83 | if err != nil { 84 | return err 85 | } 86 | req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%s/api/v1/spaces/%s/rooms/%s", c.HostURL, spaceID, id), bytes.NewReader(reqBody)) 87 | if err != nil { 88 | return err 89 | } 90 | _, err = c.doRequest(req) 91 | if err != nil { 92 | return err 93 | } 94 | return nil 95 | } 96 | 97 | func (c *Client) DeleteRoomByID(id, spaceID string) error { 98 | if id == "" { 99 | return fmt.Errorf("id is empty") 100 | } 101 | if spaceID == "" { 102 | return ErrSpaceIDRequired 103 | } 104 | req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/v1/spaces/%s/rooms/%s", c.HostURL, spaceID, id), nil) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | _, err = c.doRequest(req) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /internal/client/notification_slack.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func (c *Client) CreateSlackChannel(spaceID string, commonParams NotificationChannel, slackParams NotificationSlackChannel) (*NotificationChannel, error) { 11 | 12 | if spaceID == "" { 13 | return nil, ErrSpaceIDRequired 14 | } 15 | 16 | reqBody := notificationRequestPayload{ 17 | Name: commonParams.Name, 18 | IntegrationID: commonParams.Integration.ID, 19 | Rooms: commonParams.Rooms, 20 | NotificationOptions: commonParams.NotificationOptions, 21 | RepeatNotificationMinute: commonParams.RepeatNotificationMinute, 22 | } 23 | 24 | secretsJson, err := json.Marshal(slackParams) 25 | if err != nil { 26 | return nil, err 27 | } 28 | reqBody.Secrets = json.RawMessage(secretsJson) 29 | jsonReqBody, err := json.Marshal(reqBody) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v2/spaces/%s/channel", c.HostURL, spaceID), bytes.NewReader(jsonReqBody)) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | var respNotificationChannel NotificationChannel 40 | 41 | err = c.doRequestUnmarshal(req, &respNotificationChannel) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | err = c.EnableChannelByID(spaceID, respNotificationChannel.ID, commonParams.Enabled) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | respNotificationChannel.Enabled = commonParams.Enabled 52 | 53 | return &respNotificationChannel, nil 54 | } 55 | 56 | func (c *Client) UpdateSlackChannelByID(spaceID string, commonParams NotificationChannel, slackParams NotificationSlackChannel) (*NotificationChannel, error) { 57 | 58 | if spaceID == "" { 59 | return nil, ErrSpaceIDRequired 60 | } 61 | 62 | if commonParams.ID == "" { 63 | return nil, ErrChannelIDRequired 64 | } 65 | 66 | err := c.EnableChannelByID(spaceID, commonParams.ID, commonParams.Enabled) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | reqBody := notificationRequestPayload{ 72 | Name: commonParams.Name, 73 | Rooms: commonParams.Rooms, 74 | NotificationOptions: commonParams.NotificationOptions, 75 | RepeatNotificationMinute: commonParams.RepeatNotificationMinute, 76 | } 77 | secretsJson, err := json.Marshal(slackParams) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | reqBody.Secrets = json.RawMessage(secretsJson) 83 | jsonReqBody, err := json.Marshal(reqBody) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v2/spaces/%s/channel/%s", c.HostURL, spaceID, commonParams.ID), bytes.NewReader(jsonReqBody)) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | var respNotificationChannel NotificationChannel 94 | 95 | err = c.doRequestUnmarshal(req, &respNotificationChannel) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | return &respNotificationChannel, nil 101 | 102 | } 103 | -------------------------------------------------------------------------------- /internal/client/space_member.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func (c *Client) GetSpaceMembers(spaceID string) (*[]SpaceMember, error) { 11 | if spaceID == "" { 12 | return nil, ErrSpaceIDRequired 13 | } 14 | req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v2/spaces/%s/members", c.HostURL, spaceID), nil) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | var spaceMembers []SpaceMember 20 | 21 | err = c.doRequestUnmarshal(req, &spaceMembers) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | return &spaceMembers, nil 27 | } 28 | 29 | func (c *Client) GetSpaceMemberID(spaceID, memberID string) (*SpaceMember, error) { 30 | spaceMembers, err := c.GetSpaceMembers(spaceID) 31 | if err != nil { 32 | return nil, err 33 | } 34 | for _, spaceMember := range *spaceMembers { 35 | if spaceMember.MemberID == memberID { 36 | return &spaceMember, nil 37 | } 38 | } 39 | return nil, ErrNotFound 40 | } 41 | 42 | func (c *Client) CreateSpaceMember(spaceID, email, role string) (*SpaceMember, error) { 43 | if spaceID == "" { 44 | return nil, ErrSpaceIDRequired 45 | } 46 | if email == "" { 47 | return nil, fmt.Errorf("email is empty") 48 | } 49 | if role == "" { 50 | return nil, fmt.Errorf("role is empty") 51 | } 52 | reqBody, err := json.Marshal(map[string]string{"email": email, "role": role}) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v2/spaces/%s/members", c.HostURL, spaceID), bytes.NewReader(reqBody)) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | var spaceMember SpaceMember 63 | 64 | err = c.doRequestUnmarshal(req, &spaceMember) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | return &spaceMember, nil 70 | } 71 | 72 | func (c *Client) UpdateSpaceMemberRoleByID(spaceID, memberID, role string) error { 73 | if spaceID == "" { 74 | return ErrSpaceIDRequired 75 | } 76 | if memberID == "" { 77 | return ErrMemberIDRequired 78 | } 79 | if role == "" { 80 | return fmt.Errorf("role is empty") 81 | } 82 | reqBody, err := json.Marshal(map[string]string{"role": role}) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%s/api/v2/spaces/%s/members/%s", c.HostURL, spaceID, memberID), bytes.NewReader(reqBody)) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | _, err = c.doRequest(req) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func (c *Client) DeleteSpaceMember(spaceID, memberID string) error { 101 | if spaceID == "" { 102 | return ErrSpaceIDRequired 103 | } 104 | if memberID == "" { 105 | return ErrMemberIDRequired 106 | } 107 | req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/v2/spaces/%s/members?member_ids=%s", c.HostURL, spaceID, memberID), nil) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | _, err = c.doRequest(req) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /internal/client/notification_discord.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func (c *Client) CreateDiscordChannel(spaceID string, commonParams NotificationChannel, discordParams NotificationDiscordChannel) (*NotificationChannel, error) { 11 | 12 | if spaceID == "" { 13 | return nil, ErrSpaceIDRequired 14 | } 15 | 16 | reqBody := notificationRequestPayload{ 17 | Name: commonParams.Name, 18 | IntegrationID: commonParams.Integration.ID, 19 | Rooms: commonParams.Rooms, 20 | NotificationOptions: commonParams.NotificationOptions, 21 | RepeatNotificationMinute: commonParams.RepeatNotificationMinute, 22 | } 23 | 24 | secretsJson, err := json.Marshal(discordParams) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | reqBody.Secrets = json.RawMessage(secretsJson) 30 | jsonReqBody, err := json.Marshal(reqBody) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v2/spaces/%s/channel", c.HostURL, spaceID), bytes.NewReader(jsonReqBody)) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | var respNotificationChannel NotificationChannel 41 | 42 | err = c.doRequestUnmarshal(req, &respNotificationChannel) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | err = c.EnableChannelByID(spaceID, respNotificationChannel.ID, commonParams.Enabled) 48 | 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | respNotificationChannel.Enabled = commonParams.Enabled 54 | 55 | return &respNotificationChannel, nil 56 | } 57 | 58 | func (c *Client) UpdateDiscordChannelByID(spaceID string, commonParams NotificationChannel, discordParams NotificationDiscordChannel) (*NotificationChannel, error) { 59 | 60 | if spaceID == "" { 61 | return nil, ErrSpaceIDRequired 62 | } 63 | 64 | if commonParams.ID == "" { 65 | return nil, ErrChannelIDRequired 66 | } 67 | 68 | err := c.EnableChannelByID(spaceID, commonParams.ID, commonParams.Enabled) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | reqBody := notificationRequestPayload{ 74 | Name: commonParams.Name, 75 | Rooms: commonParams.Rooms, 76 | NotificationOptions: commonParams.NotificationOptions, 77 | RepeatNotificationMinute: commonParams.RepeatNotificationMinute, 78 | } 79 | 80 | secretsJson, err := json.Marshal(discordParams) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | reqBody.Secrets = json.RawMessage(secretsJson) 86 | jsonReqBody, err := json.Marshal(reqBody) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v2/spaces/%s/channel/%s", c.HostURL, spaceID, commonParams.ID), bytes.NewReader(jsonReqBody)) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | var respNotificationChannel NotificationChannel 97 | 98 | err = c.doRequestUnmarshal(req, &respNotificationChannel) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | return &respNotificationChannel, nil 104 | 105 | } 106 | -------------------------------------------------------------------------------- /internal/client/notification_pagerduty.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func (c *Client) CreatePagerdutyChannel(spaceID string, commonParams NotificationChannel, pagerdutyParams NotificationPagerdutyChannel) (*NotificationChannel, error) { 11 | 12 | if spaceID == "" { 13 | return nil, ErrSpaceIDRequired 14 | } 15 | 16 | reqBody := notificationRequestPayload{ 17 | Name: commonParams.Name, 18 | IntegrationID: commonParams.Integration.ID, 19 | Rooms: commonParams.Rooms, 20 | NotificationOptions: commonParams.NotificationOptions, 21 | RepeatNotificationMinute: commonParams.RepeatNotificationMinute, 22 | } 23 | 24 | secretsJson, err := json.Marshal(pagerdutyParams) 25 | if err != nil { 26 | return nil, err 27 | } 28 | reqBody.Secrets = json.RawMessage(secretsJson) 29 | jsonReqBody, err := json.Marshal(reqBody) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v2/spaces/%s/channel", c.HostURL, spaceID), bytes.NewReader(jsonReqBody)) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | var respNotificationChannel NotificationChannel 40 | 41 | err = c.doRequestUnmarshal(req, &respNotificationChannel) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | err = c.EnableChannelByID(spaceID, respNotificationChannel.ID, commonParams.Enabled) 47 | 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | respNotificationChannel.Enabled = commonParams.Enabled 53 | 54 | return &respNotificationChannel, nil 55 | } 56 | 57 | func (c *Client) UpdatePagerdutyChannelByID(spaceID string, commonParams NotificationChannel, pagerdutyParams NotificationPagerdutyChannel) (*NotificationChannel, error) { 58 | 59 | if spaceID == "" { 60 | return nil, ErrSpaceIDRequired 61 | } 62 | 63 | if commonParams.ID == "" { 64 | return nil, ErrChannelIDRequired 65 | } 66 | 67 | err := c.EnableChannelByID(spaceID, commonParams.ID, commonParams.Enabled) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | reqBody := notificationRequestPayload{ 73 | Name: commonParams.Name, 74 | Rooms: commonParams.Rooms, 75 | NotificationOptions: commonParams.NotificationOptions, 76 | RepeatNotificationMinute: commonParams.RepeatNotificationMinute, 77 | } 78 | 79 | secretsJson, err := json.Marshal(pagerdutyParams) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | reqBody.Secrets = json.RawMessage(secretsJson) 85 | jsonReqBody, err := json.Marshal(reqBody) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v2/spaces/%s/channel/%s", c.HostURL, spaceID, commonParams.ID), bytes.NewReader(jsonReqBody)) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | var respNotificationChannel NotificationChannel 96 | 97 | err = c.doRequestUnmarshal(req, &respNotificationChannel) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | return &respNotificationChannel, nil 103 | 104 | } 105 | -------------------------------------------------------------------------------- /internal/client/spaces.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func (c *Client) GetSpaces() (*[]SpaceInfo, error) { 11 | req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v3/spaces", c.HostURL), nil) 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | var spaces []SpaceInfo 17 | 18 | err = c.doRequestUnmarshal(req, &spaces) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | return &spaces, nil 24 | } 25 | 26 | func (c *Client) GetSpaceByID(id string) (*SpaceInfo, error) { 27 | spaces, err := c.GetSpaces() 28 | if err != nil { 29 | return nil, err 30 | } 31 | for _, space := range *spaces { 32 | if space.ID == id { 33 | return &space, nil 34 | } 35 | } 36 | return nil, ErrNotFound 37 | } 38 | 39 | func (c *Client) CreateSpace(name, description string) (*SpaceInfo, error) { 40 | reqBody, err := json.Marshal(map[string]string{"name": name}) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/spaces", c.HostURL), bytes.NewReader(reqBody)) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | var space SpaceInfo 51 | 52 | err = c.doRequestUnmarshal(req, &space) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | err = c.UpdateSpaceByID(space.ID, name, description) 58 | if err != nil { 59 | return nil, err 60 | } 61 | space.Name = name 62 | space.Description = description 63 | 64 | return &space, nil 65 | } 66 | 67 | func (c *Client) UpdateSpaceByID(id, name, description string) error { 68 | if id == "" { 69 | return fmt.Errorf("id is empty") 70 | } 71 | reqBody, err := json.Marshal(map[string]string{ 72 | "name": name, 73 | "description": description, 74 | }) 75 | if err != nil { 76 | return err 77 | } 78 | req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%s/api/v1/spaces/%s", c.HostURL, id), bytes.NewReader(reqBody)) 79 | if err != nil { 80 | return err 81 | } 82 | _, err = c.doRequest(req) 83 | if err != nil { 84 | return err 85 | } 86 | return nil 87 | } 88 | 89 | func (c *Client) DeleteSpaceByID(id string) error { 90 | if id == "" { 91 | return fmt.Errorf("id is empty") 92 | } 93 | req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/v1/spaces/%s", c.HostURL, id), nil) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | _, err = c.doRequest(req) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | return nil 104 | } 105 | 106 | func (c *Client) GetSpaceClaimToken(id string) (*string, error) { 107 | if id == "" { 108 | return nil, fmt.Errorf("id is empty") 109 | } 110 | req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/spaces/%s/token/rotate", c.HostURL, id), nil) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | var data map[string]interface{} 116 | 117 | err = c.doRequestUnmarshal(req, &data) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | token, ok := data["token"].(string) 123 | if !ok { 124 | return nil, fmt.Errorf("token not found") 125 | } 126 | 127 | return &token, nil 128 | } 129 | -------------------------------------------------------------------------------- /internal/provider/notifications.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" 7 | "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" 8 | "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" 9 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 10 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" 11 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 12 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 13 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 14 | "github.com/hashicorp/terraform-plugin-framework/types" 15 | ) 16 | 17 | func commonNotificationSchema(notificationType string) schema.Schema { 18 | return schema.Schema{ 19 | Description: fmt.Sprintf("Resource for managing centralized notifications for %s. Available only in paid plans.", notificationType), 20 | Attributes: map[string]schema.Attribute{ 21 | "id": schema.StringAttribute{ 22 | Description: fmt.Sprintf("The ID of the %s notification", notificationType), 23 | Computed: true, 24 | PlanModifiers: []planmodifier.String{ 25 | stringplanmodifier.UseStateForUnknown(), 26 | }, 27 | }, 28 | "name": schema.StringAttribute{ 29 | Description: fmt.Sprintf("The name of the %s notification", notificationType), 30 | Required: true, 31 | }, 32 | "enabled": schema.BoolAttribute{ 33 | Description: fmt.Sprintf("The enabled status of the %s notification", notificationType), 34 | Required: true, 35 | }, 36 | "space_id": schema.StringAttribute{ 37 | Description: fmt.Sprintf("The ID of the space for the %s notification", notificationType), 38 | Required: true, 39 | PlanModifiers: []planmodifier.String{ 40 | stringplanmodifier.RequiresReplace(), 41 | }, 42 | }, 43 | "rooms_id": schema.ListAttribute{ 44 | Description: fmt.Sprintf("The list of room IDs to set the %s notification. If the rooms list is null, the %s notification will be applied to `All rooms`", notificationType, notificationType), 45 | ElementType: types.StringType, 46 | Optional: true, 47 | Validators: []validator.List{ 48 | listvalidator.SizeAtLeast(1), 49 | }, 50 | }, 51 | "repeat_notification_min": schema.Int64Attribute{ 52 | Description: fmt.Sprintf("The time interval for the %s notification to be repeated. The interval is presented in minutes and should be between 30 and 1440, or 0 to avoid repetition, which is the default.", notificationType), 53 | Computed: true, 54 | Optional: true, 55 | Default: int64default.StaticInt64(0), 56 | Validators: []validator.Int64{ 57 | int64validator.Any( 58 | int64validator.OneOf(0), 59 | int64validator.Between(30, 1440), 60 | ), 61 | }, 62 | }, 63 | "notifications": schema.ListAttribute{ 64 | Description: fmt.Sprintf("The notification options for the %s. Valid values are: `CRITICAL`, `WARNING`, `CLEAR`, `REACHABLE`, `UNREACHABLE`", notificationType), 65 | ElementType: types.StringType, 66 | Required: true, 67 | Validators: []validator.List{ 68 | listvalidator.SizeAtLeast(1), 69 | listvalidator.UniqueValues(), 70 | listvalidator.ValueStringsAre(stringvalidator.OneOf([]string{"CRITICAL", "WARNING", "CLEAR", "REACHABLE", "UNREACHABLE"}...)), 71 | }, 72 | }, 73 | }, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internal/provider/room_data_source.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/hashicorp/terraform-plugin-framework/datasource" 9 | "github.com/hashicorp/terraform-plugin-framework/datasource/schema" 10 | "github.com/hashicorp/terraform-plugin-framework/types" 11 | "github.com/netdata/terraform-provider-netdata/internal/client" 12 | ) 13 | 14 | var ( 15 | _ datasource.DataSource = &roomDataSource{} 16 | _ datasource.DataSourceWithConfigure = &roomDataSource{} 17 | ) 18 | 19 | func NewRoomDataSource() datasource.DataSource { 20 | return &roomDataSource{} 21 | } 22 | 23 | type roomDataSource struct { 24 | client *client.Client 25 | } 26 | 27 | type roomDataSourceModel struct { 28 | ID types.String `tfsdk:"id"` 29 | SpaceID types.String `tfsdk:"space_id"` 30 | Name types.String `tfsdk:"name"` 31 | Description types.String `tfsdk:"description"` 32 | } 33 | 34 | func (s *roomDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { 35 | resp.TypeName = req.ProviderTypeName + "_room" 36 | } 37 | 38 | func (s *roomDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { 39 | resp.Schema = schema.Schema{ 40 | Description: "Use this data source to get information about a Netdata Cloud Room.", 41 | Attributes: map[string]schema.Attribute{ 42 | "id": schema.StringAttribute{ 43 | Description: "The ID of the room", 44 | Required: true, 45 | }, 46 | "space_id": schema.StringAttribute{ 47 | Description: "The ID of the space", 48 | Required: true, 49 | }, 50 | "name": schema.StringAttribute{ 51 | Description: "The name of the room", 52 | Computed: true, 53 | }, 54 | "description": schema.StringAttribute{ 55 | Description: "The description of the room", 56 | Computed: true, 57 | }, 58 | }, 59 | } 60 | } 61 | 62 | func (s *roomDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { 63 | if req.ProviderData == nil { 64 | return 65 | } 66 | 67 | client, ok := req.ProviderData.(*client.Client) 68 | if !ok { 69 | resp.Diagnostics.AddError( 70 | "Unexpected Resource Configure Type", 71 | fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), 72 | ) 73 | 74 | return 75 | } 76 | 77 | s.client = client 78 | } 79 | 80 | func (s *roomDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { 81 | var state roomDataSourceModel 82 | 83 | resp.Diagnostics.Append(req.Config.Get(ctx, &state)...) 84 | 85 | roomInfo, err := s.client.GetRoomByID(state.ID.ValueString(), state.SpaceID.ValueString()) 86 | if err != nil { 87 | if errors.Is(err, client.ErrNotFound) { 88 | resp.State.RemoveResource(ctx) 89 | return 90 | } 91 | resp.Diagnostics.AddError( 92 | "Error Getting Room", 93 | "Could Not Read Room ID: "+state.ID.ValueString()+": err: "+err.Error(), 94 | ) 95 | return 96 | } 97 | 98 | state.ID = types.StringValue(roomInfo.ID) 99 | state.Name = types.StringValue(roomInfo.Name) 100 | state.Description = types.StringValue(roomInfo.Description) 101 | resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) 102 | if resp.Diagnostics.HasError() { 103 | return 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /internal/client/models.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type SpaceInfo struct { 10 | ID string `json:"id"` 11 | Name string `json:"name"` 12 | Description string `json:"description"` 13 | } 14 | 15 | type RoomInfo struct { 16 | ID string `json:"id"` 17 | Name string `json:"name"` 18 | Description string `json:"description"` 19 | } 20 | 21 | type SpaceMember struct { 22 | Email string `json:"email"` 23 | MemberID string `json:"memberID"` 24 | Role string `json:"role"` 25 | } 26 | 27 | type RoomMember struct { 28 | SpaceMemberID string `json:"memberID"` 29 | } 30 | 31 | type NotificationIntegrations struct { 32 | Integrations []NotificationIntegration `json:"integrations"` 33 | } 34 | 35 | type NotificationChannel struct { 36 | ID string `json:"id"` 37 | Enabled bool `json:"enabled"` 38 | Name string `json:"name"` 39 | Integration NotificationIntegration `json:"integration"` 40 | NotificationOptions []string `json:"notification_options"` 41 | Rooms []string `json:"rooms"` 42 | Secrets json.RawMessage `json:"secrets"` 43 | RepeatNotificationMinute int64 `json:"repeat_notification_min,omitempty"` 44 | } 45 | 46 | type NotificationIntegration struct { 47 | ID string `json:"id"` 48 | Name string `json:"slug"` 49 | } 50 | 51 | type NotificationSlackChannel struct { 52 | URL string `json:"url"` 53 | } 54 | 55 | type NotificationDiscordChannel struct { 56 | URL string `json:"url"` 57 | ChannelParams struct { 58 | Selection string `json:"selection"` 59 | ThreadName string `json:"threadName"` 60 | } `json:"channelParams"` 61 | } 62 | 63 | type NotificationPagerdutyChannel struct { 64 | AlertEventsURL string `json:"alertEventsURL"` 65 | IntegrationKey string `json:"integrationKey"` 66 | } 67 | 68 | type notificationRequestPayload struct { 69 | Name string `json:"name"` 70 | IntegrationID string `json:"integrationID"` 71 | NotificationOptions []string `json:"notification_options"` 72 | Rooms []string `json:"rooms"` 73 | Secrets json.RawMessage `json:"secrets"` 74 | RepeatNotificationMinute int64 `json:"repeat_notification_min,omitempty"` 75 | } 76 | 77 | type Invitation struct { 78 | ID string `json:"id"` 79 | Email string `json:"email"` 80 | } 81 | 82 | type RoomNodes struct { 83 | Nodes []RoomNode `json:"nodes"` 84 | } 85 | 86 | type RoomNode struct { 87 | NodeID string `json:"nd"` 88 | NodeName string `json:"nm"` 89 | State string `json:"state"` 90 | } 91 | type NodeMembershipRule struct { 92 | ID uuid.UUID `json:"id"` 93 | SpaceID uuid.UUID `json:"spaceID"` 94 | RoomID uuid.UUID `json:"roomID"` 95 | Clauses []NodeMembershipClause `json:"clauses"` 96 | Action string `json:"action"` 97 | Description string `json:"description"` 98 | } 99 | type NodeMembershipClause struct { 100 | Label string `json:"label"` 101 | Operator string `json:"operator"` 102 | Value string `json:"value"` 103 | Negate bool `json:"negate"` 104 | } 105 | -------------------------------------------------------------------------------- /docs/resources/node_room_member.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "netdata_node_room_member Resource - terraform-provider-netdata" 4 | subcategory: "" 5 | description: |- 6 | Provides a Netdata Cloud Node Room Member resource. Use this resource to manage node membership to the room in the selected space. 7 | There are two options to add nodes to the room: 8 | providing the node names directly, but only reachable nodes will be added to the room, use node_names attribute for thiscreating rules that will automatically add nodes to the room based on the rule, use rule block for this 9 | --- 10 | 11 | # netdata_node_room_member (Resource) 12 | 13 | Provides a Netdata Cloud Node Room Member resource. Use this resource to manage node membership to the room in the selected space. 14 | There are two options to add nodes to the room: 15 | - providing the node names directly, but only reachable nodes will be added to the room, use node_names attribute for this 16 | - creating rules that will automatically add nodes to the room based on the rule, use rule block for this 17 | 18 | ## Example Usage 19 | 20 | ```terraform 21 | resource "netdata_node_room_member" "test" { 22 | space_id = "" 23 | room_id = "" 24 | node_names = [ 25 | "node1", 26 | "node2" 27 | ] 28 | rule { 29 | action = "INCLUDE" 30 | description = "Description of the rule" 31 | clause { 32 | label = "role" 33 | operator = "equals" 34 | value = "parent" 35 | negate = false 36 | } 37 | clause { 38 | label = "environment" 39 | operator = "equals" 40 | value = "production" 41 | negate = false 42 | } 43 | } 44 | } 45 | ``` 46 | 47 | 48 | ## Schema 49 | 50 | ### Required 51 | 52 | - `room_id` (String) The Room ID of the space. 53 | - `space_id` (String) Space ID of the member. 54 | 55 | ### Optional 56 | 57 | - `node_names` (List of String) List of node names to add to the room. At least one node name is required. 58 | - `rule` (Block List) The node rule to apply to the room. The logical relation between multiple rules is OR. More info [here](https://learn.netdata.cloud/docs/netdata-cloud/spaces-and-rooms/node-rule-based-room-assignment). (see [below for nested schema](#nestedblock--rule)) 59 | 60 | 61 | ### Nested Schema for `rule` 62 | 63 | Required: 64 | 65 | - `action` (String) Determines whether matching nodes will be included or excluded from the room. Valid values: INCLUDE or EXCLUDE. EXCLUDE action always takes precedence against INCLUDE. 66 | 67 | Optional: 68 | 69 | - `clause` (Block List) The clause to apply to the rule. The logical relation between multiple clauses is AND. It should be a least one clause. (see [below for nested schema](#nestedblock--rule--clause)) 70 | - `description` (String) The description of the rule. 71 | 72 | Read-Only: 73 | 74 | - `id` (String) The ID of the rule. 75 | 76 | 77 | ### Nested Schema for `rule.clause` 78 | 79 | Required: 80 | 81 | - `label` (String) The host label to check. 82 | - `negate` (Boolean) Negate the clause. 83 | - `operator` (String) Operator to compare. Valid values: equals, starts_with, ends_with, contains. 84 | - `value` (String) The value to compare against. 85 | 86 | ## Import 87 | 88 | Import is supported using the following syntax: 89 | 90 | The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: 91 | 92 | ```shell 93 | #!/bin/sh 94 | 95 | terraform import netdata_node_room_member.test space_id,room_id 96 | ``` 97 | -------------------------------------------------------------------------------- /examples/complete/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | netdata = { 4 | source = "netdata/netdata" 5 | } 6 | } 7 | required_version = ">= 1.1.0" 8 | } 9 | 10 | provider "netdata" {} 11 | 12 | resource "netdata_node_room_member" "new" { 13 | room_id = netdata_room.test.id 14 | space_id = netdata_space.test.id 15 | 16 | node_names = [ 17 | "node1", 18 | "node2" 19 | ] 20 | 21 | rule { 22 | action = "INCLUDE" 23 | description = "Description of the rule" 24 | clause { 25 | label = "role" 26 | operator = "equals" 27 | value = "parent" 28 | negate = false 29 | } 30 | clause { 31 | label = "environment" 32 | operator = "equals" 33 | value = "production" 34 | negate = false 35 | } 36 | } 37 | rule { 38 | action = "EXCLUDE" 39 | description = "Description of the rule" 40 | clause { 41 | label = "role" 42 | operator = "equals" 43 | value = "parent" 44 | negate = true 45 | } 46 | clause { 47 | label = "environment" 48 | operator = "contains" 49 | value = "production" 50 | negate = false 51 | } 52 | } 53 | } 54 | 55 | resource "netdata_space" "test" { 56 | name = "MyTestingSpace" 57 | description = "Created by Terraform" 58 | } 59 | 60 | resource "netdata_room" "test" { 61 | space_id = netdata_space.test.id 62 | name = "MyTestingRoom" 63 | description = "Created by Terraform" 64 | } 65 | 66 | resource "netdata_space_member" "test" { 67 | email = "foo@bar.local" 68 | space_id = netdata_space.test.id 69 | role = "admin" 70 | } 71 | 72 | resource "netdata_room_member" "test" { 73 | room_id = netdata_room.test.id 74 | space_id = netdata_space.test.id 75 | space_member_id = netdata_space_member.test.id 76 | } 77 | 78 | resource "netdata_node_room_member" "test" { 79 | room_id = netdata_room.test.id 80 | space_id = netdata_space.test.id 81 | node_names = [ 82 | "node1", 83 | "node2" 84 | ] 85 | } 86 | 87 | resource "netdata_notification_slack_channel" "test" { 88 | name = "slack" 89 | 90 | enabled = true 91 | space_id = netdata_space.test.id 92 | rooms_id = [netdata_room.test.id] 93 | notifications = ["CRITICAL", "WARNING", "CLEAR"] 94 | repeat_notification_min = 60 95 | webhook_url = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" 96 | } 97 | 98 | resource "netdata_notification_discord_channel" "test" { 99 | name = "discord" 100 | 101 | enabled = true 102 | space_id = netdata_space.test.id 103 | rooms_id = [netdata_room.test.id] 104 | notifications = ["CRITICAL", "WARNING", "CLEAR"] 105 | webhook_url = "https://discord.com/api/webhooks/0000000000000/XXXXXXXXXXXXXXXXXXXXXXXX" 106 | channel_type = "forum" 107 | channel_thread = "thread" 108 | } 109 | 110 | resource "netdata_notification_pagerduty_channel" "test" { 111 | name = "pagerduty" 112 | 113 | enabled = true 114 | space_id = netdata_space.test.id 115 | notifications = ["CRITICAL", "WARNING", "CLEAR"] 116 | alert_events_url = "https://events.pagerduty.com/v2/enqueue" 117 | integration_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 118 | } 119 | 120 | data "netdata_space" "test" { 121 | id = netdata_space.test.id 122 | } 123 | 124 | data "netdata_room" "test" { 125 | id = netdata_room.test.id 126 | space_id = netdata_space.test.id 127 | } 128 | 129 | output "space_name" { 130 | value = data.netdata_space.test.name 131 | } 132 | 133 | output "claim_token" { 134 | value = netdata_space.test.claim_token 135 | } 136 | -------------------------------------------------------------------------------- /internal/provider/provider.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/datasource" 8 | "github.com/hashicorp/terraform-plugin-framework/path" 9 | "github.com/hashicorp/terraform-plugin-framework/provider" 10 | "github.com/hashicorp/terraform-plugin-framework/provider/schema" 11 | "github.com/hashicorp/terraform-plugin-framework/resource" 12 | "github.com/hashicorp/terraform-plugin-framework/types" 13 | "github.com/netdata/terraform-provider-netdata/internal/client" 14 | ) 15 | 16 | var _ provider.Provider = &netdataCloudProvider{} 17 | 18 | const NetdataCloudURL = "https://app.netdata.cloud" 19 | 20 | type netdataCloudProvider struct { 21 | version string 22 | } 23 | 24 | type netdataCloudProviderModel struct { 25 | Url types.String `tfsdk:"url"` 26 | AuthToken types.String `tfsdk:"auth_token"` 27 | } 28 | 29 | func (p *netdataCloudProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { 30 | resp.TypeName = "netdata" 31 | resp.Version = p.version 32 | } 33 | 34 | func (p *netdataCloudProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { 35 | resp.Schema = schema.Schema{ 36 | Description: "The Netdata Provider allows you to manage Netdata Cloud resources.", 37 | Attributes: map[string]schema.Attribute{ 38 | "url": schema.StringAttribute{ 39 | MarkdownDescription: "Netdata Cloud URL Address by default is https://app.netdata.cloud. Can be also set as environment variable `NETDATA_CLOUD_URL`", 40 | Optional: true, 41 | }, 42 | "auth_token": schema.StringAttribute{ 43 | MarkdownDescription: "Netdata Cloud Authentication Token with `scope:all`, more [info](https://learn.netdata.cloud/docs/netdata-cloud/api-tokens). Can be also set as environment variable `NETDATA_CLOUD_AUTH_TOKEN`", 44 | Sensitive: true, 45 | Optional: true, 46 | }, 47 | }, 48 | } 49 | } 50 | 51 | func (p *netdataCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { 52 | var data netdataCloudProviderModel 53 | 54 | resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) 55 | 56 | if resp.Diagnostics.HasError() { 57 | return 58 | } 59 | 60 | url := os.Getenv("NETDATA_CLOUD_URL") 61 | auth_token := os.Getenv("NETDATA_CLOUD_AUTH_TOKEN") 62 | 63 | if !data.AuthToken.IsNull() { 64 | auth_token = data.AuthToken.ValueString() 65 | } 66 | 67 | if !data.Url.IsNull() { 68 | url = data.Url.ValueString() 69 | } 70 | 71 | if url == "" { 72 | url = NetdataCloudURL 73 | } 74 | 75 | if auth_token == "" { 76 | resp.Diagnostics.AddAttributeError( 77 | path.Root("auth_token"), 78 | "Missing Netdata Cloud Authentication Token", 79 | "Provide a valid Netdata Cloud Authentication Token to authenticate with Netdata Cloud.", 80 | ) 81 | } 82 | 83 | client := client.NewClient(url, auth_token) 84 | 85 | resp.DataSourceData = client 86 | resp.ResourceData = client 87 | } 88 | 89 | func (p *netdataCloudProvider) Resources(ctx context.Context) []func() resource.Resource { 90 | return []func() resource.Resource{ 91 | NewSpaceResource, 92 | NewRoomResource, 93 | NewSpaceMemberResource, 94 | NewRoomMemberResource, 95 | NewSlackChannelResource, 96 | NewDiscordChannelResource, 97 | NewPagerdutyChannelResource, 98 | NewNodeRoomMemberResource, 99 | } 100 | } 101 | 102 | func (p *netdataCloudProvider) DataSources(ctx context.Context) []func() datasource.DataSource { 103 | return []func() datasource.DataSource{ 104 | NewSpaceDataSource, 105 | NewRoomDataSource, 106 | } 107 | } 108 | 109 | func New(version string) func() provider.Provider { 110 | return func() provider.Provider { 111 | return &netdataCloudProvider{ 112 | version: version, 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Terraform Provider testing workflow. 2 | name: Tests 3 | 4 | # This GitHub action runs your tests for each pull request and push. 5 | # Optionally, you can turn it on using a schedule for regular testing. 6 | on: 7 | pull_request: 8 | paths-ignore: 9 | - 'README.md' 10 | schedule: 11 | - cron: '35 11 * * *' 12 | 13 | # Testing only needs permissions to read the repository contents. 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | # Ensure project builds before running testing matrix 19 | build: 20 | name: Build 21 | runs-on: ubuntu-latest 22 | timeout-minutes: 15 23 | steps: 24 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 25 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 26 | with: 27 | go-version-file: 'go.mod' 28 | cache: true 29 | - run: go mod download 30 | - run: go build -v . 31 | - name: Run linters 32 | uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3.7.0 33 | with: 34 | version: latest 35 | args: --timeout 10m0s 36 | 37 | generate: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 41 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 42 | with: 43 | go-version-file: 'go.mod' 44 | cache: true 45 | - run: go generate ./... 46 | - name: git diff 47 | run: | 48 | git diff --compact-summary --exit-code || \ 49 | (echo; echo "Unexpected difference in directories after code generation. Run 'go generate ./...' command and commit."; exit 1) 50 | - name: Check generated docs 51 | run: | 52 | go install "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs" 53 | make docs 54 | git diff --exit-code 55 | 56 | # Run acceptance tests in a matrix with Terraform CLI versions 57 | test: 58 | name: Terraform Provider Acceptance Tests 59 | needs: build 60 | runs-on: ubuntu-latest 61 | timeout-minutes: 15 62 | steps: 63 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 64 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 65 | with: 66 | go-version-file: 'go.mod' 67 | cache: true 68 | - uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1 # v2.0.3 69 | with: 70 | terraform_version: 'latest' 71 | terraform_wrapper: false 72 | - run: go mod download 73 | - env: 74 | TF_ACC: "1" 75 | NETDATA_CLOUD_URL: "${{ secrets.NETDATA_CLOUD_URL }}" 76 | NETDATA_CLOUD_AUTH_TOKEN: "${{ secrets.NETDATA_CLOUD_AUTH_TOKEN }}" 77 | SPACE_ID_NON_COMMUNITY: "${{ secrets.SPACE_ID_NON_COMMUNITY }}" 78 | run: go test -v -cover ./internal/provider/ 79 | timeout-minutes: 10 80 | - name: run sweepers 81 | env: 82 | NETDATA_CLOUD_URL: "${{ secrets.NETDATA_CLOUD_URL }}" 83 | NETDATA_CLOUD_AUTH_TOKEN: "${{ secrets.NETDATA_CLOUD_AUTH_TOKEN }}" 84 | SPACE_ID_NON_COMMUNITY: "${{ secrets.SPACE_ID_NON_COMMUNITY }}" 85 | run: go test ./... -sweep empty 86 | 87 | notify: 88 | name: Notify if fails when scheduled 89 | needs: [build, generate, test] 90 | runs-on: ubuntu-latest 91 | if: ${{ always() && contains(needs.*.result, 'failure') && github.event_name == 'schedule' }} 92 | timeout-minutes: 5 93 | steps: 94 | - name: add new github issue 95 | uses: dacbd/create-issue-action@v1.2.1 96 | with: 97 | token: ${{ secrets.NETDATABOT_GITHUB_TOKEN }} 98 | repo: infra 99 | title: Failed terraform-provider-netdata test 100 | body: | 101 | [Failed job](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) 102 | -------------------------------------------------------------------------------- /internal/provider/space_data_source.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/hashicorp/terraform-plugin-framework/datasource" 9 | "github.com/hashicorp/terraform-plugin-framework/datasource/schema" 10 | "github.com/hashicorp/terraform-plugin-framework/types" 11 | "github.com/hashicorp/terraform-plugin-log/tflog" 12 | "github.com/netdata/terraform-provider-netdata/internal/client" 13 | ) 14 | 15 | var ( 16 | _ datasource.DataSource = &spaceDataSource{} 17 | _ datasource.DataSourceWithConfigure = &spaceDataSource{} 18 | ) 19 | 20 | func NewSpaceDataSource() datasource.DataSource { 21 | return &spaceDataSource{} 22 | } 23 | 24 | type spaceDataSource struct { 25 | client *client.Client 26 | } 27 | 28 | type spaceDataSourceModel struct { 29 | ID types.String `tfsdk:"id"` 30 | Name types.String `tfsdk:"name"` 31 | Description types.String `tfsdk:"description"` 32 | ClaimToken types.String `tfsdk:"claim_token"` 33 | } 34 | 35 | func (s *spaceDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { 36 | resp.TypeName = req.ProviderTypeName + "_space" 37 | } 38 | 39 | func (s *spaceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { 40 | resp.Schema = schema.Schema{ 41 | Description: "Use this data source to get information about a Netdata Cloud Space.", 42 | Attributes: map[string]schema.Attribute{ 43 | "id": schema.StringAttribute{ 44 | Description: "The ID of the space", 45 | Required: true, 46 | }, 47 | "name": schema.StringAttribute{ 48 | Description: "The name of the space", 49 | Computed: true, 50 | }, 51 | "description": schema.StringAttribute{ 52 | Description: "The description of the space", 53 | Computed: true, 54 | }, 55 | "claim_token": schema.StringAttribute{ 56 | Description: "The claim token of the space", 57 | Computed: true, 58 | }, 59 | }, 60 | } 61 | } 62 | 63 | func (s *spaceDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { 64 | if req.ProviderData == nil { 65 | return 66 | } 67 | 68 | client, ok := req.ProviderData.(*client.Client) 69 | if !ok { 70 | resp.Diagnostics.AddError( 71 | "Unexpected Resource Configure Type", 72 | fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), 73 | ) 74 | 75 | return 76 | } 77 | 78 | s.client = client 79 | } 80 | 81 | func (s *spaceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { 82 | var state spaceDataSourceModel 83 | 84 | resp.Diagnostics.Append(req.Config.Get(ctx, &state)...) 85 | 86 | tflog.Info(ctx, "Reading Space ID:"+state.ID.ValueString()) 87 | 88 | spaceInfo, err := s.client.GetSpaceByID(state.ID.ValueString()) 89 | if err != nil { 90 | if errors.Is(err, client.ErrNotFound) { 91 | resp.State.RemoveResource(ctx) 92 | return 93 | } 94 | resp.Diagnostics.AddError( 95 | "Error Getting Space", 96 | "Could Not Read Space ID: "+state.ID.ValueString()+": err: "+err.Error(), 97 | ) 98 | return 99 | } 100 | 101 | state.ID = types.StringValue(spaceInfo.ID) 102 | state.Name = types.StringValue(spaceInfo.Name) 103 | state.Description = types.StringValue(spaceInfo.Description) 104 | if resp.Diagnostics.HasError() { 105 | return 106 | } 107 | 108 | if state.ClaimToken.IsNull() { 109 | tflog.Info(ctx, "Creating Claim Token for Space ID: "+state.ID.ValueString()) 110 | claimToken, err := s.client.GetSpaceClaimToken(spaceInfo.ID) 111 | if err != nil { 112 | resp.Diagnostics.AddError( 113 | "Error Creating Claim Token", 114 | "Could Not Create Claim Token for Space ID: "+state.ID.ValueString()+": err: "+err.Error(), 115 | ) 116 | return 117 | } 118 | state.ClaimToken = types.StringValue(*claimToken) 119 | } 120 | 121 | resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) 122 | } 123 | -------------------------------------------------------------------------------- /internal/provider/node_room_member_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 8 | ) 9 | 10 | func TestAccNodeRoomMemberResource(t *testing.T) { 11 | resource.Test(t, resource.TestCase{ 12 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 13 | Steps: []resource.TestStep{ 14 | { 15 | Config: fmt.Sprintf(` 16 | resource "netdata_space" "test" { 17 | name = "TestAccSpace" 18 | } 19 | resource "netdata_room" "test" { 20 | space_id = netdata_space.test.id 21 | name = "TestAccRoom" 22 | } 23 | resource "netdata_node_room_member" "test" { 24 | room_id = netdata_room.test.id 25 | space_id = netdata_space.test.id 26 | node_names = [ 27 | "netdata-agent" 28 | ] 29 | rule { 30 | action = "INCLUDE" 31 | description = "Description" 32 | clause { 33 | label = "role" 34 | operator = "equals" 35 | value = "parent" 36 | negate = false 37 | } 38 | clause { 39 | label = "environment" 40 | operator = "equals" 41 | value = "production" 42 | negate = true 43 | } 44 | } 45 | depends_on = [ 46 | terraform_data.install_agent 47 | ] 48 | } 49 | resource "terraform_data" "install_agent" { 50 | provisioner "local-exec" { 51 | command = < docker-compose.yml < 0 { 112 | return &result, nil 113 | } 114 | 115 | return nil, ErrNotFound 116 | 117 | } 118 | 119 | func (c *Client) EnableChannelByID(spaceID, channelID string, enabled bool) error { 120 | 121 | if spaceID == "" { 122 | return ErrSpaceIDRequired 123 | } 124 | 125 | if channelID == "" { 126 | return ErrChannelIDRequired 127 | } 128 | 129 | reqBody, err := json.Marshal(map[string]bool{ 130 | "enabled": enabled, 131 | }) 132 | 133 | if err != nil { 134 | return err 135 | } 136 | 137 | req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%s/api/v2/spaces/%s/channel/%s", c.HostURL, spaceID, channelID), bytes.NewReader(reqBody)) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | _, err = c.doRequest(req) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | return nil 148 | } 149 | 150 | func (c *Client) DeleteChannelByID(spaceID, channelID string) error { 151 | 152 | if spaceID == "" { 153 | return ErrSpaceIDRequired 154 | } 155 | 156 | if channelID == "" { 157 | return ErrChannelIDRequired 158 | } 159 | 160 | req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/v2/spaces/%s/channel/%s", c.HostURL, spaceID, channelID), nil) 161 | if err != nil { 162 | return err 163 | } 164 | 165 | _, err = c.doRequest(req) 166 | if err != nil { 167 | return err 168 | } 169 | 170 | return nil 171 | } 172 | -------------------------------------------------------------------------------- /internal/provider/notification_slack_channel_resource_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 8 | ) 9 | 10 | func TestAccSlackNotificationResource(t *testing.T) { 11 | resource.Test(t, resource.TestCase{ 12 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 13 | PreCheck: func() { testAccPreCheck(t) }, 14 | Steps: []resource.TestStep{ 15 | { 16 | Config: fmt.Sprintf(` 17 | resource "netdata_room" "test" { 18 | space_id = "%s" 19 | name = "testAcc" 20 | } 21 | resource "netdata_notification_slack_channel" "test" { 22 | name = "slack" 23 | enabled = true 24 | space_id = "%s" 25 | rooms_id = [netdata_room.test.id] 26 | webhook_url = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" 27 | notifications = ["CRITICAL","WARNING","CLEAR"] 28 | repeat_notification_min = 30 29 | } 30 | `, getNonCommunitySpaceIDEnv(), getNonCommunitySpaceIDEnv()), 31 | Check: resource.ComposeAggregateTestCheckFunc( 32 | resource.TestCheckResourceAttrSet("netdata_notification_slack_channel.test", "id"), 33 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "name", "slack"), 34 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "enabled", "true"), 35 | resource.TestCheckResourceAttrSet("netdata_notification_slack_channel.test", "space_id"), 36 | resource.TestCheckResourceAttrSet("netdata_notification_slack_channel.test", "rooms_id.0"), 37 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "notifications.0", "CRITICAL"), 38 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "notifications.1", "WARNING"), 39 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "notifications.2", "CLEAR"), 40 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "repeat_notification_min", "30"), 41 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "webhook_url", "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"), 42 | ), 43 | }, 44 | { 45 | Config: fmt.Sprintf(` 46 | resource "netdata_room" "test" { 47 | space_id = "%s" 48 | name = "testAcc" 49 | } 50 | resource "netdata_notification_slack_channel" "test" { 51 | name = "slack" 52 | enabled = true 53 | space_id = "%s" 54 | rooms_id = [netdata_room.test.id] 55 | webhook_url = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" 56 | notifications = ["CLEAR","CRITICAL"] 57 | repeat_notification_min = 60 58 | } 59 | `, getNonCommunitySpaceIDEnv(), getNonCommunitySpaceIDEnv()), 60 | Check: resource.ComposeAggregateTestCheckFunc( 61 | resource.TestCheckResourceAttrSet("netdata_notification_slack_channel.test", "id"), 62 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "name", "slack"), 63 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "enabled", "true"), 64 | resource.TestCheckResourceAttrSet("netdata_notification_slack_channel.test", "space_id"), 65 | resource.TestCheckResourceAttrSet("netdata_notification_slack_channel.test", "rooms_id.0"), 66 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "notifications.0", "CLEAR"), 67 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "notifications.1", "CRITICAL"), 68 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "repeat_notification_min", "60"), 69 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "webhook_url", "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"), 70 | ), 71 | }, 72 | { 73 | Config: fmt.Sprintf(` 74 | resource "netdata_room" "test" { 75 | space_id = "%s" 76 | name = "testAcc" 77 | } 78 | resource "netdata_notification_slack_channel" "test" { 79 | name = "slack" 80 | enabled = false 81 | space_id = "%s" 82 | rooms_id = null 83 | webhook_url = "https://hooks.slack.com/services/T10000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" 84 | notifications = ["CRITICAL","WARNING","CLEAR"] 85 | } 86 | `, getNonCommunitySpaceIDEnv(), getNonCommunitySpaceIDEnv()), 87 | Check: resource.ComposeAggregateTestCheckFunc( 88 | resource.TestCheckResourceAttrSet("netdata_notification_slack_channel.test", "id"), 89 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "name", "slack"), 90 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "enabled", "false"), 91 | resource.TestCheckResourceAttrSet("netdata_notification_slack_channel.test", "space_id"), 92 | resource.TestCheckNoResourceAttr("netdata_notification_slack_channel.test", "rooms_id.0"), 93 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "notifications.0", "CRITICAL"), 94 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "notifications.1", "WARNING"), 95 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "notifications.2", "CLEAR"), 96 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "repeat_notification_min", "0"), 97 | resource.TestCheckResourceAttr("netdata_notification_slack_channel.test", "webhook_url", "https://hooks.slack.com/services/T10000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"), 98 | ), 99 | }, 100 | }, 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /internal/provider/notification_pagerduty_channel_resource_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 8 | ) 9 | 10 | func TestAccPagerdutyNotificationResource(t *testing.T) { 11 | resource.Test(t, resource.TestCase{ 12 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 13 | PreCheck: func() { testAccPreCheck(t) }, 14 | Steps: []resource.TestStep{ 15 | { 16 | Config: fmt.Sprintf(` 17 | resource "netdata_room" "test" { 18 | space_id = "%s" 19 | name = "testAcc" 20 | } 21 | resource "netdata_notification_pagerduty_channel" "test" { 22 | name = "pagerduty" 23 | enabled = true 24 | space_id = "%s" 25 | rooms_id = [netdata_room.test.id] 26 | notifications = ["CRITICAL","WARNING","CLEAR"] 27 | alert_events_url = "https://events.pagerduty.com/v2/enqueue" 28 | integration_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 29 | repeat_notification_min = 30 30 | } 31 | `, getNonCommunitySpaceIDEnv(), getNonCommunitySpaceIDEnv()), 32 | Check: resource.ComposeAggregateTestCheckFunc( 33 | resource.TestCheckResourceAttrSet("netdata_notification_pagerduty_channel.test", "id"), 34 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "name", "pagerduty"), 35 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "enabled", "true"), 36 | resource.TestCheckResourceAttrSet("netdata_notification_pagerduty_channel.test", "space_id"), 37 | resource.TestCheckResourceAttrSet("netdata_notification_pagerduty_channel.test", "rooms_id.0"), 38 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "notifications.0", "CRITICAL"), 39 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "notifications.1", "WARNING"), 40 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "notifications.2", "CLEAR"), 41 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "repeat_notification_min", "30"), 42 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "alert_events_url", "https://events.pagerduty.com/v2/enqueue"), 43 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "integration_key", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"), 44 | ), 45 | }, 46 | { 47 | Config: fmt.Sprintf(` 48 | resource "netdata_room" "test" { 49 | space_id = "%s" 50 | name = "testAcc" 51 | } 52 | resource "netdata_notification_pagerduty_channel" "test" { 53 | name = "pagerduty" 54 | enabled = true 55 | space_id = "%s" 56 | rooms_id = [netdata_room.test.id] 57 | notifications = ["CLEAR","CRITICAL"] 58 | alert_events_url = "https://events.pagerduty.com/v2/enqueue" 59 | integration_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 60 | repeat_notification_min = 60 61 | } 62 | `, getNonCommunitySpaceIDEnv(), getNonCommunitySpaceIDEnv()), 63 | Check: resource.ComposeAggregateTestCheckFunc( 64 | resource.TestCheckResourceAttrSet("netdata_notification_pagerduty_channel.test", "id"), 65 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "name", "pagerduty"), 66 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "enabled", "true"), 67 | resource.TestCheckResourceAttrSet("netdata_notification_pagerduty_channel.test", "space_id"), 68 | resource.TestCheckResourceAttrSet("netdata_notification_pagerduty_channel.test", "rooms_id.0"), 69 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "notifications.0", "CLEAR"), 70 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "notifications.1", "CRITICAL"), 71 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "repeat_notification_min", "60"), 72 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "alert_events_url", "https://events.pagerduty.com/v2/enqueue"), 73 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "integration_key", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"), 74 | ), 75 | }, 76 | { 77 | Config: fmt.Sprintf(` 78 | resource "netdata_room" "test" { 79 | space_id = "%s" 80 | name = "testAcc" 81 | } 82 | resource "netdata_notification_pagerduty_channel" "test" { 83 | name = "pagerduty" 84 | enabled = false 85 | space_id = "%s" 86 | rooms_id = null 87 | notifications = ["CRITICAL","WARNING","CLEAR"] 88 | alert_events_url = "https://events.pagerduty.com/v2/enqueue" 89 | integration_key = "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY" 90 | } 91 | `, getNonCommunitySpaceIDEnv(), getNonCommunitySpaceIDEnv()), 92 | Check: resource.ComposeAggregateTestCheckFunc( 93 | resource.TestCheckResourceAttrSet("netdata_notification_pagerduty_channel.test", "id"), 94 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "name", "pagerduty"), 95 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "enabled", "false"), 96 | resource.TestCheckResourceAttrSet("netdata_notification_pagerduty_channel.test", "space_id"), 97 | resource.TestCheckNoResourceAttr("netdata_notification_pagerduty_channel.test", "rooms_id.0"), 98 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "notifications.0", "CRITICAL"), 99 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "notifications.1", "WARNING"), 100 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "notifications.2", "CLEAR"), 101 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "repeat_notification_min", "0"), 102 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "alert_events_url", "https://events.pagerduty.com/v2/enqueue"), 103 | resource.TestCheckResourceAttr("netdata_notification_pagerduty_channel.test", "integration_key", "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY"), 104 | ), 105 | }, 106 | }, 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /internal/provider/notification_discord_channel_resource_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 8 | ) 9 | 10 | func TestAccDiscordNotificationResource(t *testing.T) { 11 | resource.Test(t, resource.TestCase{ 12 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 13 | PreCheck: func() { testAccPreCheck(t) }, 14 | Steps: []resource.TestStep{ 15 | { 16 | Config: fmt.Sprintf(` 17 | resource "netdata_room" "test" { 18 | space_id = "%s" 19 | name = "testAcc" 20 | } 21 | resource "netdata_notification_discord_channel" "test" { 22 | name = "discord" 23 | enabled = true 24 | space_id = "%s" 25 | rooms_id = [netdata_room.test.id] 26 | webhook_url = "https://discord.com/api/webhooks/0000000000000/XXXXXXXXXXXXXXXXXXXXXXXX" 27 | notifications = ["CRITICAL","WARNING","CLEAR"] 28 | channel_type = "text" 29 | repeat_notification_min = 30 30 | } 31 | `, getNonCommunitySpaceIDEnv(), getNonCommunitySpaceIDEnv()), 32 | Check: resource.ComposeAggregateTestCheckFunc( 33 | resource.TestCheckResourceAttrSet("netdata_notification_discord_channel.test", "id"), 34 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "name", "discord"), 35 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "enabled", "true"), 36 | resource.TestCheckResourceAttrSet("netdata_notification_discord_channel.test", "space_id"), 37 | resource.TestCheckResourceAttrSet("netdata_notification_discord_channel.test", "rooms_id.0"), 38 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "notifications.0", "CRITICAL"), 39 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "notifications.1", "WARNING"), 40 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "notifications.2", "CLEAR"), 41 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "repeat_notification_min", "30"), 42 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "webhook_url", "https://discord.com/api/webhooks/0000000000000/XXXXXXXXXXXXXXXXXXXXXXXX"), 43 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "channel_type", "text"), 44 | ), 45 | }, 46 | { 47 | Config: fmt.Sprintf(` 48 | resource "netdata_room" "test" { 49 | space_id = "%s" 50 | name = "testAcc" 51 | } 52 | resource "netdata_notification_discord_channel" "test" { 53 | name = "discord" 54 | enabled = true 55 | space_id = "%s" 56 | rooms_id = [netdata_room.test.id] 57 | webhook_url = "https://discord.com/api/webhooks/0000000000000/XXXXXXXXXXXXXXXXXXXXXXXX" 58 | notifications = ["CLEAR","CRITICAL"] 59 | channel_type = "text" 60 | repeat_notification_min = 60 61 | } 62 | `, getNonCommunitySpaceIDEnv(), getNonCommunitySpaceIDEnv()), 63 | Check: resource.ComposeAggregateTestCheckFunc( 64 | resource.TestCheckResourceAttrSet("netdata_notification_discord_channel.test", "id"), 65 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "name", "discord"), 66 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "enabled", "true"), 67 | resource.TestCheckResourceAttrSet("netdata_notification_discord_channel.test", "space_id"), 68 | resource.TestCheckResourceAttrSet("netdata_notification_discord_channel.test", "rooms_id.0"), 69 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "notifications.0", "CLEAR"), 70 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "notifications.1", "CRITICAL"), 71 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "repeat_notification_min", "60"), 72 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "webhook_url", "https://discord.com/api/webhooks/0000000000000/XXXXXXXXXXXXXXXXXXXXXXXX"), 73 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "channel_type", "text"), 74 | ), 75 | }, 76 | { 77 | Config: fmt.Sprintf(` 78 | resource "netdata_room" "test" { 79 | space_id = "%s" 80 | name = "testAcc" 81 | } 82 | resource "netdata_notification_discord_channel" "test" { 83 | name = "discord" 84 | enabled = false 85 | space_id = "%s" 86 | rooms_id = null 87 | webhook_url = "https://discord.com/api/webhooks/1000000000000/XXXXXXXXXXXXXXXXXXXXXXXX" 88 | notifications = ["CRITICAL","WARNING","CLEAR"] 89 | channel_type = "forum" 90 | channel_thread = "thread" 91 | } 92 | `, getNonCommunitySpaceIDEnv(), getNonCommunitySpaceIDEnv()), 93 | Check: resource.ComposeAggregateTestCheckFunc( 94 | resource.TestCheckResourceAttrSet("netdata_notification_discord_channel.test", "id"), 95 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "name", "discord"), 96 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "enabled", "false"), 97 | resource.TestCheckResourceAttrSet("netdata_notification_discord_channel.test", "space_id"), 98 | resource.TestCheckNoResourceAttr("netdata_notification_discord_channel.test", "rooms_id.0"), 99 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "notifications.0", "CRITICAL"), 100 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "notifications.1", "WARNING"), 101 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "notifications.2", "CLEAR"), 102 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "repeat_notification_min", "0"), 103 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "webhook_url", "https://discord.com/api/webhooks/1000000000000/XXXXXXXXXXXXXXXXXXXXXXXX"), 104 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "channel_type", "forum"), 105 | resource.TestCheckResourceAttr("netdata_notification_discord_channel.test", "channel_thread", "thread"), 106 | ), 107 | }, 108 | }, 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /internal/client/node_room_member.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | func (c *Client) GetRoomNodes(spaceID, roomID string) (*RoomNodes, error) { 11 | if spaceID == "" { 12 | return nil, ErrSpaceIDRequired 13 | } 14 | if roomID == "" { 15 | return nil, ErrRoomIDRequired 16 | } 17 | 18 | reqBody := []byte(`{"scope":{"nodes":[]}}`) 19 | 20 | req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v3/spaces/%s/rooms/%s/nodes", c.HostURL, spaceID, roomID), bytes.NewReader(reqBody)) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | var roomNodes RoomNodes 26 | 27 | err = c.doRequestUnmarshal(req, &roomNodes) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return &roomNodes, nil 33 | } 34 | 35 | func (c *Client) GetAllNodes(spaceID string) (*RoomNodes, error) { 36 | if spaceID == "" { 37 | return nil, ErrSpaceIDRequired 38 | } 39 | 40 | allRooms, err := c.GetRooms(spaceID) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | var allNodesRoomID string 46 | for _, room := range *allRooms { 47 | if room.Name == "All nodes" { 48 | allNodesRoomID = room.ID 49 | break 50 | } 51 | } 52 | 53 | roomNodes, err := c.GetRoomNodes(spaceID, allNodesRoomID) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return roomNodes, nil 59 | } 60 | 61 | func (c *Client) ListNodeMembershipRules(spaceID, roomID string) ([]NodeMembershipRule, error) { 62 | if spaceID == "" { 63 | return nil, ErrSpaceIDRequired 64 | } 65 | if roomID == "" { 66 | return nil, ErrRoomIDRequired 67 | } 68 | 69 | req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v3/spaces/%s/rooms/%s/node-membership-rules", c.HostURL, spaceID, roomID), nil) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | var nodeMembershipRule []NodeMembershipRule 75 | 76 | err = c.doRequestUnmarshal(req, &nodeMembershipRule) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | return nodeMembershipRule, nil 82 | } 83 | 84 | func (c *Client) GetNodeMembershipRule(spaceID, roomID, nodeMembershipID string) (*NodeMembershipRule, error) { 85 | if spaceID == "" { 86 | return nil, ErrSpaceIDRequired 87 | } 88 | if roomID == "" { 89 | return nil, ErrRoomIDRequired 90 | } 91 | if nodeMembershipID == "" { 92 | return nil, ErrNodeMembershipIDRequired 93 | } 94 | 95 | req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v3/spaces/%s/rooms/%s/node-membership-rules/%s", c.HostURL, spaceID, roomID, nodeMembershipID), nil) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | var nodeMembershipRule NodeMembershipRule 101 | 102 | err = c.doRequestUnmarshal(req, &nodeMembershipRule) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | return &nodeMembershipRule, nil 108 | } 109 | 110 | func (c *Client) CreateNodeMembershipRule(spaceID, roomID, action, description string, clauses []NodeMembershipClause) (*NodeMembershipRule, error) { 111 | if spaceID == "" { 112 | return nil, ErrSpaceIDRequired 113 | } 114 | if roomID == "" { 115 | return nil, ErrRoomIDRequired 116 | } 117 | if action == "" { 118 | return nil, ErrNodeMembershipActionRequired 119 | } 120 | 121 | reqBody, err := json.Marshal(map[string]interface{}{ 122 | "action": action, 123 | "description": description, 124 | "clauses": clauses, 125 | }) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v3/spaces/%s/rooms/%s/node-membership-rules", c.HostURL, spaceID, roomID), bytes.NewReader(reqBody)) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | var nodeMembershipRule NodeMembershipRule 136 | 137 | err = c.doRequestUnmarshal(req, &nodeMembershipRule) 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | return &nodeMembershipRule, nil 143 | } 144 | 145 | func (c *Client) UpdateNodeMembershipRule(spaceID, roomID, nodeMembershipID, action, description string, clauses []NodeMembershipClause) (*NodeMembershipRule, error) { 146 | if spaceID == "" { 147 | return nil, ErrSpaceIDRequired 148 | } 149 | if roomID == "" { 150 | return nil, ErrRoomIDRequired 151 | } 152 | if nodeMembershipID == "" { 153 | return nil, ErrNodeMembershipIDRequired 154 | } 155 | if action == "" { 156 | return nil, ErrNodeMembershipActionRequired 157 | } 158 | 159 | reqBody, err := json.Marshal(map[string]interface{}{ 160 | "action": action, 161 | "description": description, 162 | "clauses": clauses, 163 | }) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v3/spaces/%s/rooms/%s/node-membership-rules/%s", c.HostURL, spaceID, roomID, nodeMembershipID), bytes.NewReader(reqBody)) 169 | if err != nil { 170 | return nil, err 171 | } 172 | 173 | var nodeMembershipRule NodeMembershipRule 174 | 175 | err = c.doRequestUnmarshal(req, &nodeMembershipRule) 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | return &nodeMembershipRule, nil 181 | } 182 | 183 | func (c *Client) DeleteNodeMembershipRule(spaceID, roomID, nodeMembershipID string) error { 184 | if spaceID == "" { 185 | return ErrSpaceIDRequired 186 | } 187 | if roomID == "" { 188 | return ErrRoomIDRequired 189 | } 190 | if nodeMembershipID == "" { 191 | return ErrNodeMembershipIDRequired 192 | } 193 | 194 | req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/v3/spaces/%s/rooms/%s/node-membership-rules/%s", c.HostURL, spaceID, roomID, nodeMembershipID), nil) 195 | if err != nil { 196 | return err 197 | } 198 | 199 | _, err = c.doRequest(req) 200 | if err != nil { 201 | return err 202 | } 203 | 204 | return nil 205 | } 206 | 207 | func (c *Client) CreateNodeRoomMember(spaceID, roomID, nodeID string) error { 208 | if spaceID == "" { 209 | return ErrSpaceIDRequired 210 | } 211 | if roomID == "" { 212 | return ErrRoomIDRequired 213 | } 214 | if nodeID == "" { 215 | return ErrNodeID 216 | } 217 | 218 | reqBody, err := json.Marshal([]string{nodeID}) 219 | if err != nil { 220 | return err 221 | } 222 | 223 | req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/spaces/%s/rooms/%s/claimed-nodes", c.HostURL, spaceID, roomID), bytes.NewReader(reqBody)) 224 | if err != nil { 225 | return err 226 | } 227 | 228 | _, err = c.doRequest(req) 229 | if err != nil { 230 | return err 231 | } 232 | 233 | return nil 234 | 235 | } 236 | 237 | func (c *Client) DeleteNodeRoomMember(spaceID, roomID, nodeID string) error { 238 | if spaceID == "" { 239 | return ErrSpaceIDRequired 240 | } 241 | if roomID == "" { 242 | return ErrRoomIDRequired 243 | } 244 | if nodeID == "" { 245 | return ErrNodeID 246 | } 247 | 248 | req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/v1/spaces/%s/rooms/%s/claimed-nodes?node_ids=%s", c.HostURL, spaceID, roomID, nodeID), nil) 249 | if err != nil { 250 | return err 251 | } 252 | 253 | _, err = c.doRequest(req) 254 | if err != nil { 255 | return err 256 | } 257 | 258 | return nil 259 | } 260 | -------------------------------------------------------------------------------- /internal/provider/room_resource.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/hashicorp/terraform-plugin-framework/path" 10 | "github.com/hashicorp/terraform-plugin-framework/resource" 11 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 12 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 13 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" 14 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 15 | "github.com/hashicorp/terraform-plugin-framework/types" 16 | "github.com/netdata/terraform-provider-netdata/internal/client" 17 | ) 18 | 19 | var ( 20 | _ resource.Resource = &roomResource{} 21 | _ resource.ResourceWithConfigure = &roomResource{} 22 | ) 23 | 24 | func NewRoomResource() resource.Resource { 25 | return &roomResource{} 26 | } 27 | 28 | type roomResource struct { 29 | client *client.Client 30 | } 31 | 32 | type roomResourceModel struct { 33 | ID types.String `tfsdk:"id"` 34 | SpaceID types.String `tfsdk:"space_id"` 35 | Name types.String `tfsdk:"name"` 36 | Description types.String `tfsdk:"description"` 37 | } 38 | 39 | func (s *roomResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 40 | resp.TypeName = req.ProviderTypeName + "_room" 41 | } 42 | 43 | func (s *roomResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { 44 | resp.Schema = schema.Schema{ 45 | Description: "Provides a Netdata Cloud Room resource. Use this resource to manage rooms in the selected space.", 46 | Attributes: map[string]schema.Attribute{ 47 | "id": schema.StringAttribute{ 48 | Description: "The ID of the room", 49 | Computed: true, 50 | PlanModifiers: []planmodifier.String{ 51 | stringplanmodifier.UseStateForUnknown(), 52 | }, 53 | }, 54 | "space_id": schema.StringAttribute{ 55 | Description: "The ID of the space", 56 | Required: true, 57 | PlanModifiers: []planmodifier.String{ 58 | stringplanmodifier.RequiresReplace(), 59 | }, 60 | }, 61 | "name": schema.StringAttribute{ 62 | Description: "The name of the room", 63 | Required: true, 64 | }, 65 | "description": schema.StringAttribute{ 66 | Description: "The description of the room", 67 | Optional: true, 68 | Computed: true, 69 | Default: stringdefault.StaticString(""), 70 | }, 71 | }, 72 | } 73 | } 74 | 75 | func (s *roomResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 76 | if req.ProviderData == nil { 77 | return 78 | } 79 | 80 | client, ok := req.ProviderData.(*client.Client) 81 | if !ok { 82 | resp.Diagnostics.AddError( 83 | "Unexpected Resource Configure Type", 84 | fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), 85 | ) 86 | 87 | return 88 | } 89 | 90 | s.client = client 91 | } 92 | 93 | func (s *roomResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 94 | var plan roomResourceModel 95 | 96 | diags := req.Plan.Get(ctx, &plan) 97 | resp.Diagnostics.Append(diags...) 98 | if resp.Diagnostics.HasError() { 99 | return 100 | } 101 | 102 | if resp.Diagnostics.HasError() { 103 | return 104 | } 105 | 106 | roomInfo, err := s.client.CreateRoom(plan.SpaceID.ValueString(), plan.Name.ValueString(), plan.Description.ValueString()) 107 | if err != nil { 108 | resp.Diagnostics.AddError( 109 | "Error Creating Room", 110 | "err: "+err.Error(), 111 | ) 112 | return 113 | } 114 | 115 | plan.ID = types.StringValue(roomInfo.ID) 116 | plan.Name = types.StringValue(roomInfo.Name) 117 | plan.Description = types.StringValue(roomInfo.Description) 118 | 119 | diags = resp.State.Set(ctx, plan) 120 | resp.Diagnostics.Append(diags...) 121 | if resp.Diagnostics.HasError() { 122 | return 123 | } 124 | } 125 | 126 | func (s *roomResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 127 | var state roomResourceModel 128 | diags := req.State.Get(ctx, &state) 129 | resp.Diagnostics.Append(diags...) 130 | if resp.Diagnostics.HasError() { 131 | return 132 | } 133 | 134 | roomInfo, err := s.client.GetRoomByID(state.ID.ValueString(), state.SpaceID.ValueString()) 135 | if err != nil { 136 | if errors.Is(err, client.ErrNotFound) { 137 | resp.State.RemoveResource(ctx) 138 | return 139 | } 140 | resp.Diagnostics.AddError( 141 | "Error Getting Room", 142 | "Could Not Read Room ID: "+state.ID.ValueString()+": err: "+err.Error(), 143 | ) 144 | return 145 | } 146 | 147 | state.ID = types.StringValue(roomInfo.ID) 148 | state.Name = types.StringValue(roomInfo.Name) 149 | state.Description = types.StringValue(roomInfo.Description) 150 | diags = resp.State.Set(ctx, &state) 151 | resp.Diagnostics.Append(diags...) 152 | if resp.Diagnostics.HasError() { 153 | return 154 | } 155 | } 156 | 157 | func (s *roomResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 158 | var plan roomResourceModel 159 | diags := req.Plan.Get(ctx, &plan) 160 | resp.Diagnostics.Append(diags...) 161 | if resp.Diagnostics.HasError() { 162 | return 163 | } 164 | 165 | err := s.client.UpdateRoomByID(plan.ID.ValueString(), plan.SpaceID.ValueString(), plan.Name.ValueString(), plan.Description.ValueString()) 166 | if err != nil { 167 | resp.Diagnostics.AddError( 168 | "Error Updating room", 169 | "Could Not Update Room ID: "+plan.ID.ValueString()+": err: "+err.Error(), 170 | ) 171 | return 172 | } 173 | 174 | roomInfo, err := s.client.GetRoomByID(plan.ID.ValueString(), plan.SpaceID.ValueString()) 175 | if err != nil { 176 | resp.Diagnostics.AddError( 177 | "Error Getting Room", 178 | "Could Not Read Room ID: "+plan.ID.ValueString()+": err: "+err.Error(), 179 | ) 180 | return 181 | } 182 | 183 | plan.ID = types.StringValue(roomInfo.ID) 184 | plan.Name = types.StringValue(roomInfo.Name) 185 | plan.Description = types.StringValue(roomInfo.Description) 186 | 187 | diags = resp.State.Set(ctx, plan) 188 | resp.Diagnostics.Append(diags...) 189 | if resp.Diagnostics.HasError() { 190 | return 191 | } 192 | 193 | } 194 | 195 | func (s *roomResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 196 | var state roomResourceModel 197 | diags := req.State.Get(ctx, &state) 198 | resp.Diagnostics.Append(diags...) 199 | if resp.Diagnostics.HasError() { 200 | return 201 | } 202 | 203 | err := s.client.DeleteRoomByID(state.ID.ValueString(), state.SpaceID.ValueString()) 204 | if err != nil { 205 | resp.Diagnostics.AddError( 206 | "Error Deleting Room", 207 | "Could Not Delete Room ID: "+state.ID.ValueString()+": err: "+err.Error(), 208 | ) 209 | return 210 | } 211 | } 212 | 213 | func (s *roomResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 214 | idParts := strings.Split(req.ID, ",") 215 | 216 | if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { 217 | resp.Diagnostics.AddError( 218 | "Unexpected Import Identifier", 219 | fmt.Sprintf("Expected import identifier with format: space_id,id. Got: %q", req.ID), 220 | ) 221 | return 222 | } 223 | 224 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("space_id"), idParts[0])...) 225 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), idParts[1])...) 226 | } 227 | -------------------------------------------------------------------------------- /internal/provider/room_member_resource.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/hashicorp/terraform-plugin-framework/path" 10 | "github.com/hashicorp/terraform-plugin-framework/resource" 11 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 12 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 13 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 14 | "github.com/hashicorp/terraform-plugin-framework/types" 15 | "github.com/hashicorp/terraform-plugin-log/tflog" 16 | "github.com/netdata/terraform-provider-netdata/internal/client" 17 | ) 18 | 19 | var ( 20 | _ resource.Resource = &roomMemberResource{} 21 | _ resource.ResourceWithConfigure = &roomMemberResource{} 22 | ) 23 | 24 | func NewRoomMemberResource() resource.Resource { 25 | return &roomMemberResource{} 26 | } 27 | 28 | type roomMemberResource struct { 29 | client *client.Client 30 | } 31 | 32 | type roomMemberResourceModel struct { 33 | RoomID types.String `tfsdk:"room_id"` 34 | SpaceID types.String `tfsdk:"space_id"` 35 | SpaceMemberID types.String `tfsdk:"space_member_id"` 36 | } 37 | 38 | func (s *roomMemberResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 39 | resp.TypeName = req.ProviderTypeName + "_room_member" 40 | } 41 | 42 | func (s *roomMemberResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { 43 | resp.Schema = schema.Schema{ 44 | Description: "Provides a Netdata Cloud Room Member resource. Use this resource to manage user membership to the room in the selected space. It is referring to the user created at the space level.", 45 | Attributes: map[string]schema.Attribute{ 46 | "room_id": schema.StringAttribute{ 47 | Description: "The Room ID of the space", 48 | Required: true, 49 | PlanModifiers: []planmodifier.String{ 50 | stringplanmodifier.RequiresReplace(), 51 | }, 52 | }, 53 | "space_id": schema.StringAttribute{ 54 | Description: "Space ID of the member", 55 | Required: true, 56 | PlanModifiers: []planmodifier.String{ 57 | stringplanmodifier.RequiresReplace(), 58 | }, 59 | }, 60 | "space_member_id": schema.StringAttribute{ 61 | Description: "The Space Member ID of the space", 62 | Required: true, 63 | PlanModifiers: []planmodifier.String{ 64 | stringplanmodifier.RequiresReplace(), 65 | }, 66 | }, 67 | }, 68 | } 69 | } 70 | 71 | func (s *roomMemberResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 72 | if req.ProviderData == nil { 73 | return 74 | } 75 | 76 | client, ok := req.ProviderData.(*client.Client) 77 | if !ok { 78 | resp.Diagnostics.AddError( 79 | "Unexpected Resource Configure Type", 80 | fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), 81 | ) 82 | 83 | return 84 | } 85 | 86 | s.client = client 87 | } 88 | 89 | func (s *roomMemberResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 90 | var plan roomMemberResourceModel 91 | 92 | diags := req.Plan.Get(ctx, &plan) 93 | resp.Diagnostics.Append(diags...) 94 | if resp.Diagnostics.HasError() { 95 | return 96 | } 97 | 98 | tflog.Info(ctx, fmt.Sprintf("Creating room member for space_id/room_id/space_member_id: %s/%s/%s", plan.SpaceID.ValueString(), plan.RoomID.ValueString(), plan.SpaceMemberID.ValueString())) 99 | 100 | err := s.client.CreateRoomMember(plan.SpaceID.ValueString(), plan.RoomID.ValueString(), plan.SpaceMemberID.ValueString()) 101 | if err != nil { 102 | resp.Diagnostics.AddError( 103 | "Error Creating Room Member", 104 | "err: "+err.Error(), 105 | ) 106 | return 107 | } 108 | 109 | plan.RoomID = types.StringValue(plan.RoomID.ValueString()) 110 | plan.SpaceID = types.StringValue(plan.SpaceID.ValueString()) 111 | plan.SpaceMemberID = types.StringValue(plan.SpaceMemberID.ValueString()) 112 | 113 | diags = resp.State.Set(ctx, plan) 114 | resp.Diagnostics.Append(diags...) 115 | if resp.Diagnostics.HasError() { 116 | return 117 | } 118 | } 119 | 120 | func (s *roomMemberResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 121 | var state roomMemberResourceModel 122 | 123 | diags := req.State.Get(ctx, &state) 124 | resp.Diagnostics.Append(diags...) 125 | if resp.Diagnostics.HasError() { 126 | return 127 | } 128 | 129 | roomMemberInfo, err := s.client.GetRoomMemberID(state.SpaceID.ValueString(), state.RoomID.ValueString(), state.SpaceMemberID.ValueString()) 130 | if err != nil { 131 | if errors.Is(err, client.ErrNotFound) { 132 | resp.State.RemoveResource(ctx) 133 | return 134 | } 135 | resp.Diagnostics.AddError( 136 | "Error Getting Room Member", 137 | fmt.Sprintf("Could not read room member for space_id/room_id/space_member_id: %s/%s/%s err: %v", state.SpaceID.ValueString(), state.RoomID.ValueString(), state.SpaceMemberID.ValueString(), err.Error()), 138 | ) 139 | return 140 | } 141 | 142 | state.SpaceMemberID = types.StringValue(roomMemberInfo.SpaceMemberID) 143 | diags = resp.State.Set(ctx, &state) 144 | resp.Diagnostics.Append(diags...) 145 | if resp.Diagnostics.HasError() { 146 | return 147 | } 148 | } 149 | 150 | func (s *roomMemberResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 151 | var plan roomMemberResourceModel 152 | 153 | diags := req.Plan.Get(ctx, &plan) 154 | resp.Diagnostics.Append(diags...) 155 | if resp.Diagnostics.HasError() { 156 | return 157 | } 158 | 159 | roomMemberInfo, err := s.client.GetRoomMemberID(plan.SpaceID.ValueString(), plan.RoomID.ValueString(), plan.SpaceMemberID.ValueString()) 160 | if err != nil { 161 | resp.Diagnostics.AddError( 162 | "Error Getting Room Member", 163 | fmt.Sprintf("Could not read room member for space_id/room_id/space_member_id: %s/%s/%s err: %v", plan.SpaceID.ValueString(), plan.RoomID.ValueString(), plan.SpaceMemberID.ValueString(), err.Error()), 164 | ) 165 | return 166 | } 167 | 168 | plan.SpaceMemberID = types.StringValue(roomMemberInfo.SpaceMemberID) 169 | 170 | diags = resp.State.Set(ctx, plan) 171 | resp.Diagnostics.Append(diags...) 172 | if resp.Diagnostics.HasError() { 173 | return 174 | } 175 | 176 | } 177 | 178 | func (s *roomMemberResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 179 | var state roomMemberResourceModel 180 | 181 | diags := req.State.Get(ctx, &state) 182 | resp.Diagnostics.Append(diags...) 183 | if resp.Diagnostics.HasError() { 184 | return 185 | } 186 | 187 | err := s.client.DeleteRoomMember(state.SpaceID.ValueString(), state.RoomID.ValueString(), state.SpaceMemberID.ValueString()) 188 | if err != nil { 189 | resp.Diagnostics.AddError( 190 | "Error Deleting Room Member", 191 | fmt.Sprintf("Could not delete room member for space_id/room_id: %s/%s/%s err: %v", state.SpaceID.ValueString(), state.RoomID.ValueString(), state.SpaceMemberID.ValueString(), err.Error()), 192 | ) 193 | return 194 | } 195 | } 196 | 197 | func (s *roomMemberResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 198 | idParts := strings.Split(req.ID, ",") 199 | 200 | if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { 201 | resp.Diagnostics.AddError( 202 | "Unexpected Import Identifier", 203 | fmt.Sprintf("Expected import identifier with format: space_id,room_id,space_member_id. Got: %q", req.ID), 204 | ) 205 | return 206 | } 207 | 208 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("space_id"), idParts[0])...) 209 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("room_id"), idParts[1])...) 210 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("space_member_id"), idParts[2])...) 211 | } 212 | -------------------------------------------------------------------------------- /internal/provider/space_resource.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" 9 | "github.com/hashicorp/terraform-plugin-framework/path" 10 | "github.com/hashicorp/terraform-plugin-framework/resource" 11 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 12 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 13 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" 14 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 15 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 16 | "github.com/hashicorp/terraform-plugin-framework/types" 17 | "github.com/hashicorp/terraform-plugin-log/tflog" 18 | "github.com/netdata/terraform-provider-netdata/internal/client" 19 | ) 20 | 21 | var ( 22 | _ resource.Resource = &spaceResource{} 23 | _ resource.ResourceWithConfigure = &spaceResource{} 24 | ) 25 | 26 | func NewSpaceResource() resource.Resource { 27 | return &spaceResource{} 28 | } 29 | 30 | type spaceResource struct { 31 | client *client.Client 32 | } 33 | 34 | type spaceResourceModel struct { 35 | ID types.String `tfsdk:"id"` 36 | Name types.String `tfsdk:"name"` 37 | Description types.String `tfsdk:"description"` 38 | ClaimToken types.String `tfsdk:"claim_token"` 39 | } 40 | 41 | func (s *spaceResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 42 | resp.TypeName = req.ProviderTypeName + "_space" 43 | } 44 | 45 | func (s *spaceResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { 46 | resp.Schema = schema.Schema{ 47 | Description: "Provides a Netdata Cloud Space resource. Use this resource to manage spaces.", 48 | Attributes: map[string]schema.Attribute{ 49 | "id": schema.StringAttribute{ 50 | Description: "The ID of the space", 51 | Computed: true, 52 | PlanModifiers: []planmodifier.String{ 53 | stringplanmodifier.UseStateForUnknown(), 54 | }, 55 | }, 56 | "name": schema.StringAttribute{ 57 | Description: "The name of the space", 58 | Required: true, 59 | Validators: []validator.String{ 60 | stringvalidator.LengthAtLeast(5), 61 | }, 62 | }, 63 | "description": schema.StringAttribute{ 64 | Description: "The description of the space", 65 | Optional: true, 66 | Computed: true, 67 | Default: stringdefault.StaticString(""), 68 | }, 69 | "claim_token": schema.StringAttribute{ 70 | Description: "The claim token of the space", 71 | Computed: true, 72 | PlanModifiers: []planmodifier.String{ 73 | stringplanmodifier.UseStateForUnknown(), 74 | }, 75 | }, 76 | }, 77 | } 78 | } 79 | 80 | func (s *spaceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 81 | if req.ProviderData == nil { 82 | return 83 | } 84 | 85 | client, ok := req.ProviderData.(*client.Client) 86 | if !ok { 87 | resp.Diagnostics.AddError( 88 | "Unexpected Resource Configure Type", 89 | fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), 90 | ) 91 | 92 | return 93 | } 94 | 95 | s.client = client 96 | } 97 | 98 | func (s *spaceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 99 | var plan spaceResourceModel 100 | 101 | diags := req.Plan.Get(ctx, &plan) 102 | resp.Diagnostics.Append(diags...) 103 | if resp.Diagnostics.HasError() { 104 | return 105 | } 106 | 107 | if resp.Diagnostics.HasError() { 108 | return 109 | } 110 | 111 | tflog.Info(ctx, "Creating space: "+plan.Name.ValueString()) 112 | 113 | spaceInfo, err := s.client.CreateSpace(plan.Name.ValueString(), plan.Description.ValueString()) 114 | if err != nil { 115 | resp.Diagnostics.AddError( 116 | "Error Creating Space", 117 | "err: "+err.Error(), 118 | ) 119 | return 120 | } 121 | 122 | plan.ID = types.StringValue(spaceInfo.ID) 123 | plan.Name = types.StringValue(spaceInfo.Name) 124 | plan.Description = types.StringValue(spaceInfo.Description) 125 | 126 | tflog.Info(ctx, "Creating Claim Token for Space ID: "+spaceInfo.ID) 127 | 128 | claimToken, err := s.client.GetSpaceClaimToken(spaceInfo.ID) 129 | if err != nil { 130 | resp.Diagnostics.AddError( 131 | "Error Creating Claim Token", 132 | "Could Not Create Claim Token for Space ID: "+spaceInfo.ID+": err: "+err.Error(), 133 | ) 134 | return 135 | } 136 | 137 | plan.ClaimToken = types.StringValue(*claimToken) 138 | 139 | diags = resp.State.Set(ctx, plan) 140 | resp.Diagnostics.Append(diags...) 141 | if resp.Diagnostics.HasError() { 142 | return 143 | } 144 | } 145 | 146 | func (s *spaceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 147 | var state spaceResourceModel 148 | diags := req.State.Get(ctx, &state) 149 | resp.Diagnostics.Append(diags...) 150 | if resp.Diagnostics.HasError() { 151 | return 152 | } 153 | 154 | spaceInfo, err := s.client.GetSpaceByID(state.ID.ValueString()) 155 | if err != nil { 156 | if errors.Is(err, client.ErrNotFound) { 157 | resp.State.RemoveResource(ctx) 158 | return 159 | } 160 | resp.Diagnostics.AddError( 161 | "Error Getting Space", 162 | "Could Not Read Space ID: "+state.ID.ValueString()+": err: "+err.Error(), 163 | ) 164 | return 165 | } 166 | 167 | if state.ClaimToken.IsNull() { 168 | tflog.Info(ctx, "Creating Claim Token for Space ID: "+spaceInfo.ID) 169 | claimToken, err := s.client.GetSpaceClaimToken(spaceInfo.ID) 170 | if err != nil { 171 | resp.Diagnostics.AddError( 172 | "Error Creating Claim Token", 173 | "Could Not Create Claim Token for Space ID: "+spaceInfo.ID+": err: "+err.Error(), 174 | ) 175 | return 176 | } 177 | state.ClaimToken = types.StringValue(*claimToken) 178 | } 179 | 180 | state.Name = types.StringValue(spaceInfo.Name) 181 | state.Description = types.StringValue(spaceInfo.Description) 182 | diags = resp.State.Set(ctx, &state) 183 | resp.Diagnostics.Append(diags...) 184 | if resp.Diagnostics.HasError() { 185 | return 186 | } 187 | } 188 | 189 | func (s *spaceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 190 | var plan spaceResourceModel 191 | diags := req.Plan.Get(ctx, &plan) 192 | resp.Diagnostics.Append(diags...) 193 | if resp.Diagnostics.HasError() { 194 | return 195 | } 196 | 197 | err := s.client.UpdateSpaceByID(plan.ID.ValueString(), plan.Name.ValueString(), plan.Description.ValueString()) 198 | if err != nil { 199 | resp.Diagnostics.AddError( 200 | "Error Updating Space", 201 | "Could Not Update Space ID: "+plan.ID.ValueString()+": err: "+err.Error(), 202 | ) 203 | return 204 | } 205 | 206 | spaceInfo, err := s.client.GetSpaceByID(plan.ID.ValueString()) 207 | if err != nil { 208 | resp.Diagnostics.AddError( 209 | "Error Getting Space", 210 | "Could Not Read Space ID: "+plan.ID.ValueString()+": err: "+err.Error(), 211 | ) 212 | return 213 | } 214 | 215 | plan.ID = types.StringValue(spaceInfo.ID) 216 | plan.Name = types.StringValue(spaceInfo.Name) 217 | plan.Description = types.StringValue(spaceInfo.Description) 218 | 219 | diags = resp.State.Set(ctx, plan) 220 | resp.Diagnostics.Append(diags...) 221 | if resp.Diagnostics.HasError() { 222 | return 223 | } 224 | 225 | } 226 | 227 | func (s *spaceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 228 | var state spaceResourceModel 229 | diags := req.State.Get(ctx, &state) 230 | resp.Diagnostics.Append(diags...) 231 | if resp.Diagnostics.HasError() { 232 | return 233 | } 234 | 235 | err := s.client.DeleteSpaceByID(state.ID.ValueString()) 236 | if err != nil { 237 | resp.Diagnostics.AddError( 238 | "Error Deleting Space", 239 | "Could Not Delete Space ID: "+state.ID.ValueString()+": err: "+err.Error(), 240 | ) 241 | return 242 | } 243 | } 244 | 245 | func (s *spaceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 246 | resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) 247 | } 248 | -------------------------------------------------------------------------------- /internal/provider/space_member_resource.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" 11 | "github.com/hashicorp/terraform-plugin-framework/path" 12 | "github.com/hashicorp/terraform-plugin-framework/resource" 13 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 14 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 15 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 16 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 17 | "github.com/hashicorp/terraform-plugin-framework/types" 18 | "github.com/hashicorp/terraform-plugin-log/tflog" 19 | "github.com/netdata/terraform-provider-netdata/internal/client" 20 | ) 21 | 22 | var ( 23 | _ resource.Resource = &spaceMemberResource{} 24 | _ resource.ResourceWithConfigure = &spaceMemberResource{} 25 | ) 26 | 27 | func NewSpaceMemberResource() resource.Resource { 28 | return &spaceMemberResource{} 29 | } 30 | 31 | type spaceMemberResource struct { 32 | client *client.Client 33 | } 34 | 35 | type spaceMemberResourceModel struct { 36 | ID types.String `tfsdk:"id"` 37 | Email types.String `tfsdk:"email"` 38 | Role types.String `tfsdk:"role"` 39 | SpaceID types.String `tfsdk:"space_id"` 40 | } 41 | 42 | func (s *spaceMemberResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 43 | resp.TypeName = req.ProviderTypeName + "_space_member" 44 | } 45 | 46 | func (s *spaceMemberResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { 47 | resp.Schema = schema.Schema{ 48 | Description: "Provides a Netdata Cloud Space Member resource. Use this resource to manage user membership to the space.", 49 | Attributes: map[string]schema.Attribute{ 50 | "id": schema.StringAttribute{ 51 | Description: "The Member ID of the space", 52 | Computed: true, 53 | PlanModifiers: []planmodifier.String{ 54 | stringplanmodifier.UseStateForUnknown(), 55 | }, 56 | }, 57 | "email": schema.StringAttribute{ 58 | Description: "Email of the member", 59 | Required: true, 60 | Validators: []validator.String{ 61 | stringvalidator.RegexMatches( 62 | regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`), 63 | "Invalid email format", 64 | ), 65 | }, 66 | PlanModifiers: []planmodifier.String{ 67 | stringplanmodifier.RequiresReplace(), 68 | }, 69 | }, 70 | "role": schema.StringAttribute{ 71 | Description: "Role of the member. The community plan can only set the role to `admin`", 72 | Required: true, 73 | Validators: []validator.String{ 74 | stringvalidator.RegexMatches( 75 | regexp.MustCompile(`^[a-z0-9]+$`), 76 | "Role should be lowercase", 77 | ), 78 | }, 79 | }, 80 | "space_id": schema.StringAttribute{ 81 | Description: "Space ID of the member", 82 | Required: true, 83 | PlanModifiers: []planmodifier.String{ 84 | stringplanmodifier.RequiresReplace(), 85 | }, 86 | }, 87 | }, 88 | } 89 | } 90 | 91 | func (s *spaceMemberResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 92 | if req.ProviderData == nil { 93 | return 94 | } 95 | 96 | client, ok := req.ProviderData.(*client.Client) 97 | if !ok { 98 | resp.Diagnostics.AddError( 99 | "Unexpected Resource Configure Type", 100 | fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), 101 | ) 102 | 103 | return 104 | } 105 | 106 | s.client = client 107 | } 108 | 109 | func (s *spaceMemberResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 110 | var plan spaceMemberResourceModel 111 | 112 | diags := req.Plan.Get(ctx, &plan) 113 | resp.Diagnostics.Append(diags...) 114 | if resp.Diagnostics.HasError() { 115 | return 116 | } 117 | 118 | tflog.Info(ctx, "Creating space member for email: "+plan.Email.ValueString()) 119 | 120 | spaceMemberInfo, err := s.client.CreateSpaceMember(plan.SpaceID.ValueString(), plan.Email.ValueString(), plan.Role.ValueString()) 121 | if err != nil { 122 | resp.Diagnostics.AddError( 123 | "Error Creating Space Member", 124 | "err: "+err.Error(), 125 | ) 126 | return 127 | } 128 | 129 | plan.ID = types.StringValue(spaceMemberInfo.MemberID) 130 | plan.Email = types.StringValue(plan.Email.ValueString()) 131 | plan.Role = types.StringValue(spaceMemberInfo.Role) 132 | plan.SpaceID = types.StringValue(plan.SpaceID.ValueString()) 133 | 134 | diags = resp.State.Set(ctx, plan) 135 | resp.Diagnostics.Append(diags...) 136 | if resp.Diagnostics.HasError() { 137 | return 138 | } 139 | } 140 | 141 | func (s *spaceMemberResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 142 | var state spaceMemberResourceModel 143 | 144 | diags := req.State.Get(ctx, &state) 145 | resp.Diagnostics.Append(diags...) 146 | if resp.Diagnostics.HasError() { 147 | return 148 | } 149 | 150 | tflog.Info(ctx, "Reading space member for space_id/id: "+state.Email.ValueString()+"/"+state.ID.ValueString()) 151 | 152 | spaceMemberInfo, err := s.client.GetSpaceMemberID(state.SpaceID.ValueString(), state.ID.ValueString()) 153 | if err != nil { 154 | if errors.Is(err, client.ErrNotFound) { 155 | resp.State.RemoveResource(ctx) 156 | return 157 | } 158 | resp.Diagnostics.AddError( 159 | "Error Getting Space Member", 160 | fmt.Sprintf("Could not read space member for space_id/space_member_id: %s/%s err: %v", state.SpaceID.ValueString(), state.ID.ValueString(), err.Error()), 161 | ) 162 | return 163 | } 164 | 165 | state.ID = types.StringValue(spaceMemberInfo.MemberID) 166 | state.Email = types.StringValue(spaceMemberInfo.Email) 167 | state.Role = types.StringValue(spaceMemberInfo.Role) 168 | diags = resp.State.Set(ctx, &state) 169 | resp.Diagnostics.Append(diags...) 170 | if resp.Diagnostics.HasError() { 171 | return 172 | } 173 | } 174 | 175 | func (s *spaceMemberResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 176 | var plan spaceMemberResourceModel 177 | 178 | diags := req.Plan.Get(ctx, &plan) 179 | resp.Diagnostics.Append(diags...) 180 | if resp.Diagnostics.HasError() { 181 | return 182 | } 183 | 184 | err := s.client.UpdateSpaceMemberRoleByID(plan.SpaceID.ValueString(), plan.ID.ValueString(), plan.Role.ValueString()) 185 | if err != nil { 186 | resp.Diagnostics.AddError( 187 | "Error Updating Space Member Role", 188 | fmt.Sprintf("Could not update space member for space_id/space_member_id: %s/%s err: %v", plan.SpaceID.ValueString(), plan.ID.ValueString(), err.Error()), 189 | ) 190 | return 191 | } 192 | 193 | spaceMemberInfo, err := s.client.GetSpaceMemberID(plan.SpaceID.ValueString(), plan.ID.ValueString()) 194 | if err != nil { 195 | resp.Diagnostics.AddError( 196 | "Error Getting Space Member", 197 | fmt.Sprintf("Could not read space member for space_id/space_member_id: %s/%s err: %v", plan.SpaceID.ValueString(), plan.ID.ValueString(), err.Error()), 198 | ) 199 | return 200 | } 201 | 202 | plan.ID = types.StringValue(spaceMemberInfo.MemberID) 203 | plan.Email = types.StringValue(spaceMemberInfo.Email) 204 | plan.Role = types.StringValue(spaceMemberInfo.Role) 205 | 206 | diags = resp.State.Set(ctx, plan) 207 | resp.Diagnostics.Append(diags...) 208 | if resp.Diagnostics.HasError() { 209 | return 210 | } 211 | 212 | } 213 | 214 | func (s *spaceMemberResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 215 | var state spaceMemberResourceModel 216 | 217 | diags := req.State.Get(ctx, &state) 218 | resp.Diagnostics.Append(diags...) 219 | if resp.Diagnostics.HasError() { 220 | return 221 | } 222 | 223 | err := s.client.DeleteSpaceMember(state.SpaceID.ValueString(), state.ID.ValueString()) 224 | if err != nil { 225 | resp.Diagnostics.AddError( 226 | "Error Deleting Space Member", 227 | fmt.Sprintf("Could not delete space member for space_id/space_member_id: %s/%s err: %v", state.SpaceID.ValueString(), state.ID.ValueString(), err.Error()), 228 | ) 229 | return 230 | } 231 | } 232 | 233 | func (s *spaceMemberResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 234 | idParts := strings.Split(req.ID, ",") 235 | 236 | if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { 237 | resp.Diagnostics.AddError( 238 | "Unexpected Import Identifier", 239 | fmt.Sprintf("Expected import identifier with format: space_id,id. Got: %q", req.ID), 240 | ) 241 | return 242 | } 243 | 244 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("space_id"), idParts[0])...) 245 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), idParts[1])...) 246 | } 247 | -------------------------------------------------------------------------------- /internal/provider/notification_slack_channel_resource.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/hashicorp/terraform-plugin-framework/path" 11 | "github.com/hashicorp/terraform-plugin-framework/resource" 12 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 13 | "github.com/hashicorp/terraform-plugin-framework/types" 14 | "github.com/netdata/terraform-provider-netdata/internal/client" 15 | ) 16 | 17 | var ( 18 | _ resource.Resource = &slackChannelResource{} 19 | _ resource.ResourceWithConfigure = &slackChannelResource{} 20 | ) 21 | 22 | func NewSlackChannelResource() resource.Resource { 23 | return &slackChannelResource{} 24 | } 25 | 26 | type slackChannelResource struct { 27 | client *client.Client 28 | } 29 | 30 | type slackChannelResourceModel struct { 31 | ID types.String `tfsdk:"id"` 32 | Name types.String `tfsdk:"name"` 33 | Enabled types.Bool `tfsdk:"enabled"` 34 | SpaceID types.String `tfsdk:"space_id"` 35 | RoomsID types.List `tfsdk:"rooms_id"` 36 | NotificationOptions types.List `tfsdk:"notifications"` 37 | RepeatNotificationMinute types.Int64 `tfsdk:"repeat_notification_min"` 38 | WebhookURL types.String `tfsdk:"webhook_url"` 39 | } 40 | 41 | func (s *slackChannelResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 42 | resp.TypeName = req.ProviderTypeName + "_notification_slack_channel" 43 | } 44 | 45 | func (s *slackChannelResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { 46 | fullSchema := commonNotificationSchema("Slack") 47 | fullSchema.Attributes["webhook_url"] = schema.StringAttribute{ 48 | Description: "Slack webhook URL", 49 | Required: true, 50 | Sensitive: true, 51 | } 52 | resp.Schema = fullSchema 53 | } 54 | 55 | func (s *slackChannelResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 56 | if req.ProviderData == nil { 57 | return 58 | } 59 | 60 | client, ok := req.ProviderData.(*client.Client) 61 | if !ok { 62 | resp.Diagnostics.AddError( 63 | "Unexpected Resource Configure Type", 64 | fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), 65 | ) 66 | 67 | return 68 | } 69 | 70 | s.client = client 71 | } 72 | 73 | func (s *slackChannelResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 74 | var plan slackChannelResourceModel 75 | 76 | diags := req.Plan.Get(ctx, &plan) 77 | resp.Diagnostics.Append(diags...) 78 | if resp.Diagnostics.HasError() { 79 | return 80 | } 81 | 82 | notificationIntegration, err := s.client.GetNotificationIntegrationByType(plan.SpaceID.ValueString(), "slack") 83 | if err != nil { 84 | resp.Diagnostics.AddError( 85 | "Error Creating Slack Notification", 86 | "err: "+err.Error(), 87 | ) 88 | return 89 | } 90 | 91 | var roomsID []string 92 | plan.RoomsID.ElementsAs(ctx, &roomsID, false) 93 | 94 | var notificationOptions []string 95 | plan.NotificationOptions.ElementsAs(ctx, ¬ificationOptions, false) 96 | 97 | commonParams := client.NotificationChannel{ 98 | Name: plan.Name.ValueString(), 99 | Integration: *notificationIntegration, 100 | Rooms: roomsID, 101 | NotificationOptions: notificationOptions, 102 | Enabled: plan.Enabled.ValueBool(), 103 | RepeatNotificationMinute: plan.RepeatNotificationMinute.ValueInt64(), 104 | } 105 | 106 | slackParams := client.NotificationSlackChannel{ 107 | URL: plan.WebhookURL.ValueString(), 108 | } 109 | 110 | notificationChannel, err := s.client.CreateSlackChannel(plan.SpaceID.ValueString(), commonParams, slackParams) 111 | if err != nil { 112 | resp.Diagnostics.AddError( 113 | "Error Creating Slack Notification", 114 | "err: "+err.Error(), 115 | ) 116 | return 117 | } 118 | 119 | plan.ID = types.StringValue(notificationChannel.ID) 120 | plan.Name = types.StringValue(notificationChannel.Name) 121 | plan.Enabled = types.BoolValue(notificationChannel.Enabled) 122 | plan.RoomsID, _ = types.ListValueFrom(ctx, types.StringType, notificationChannel.Rooms) 123 | plan.NotificationOptions, _ = types.ListValueFrom(ctx, types.StringType, notificationChannel.NotificationOptions) 124 | plan.RepeatNotificationMinute = types.Int64Value(notificationChannel.RepeatNotificationMinute) 125 | 126 | diags = resp.State.Set(ctx, plan) 127 | resp.Diagnostics.Append(diags...) 128 | if resp.Diagnostics.HasError() { 129 | return 130 | } 131 | } 132 | 133 | func (s *slackChannelResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 134 | var state slackChannelResourceModel 135 | 136 | diags := req.State.Get(ctx, &state) 137 | resp.Diagnostics.Append(diags...) 138 | if resp.Diagnostics.HasError() { 139 | return 140 | } 141 | 142 | notificationChannel, err := s.client.GetNotificationChannelByIDAndType(state.SpaceID.ValueString(), state.ID.ValueString(), "slack") 143 | if err != nil { 144 | if errors.Is(err, client.ErrNotFound) { 145 | resp.State.RemoveResource(ctx) 146 | return 147 | } 148 | resp.Diagnostics.AddError( 149 | "Error Getting Slack Notification", 150 | fmt.Sprintf("Could not read slack notification for space_id/channel_id: %s/%s err: %v", state.SpaceID.ValueString(), state.ID.ValueString(), err.Error()), 151 | ) 152 | return 153 | } 154 | 155 | var notificationSecrets client.NotificationSlackChannel 156 | err = json.Unmarshal(notificationChannel.Secrets, ¬ificationSecrets) 157 | if err != nil { 158 | resp.Diagnostics.AddError( 159 | "Error Getting Slack Notification", 160 | fmt.Sprintf("Could not unmarshal slack notification secrets for space_id/channel_id: %s/%s err: %v", state.SpaceID.ValueString(), state.ID.ValueString(), err.Error()), 161 | ) 162 | return 163 | } 164 | state.Name = types.StringValue(notificationChannel.Name) 165 | state.Enabled = types.BoolValue(notificationChannel.Enabled) 166 | state.RoomsID, _ = types.ListValueFrom(ctx, types.StringType, notificationChannel.Rooms) 167 | state.NotificationOptions, _ = types.ListValueFrom(ctx, types.StringType, notificationChannel.NotificationOptions) 168 | state.RepeatNotificationMinute = types.Int64Value(notificationChannel.RepeatNotificationMinute) 169 | state.WebhookURL = types.StringValue(notificationSecrets.URL) 170 | diags = resp.State.Set(ctx, &state) 171 | resp.Diagnostics.Append(diags...) 172 | if resp.Diagnostics.HasError() { 173 | return 174 | } 175 | } 176 | 177 | func (s *slackChannelResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 178 | var plan slackChannelResourceModel 179 | 180 | diags := req.Plan.Get(ctx, &plan) 181 | resp.Diagnostics.Append(diags...) 182 | if resp.Diagnostics.HasError() { 183 | return 184 | } 185 | 186 | var roomsID []string 187 | plan.RoomsID.ElementsAs(ctx, &roomsID, false) 188 | 189 | var notificationOptions []string 190 | plan.NotificationOptions.ElementsAs(ctx, ¬ificationOptions, false) 191 | 192 | commonParams := client.NotificationChannel{ 193 | ID: plan.ID.ValueString(), 194 | Name: plan.Name.ValueString(), 195 | Rooms: roomsID, 196 | NotificationOptions: notificationOptions, 197 | Enabled: plan.Enabled.ValueBool(), 198 | RepeatNotificationMinute: plan.RepeatNotificationMinute.ValueInt64(), 199 | } 200 | 201 | slackParams := client.NotificationSlackChannel{ 202 | URL: plan.WebhookURL.ValueString(), 203 | } 204 | 205 | notificationChannel, err := s.client.UpdateSlackChannelByID(plan.SpaceID.ValueString(), commonParams, slackParams) 206 | if err != nil { 207 | resp.Diagnostics.AddError( 208 | "Error Updating Slack Notification", 209 | fmt.Sprintf("Could not update slack notification for space_id/channel_id: %s/%s err: %v", plan.SpaceID.ValueString(), plan.ID.ValueString(), err.Error()), 210 | ) 211 | return 212 | } 213 | 214 | plan.ID = types.StringValue(notificationChannel.ID) 215 | plan.Name = types.StringValue(notificationChannel.Name) 216 | plan.Enabled = types.BoolValue(notificationChannel.Enabled) 217 | plan.RoomsID, _ = types.ListValueFrom(ctx, types.StringType, notificationChannel.Rooms) 218 | plan.NotificationOptions, _ = types.ListValueFrom(ctx, types.StringType, notificationChannel.NotificationOptions) 219 | plan.RepeatNotificationMinute = types.Int64Value(notificationChannel.RepeatNotificationMinute) 220 | 221 | diags = resp.State.Set(ctx, plan) 222 | resp.Diagnostics.Append(diags...) 223 | if resp.Diagnostics.HasError() { 224 | return 225 | } 226 | 227 | } 228 | 229 | func (s *slackChannelResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 230 | var state slackChannelResourceModel 231 | 232 | diags := req.State.Get(ctx, &state) 233 | resp.Diagnostics.Append(diags...) 234 | if resp.Diagnostics.HasError() { 235 | return 236 | } 237 | 238 | err := s.client.DeleteChannelByID(state.SpaceID.ValueString(), state.ID.ValueString()) 239 | if err != nil { 240 | resp.Diagnostics.AddError( 241 | "Error Deleting Slack Notification", 242 | fmt.Sprintf("Could not delete slack notification for space_id/channel_id: %s/%s err: %v", state.SpaceID.ValueString(), state.ID.ValueString(), err.Error()), 243 | ) 244 | return 245 | } 246 | } 247 | 248 | func (s *slackChannelResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 249 | idParts := strings.Split(req.ID, ",") 250 | 251 | if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { 252 | resp.Diagnostics.AddError( 253 | "Unexpected Import Identifier", 254 | fmt.Sprintf("Expected import identifier with format: space_id,channel_id. Got: %q", req.ID), 255 | ) 256 | return 257 | } 258 | 259 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("space_id"), idParts[0])...) 260 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), idParts[1])...) 261 | } 262 | -------------------------------------------------------------------------------- /internal/provider/notification_pagerduty_channel_resource.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/hashicorp/terraform-plugin-framework/path" 11 | "github.com/hashicorp/terraform-plugin-framework/resource" 12 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 13 | "github.com/hashicorp/terraform-plugin-framework/types" 14 | "github.com/netdata/terraform-provider-netdata/internal/client" 15 | ) 16 | 17 | var ( 18 | _ resource.Resource = &pagerdutyChannelResource{} 19 | _ resource.ResourceWithConfigure = &pagerdutyChannelResource{} 20 | ) 21 | 22 | func NewPagerdutyChannelResource() resource.Resource { 23 | return &pagerdutyChannelResource{} 24 | } 25 | 26 | type pagerdutyChannelResource struct { 27 | client *client.Client 28 | } 29 | 30 | type pagerdutyChannelResourceModel struct { 31 | ID types.String `tfsdk:"id"` 32 | Name types.String `tfsdk:"name"` 33 | Enabled types.Bool `tfsdk:"enabled"` 34 | SpaceID types.String `tfsdk:"space_id"` 35 | RoomsID types.List `tfsdk:"rooms_id"` 36 | NotificationOptions types.List `tfsdk:"notifications"` 37 | RepeatNotificationMinute types.Int64 `tfsdk:"repeat_notification_min"` 38 | AlertEventsURL types.String `tfsdk:"alert_events_url"` 39 | IntegrationKey types.String `tfsdk:"integration_key"` 40 | } 41 | 42 | func (s *pagerdutyChannelResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 43 | resp.TypeName = req.ProviderTypeName + "_notification_pagerduty_channel" 44 | } 45 | 46 | func (s *pagerdutyChannelResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { 47 | fullSchema := commonNotificationSchema("Pagerduty") 48 | fullSchema.Attributes["alert_events_url"] = schema.StringAttribute{ 49 | Description: "URL for alert events", 50 | Required: true, 51 | } 52 | fullSchema.Attributes["integration_key"] = schema.StringAttribute{ 53 | Description: "Integration key", 54 | Required: true, 55 | Sensitive: true, 56 | } 57 | resp.Schema = fullSchema 58 | } 59 | 60 | func (s *pagerdutyChannelResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 61 | if req.ProviderData == nil { 62 | return 63 | } 64 | 65 | client, ok := req.ProviderData.(*client.Client) 66 | if !ok { 67 | resp.Diagnostics.AddError( 68 | "Unexpected Resource Configure Type", 69 | fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), 70 | ) 71 | 72 | return 73 | } 74 | 75 | s.client = client 76 | } 77 | 78 | func (s *pagerdutyChannelResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 79 | var plan pagerdutyChannelResourceModel 80 | 81 | diags := req.Plan.Get(ctx, &plan) 82 | resp.Diagnostics.Append(diags...) 83 | if resp.Diagnostics.HasError() { 84 | return 85 | } 86 | 87 | notificationIntegration, err := s.client.GetNotificationIntegrationByType(plan.SpaceID.ValueString(), "pagerduty") 88 | 89 | if err != nil { 90 | resp.Diagnostics.AddError( 91 | "Error Creating Pagerduty Notification", 92 | "err: "+err.Error(), 93 | ) 94 | return 95 | } 96 | 97 | var roomsID []string 98 | plan.RoomsID.ElementsAs(ctx, &roomsID, false) 99 | 100 | var notificationOptions []string 101 | plan.NotificationOptions.ElementsAs(ctx, ¬ificationOptions, false) 102 | 103 | commonParams := client.NotificationChannel{ 104 | Name: plan.Name.ValueString(), 105 | Integration: *notificationIntegration, 106 | Rooms: roomsID, 107 | NotificationOptions: notificationOptions, 108 | Enabled: plan.Enabled.ValueBool(), 109 | RepeatNotificationMinute: plan.RepeatNotificationMinute.ValueInt64(), 110 | } 111 | 112 | pagerdutyParams := client.NotificationPagerdutyChannel{ 113 | AlertEventsURL: plan.AlertEventsURL.ValueString(), 114 | IntegrationKey: plan.IntegrationKey.ValueString(), 115 | } 116 | 117 | notificationChannel, err := s.client.CreatePagerdutyChannel(plan.SpaceID.ValueString(), commonParams, pagerdutyParams) 118 | if err != nil { 119 | resp.Diagnostics.AddError( 120 | "Error Creating Pagerduty Notification", 121 | "err: "+err.Error(), 122 | ) 123 | return 124 | } 125 | 126 | plan.ID = types.StringValue(notificationChannel.ID) 127 | plan.Name = types.StringValue(notificationChannel.Name) 128 | plan.Enabled = types.BoolValue(notificationChannel.Enabled) 129 | plan.RoomsID, _ = types.ListValueFrom(ctx, types.StringType, notificationChannel.Rooms) 130 | plan.NotificationOptions, _ = types.ListValueFrom(ctx, types.StringType, notificationChannel.NotificationOptions) 131 | plan.RepeatNotificationMinute = types.Int64Value(notificationChannel.RepeatNotificationMinute) 132 | 133 | diags = resp.State.Set(ctx, plan) 134 | resp.Diagnostics.Append(diags...) 135 | if resp.Diagnostics.HasError() { 136 | return 137 | } 138 | } 139 | 140 | func (s *pagerdutyChannelResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 141 | var state pagerdutyChannelResourceModel 142 | 143 | diags := req.State.Get(ctx, &state) 144 | resp.Diagnostics.Append(diags...) 145 | if resp.Diagnostics.HasError() { 146 | return 147 | } 148 | 149 | notificationChannel, err := s.client.GetNotificationChannelByIDAndType(state.SpaceID.ValueString(), state.ID.ValueString(), "pagerduty") 150 | if err != nil { 151 | if errors.Is(err, client.ErrNotFound) { 152 | resp.State.RemoveResource(ctx) 153 | return 154 | } 155 | resp.Diagnostics.AddError( 156 | "Error Getting Pagerduty Notification", 157 | fmt.Sprintf("Could not read pagerduty notification for space_id/channel_id: %s/%s err: %v", state.SpaceID.ValueString(), state.ID.ValueString(), err.Error()), 158 | ) 159 | return 160 | } 161 | 162 | var notificationSecrets client.NotificationPagerdutyChannel 163 | err = json.Unmarshal(notificationChannel.Secrets, ¬ificationSecrets) 164 | if err != nil { 165 | resp.Diagnostics.AddError( 166 | "Error Getting Pagerduty Notification", 167 | fmt.Sprintf("Could not unmarshal pagerduty notification secrets for space_id/channel_id: %s/%s err: %v", state.SpaceID.ValueString(), state.ID.ValueString(), err.Error()), 168 | ) 169 | return 170 | } 171 | state.Name = types.StringValue(notificationChannel.Name) 172 | state.Enabled = types.BoolValue(notificationChannel.Enabled) 173 | state.RoomsID, _ = types.ListValueFrom(ctx, types.StringType, notificationChannel.Rooms) 174 | state.NotificationOptions, _ = types.ListValueFrom(ctx, types.StringType, notificationChannel.NotificationOptions) 175 | state.RepeatNotificationMinute = types.Int64Value(notificationChannel.RepeatNotificationMinute) 176 | state.AlertEventsURL = types.StringValue(notificationSecrets.AlertEventsURL) 177 | state.IntegrationKey = types.StringValue(notificationSecrets.IntegrationKey) 178 | diags = resp.State.Set(ctx, &state) 179 | resp.Diagnostics.Append(diags...) 180 | if resp.Diagnostics.HasError() { 181 | return 182 | } 183 | } 184 | 185 | func (s *pagerdutyChannelResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 186 | var plan pagerdutyChannelResourceModel 187 | 188 | diags := req.Plan.Get(ctx, &plan) 189 | resp.Diagnostics.Append(diags...) 190 | if resp.Diagnostics.HasError() { 191 | return 192 | } 193 | 194 | var roomsID []string 195 | plan.RoomsID.ElementsAs(ctx, &roomsID, false) 196 | 197 | var notificationOptions []string 198 | plan.NotificationOptions.ElementsAs(ctx, ¬ificationOptions, false) 199 | 200 | commonParams := client.NotificationChannel{ 201 | ID: plan.ID.ValueString(), 202 | Name: plan.Name.ValueString(), 203 | Rooms: roomsID, 204 | NotificationOptions: notificationOptions, 205 | Enabled: plan.Enabled.ValueBool(), 206 | RepeatNotificationMinute: plan.RepeatNotificationMinute.ValueInt64(), 207 | } 208 | 209 | pagerdutyParams := client.NotificationPagerdutyChannel{ 210 | AlertEventsURL: plan.AlertEventsURL.ValueString(), 211 | IntegrationKey: plan.IntegrationKey.ValueString(), 212 | } 213 | 214 | notificationChannel, err := s.client.UpdatePagerdutyChannelByID(plan.SpaceID.ValueString(), commonParams, pagerdutyParams) 215 | 216 | if err != nil { 217 | resp.Diagnostics.AddError( 218 | "Error Updating Pagerduty Notification", 219 | fmt.Sprintf("Could not update pagerduty notification for space_id/channel_id: %s/%s err: %v", plan.SpaceID.ValueString(), plan.ID.ValueString(), err.Error()), 220 | ) 221 | return 222 | } 223 | 224 | plan.ID = types.StringValue(notificationChannel.ID) 225 | plan.Name = types.StringValue(notificationChannel.Name) 226 | plan.Enabled = types.BoolValue(notificationChannel.Enabled) 227 | plan.RoomsID, _ = types.ListValueFrom(ctx, types.StringType, notificationChannel.Rooms) 228 | plan.NotificationOptions, _ = types.ListValueFrom(ctx, types.StringType, notificationChannel.NotificationOptions) 229 | plan.RepeatNotificationMinute = types.Int64Value(notificationChannel.RepeatNotificationMinute) 230 | 231 | diags = resp.State.Set(ctx, plan) 232 | resp.Diagnostics.Append(diags...) 233 | if resp.Diagnostics.HasError() { 234 | return 235 | } 236 | 237 | } 238 | 239 | func (s *pagerdutyChannelResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 240 | var state pagerdutyChannelResourceModel 241 | 242 | diags := req.State.Get(ctx, &state) 243 | resp.Diagnostics.Append(diags...) 244 | if resp.Diagnostics.HasError() { 245 | return 246 | } 247 | 248 | err := s.client.DeleteChannelByID(state.SpaceID.ValueString(), state.ID.ValueString()) 249 | if err != nil { 250 | resp.Diagnostics.AddError( 251 | "Error Deleting Pagerduty Notification", 252 | fmt.Sprintf("Could not delete pagerduty notification for space_id/channel_id: %s/%s err: %v", state.SpaceID.ValueString(), state.ID.ValueString(), err.Error()), 253 | ) 254 | return 255 | } 256 | } 257 | 258 | func (s *pagerdutyChannelResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 259 | idParts := strings.Split(req.ID, ",") 260 | 261 | if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { 262 | resp.Diagnostics.AddError( 263 | "Unexpected Import Identifier", 264 | fmt.Sprintf("Expected import identifier with format: space_id,channel_id. Got: %q", req.ID), 265 | ) 266 | return 267 | } 268 | 269 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("space_id"), idParts[0])...) 270 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), idParts[1])...) 271 | } 272 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /internal/provider/notification_discord_channel_resource.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" 12 | "github.com/hashicorp/terraform-plugin-framework/path" 13 | "github.com/hashicorp/terraform-plugin-framework/resource" 14 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 15 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 16 | "github.com/hashicorp/terraform-plugin-framework/types" 17 | "github.com/netdata/terraform-provider-netdata/internal/client" 18 | ) 19 | 20 | var ( 21 | _ resource.Resource = &discordChannelResource{} 22 | _ resource.ResourceWithConfigure = &discordChannelResource{} 23 | ) 24 | 25 | func NewDiscordChannelResource() resource.Resource { 26 | return &discordChannelResource{} 27 | } 28 | 29 | type discordChannelResource struct { 30 | client *client.Client 31 | } 32 | 33 | type discordChannelResourceModel struct { 34 | ID types.String `tfsdk:"id"` 35 | Name types.String `tfsdk:"name"` 36 | Enabled types.Bool `tfsdk:"enabled"` 37 | SpaceID types.String `tfsdk:"space_id"` 38 | RoomsID types.List `tfsdk:"rooms_id"` 39 | NotificationOptions types.List `tfsdk:"notifications"` 40 | RepeatNotificationMinute types.Int64 `tfsdk:"repeat_notification_min"` 41 | WebhookURL types.String `tfsdk:"webhook_url"` 42 | ChannelType types.String `tfsdk:"channel_type"` 43 | ChannelThread types.String `tfsdk:"channel_thread"` 44 | } 45 | 46 | func (s *discordChannelResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 47 | resp.TypeName = req.ProviderTypeName + "_notification_discord_channel" 48 | } 49 | 50 | func (s *discordChannelResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { 51 | fullSchema := commonNotificationSchema("Discord") 52 | fullSchema.Attributes["webhook_url"] = schema.StringAttribute{ 53 | Description: "Discord webhook URL", 54 | Required: true, 55 | Sensitive: true, 56 | } 57 | fullSchema.Attributes["channel_type"] = schema.StringAttribute{ 58 | Description: "Discord channel type. Valid values are: `text`, `forum`", 59 | Required: true, 60 | Validators: []validator.String{ 61 | stringvalidator.RegexMatches( 62 | regexp.MustCompile(`^(text|forum)$`), 63 | "Invalid channel type", 64 | ), 65 | }, 66 | } 67 | fullSchema.Attributes["channel_thread"] = schema.StringAttribute{ 68 | Description: "Discord channel thread name required if channel type is `forum`", 69 | Optional: true, 70 | } 71 | resp.Schema = fullSchema 72 | } 73 | 74 | func (s *discordChannelResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 75 | if req.ProviderData == nil { 76 | return 77 | } 78 | 79 | client, ok := req.ProviderData.(*client.Client) 80 | if !ok { 81 | resp.Diagnostics.AddError( 82 | "Unexpected Resource Configure Type", 83 | fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), 84 | ) 85 | 86 | return 87 | } 88 | 89 | s.client = client 90 | } 91 | 92 | func (s *discordChannelResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 93 | var plan discordChannelResourceModel 94 | 95 | diags := req.Plan.Get(ctx, &plan) 96 | resp.Diagnostics.Append(diags...) 97 | if resp.Diagnostics.HasError() { 98 | return 99 | } 100 | 101 | if plan.ChannelType.ValueString() == "forum" && plan.ChannelThread.ValueString() == "" { 102 | resp.Diagnostics.AddError( 103 | "Error Creating Discord Notification", 104 | "channel_thread is required if channel_type is forum", 105 | ) 106 | return 107 | } 108 | 109 | notificationIntegration, err := s.client.GetNotificationIntegrationByType(plan.SpaceID.ValueString(), "discord") 110 | if err != nil { 111 | resp.Diagnostics.AddError( 112 | "Error Creating Discord Notification", 113 | "err: "+err.Error(), 114 | ) 115 | return 116 | } 117 | 118 | var roomsID []string 119 | plan.RoomsID.ElementsAs(ctx, &roomsID, false) 120 | 121 | var notificationOptions []string 122 | plan.NotificationOptions.ElementsAs(ctx, ¬ificationOptions, false) 123 | 124 | commonParams := client.NotificationChannel{ 125 | Name: plan.Name.ValueString(), 126 | Integration: *notificationIntegration, 127 | Rooms: roomsID, 128 | NotificationOptions: notificationOptions, 129 | Enabled: plan.Enabled.ValueBool(), 130 | RepeatNotificationMinute: plan.RepeatNotificationMinute.ValueInt64(), 131 | } 132 | 133 | discordParams := client.NotificationDiscordChannel{ 134 | URL: plan.WebhookURL.ValueString(), 135 | } 136 | 137 | switch plan.ChannelType.ValueString() { 138 | case "text": 139 | discordParams.ChannelParams.Selection = "text" 140 | case "forum": 141 | discordParams.ChannelParams.Selection = "forum" 142 | discordParams.ChannelParams.ThreadName = plan.ChannelThread.ValueString() 143 | } 144 | 145 | notificationChannel, err := s.client.CreateDiscordChannel(plan.SpaceID.ValueString(), commonParams, discordParams) 146 | if err != nil { 147 | resp.Diagnostics.AddError( 148 | "Error Creating Discord Notification", 149 | "err: "+err.Error(), 150 | ) 151 | return 152 | } 153 | 154 | plan.ID = types.StringValue(notificationChannel.ID) 155 | plan.Name = types.StringValue(notificationChannel.Name) 156 | plan.Enabled = types.BoolValue(notificationChannel.Enabled) 157 | plan.RoomsID, _ = types.ListValueFrom(ctx, types.StringType, notificationChannel.Rooms) 158 | plan.NotificationOptions, _ = types.ListValueFrom(ctx, types.StringType, notificationChannel.NotificationOptions) 159 | plan.RepeatNotificationMinute = types.Int64Value(notificationChannel.RepeatNotificationMinute) 160 | 161 | diags = resp.State.Set(ctx, plan) 162 | resp.Diagnostics.Append(diags...) 163 | if resp.Diagnostics.HasError() { 164 | return 165 | } 166 | } 167 | 168 | func (s *discordChannelResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 169 | var state discordChannelResourceModel 170 | 171 | diags := req.State.Get(ctx, &state) 172 | resp.Diagnostics.Append(diags...) 173 | if resp.Diagnostics.HasError() { 174 | return 175 | } 176 | 177 | notificationChannel, err := s.client.GetNotificationChannelByIDAndType(state.SpaceID.ValueString(), state.ID.ValueString(), "discord") 178 | if err != nil { 179 | if errors.Is(err, client.ErrNotFound) { 180 | resp.State.RemoveResource(ctx) 181 | return 182 | } 183 | resp.Diagnostics.AddError( 184 | "Error Getting Discord Notification", 185 | fmt.Sprintf("Could not read discord notification for space_id/channel_id: %s/%s err: %v", state.SpaceID.ValueString(), state.ID.ValueString(), err.Error()), 186 | ) 187 | return 188 | } 189 | 190 | var notificationSecrets client.NotificationDiscordChannel 191 | err = json.Unmarshal(notificationChannel.Secrets, ¬ificationSecrets) 192 | if err != nil { 193 | resp.Diagnostics.AddError( 194 | "Error Getting Discord Notification", 195 | fmt.Sprintf("Could not unmarshal discord notification secrets for space_id/channel_id: %s/%s err: %v", state.SpaceID.ValueString(), state.ID.ValueString(), err.Error()), 196 | ) 197 | return 198 | } 199 | state.Name = types.StringValue(notificationChannel.Name) 200 | state.Enabled = types.BoolValue(notificationChannel.Enabled) 201 | state.RoomsID, _ = types.ListValueFrom(ctx, types.StringType, notificationChannel.Rooms) 202 | state.NotificationOptions, _ = types.ListValueFrom(ctx, types.StringType, notificationChannel.NotificationOptions) 203 | state.RepeatNotificationMinute = types.Int64Value(notificationChannel.RepeatNotificationMinute) 204 | state.WebhookURL = types.StringValue(notificationSecrets.URL) 205 | state.ChannelType = types.StringValue(notificationSecrets.ChannelParams.Selection) 206 | if notificationSecrets.ChannelParams.Selection == "forum" { 207 | state.ChannelThread = types.StringValue(notificationSecrets.ChannelParams.ThreadName) 208 | } 209 | 210 | diags = resp.State.Set(ctx, &state) 211 | resp.Diagnostics.Append(diags...) 212 | if resp.Diagnostics.HasError() { 213 | return 214 | } 215 | } 216 | 217 | func (s *discordChannelResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 218 | var plan discordChannelResourceModel 219 | 220 | diags := req.Plan.Get(ctx, &plan) 221 | resp.Diagnostics.Append(diags...) 222 | if resp.Diagnostics.HasError() { 223 | return 224 | } 225 | 226 | if plan.ChannelType.ValueString() == "forum" && plan.ChannelThread.ValueString() == "" { 227 | resp.Diagnostics.AddError( 228 | "Error Creating Discord Notification", 229 | "channel_thread is required if channel_type is forum", 230 | ) 231 | return 232 | } 233 | 234 | var roomsID []string 235 | plan.RoomsID.ElementsAs(ctx, &roomsID, false) 236 | 237 | var notificationOptions []string 238 | plan.NotificationOptions.ElementsAs(ctx, ¬ificationOptions, false) 239 | 240 | commonParams := client.NotificationChannel{ 241 | ID: plan.ID.ValueString(), 242 | Name: plan.Name.ValueString(), 243 | Rooms: roomsID, 244 | NotificationOptions: notificationOptions, 245 | Enabled: plan.Enabled.ValueBool(), 246 | RepeatNotificationMinute: plan.RepeatNotificationMinute.ValueInt64(), 247 | } 248 | 249 | discordParams := client.NotificationDiscordChannel{ 250 | URL: plan.WebhookURL.ValueString(), 251 | } 252 | 253 | switch plan.ChannelType.ValueString() { 254 | case "text": 255 | discordParams.ChannelParams.Selection = "text" 256 | case "forum": 257 | discordParams.ChannelParams.Selection = "forum" 258 | discordParams.ChannelParams.ThreadName = plan.ChannelThread.ValueString() 259 | } 260 | 261 | notificationChannel, err := s.client.UpdateDiscordChannelByID(plan.SpaceID.ValueString(), commonParams, discordParams) 262 | if err != nil { 263 | resp.Diagnostics.AddError( 264 | "Error Updating Discord Notification", 265 | fmt.Sprintf("Could not update discord notification for space_id/channel_id: %s/%s err: %v", plan.SpaceID.ValueString(), plan.ID.ValueString(), err.Error()), 266 | ) 267 | return 268 | } 269 | 270 | plan.ID = types.StringValue(notificationChannel.ID) 271 | plan.Name = types.StringValue(notificationChannel.Name) 272 | plan.Enabled = types.BoolValue(notificationChannel.Enabled) 273 | plan.RoomsID, _ = types.ListValueFrom(ctx, types.StringType, notificationChannel.Rooms) 274 | plan.NotificationOptions, _ = types.ListValueFrom(ctx, types.StringType, notificationChannel.NotificationOptions) 275 | plan.RepeatNotificationMinute = types.Int64Value(notificationChannel.RepeatNotificationMinute) 276 | 277 | diags = resp.State.Set(ctx, plan) 278 | resp.Diagnostics.Append(diags...) 279 | if resp.Diagnostics.HasError() { 280 | return 281 | } 282 | 283 | } 284 | 285 | func (s *discordChannelResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 286 | var state discordChannelResourceModel 287 | 288 | diags := req.State.Get(ctx, &state) 289 | resp.Diagnostics.Append(diags...) 290 | if resp.Diagnostics.HasError() { 291 | return 292 | } 293 | 294 | err := s.client.DeleteChannelByID(state.SpaceID.ValueString(), state.ID.ValueString()) 295 | if err != nil { 296 | resp.Diagnostics.AddError( 297 | "Error Deleting Discord Notification", 298 | fmt.Sprintf("Could not delete discord notification for space_id/channel_id: %s/%s err: %v", state.SpaceID.ValueString(), state.ID.ValueString(), err.Error()), 299 | ) 300 | return 301 | } 302 | } 303 | 304 | func (s *discordChannelResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 305 | idParts := strings.Split(req.ID, ",") 306 | 307 | if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { 308 | resp.Diagnostics.AddError( 309 | "Unexpected Import Identifier", 310 | fmt.Sprintf("Expected import identifier with format: space_id,channel_id. Got: %q", req.ID), 311 | ) 312 | return 313 | } 314 | 315 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("space_id"), idParts[0])...) 316 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), idParts[1])...) 317 | } 318 | --------------------------------------------------------------------------------