-
Notifications
You must be signed in to change notification settings - Fork 11
feat: add project API key resource #147
Changes from 2 commits
08dbfd4
0a4853c
5101df2
d57378b
85e85ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package metal | ||
|
||
import ( | ||
"log" | ||
|
||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" | ||
"github.com/packethost/packngo" | ||
) | ||
|
||
func metalProjectAPIKey() map[string]*schema.Schema { | ||
return map[string]*schema.Schema{ | ||
"project_id": { | ||
Type: schema.TypeString, | ||
ForceNew: true, | ||
Required: true, | ||
}, | ||
"read_only": { | ||
Type: schema.TypeBool, | ||
Default: true, | ||
ForceNew: true, | ||
Optional: true, | ||
}, | ||
"description": { | ||
Type: schema.TypeString, | ||
Required: true, | ||
}, | ||
} | ||
} | ||
|
||
func resourceMetalProjectAPIKey() *schema.Resource { | ||
return &schema.Resource{ | ||
Create: resourceMetalProjectAPIKeyCreate, | ||
Read: resourceMetalProjectAPIKeyRead, | ||
Update: resourceMetalProjectAPIKeyUpdate, | ||
Delete: resourceMetalProjectAPIKeyDelete, | ||
Importer: &schema.ResourceImporter{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It can be solved by parsing both IDs from a single string "<project_id>:<key_id>" (https://www.terraform.io/docs/extend/resources/import.html#importer-state-function), and implementing custom Resource Importer, but I'm not sure if we need import of this resource right here right now. Maybe it's better if you just remove the importer (for the project_api_key). |
||
State: schema.ImportStatePassthrough, | ||
}, | ||
|
||
Schema: metalProjectAPIKey(), | ||
} | ||
} | ||
|
||
func resourceMetalProjectAPIKeyCreate(d *schema.ResourceData, meta interface{}) error { | ||
client := meta.(*packngo.Client) | ||
|
||
createRequest := &packngo.APIKeyCreateRequest{ | ||
ProjectID: d.Get("project_id").(string), | ||
ReadOnly: d.Get("read_only").(bool), | ||
Description: d.Get("description").(string), | ||
} | ||
|
||
apiKey, _, err := client.APIKeys.Create(createRequest) | ||
if err != nil { | ||
return friendlyError(err) | ||
} | ||
|
||
d.SetId(apiKey.ID) | ||
|
||
return resourceMetalProjectAPIKeyRead(d, meta) | ||
} | ||
|
||
func resourceMetalProjectAPIKeyRead(d *schema.ResourceData, meta interface{}) error { | ||
client := meta.(*packngo.Client) | ||
|
||
apiKey, err := client.APIKeys.ProjectGet(d.Id(), d.Get("project_id").(string), nil) | ||
if err != nil { | ||
err = friendlyError(err) | ||
|
||
// If the key is somehow already destroyed, mark as | ||
// succesfully gone | ||
if isNotFound(err) { | ||
log.Printf("[WARN] Project APIKey (%s) not found, removing from state", d.Id()) | ||
d.SetId("") | ||
return nil | ||
} | ||
|
||
return err | ||
} | ||
|
||
d.SetId(apiKey.ID) | ||
d.Set("project_id", apiKey.Project.ID) | ||
d.Set("description", apiKey.Description) | ||
d.Set("read_only", apiKey.ReadOnly) | ||
d.Set("created", apiKey.Created) | ||
d.Set("updated", apiKey.Updated) | ||
|
||
return nil | ||
} | ||
|
||
func resourceMetalProjectAPIKeyUpdate(d *schema.ResourceData, meta interface{}) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update of API key is impossible. Remove the Update method. |
||
return resourceMetalProjectAPIKeyRead(d, meta) | ||
} | ||
|
||
func resourceMetalProjectAPIKeyDelete(d *schema.ResourceData, meta interface{}) error { | ||
client := meta.(*packngo.Client) | ||
|
||
resp, err := client.APIKeys.Delete(d.Id()) | ||
if ignoreResponseErrors(httpForbidden, httpNotFound)(resp, err) != nil { | ||
return friendlyError(err) | ||
} | ||
|
||
d.SetId("") | ||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
package metal | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform" | ||
"github.com/packethost/packngo" | ||
) | ||
|
||
func metalProjectAPIKeyConfig_Basic(description string) string { | ||
return fmt.Sprintf(` | ||
resource "metal_project" "test" { | ||
name = "tfacc-project_api_key-%s" | ||
} | ||
|
||
resource "metal_project_api_key" "test-ro" { | ||
description = "tfacc-project-api-key-ro-test" | ||
project_id = "${metal_project.test.id}" | ||
} | ||
|
||
resource "metal_project_api_key" "test-rw" { | ||
description = "tfacc-project-api-key-rw-test" | ||
read_only = false | ||
project_id = "${metal_project.test.id}" | ||
} | ||
`, description) | ||
} | ||
|
||
func TestAccMetalProjectAPIKey_Basic(t *testing.T) { | ||
rs := acctest.RandString(10) | ||
var key packngo.APIKey | ||
|
||
cfg := metalProjectAPIKeyConfig_Basic(rs) | ||
|
||
resource.Test(t, resource.TestCase{ | ||
PreCheck: func() { testAccPreCheck(t) }, | ||
Providers: testAccProviders, | ||
CheckDestroy: testAccCheckMetalProjectAPIKeyDestroy, | ||
Steps: []resource.TestStep{ | ||
{ | ||
Config: cfg, | ||
Check: resource.ComposeTestCheckFunc( | ||
testAccCheckMetalProjectAPIKeyExists("metal_project_api_key.test-ro", &key), | ||
resource.TestCheckResourceAttr( | ||
"metal_project_api_key.test", "read_only", "true"), | ||
resource.TestCheckResourceAttr( | ||
"metal_project_api_key.test", "description", "tfacc-project-api-key-ro-test"), | ||
), | ||
}, | ||
{ | ||
Config: cfg, | ||
Check: resource.ComposeTestCheckFunc( | ||
testAccCheckMetalProjectAPIKeyExists("metal_project_api_key.test-rw", &key), | ||
resource.TestCheckResourceAttr( | ||
"metal_project_api_key.test", "read_only", "false"), | ||
resource.TestCheckResourceAttr( | ||
"metal_project_api_key.test", "description", "tfacc-project-api-key-rw-test"), | ||
), | ||
}, | ||
}, | ||
}) | ||
} | ||
|
||
func testAccCheckMetalProjectAPIKeyDestroy(s *terraform.State) error { | ||
client := testAccProvider.Meta().(*packngo.Client) | ||
|
||
for _, rs := range s.RootModule().Resources { | ||
if rs.Type != "metal_project_api_key" { | ||
continue | ||
} | ||
|
||
if _, err := client.APIKeys.ProjectGet(rs.Primary.Attributes["project_id"], rs.Primary.ID, nil); err == nil { | ||
return fmt.Errorf("Project API key still exists") | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func testAccCheckMetalProjectAPIKeyExists(n string, key *packngo.APIKey) 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 Record ID is set") | ||
} | ||
|
||
client := testAccProvider.Meta().(*packngo.Client) | ||
|
||
foundAPIKey, err := client.APIKeys.ProjectGet(key.Project.ID, rs.Primary.ID, nil) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if foundAPIKey.ID != rs.Primary.ID { | ||
return fmt.Errorf("Project API Key not found: %v - %v", rs.Primary.ID, foundAPIKey.ID) | ||
} | ||
|
||
*key = *foundAPIKey | ||
|
||
return nil | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this field were not required, this resource could be
metal_api_key
with optional project scoping.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, I recognize that we have both
metal_project_ssh_key
andmetal_ssh_key
.I'm not sure that I see a reason for the distinction, then or now.
project_ssh_keys
came a few years afterssh_keys
.https://github.com/equinix/terraform-provider-metal/blob/main/metal/resource_metal_project_ssh_key.go
When I was first introduced to the API, I found some justification for this because some resources are created behind /projects/id/foo, while others are not. After a bit more exposure to the API, I find that resources (project or not) are all typically accessible behind /foo/id. Project scoped or not, these optionally scoped resources have the same response body, other than the project id field.
When a resource is project scoped, the API path contains the (optional) project_id rather than the POST body. If a listing endpoint is present in /projects/id/foo, it is offered as a convenience to /foo?project_id=id.
They are otherwise the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rawkode @displague I think we can do both
metal_user_api_key
andmetal_project_api_key
in a similar way as the ssh_keys.Just make the project_id
Optional: true
. Create and Delete methods can be shared betweenmetal_user_api_key
andmetal_project_api_key
. You will need to have separate Read methods.@displague I dived to the API key methods and created equinixmetal-archive/packngo#297
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For what it's worth, I did consider using a single resource for both and I do prefer that API, but unfortunately the backing API at EM doesn't provide a single read endpoint. I did consider working around that, but importing a resource wouldn't work and I thought that was more important. Perhaps we can work with engineering to clean up this API. 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rawkode you're right that the read endpoint for user API key is different. You could create a resource for the API key, reusing some of the project API key code. If you'd change the project_id to
Optional: true
, you could just addresource_metal_user_api_key.go
as.. the user API key resource would actually be importable.
If you don't want to do it in this PR, it's OK we can add
metal_user_api_key
later. Thsis is good to go after you remove the Import and the Update method.