Skip to content

Commit 1a18e90

Browse files
authored
Add vcfa_api_token resource (#22)
Signed-off-by: abarreiro <abarreiro@vmware.com>
1 parent 110cde6 commit 1a18e90

File tree

7 files changed

+356
-2
lines changed

7 files changed

+356
-2
lines changed

.changes/v1.0.0/22-features.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* **New Resource:** `vcfa_api_token` to manage API Tokens [GH-22]

vcfa/provider.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ var globalResourceMap = map[string]*schema.Resource{
7474
"vcfa_org_oidc": resourceVcfaOrgOidc(), // 1.0
7575
"vcfa_rights_bundle": resourceVcfaRightsBundle(), // 1.0
7676
"vcfa_role": resourceVcfaRole(), // 1.0
77+
"vcfa_api_token": resourceVcfaApiToken(), // 1.0
7778
}
7879

7980
// Provider returns a terraform.ResourceProvider.
@@ -116,14 +117,14 @@ func Provider() *schema.Provider {
116117
Type: schema.TypeString,
117118
Optional: true,
118119
DefaultFunc: schema.EnvDefaultFunc("VCFA_API_TOKEN", nil),
119-
Description: "The API token used instead of username/password for VCFA API operations. (Requires VCFA 10.3.1+)",
120+
Description: "The API token used instead of username/password for VCFA API operations",
120121
},
121122

122123
"api_token_file": {
123124
Type: schema.TypeString,
124125
Optional: true,
125126
DefaultFunc: schema.EnvDefaultFunc("VCFA_API_TOKEN_FILE", nil),
126-
Description: "The API token file instead of username/password for VCFA API operations. (Requires VCFA 10.3.1+)",
127+
Description: "The API token file instead of username/password for VCFA API operations",
127128
},
128129

129130
"allow_api_token_file": {

vcfa/resource_vcfa_api_token.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package vcfa
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/hashicorp/go-cty/cty"
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9+
"github.com/vmware/go-vcloud-director/v3/govcd"
10+
"log"
11+
)
12+
13+
const labelVcfaApiToken = "API Token"
14+
15+
func resourceVcfaApiToken() *schema.Resource {
16+
return &schema.Resource{
17+
CreateContext: resourceVcfaApiTokenCreate,
18+
ReadContext: resourceVcfaApiTokenRead,
19+
DeleteContext: resourceVcfaApiTokenDelete,
20+
Importer: &schema.ResourceImporter{
21+
StateContext: resourceVcfaApiTokenImport,
22+
},
23+
24+
Schema: map[string]*schema.Schema{
25+
"name": {
26+
Type: schema.TypeString,
27+
Required: true,
28+
ForceNew: true,
29+
Description: fmt.Sprintf("Name of %s", labelVcfaApiToken),
30+
},
31+
"file_name": {
32+
Type: schema.TypeString,
33+
Required: true,
34+
ForceNew: true,
35+
Description: fmt.Sprintf("Name of the file that the %s will be saved to", labelVcfaApiToken),
36+
},
37+
"allow_token_file": {
38+
Type: schema.TypeBool,
39+
Required: true,
40+
ForceNew: true,
41+
Description: fmt.Sprintf("Set this to true if you understand the security risks of using"+
42+
" %s files and agree to creating them", labelVcfaApiToken),
43+
ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics {
44+
value := i.(bool)
45+
if !value {
46+
return diag.Diagnostics{
47+
diag.Diagnostic{
48+
Severity: diag.Error,
49+
Summary: "This field must be set to true",
50+
Detail: fmt.Sprintf("The %s file should be considered SENSITIVE INFORMATION. "+
51+
"If you acknowledge that, set 'allow_token_file' to 'true'.", labelVcfaApiToken),
52+
AttributePath: path,
53+
},
54+
}
55+
}
56+
return nil
57+
},
58+
},
59+
},
60+
}
61+
}
62+
63+
func resourceVcfaApiTokenCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
64+
vcdClient := meta.(*VCDClient)
65+
66+
// System Admin can't create API tokens outside SysOrg,
67+
// just as Org admins can't create API tokens in other Orgs
68+
org := vcdClient.SysOrg
69+
if org == "" {
70+
org = vcdClient.Org
71+
}
72+
73+
tokenName := d.Get("name").(string)
74+
token, err := vcdClient.CreateToken(org, tokenName)
75+
if err != nil {
76+
return diag.Errorf("[%s create] error creating %s: %s", labelVcfaApiToken, labelVcfaApiToken, err)
77+
}
78+
d.SetId(token.Token.ID)
79+
80+
apiToken, err := token.GetInitialApiToken()
81+
if err != nil {
82+
return diag.Errorf("[%s create] error getting refresh token from %s: %s", labelVcfaApiToken, labelVcfaApiToken, err)
83+
}
84+
85+
filename := d.Get("file_name").(string)
86+
87+
err = govcd.SaveApiTokenToFile(filename, vcdClient.Client.UserAgent, apiToken)
88+
if err != nil {
89+
return diag.Errorf("[%s create] error saving %s to file: %s", labelVcfaApiToken, labelVcfaApiToken, err)
90+
}
91+
92+
return resourceVcfaApiTokenRead(ctx, d, meta)
93+
}
94+
95+
func resourceVcfaApiTokenRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
96+
vcdClient := meta.(*VCDClient)
97+
98+
token, err := vcdClient.GetTokenById(d.Id())
99+
if govcd.ContainsNotFound(err) {
100+
d.SetId("")
101+
log.Printf("[DEBUG] %s no longer exists. Removing from tfstate", labelVcfaApiToken)
102+
}
103+
if err != nil {
104+
return diag.Errorf("[%s read] error getting %s: %s", labelVcfaApiToken, labelVcfaApiToken, err)
105+
}
106+
107+
d.SetId(token.Token.ID)
108+
dSet(d, "name", token.Token.Name)
109+
110+
return nil
111+
}
112+
113+
func resourceVcfaApiTokenDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
114+
vcdClient := meta.(*VCDClient)
115+
116+
token, err := vcdClient.GetTokenById(d.Id())
117+
if err != nil {
118+
return diag.Errorf("[%s delete] error getting %s: %s", labelVcfaApiToken, labelVcfaApiToken, err)
119+
}
120+
121+
err = token.Delete()
122+
if err != nil {
123+
return diag.Errorf("[%s delete] error deleting %s: %s", labelVcfaApiToken, labelVcfaApiToken, err)
124+
}
125+
126+
return nil
127+
}
128+
129+
func resourceVcfaApiTokenImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
130+
log.Printf("[TRACE] %s import initiated", labelVcfaApiToken)
131+
132+
vcdClient := meta.(*VCDClient)
133+
sessionInfo, err := vcdClient.Client.GetSessionInfo()
134+
if err != nil {
135+
return []*schema.ResourceData{}, fmt.Errorf("[%s import] error getting username: %s", labelVcfaApiToken, err)
136+
}
137+
138+
token, err := vcdClient.GetTokenByNameAndUsername(d.Id(), sessionInfo.User.Name)
139+
if err != nil {
140+
return []*schema.ResourceData{}, fmt.Errorf("[%s import] error getting %s by name: %s", labelVcfaApiToken, labelVcfaApiToken, err)
141+
}
142+
143+
d.SetId(token.Token.ID)
144+
dSet(d, "name", token.Token.Name)
145+
146+
return []*schema.ResourceData{d}, nil
147+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
//go:build api || ALL || functional
2+
3+
package vcfa
4+
5+
import (
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"testing"
10+
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
12+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
13+
)
14+
15+
// TODO: TM: Review whether this test should be skipped when an API Token or service account
16+
// is provided instead of user + password, in test configuration
17+
func TestAccVcfaApiToken(t *testing.T) {
18+
preTestChecks(t)
19+
20+
var params = StringMap{
21+
"TokenName": t.Name(),
22+
"FileName": t.Name(),
23+
}
24+
testParamsNotEmpty(t, params)
25+
26+
filename := params["FileName"].(string)
27+
28+
configText := templateFill(testAccVcfaApiToken, params)
29+
if vcfaShortTest {
30+
t.Skip(acceptanceTestsSkipped)
31+
return
32+
}
33+
t.Cleanup(deleteApiTokenFile(filename, t))
34+
debugPrintf("#[DEBUG] CONFIGURATION: %s", configText)
35+
36+
resourceName := "vcfa_api_token.custom"
37+
resource.Test(t, resource.TestCase{
38+
ProviderFactories: testAccProviders,
39+
CheckDestroy: testAccCheckApiTokenDestroy(params["TokenName"].(string)),
40+
Steps: []resource.TestStep{
41+
{
42+
Config: configText,
43+
Check: resource.ComposeTestCheckFunc(
44+
resource.TestCheckResourceAttr(resourceName, "name", t.Name()),
45+
testCheckFileExists(params["FileName"].(string)),
46+
),
47+
},
48+
},
49+
})
50+
postTestChecks(t)
51+
}
52+
53+
// #nosec G101 -- No hardcoded credentials here
54+
const testAccVcfaApiToken = `
55+
resource "vcfa_api_token" "custom" {
56+
name = "{{.TokenName}}"
57+
58+
file_name = "{{.FileName}}"
59+
allow_token_file = true
60+
}
61+
`
62+
63+
// This is a helper function that attempts to remove created API token file no matter of the test outcome
64+
func deleteApiTokenFile(filename string, t *testing.T) func() {
65+
return func() {
66+
err := os.Remove(filename)
67+
if err != nil {
68+
t.Errorf("Failed to delete file: %s", err)
69+
}
70+
}
71+
}
72+
73+
func testCheckFileExists(filename string) resource.TestCheckFunc {
74+
return func(s *terraform.State) error {
75+
filename = filepath.Clean(filename)
76+
_, err := os.ReadFile(filename)
77+
if err != nil {
78+
return err
79+
}
80+
return nil
81+
}
82+
}
83+
84+
func testAccCheckApiTokenDestroy(tokenName string) resource.TestCheckFunc {
85+
return func(s *terraform.State) error {
86+
conn := testAccProvider.Meta().(*VCDClient)
87+
88+
for _, rs := range s.RootModule().Resources {
89+
if rs.Type != "vcfa_api_token" || rs.Primary.Attributes["name"] != tokenName {
90+
continue
91+
}
92+
93+
_, err := conn.GetTokenById(rs.Primary.ID)
94+
if err == nil {
95+
return fmt.Errorf("error: %s still exists post-destroy", labelVcfaApiToken)
96+
}
97+
98+
return nil
99+
}
100+
101+
return nil
102+
}
103+
}

website/docs/index.html.markdown

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,51 @@ data "vcfa_tm_version" "version" {
5151
condition = ">= 10.7.0"
5252
fail_if_not_match = false
5353
}
54+
```
55+
56+
## Example usage (API token)
57+
58+
```hcl
59+
provider "vcfa" {
60+
api_token = var.api_token
61+
auth_type = "api_token"
62+
org = "System"
63+
url = var.vcfa_url
64+
allow_unverified_ssl = var.vcfa_allow_unverified_ssl
65+
logging = true # Enables logging
66+
logging_file = "vcfa.log"
67+
}
68+
69+
# Fetch the Tenant Manager version
70+
data "vcfa_tm_version" "version" {
71+
condition = ">= 10.7.0"
72+
fail_if_not_match = false
73+
}
74+
```
75+
76+
## Example usage (API token file)
5477

78+
```hcl
79+
provider "vcfa" {
80+
api_token_file = "token.json"
81+
auth_type = "api_token_file"
82+
org = "System"
83+
url = var.vcfa_url
84+
allow_unverified_ssl = var.vcfa_allow_unverified_ssl
85+
logging = true # Enables logging
86+
logging_file = "vcfa.log"
87+
}
88+
89+
# Fetch the Tenant Manager version
90+
data "vcfa_tm_version" "version" {
91+
condition = ">= 10.7.0"
92+
fail_if_not_match = false
93+
}
5594
```
5695

96+
The file containing the API Token can be generated by using the
97+
[`api_token`](/providers/vmware/vcfa/latest/docs/resources/api_token) resource.
98+
5799
## Argument Reference
58100

59101
The following arguments are used to configure the VMware Cloud Foundation Automation Provider:
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
layout: "vcfa"
3+
page_title: "VMware Cloud Foundation Automation: vcfa_api_token"
4+
sidebar_current: "docs-vcfa-resource-api-token"
5+
description: |-
6+
Provides a resource to manage API Tokens. API Tokens are an easy way to authenticate to VCFA.
7+
They are user-based and have the same role as the user.
8+
---
9+
10+
# vcfa\_api\_token
11+
12+
Provides a resource to manage API Tokens. API Tokens are an easy way to authenticate to VCFA.
13+
They are user-based and have the same role as the user.
14+
15+
## Example usage
16+
17+
```hcl
18+
resource "vcfa_api_token" "example_token" {
19+
name = "example_token"
20+
file_name = "example_token.json"
21+
allow_token_file = true
22+
}
23+
```
24+
25+
## Argument reference
26+
27+
The following arguments are supported:
28+
29+
* `name` - (Required) The unique name of the API Token for a specific user.
30+
* `file_name` - (Required) The name of the file which will be created containing the API Token. The file will have the following
31+
JSON contents:
32+
```json
33+
{
34+
"token_type": "API Token",
35+
"refresh_token": "24JVMAQvaayIDuA7wayPPfa376mrfraB",
36+
"updated_by": "terraform-provider-vcfa/ (darwin/amd64; isProvider:true)",
37+
"updated_on": "2025-01-29T10:00:43+01:00"
38+
}
39+
```
40+
* `allow_token_file` - (Required) An additional check that the user is aware that the file contains
41+
SENSITIVE information. Must be set to `true` or it will return a validation error.
42+
43+
## Importing
44+
45+
~> **Note:** The current implementation of Terraform import can only import resources into the state. It does not generate
46+
configuration. However, an experimental feature in Terraform 1.5+ allows also code generation.
47+
See [Importing resources][importing-resources] for more information.
48+
49+
An existing API Token can be [imported][docs-import] into this resource via supplying
50+
the full dot separated path. An example is below:
51+
52+
```
53+
terraform import vcfa_api_token.example_token example_token
54+
```
55+
56+
[docs-import]: https://www.terraform.io/docs/import/
57+
[provider-api-token-file]: /providers/vmware/vcfa/latest/docs#api_token_file

website/vcfa.erb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@
143143
<li<%= sidebar_current("docs-vcfa-resource-role") %>>
144144
<a href="/docs/providers/vcfa/r/role.html">vcfa_role</a>
145145
</li>
146+
<li<%= sidebar_current("docs-vcfa-resource-api-token") %>>
147+
<a href="/docs/providers/vcfa/r/api_token.html">vcfa_api_token</a>
148+
</li>
146149
</ul>
147150
</li>
148151
</ul>

0 commit comments

Comments
 (0)