From 18af9458a0da894ce30d48c084b374e5f27f0f50 Mon Sep 17 00:00:00 2001 From: Sebastian Nallar Date: Wed, 6 Nov 2024 11:59:26 -0300 Subject: [PATCH] feature: accounts on terraform --- nullplatform/account.go | 114 ++++++++++++++++++ nullplatform/null_client.go | 5 + nullplatform/provider.go | 1 + nullplatform/provider_test.go | 3 + nullplatform/resource_account.go | 160 ++++++++++++++++++++++++++ nullplatform/resource_account_test.go | 120 +++++++++++++++++++ 6 files changed, 403 insertions(+) create mode 100644 nullplatform/account.go create mode 100644 nullplatform/resource_account.go create mode 100644 nullplatform/resource_account_test.go diff --git a/nullplatform/account.go b/nullplatform/account.go new file mode 100644 index 0000000..4360aa8 --- /dev/null +++ b/nullplatform/account.go @@ -0,0 +1,114 @@ +package nullplatform + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +const ACCOUNT_PATH = "/account" + +type Account struct { + Id int `json:"id,omitempty"` + Name string `json:"name,omitempty"` + OrganizationId string `json:"organization_id,omitempty"` + RepositoryPrefix string `json:"repository_prefix,omitempty"` + RepositoryProvider string `json:"repository_provider,omitempty"` + Slug string `json:"slug,omitempty"` + Status string `json:"status,omitempty"` +} + +func (c *NullClient) CreateAccount(account *Account) (*Account, error) { + var buf bytes.Buffer + err := json.NewEncoder(&buf).Encode(*account) + + if err != nil { + return nil, err + } + + res, err := c.MakeRequest("POST", ACCOUNT_PATH, &buf) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + var nErr NullErrors + if err := json.NewDecoder(res.Body).Decode(&nErr); err != nil { + return nil, fmt.Errorf("failed to decode null error response: %w", err) + } + return nil, fmt.Errorf("error creating account resource, got status code: %d, message: %s", res.StatusCode, nErr.Message) + } + + accountRes := &Account{} + derr := json.NewDecoder(res.Body).Decode(accountRes) + + if derr != nil { + return nil, derr + } + + return accountRes, nil +} + +func (c *NullClient) PatchAccount(accountId string, account *Account) error { + path := fmt.Sprintf("%s/%s", ACCOUNT_PATH, accountId) + + var buf bytes.Buffer + err := json.NewEncoder(&buf).Encode(*account) + + if err != nil { + return err + } + + res, err := c.MakeRequest("PATCH", path, &buf) + if err != nil { + return err + } + defer res.Body.Close() + + if (res.StatusCode != http.StatusOK) && (res.StatusCode != http.StatusNoContent) { + var nErr NullErrors + if err := json.NewDecoder(res.Body).Decode(&nErr); err != nil { + return fmt.Errorf("failed to decode null error response: %w", err) + } + return fmt.Errorf("error updating account resource, got status code: %d, message: %s", res.StatusCode, nErr.Message) + } + + return nil +} + +func (c *NullClient) GetAccount(accountId string) (*Account, error) { + path := fmt.Sprintf("%s/%s", ACCOUNT_PATH, accountId) + + res, err := c.MakeRequest("GET", path, nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + + account := &Account{} + derr := json.NewDecoder(res.Body).Decode(account) + + if derr != nil { + return nil, derr + } + + if account.Status == "deleted" { + return account, fmt.Errorf("error getting account resource, the status is %s", account.Status) + } + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("error getting account resource, got %d for %s", res.StatusCode, accountId) + } + + return account, nil +} + +func (c *NullClient) DeleteAccount(accountId string) error { + account := &Account{ + Status: "inactive", + } + + return c.PatchAccount(accountId, account) +} diff --git a/nullplatform/null_client.go b/nullplatform/null_client.go index ea84780..261524c 100644 --- a/nullplatform/null_client.go +++ b/nullplatform/null_client.go @@ -115,6 +115,11 @@ type NullOps interface { CreateDimensionValue(dv *DimensionValue) (*DimensionValue, error) GetDimensionValue(dimensionID, valueID int) (*DimensionValue, error) DeleteDimensionValue(dimensionID, valueID int) error + + CreateAccount(account *Account) (*Account, error) + GetAccount(accountId string) (*Account, error) + PatchAccount(accountId string, account *Account) error + DeleteAccount(accountId string) error } func (c *NullClient) MakeRequest(method, path string, body *bytes.Buffer) (*http.Response, error) { diff --git a/nullplatform/provider.go b/nullplatform/provider.go index d9f1450..ca9cc8b 100644 --- a/nullplatform/provider.go +++ b/nullplatform/provider.go @@ -39,6 +39,7 @@ func Provider() *schema.Provider { }, }, ResourcesMap: map[string]*schema.Resource{ + "nullplatform_account": resourceAccount(), "nullplatform_scope": resourceScope(), "nullplatform_service": resourceService(), "nullplatform_link": resourceLink(), diff --git a/nullplatform/provider_test.go b/nullplatform/provider_test.go index b7ee23a..2c945e9 100644 --- a/nullplatform/provider_test.go +++ b/nullplatform/provider_test.go @@ -30,6 +30,7 @@ func testAccPreCheck(t *testing.T) { func TestProvider_HasChildResources(t *testing.T) { expectedResources := []string{ + "nullplatform_account", "nullplatform_scope", "nullplatform_service", "nullplatform_link", @@ -40,6 +41,8 @@ func TestProvider_HasChildResources(t *testing.T) { "nullplatform_notification_channel", "nullplatform_runtime_configuration", "nullplatform_provider_config", + "nullplatform_dimension", + "nullplatform_dimension_value", } resources := nullplatform.Provider().ResourcesMap diff --git a/nullplatform/resource_account.go b/nullplatform/resource_account.go new file mode 100644 index 0000000..b5c8bc2 --- /dev/null +++ b/nullplatform/resource_account.go @@ -0,0 +1,160 @@ +package nullplatform + +import ( + "context" + "fmt" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceAccount() *schema.Resource { + return &schema.Resource{ + Description: "The account resource allows you to configure a nullplatform account", + + Create: AccountCreate, + Read: AccountRead, + Update: AccountUpdate, + Delete: AccountDelete, + + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + d.Set("id", d.Id()) + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the account", + }, + "organization_id": { + Type: schema.TypeString, + Computed: true, + ForceNew: true, + Description: "The ID of the organization this account belongs to (computed from authentication token)", + }, + "repository_prefix": { + Type: schema.TypeString, + Required: true, + Description: "The prefix used for repositories in this account", + }, + "repository_provider": { + Type: schema.TypeString, + Required: true, + Description: "The repository provider for this account", + }, + "slug": { + Type: schema.TypeString, + Required: true, + Description: "The unique slug identifier for the account", + }, + }, + } +} + +func AccountCreate(d *schema.ResourceData, m any) error { + nullOps := m.(NullOps) + client := nullOps.(*NullClient) + + organizationID, err := client.GetOrganizationIDFromToken() + if err != nil { + return fmt.Errorf("error getting organization ID from token: %w", err) + } + + newAccount := &Account{ + Name: d.Get("name").(string), + OrganizationId: organizationID, + RepositoryPrefix: d.Get("repository_prefix").(string), + RepositoryProvider: d.Get("repository_provider").(string), + Slug: d.Get("slug").(string), + } + + account, err := nullOps.CreateAccount(newAccount) + if err != nil { + return err + } + + d.SetId(strconv.Itoa(account.Id)) + + // Set the computed organization_id in the state + if err := d.Set("organization_id", account.OrganizationId); err != nil { + return fmt.Errorf("error setting organization_id: %w", err) + } + + return AccountRead(d, m) +} + +func AccountRead(d *schema.ResourceData, m any) error { + nullOps := m.(NullOps) + accountId := d.Id() + + account, err := nullOps.GetAccount(accountId) + if err != nil { + if account.Status == "inactive" { + d.SetId("") + return nil + } + return err + } + + if err := d.Set("name", account.Name); err != nil { + return err + } + if err := d.Set("organization_id", account.OrganizationId); err != nil { + return err + } + if err := d.Set("repository_prefix", account.RepositoryPrefix); err != nil { + return err + } + if err := d.Set("repository_provider", account.RepositoryProvider); err != nil { + return err + } + if err := d.Set("slug", account.Slug); err != nil { + return err + } + + return nil +} + +func AccountUpdate(d *schema.ResourceData, m any) error { + nullOps := m.(NullOps) + accountId := d.Id() + + account := &Account{} + + if d.HasChange("name") { + account.Name = d.Get("name").(string) + } + if d.HasChange("repository_prefix") { + account.RepositoryPrefix = d.Get("repository_prefix").(string) + } + if d.HasChange("repository_provider") { + account.RepositoryProvider = d.Get("repository_provider").(string) + } + if d.HasChange("slug") { + account.Slug = d.Get("slug").(string) + } + + err := nullOps.PatchAccount(accountId, account) + if err != nil { + return err + } + + return AccountRead(d, m) +} + +func AccountDelete(d *schema.ResourceData, m any) error { + nullOps := m.(NullOps) + accountId := d.Id() + + err := nullOps.DeleteAccount(accountId) + if err != nil { + return err + } + + d.SetId("") + return nil +} diff --git a/nullplatform/resource_account_test.go b/nullplatform/resource_account_test.go new file mode 100644 index 0000000..90c9d7d --- /dev/null +++ b/nullplatform/resource_account_test.go @@ -0,0 +1,120 @@ +package nullplatform_test + +import ( + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/nullplatform/terraform-provider-nullplatform/nullplatform" +) + +func TestAccResourceAccount(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAccountDestroy, + Steps: []resource.TestStep{ + { + Config: testAccResourceAccount_basic(), + Check: resource.ComposeTestCheckFunc( + testAccCheckAccountExists("nullplatform_account.test"), + resource.TestCheckResourceAttr("nullplatform_account.test", "name", "test-account"), + resource.TestCheckResourceAttrSet("nullplatform_account.test", "organization_id"), + resource.TestCheckResourceAttr("nullplatform_account.test", "repository_prefix", "test-prefix"), + resource.TestCheckResourceAttr("nullplatform_account.test", "repository_provider", "github"), + resource.TestCheckResourceAttr("nullplatform_account.test", "slug", "test-account"), + ), + }, + { + Config: testAccResourceAccount_update(), + Check: resource.ComposeTestCheckFunc( + testAccCheckAccountExists("nullplatform_account.test"), + resource.TestCheckResourceAttr("nullplatform_account.test", "name", "updated-test-account"), + resource.TestCheckResourceAttrSet("nullplatform_account.test", "organization_id"), + resource.TestCheckResourceAttr("nullplatform_account.test", "repository_prefix", "updated-prefix"), + resource.TestCheckResourceAttr("nullplatform_account.test", "repository_provider", "gitlab"), + resource.TestCheckResourceAttr("nullplatform_account.test", "slug", "updated-test-account"), + ), + }, + { + // Test importing the resource + ResourceName: "nullplatform_account.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAccountExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set for the resource") + } + + client := testAccProviders["nullplatform"].Meta().(nullplatform.NullOps) + if client == nil { + return fmt.Errorf("provider meta is nil, ensure the provider is properly configured and initialized") + } + + foundAccount, err := client.GetAccount(rs.Primary.ID) + if err != nil { + return err + } + + if strconv.Itoa(foundAccount.Id) != rs.Primary.ID { + return fmt.Errorf("Account not found") + } + + return nil + } +} + +func testAccCheckAccountDestroy(s *terraform.State) error { + client := testAccProviders["nullplatform"].Meta().(nullplatform.NullOps) + if client == nil { + return fmt.Errorf("provider meta is nil, ensure the provider is properly configured and initialized") + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "nullplatform_account" { + continue + } + + account, err := client.GetAccount(rs.Primary.ID) + if err == nil && account.Status != "inactive" { + return fmt.Errorf("Account with ID %s still exists and is not inactive", rs.Primary.ID) + } + } + + return nil +} + +func testAccResourceAccount_basic() string { + return ` +resource "nullplatform_account" "test" { + name = "test-account" + repository_prefix = "test-prefix" + repository_provider = "github" + slug = "test-account" +} +` +} + +func testAccResourceAccount_update() string { + return ` +resource "nullplatform_account" "test" { + name = "updated-test-account" + repository_prefix = "updated-prefix" + repository_provider = "gitlab" + slug = "updated-test-account" +} +` +}