From e5c036569521852e3e2c142151d2b14d7b58c569 Mon Sep 17 00:00:00 2001 From: Jacob Hull Date: Wed, 11 Jan 2023 11:15:18 -0800 Subject: [PATCH] feat: add new "logdna_enterprise_key" resource This new resource allows creation and management of enterprise-level keys. This resource is separate from "logdna_key" which controls the management of non-enterpise accounts. Ref: LOG-15052 Signed-off-by: Jacob Hull --- docs/resources/logdna_enterprise_key.md | 51 ++++++++ logdna/request_test.go | 26 ++++ logdna/resource_enterprise_key.go | 156 ++++++++++++++++++++++++ 3 files changed, 233 insertions(+) create mode 100644 docs/resources/logdna_enterprise_key.md create mode 100644 logdna/resource_enterprise_key.go diff --git a/docs/resources/logdna_enterprise_key.md b/docs/resources/logdna_enterprise_key.md new file mode 100644 index 0000000..4ca948a --- /dev/null +++ b/docs/resources/logdna_enterprise_key.md @@ -0,0 +1,51 @@ +# Resource: `logdna_enterprise_key` + +This resource allows you to manage enterprise keys. + +## Example + +```hcl +provider "logdna" { + servicekey = "xxxxxxxxxxxxxxxxxxxxxxxx" +} + +resource "logdna_enterprise_key" "enterprise_service_key" { + lifecycle { + create_before_destroy = true + } +} +``` + +The `create_before_destroy` and `lifecycle` meta-argument are not required; however, these options ensure a valid key is always available when a key is being recreated. This helps avoid any disruptions in service. + +~> **NOTE:** We recommend prefixing the name of your terraform resources so they can be distinguished from other resources in the UI. + +## Key Rotation + +This resource can be used in conjuction with automated scripts to perform automatic key rotations, e.g., + +```sh +# Run this every time you want to rotate the key +$ terraform apply -replace="logdna_enterprise_key.my_key" +``` + +## Argument Reference + +This resource does not support any input arguments. + +## Attributes Reference + +In addition to all the arguments above, the following attributes are exported: + +- `id`: **string** The unique identifier of this key. +- `key`: **string** The actual key value. +- `name`: **string** The name of the key. +- `created`: **int** The date the key was created in Unix time milliseconds. + +## Import + +A key can be imported using the `id`, e.g., + +```sh +$ terraform import logdna_enterprise_key.my_key +``` diff --git a/logdna/request_test.go b/logdna/request_test.go index 54b99aa..dfdf2a8 100644 --- a/logdna/request_test.go +++ b/logdna/request_test.go @@ -121,6 +121,32 @@ func TestRequest_MakeRequest(t *testing.T) { assert.Nil(err, "No errors") }) + t.Run("Server receives proper method, URL, and headers for enterprise org", func(t *testing.T) { + enterprisePC := providerConfig{serviceKey: "abc123", orgType: OrgTypeEnterprise, httpClient: &http.Client{Timeout: 15 * time.Second}} + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal("GET", r.Method, "method is correct") + assert.Equal(fmt.Sprintf("/someapi/%s", resourceID), r.URL.String(), "URL is correct") + key, ok := r.Header["Enterprise-Servicekey"] + assert.Equal(true, ok, "enterprise-servicekey header exists") + assert.Equal(1, len(key), "enterprise-servicekey header is correct") + key = r.Header["Content-Type"] + assert.Equal("application/json", key[0], "content-type header is correct") + })) + defer ts.Close() + + enterprisePC.baseURL = ts.URL + + req := newRequestConfig( + &enterprisePC, + "GET", + fmt.Sprintf("/someapi/%s", resourceID), + nil, + ) + + _, err := req.MakeRequest() + assert.Nil(err, "No errors") + }) + t.Run("Reads and decodes response from the server", func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := json.NewEncoder(w).Encode(viewResponse{ViewID: "test123456"}) diff --git a/logdna/resource_enterprise_key.go b/logdna/resource_enterprise_key.go new file mode 100644 index 0000000..bed9af3 --- /dev/null +++ b/logdna/resource_enterprise_key.go @@ -0,0 +1,156 @@ +package logdna + +import ( + "context" + "encoding/json" + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var _ = registerTerraform(TerraformInfo{ + name: "logdna_enterprise_key", + orgType: OrgTypeEnterprise, + terraformType: TerraformTypeResource, + schema: resourceEnterpriseKey(), +}) + +func resourceEnterpriseKeyCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var diags diag.Diagnostics + pc := m.(*providerConfig) + + key := keyRequest{} + + if diags = key.CreateRequestBody(d); diags.HasError() { + return diags + } + + req := newRequestConfig( + pc, + "POST", + "/v1/enterprise/keys", + nil, + ) + + body, err := req.MakeRequest() + log.Printf("[DEBUG] %s %s, payload is: %s", req.method, req.apiURL, body) + + if err != nil { + return diag.FromErr(err) + } + + createdKey := keyResponse{} + err = json.Unmarshal(body, &createdKey) + if err != nil { + return diag.FromErr(err) + } + log.Printf("[DEBUG] After %s key, the created key is %+v", req.method, createdKey) + + d.SetId(createdKey.KeyID) + + return resourceEnterpriseKeyRead(ctx, d, m) +} + +func resourceEnterpriseKeyRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var diags diag.Diagnostics + + pc := m.(*providerConfig) + keyID := d.Id() + + req := newRequestConfig( + pc, + "GET", + fmt.Sprintf("/v1/enterprise/keys/%s", keyID), + nil, + ) + + body, err := req.MakeRequest() + + log.Printf("[DEBUG] GET key raw response body %s\n", body) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Cannot read the remote key resource", + Detail: err.Error(), + }) + return diags + } + + key := keyResponse{} + err = json.Unmarshal(body, &key) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Cannot unmarshal response from the remote key resource", + Detail: err.Error(), + }) + return diags + } + log.Printf("[DEBUG] The GET key structure is as follows: %+v\n", key) + + // Top level keys can be set directly + appendError(d.Set("name", key.Name), &diags) + appendError(d.Set("id", key.KeyID), &diags) + appendError(d.Set("key", key.Key), &diags) + appendError(d.Set("created", key.Created), &diags) + + return diags +} + +func resourceEnterpriseKeyDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + pc := m.(*providerConfig) + keyID := d.Id() + + req := newRequestConfig( + pc, + "DELETE", + fmt.Sprintf("/v1/enterprise/keys/%s", keyID), + nil, + ) + + body, err := req.MakeRequest() + log.Printf("[DEBUG] %s %s key %s", req.method, req.apiURL, body) + + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + return nil +} + +func resourceEnterpriseKey() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceEnterpriseKeyCreate, + ReadContext: resourceEnterpriseKeyRead, + DeleteContext: resourceEnterpriseKeyDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "id": { + Type: schema.TypeString, + ForceNew: true, + Computed: true, + }, + "key": { + Type: schema.TypeString, + ForceNew: true, + Sensitive: true, + Computed: true, + }, + "created": { + Type: schema.TypeInt, + ForceNew: true, + Computed: true, + }, + }, + } +}