mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-06-30 02:31:10 +00:00
feat(access): add proxmox user token (#1159)
This commit is contained in:
parent
41ec34e04b
commit
8220271eee
61
docs/resources/virtual_environment_user_token.md
Normal file
61
docs/resources/virtual_environment_user_token.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
layout: page
|
||||||
|
title: proxmox_virtual_environment_user_token
|
||||||
|
parent: Resources
|
||||||
|
subcategory: Virtual Environment
|
||||||
|
description: |-
|
||||||
|
User API tokens.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Resource: proxmox_virtual_environment_user_token
|
||||||
|
|
||||||
|
User API tokens.
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```terraform
|
||||||
|
# if creating a user token, the user must be created first
|
||||||
|
resource "proxmox_virtual_environment_user" "user" {
|
||||||
|
comment = "Managed by Terraform"
|
||||||
|
email = "user@pve"
|
||||||
|
enabled = true
|
||||||
|
expiration_date = "2034-01-01T22:00:00Z"
|
||||||
|
user_id = "user@pve"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "proxmox_virtual_environment_user_token" "user_token" {
|
||||||
|
comment = "Managed by Terraform"
|
||||||
|
expiration_date = "2033-01-01T22:00:00Z"
|
||||||
|
token_name = "tk1"
|
||||||
|
user_id = proxmox_virtual_environment_user.user.user_id
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- schema generated by tfplugindocs -->
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
### Required
|
||||||
|
|
||||||
|
- `token_name` (String) User-specific token identifier.
|
||||||
|
- `user_id` (String) User identifier.
|
||||||
|
|
||||||
|
### Optional
|
||||||
|
|
||||||
|
- `comment` (String) Comment for the token.
|
||||||
|
- `expiration_date` (String) Expiration date for the token.
|
||||||
|
- `privileges_separation` (Boolean) Restrict API token privileges with separate ACLs (default), or give full privileges of corresponding user.
|
||||||
|
|
||||||
|
### Read-Only
|
||||||
|
|
||||||
|
- `id` (String) Unique token identifier with format `<user_id>!<token_name>`.
|
||||||
|
- `value` (String, Sensitive) API token value used for authentication. It is populated only when creating a new token, and can't be retrieved at import.
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
Import is supported using the following syntax:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
#Tokens can be imported using they identifiers in format `user_id!token_name` format, e.g.:
|
||||||
|
terraform import proxmox_virtual_environment_user_token.token1 user@pve!token1
|
||||||
|
```
|
@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
#Tokens can be imported using they identifiers in format `user_id!token_name` format, e.g.:
|
||||||
|
terraform import proxmox_virtual_environment_user_token.token1 user@pve!token1
|
@ -0,0 +1,15 @@
|
|||||||
|
# if creating a user token, the user must be created first
|
||||||
|
resource "proxmox_virtual_environment_user" "user" {
|
||||||
|
comment = "Managed by Terraform"
|
||||||
|
email = "user@pve"
|
||||||
|
enabled = true
|
||||||
|
expiration_date = "2034-01-01T22:00:00Z"
|
||||||
|
user_id = "user@pve"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "proxmox_virtual_environment_user_token" "user_token" {
|
||||||
|
comment = "Managed by Terraform"
|
||||||
|
expiration_date = "2033-01-01T22:00:00Z"
|
||||||
|
token_name = "tk1"
|
||||||
|
user_id = proxmox_virtual_environment_user.user.user_id
|
||||||
|
}
|
306
fwprovider/access/resource_user_token.go
Normal file
306
fwprovider/access/resource_user_token.go
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
package access
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
|
||||||
|
"github.com/bpg/terraform-provider-proxmox/fwprovider/structure"
|
||||||
|
"github.com/bpg/terraform-provider-proxmox/fwprovider/validators"
|
||||||
|
"github.com/bpg/terraform-provider-proxmox/proxmox"
|
||||||
|
"github.com/bpg/terraform-provider-proxmox/proxmox/access"
|
||||||
|
proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &userTokenResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &userTokenResource{}
|
||||||
|
_ resource.ResourceWithImportState = &userTokenResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type userTokenResource struct {
|
||||||
|
client proxmox.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type userTokenModel struct {
|
||||||
|
Comment types.String `tfsdk:"comment"`
|
||||||
|
ExpirationDate types.String `tfsdk:"expiration_date"`
|
||||||
|
ID types.String `tfsdk:"id"`
|
||||||
|
PrivSeparation types.Bool `tfsdk:"privileges_separation"`
|
||||||
|
UserID types.String `tfsdk:"user_id"`
|
||||||
|
TokenName types.String `tfsdk:"token_name"`
|
||||||
|
Value types.String `tfsdk:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserTokenResource creates a new user token resource.
|
||||||
|
func NewUserTokenResource() resource.Resource {
|
||||||
|
return &userTokenResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userTokenResource) Schema(
|
||||||
|
_ context.Context,
|
||||||
|
_ resource.SchemaRequest,
|
||||||
|
resp *resource.SchemaResponse,
|
||||||
|
) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
Description: "User API tokens.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"comment": schema.StringAttribute{
|
||||||
|
Description: "Comment for the token.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"expiration_date": schema.StringAttribute{
|
||||||
|
Description: "Expiration date for the token.",
|
||||||
|
Optional: true,
|
||||||
|
Validators: []validator.String{
|
||||||
|
validators.NewParseValidator(func(s string) (time.Time, error) {
|
||||||
|
return time.Parse(time.RFC3339, s)
|
||||||
|
}, "must be a valid RFC3339 date"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"id": structure.IDAttribute("Unique token identifier with format `<user_id>!<token_name>`."),
|
||||||
|
"privileges_separation": schema.BoolAttribute{
|
||||||
|
Description: "Restrict API token privileges with separate ACLs (default)",
|
||||||
|
MarkdownDescription: "Restrict API token privileges with separate ACLs (default), " +
|
||||||
|
"or give full privileges of corresponding user.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Default: booldefault.StaticBool(true),
|
||||||
|
},
|
||||||
|
"token_name": schema.StringAttribute{
|
||||||
|
Description: "User-specific token identifier.",
|
||||||
|
Required: true,
|
||||||
|
Validators: []validator.String{
|
||||||
|
stringvalidator.RegexMatches(regexp.MustCompile(`[A-Za-z][A-Za-z0-9.\-_]+`), "must be a valid token identifier"),
|
||||||
|
},
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"user_id": schema.StringAttribute{
|
||||||
|
Description: "User identifier.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"value": schema.StringAttribute{
|
||||||
|
Description: "API token value used for authentication.",
|
||||||
|
MarkdownDescription: "API token value used for authentication. It is populated only when creating a new token, " +
|
||||||
|
"and can't be retrieved at import.",
|
||||||
|
Computed: true,
|
||||||
|
Sensitive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userTokenResource) Configure(
|
||||||
|
_ context.Context,
|
||||||
|
req resource.ConfigureRequest,
|
||||||
|
resp *resource.ConfigureResponse,
|
||||||
|
) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, ok := req.ProviderData.(proxmox.Client)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *proxmox.Client, got: %T", req.ProviderData),
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.client = client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userTokenResource) Metadata(
|
||||||
|
_ context.Context,
|
||||||
|
req resource.MetadataRequest,
|
||||||
|
resp *resource.MetadataResponse,
|
||||||
|
) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_user_token"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userTokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var plan userTokenModel
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||||
|
|
||||||
|
body := access.UserTokenCreateRequestBody{
|
||||||
|
Comment: plan.Comment.ValueStringPointer(),
|
||||||
|
PrivSeparate: proxmoxtypes.CustomBoolPtr(plan.PrivSeparation.ValueBoolPointer()),
|
||||||
|
}
|
||||||
|
|
||||||
|
if !plan.ExpirationDate.IsNull() && plan.ExpirationDate.ValueString() != "" {
|
||||||
|
expirationDate, err := time.Parse(
|
||||||
|
time.RFC3339,
|
||||||
|
plan.ExpirationDate.ValueString(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error parsing expiration date", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
v := expirationDate.Unix()
|
||||||
|
body.ExpirationDate = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := r.client.Access().CreateUserToken(ctx, plan.UserID.ValueString(), plan.TokenName.ValueString(), &body)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error creating user token", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.ID = types.StringValue(plan.UserID.ValueString() + "!" + plan.TokenName.ValueString())
|
||||||
|
plan.Value = types.StringValue(value)
|
||||||
|
resp.State.Set(ctx, plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userTokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var state userTokenModel
|
||||||
|
diags := req.State.Get(ctx, &state)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := r.client.Access().GetUserToken(ctx, state.UserID.ValueString(), state.TokenName.ValueString())
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error reading user token", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Comment = types.StringPointerValue(data.Comment)
|
||||||
|
|
||||||
|
if data.ExpirationDate != nil {
|
||||||
|
dt := time.Unix(int64(*data.ExpirationDate), 0).UTC().Format(time.RFC3339)
|
||||||
|
state.ExpirationDate = types.StringValue(dt)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.PrivSeparation = types.BoolPointerValue(data.PrivSeparate.PointerBool())
|
||||||
|
|
||||||
|
diags = resp.State.Set(ctx, state)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userTokenResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var plan, state userTokenModel
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body := access.UserTokenUpdateRequestBody{
|
||||||
|
Comment: plan.Comment.ValueStringPointer(),
|
||||||
|
PrivSeparate: proxmoxtypes.CustomBoolPtr(plan.PrivSeparation.ValueBoolPointer()),
|
||||||
|
}
|
||||||
|
|
||||||
|
if !plan.ExpirationDate.IsNull() && plan.ExpirationDate.ValueString() != "" {
|
||||||
|
expirationDate, err := time.Parse(
|
||||||
|
time.RFC3339,
|
||||||
|
plan.ExpirationDate.ValueString(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error parsing expiration date", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
v := expirationDate.Unix()
|
||||||
|
body.ExpirationDate = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.client.Access().UpdateUserToken(ctx, plan.UserID.ValueString(), plan.TokenName.ValueString(), &body)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error creating user token", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.Value = types.StringNull()
|
||||||
|
|
||||||
|
resp.State.Set(ctx, plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userTokenResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
|
var state userTokenModel
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.client.Access().DeleteUserToken(ctx, state.UserID.ValueString(), state.TokenName.ValueString())
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error deleting user token", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.State.RemoveResource(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userTokenResource) ImportState(
|
||||||
|
ctx context.Context,
|
||||||
|
req resource.ImportStateRequest,
|
||||||
|
resp *resource.ImportStateResponse,
|
||||||
|
) {
|
||||||
|
idParts := strings.Split(req.ID, "!")
|
||||||
|
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Import Identifier",
|
||||||
|
fmt.Sprintf("Expected import identifier with format: 'user_id!token_name'. Got: %q", req.ID),
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := idParts[0]
|
||||||
|
tokenName := idParts[1]
|
||||||
|
|
||||||
|
data, err := r.client.Access().GetUserToken(ctx, userID, tokenName)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Error reading user token", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state := userTokenModel{
|
||||||
|
Comment: types.StringPointerValue(data.Comment),
|
||||||
|
ID: types.StringValue(req.ID),
|
||||||
|
PrivSeparation: types.BoolPointerValue(data.PrivSeparate.PointerBool()),
|
||||||
|
UserID: types.StringValue(userID),
|
||||||
|
TokenName: types.StringValue(tokenName),
|
||||||
|
Value: types.StringNull(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.ExpirationDate != nil {
|
||||||
|
state.ExpirationDate = types.StringValue(time.Unix(int64(*data.ExpirationDate), 0).UTC().Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
diags := resp.State.Set(ctx, state)
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
}
|
@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||||
|
|
||||||
|
"github.com/bpg/terraform-provider-proxmox/fwprovider/access"
|
||||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/ha"
|
"github.com/bpg/terraform-provider-proxmox/fwprovider/ha"
|
||||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/hardwaremapping"
|
"github.com/bpg/terraform-provider-proxmox/fwprovider/hardwaremapping"
|
||||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/network"
|
"github.com/bpg/terraform-provider-proxmox/fwprovider/network"
|
||||||
@ -446,6 +447,7 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc
|
|||||||
hardwaremapping.NewResourceUSB,
|
hardwaremapping.NewResourceUSB,
|
||||||
network.NewLinuxBridgeResource,
|
network.NewLinuxBridgeResource,
|
||||||
network.NewLinuxVLANResource,
|
network.NewLinuxVLANResource,
|
||||||
|
access.NewUserTokenResource,
|
||||||
vm.NewVMResource,
|
vm.NewVMResource,
|
||||||
NewClusterOptionsResource,
|
NewClusterOptionsResource,
|
||||||
NewDownloadFileResource,
|
NewDownloadFileResource,
|
||||||
|
@ -7,9 +7,15 @@
|
|||||||
package tests
|
package tests
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/brianvoe/gofakeit/v7"
|
||||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/bpg/terraform-provider-proxmox/proxmox/access"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAccResourceUser(t *testing.T) {
|
func TestAccResourceUser(t *testing.T) {
|
||||||
@ -17,45 +23,55 @@ func TestAccResourceUser(t *testing.T) {
|
|||||||
|
|
||||||
te := initTestEnvironment(t)
|
te := initTestEnvironment(t)
|
||||||
|
|
||||||
|
username := fmt.Sprintf("%s@pve", gofakeit.Username())
|
||||||
|
te.addTemplateVars(map[string]any{
|
||||||
|
"Username": username,
|
||||||
|
})
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
steps []resource.TestStep
|
steps []resource.TestStep
|
||||||
}{
|
}{
|
||||||
{"create and update user", []resource.TestStep{
|
{"create and update user", []resource.TestStep{
|
||||||
{
|
{
|
||||||
Config: `resource "proxmox_virtual_environment_user" "user1" {
|
Config: te.renderConfig(`resource "proxmox_virtual_environment_user" "user" {
|
||||||
comment = "Managed by Terraform"
|
comment = "Managed by Terraform"
|
||||||
email = "user1@pve"
|
email = "{{.Username}}"
|
||||||
enabled = true
|
enabled = true
|
||||||
expiration_date = "2034-01-01T22:00:00Z"
|
expiration_date = "2034-01-01T22:00:00Z"
|
||||||
first_name = "First"
|
first_name = "First"
|
||||||
last_name = "Last"
|
last_name = "Last"
|
||||||
user_id = "user1@pve"
|
user_id = "{{.Username}}"
|
||||||
}`,
|
}`),
|
||||||
Check: testResourceAttributes("proxmox_virtual_environment_user.user1", map[string]string{
|
Check: testResourceAttributes("proxmox_virtual_environment_user.user", map[string]string{
|
||||||
"comment": "Managed by Terraform",
|
"comment": "Managed by Terraform",
|
||||||
"email": "user1@pve",
|
"email": username,
|
||||||
"enabled": "true",
|
"enabled": "true",
|
||||||
"expiration_date": "2034-01-01T22:00:00Z",
|
"expiration_date": "2034-01-01T22:00:00Z",
|
||||||
"first_name": "First",
|
"first_name": "First",
|
||||||
"last_name": "Last",
|
"last_name": "Last",
|
||||||
"user_id": "user1@pve",
|
"user_id": username,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Config: `resource "proxmox_virtual_environment_user" "user1" {
|
Config: te.renderConfig(`resource "proxmox_virtual_environment_user" "user" {
|
||||||
enabled = false
|
enabled = false
|
||||||
expiration_date = "2035-01-01T22:00:00Z"
|
expiration_date = "2035-01-01T22:00:00Z"
|
||||||
user_id = "user1@pve"
|
user_id = "{{.Username}}"
|
||||||
first_name = "First One"
|
first_name = "First One"
|
||||||
}`,
|
}`),
|
||||||
Check: testResourceAttributes("proxmox_virtual_environment_user.user1", map[string]string{
|
Check: testResourceAttributes("proxmox_virtual_environment_user.user", map[string]string{
|
||||||
"enabled": "false",
|
"enabled": "false",
|
||||||
"expiration_date": "2035-01-01T22:00:00Z",
|
"expiration_date": "2035-01-01T22:00:00Z",
|
||||||
"first_name": "First One",
|
"first_name": "First One",
|
||||||
"user_id": "user1@pve",
|
"user_id": username,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ResourceName: "proxmox_virtual_environment_user.user",
|
||||||
|
ImportState: true,
|
||||||
|
ImportStateVerify: true,
|
||||||
|
},
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,3 +84,91 @@ func TestAccResourceUser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAccResourceUserToken(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
te := initTestEnvironment(t)
|
||||||
|
username := fmt.Sprintf("%s@pve", gofakeit.Username())
|
||||||
|
tokenName := gofakeit.Word()
|
||||||
|
|
||||||
|
te.addTemplateVars(map[string]any{
|
||||||
|
"Username": username,
|
||||||
|
"TokenName": tokenName,
|
||||||
|
})
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
preCheck func()
|
||||||
|
steps []resource.TestStep
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"create and update user token",
|
||||||
|
func() {
|
||||||
|
err := te.accessClient().CreateUser(context.Background(), &access.UserCreateRequestBody{
|
||||||
|
ID: username,
|
||||||
|
Password: gofakeit.Password(true, true, true, true, false, 8),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := te.accessClient().DeleteUser(context.Background(), username)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[]resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: te.renderConfig(`resource "proxmox_virtual_environment_user_token" "user_token" {
|
||||||
|
comment = "Managed by Terraform"
|
||||||
|
expiration_date = "2034-01-01T22:00:00Z"
|
||||||
|
token_name = "{{.TokenName}}"
|
||||||
|
user_id = "{{.Username}}"
|
||||||
|
}`),
|
||||||
|
Check: testResourceAttributes("proxmox_virtual_environment_user_token.user_token", map[string]string{
|
||||||
|
"comment": "Managed by Terraform",
|
||||||
|
"expiration_date": "2034-01-01T22:00:00Z",
|
||||||
|
"id": fmt.Sprintf("%s!%s", username, tokenName),
|
||||||
|
"user_id": username,
|
||||||
|
"value": fmt.Sprintf("%s!%s=.*", username, tokenName),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Config: te.renderConfig(`resource "proxmox_virtual_environment_user_token" "user_token" {
|
||||||
|
comment = "Managed by Terraform 2"
|
||||||
|
expiration_date = "2033-01-01T01:01:01Z"
|
||||||
|
privileges_separation = false
|
||||||
|
token_name = "{{.TokenName}}"
|
||||||
|
user_id = "{{.Username}}"
|
||||||
|
}`),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testResourceAttributes("proxmox_virtual_environment_user_token.user_token", map[string]string{
|
||||||
|
"comment": "Managed by Terraform 2",
|
||||||
|
"expiration_date": "2033-01-01T01:01:01Z",
|
||||||
|
"privileges_separation": "false",
|
||||||
|
"token_name": tokenName,
|
||||||
|
"user_id": username,
|
||||||
|
}),
|
||||||
|
testNoResourceAttributesSet("proxmox_virtual_environment_user_token.user_token", []string{
|
||||||
|
"value",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ResourceName: "proxmox_virtual_environment_user_token.user_token",
|
||||||
|
ImportState: true,
|
||||||
|
ImportStateVerify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
ProtoV6ProviderFactories: te.accProviders,
|
||||||
|
PreCheck: tt.preCheck,
|
||||||
|
Steps: tt.steps,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/bpg/terraform-provider-proxmox/proxmox/access"
|
||||||
sdkV2provider "github.com/bpg/terraform-provider-proxmox/proxmoxtf/provider"
|
sdkV2provider "github.com/bpg/terraform-provider-proxmox/proxmoxtf/provider"
|
||||||
|
|
||||||
"github.com/bpg/terraform-provider-proxmox/fwprovider"
|
"github.com/bpg/terraform-provider-proxmox/fwprovider"
|
||||||
@ -35,7 +36,7 @@ type testEnvironment struct {
|
|||||||
|
|
||||||
accProviders map[string]func() (tfprotov6.ProviderServer, error)
|
accProviders map[string]func() (tfprotov6.ProviderServer, error)
|
||||||
once sync.Once
|
once sync.Once
|
||||||
nc *nodes.Client
|
c api.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func initTestEnvironment(t *testing.T) *testEnvironment {
|
func initTestEnvironment(t *testing.T) *testEnvironment {
|
||||||
@ -109,8 +110,8 @@ func (e *testEnvironment) renderConfig(cfg string) string {
|
|||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *testEnvironment) nodeClient() *nodes.Client {
|
func (e *testEnvironment) client() api.Client {
|
||||||
if e.nc == nil {
|
if e.c == nil {
|
||||||
e.once.Do(
|
e.once.Do(
|
||||||
func() {
|
func() {
|
||||||
username := utils.GetAnyStringEnv("PROXMOX_VE_USERNAME")
|
username := utils.GetAnyStringEnv("PROXMOX_VE_USERNAME")
|
||||||
@ -128,21 +129,26 @@ func (e *testEnvironment) nodeClient() *nodes.Client {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := api.NewClient(creds, conn)
|
e.c, err = api.NewClient(creds, conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.nc = &nodes.Client{Client: client, NodeName: e.nodeName}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return e.nc
|
return e.c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *testEnvironment) accessClient() *access.Client {
|
||||||
|
return &access.Client{Client: e.client()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *testEnvironment) nodeClient() *nodes.Client {
|
||||||
|
return &nodes.Client{Client: e.client(), NodeName: e.nodeName}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *testEnvironment) nodeStorageClient() *storage.Client {
|
func (e *testEnvironment) nodeStorageClient() *storage.Client {
|
||||||
nodesClient := e.nodeClient()
|
return &storage.Client{Client: e.nodeClient(), StorageName: e.datastoreID}
|
||||||
return &storage.Client{Client: nodesClient, StorageName: e.datastoreID}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// testAccMuxProviders returns a map of mux servers for the acceptance tests.
|
// testAccMuxProviders returns a map of mux servers for the acceptance tests.
|
||||||
|
93
proxmox/access/user_token.go
Normal file
93
proxmox/access/user_token.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package access
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) userTokensPath(id string) string {
|
||||||
|
return fmt.Sprintf("%s/%s/token", c.usersPath(), url.PathEscape(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) userTokenPath(userid, id string) string {
|
||||||
|
return fmt.Sprintf("%s/%s", c.userTokensPath(userid), url.PathEscape(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUserToken creates a user token.
|
||||||
|
func (c *Client) CreateUserToken(
|
||||||
|
ctx context.Context,
|
||||||
|
userid string,
|
||||||
|
id string,
|
||||||
|
d *UserTokenCreateRequestBody,
|
||||||
|
) (string, error) {
|
||||||
|
resBody := &UserTokenCreateResponseBody{}
|
||||||
|
|
||||||
|
err := c.DoRequest(ctx, http.MethodPost, c.userTokenPath(userid, id), d, resBody)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error creating user token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resBody.Data.FullTokenID + "=" + resBody.Data.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUserToken deletes an user token.
|
||||||
|
func (c *Client) DeleteUserToken(ctx context.Context, userid string, id string) error {
|
||||||
|
err := c.DoRequest(ctx, http.MethodDelete, c.userTokenPath(userid, id), nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error deleting user token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserToken retrieves a user token.
|
||||||
|
func (c *Client) GetUserToken(ctx context.Context, userid string, id string) (*UserTokenGetResponseData, error) {
|
||||||
|
resBody := &UserTokenGetResponseBody{}
|
||||||
|
|
||||||
|
err := c.DoRequest(ctx, http.MethodGet, c.userTokenPath(userid, id), nil, resBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error retrieving user token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resBody.Data == nil {
|
||||||
|
return nil, api.ErrNoDataObjectInResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
return resBody.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUserTokens retrieves a list of user tokens.
|
||||||
|
func (c *Client) ListUserTokens(ctx context.Context, userid string) ([]*UserTokenListResponseData, error) {
|
||||||
|
resBody := &UserTokenListResponseBody{}
|
||||||
|
|
||||||
|
err := c.DoRequest(ctx, http.MethodGet, c.userTokensPath(userid), nil, resBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error listing user tokens: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resBody.Data == nil {
|
||||||
|
return nil, api.ErrNoDataObjectInResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
return resBody.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserToken updates the user token.
|
||||||
|
func (c *Client) UpdateUserToken(ctx context.Context, userid string, id string, d *UserTokenUpdateRequestBody) error {
|
||||||
|
err := c.DoRequest(ctx, http.MethodPut, c.userTokenPath(userid, id), d, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error updating user token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
58
proxmox/access/user_token_types.go
Normal file
58
proxmox/access/user_token_types.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package access
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserTokenCreateRequestBody contains the data for a user token create request.
|
||||||
|
type UserTokenCreateRequestBody struct {
|
||||||
|
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
|
||||||
|
ExpirationDate *int64 `json:"expire,omitempty" url:"expire,omitempty"`
|
||||||
|
PrivSeparate *types.CustomBool `json:"privsep,omitempty" url:"privsep,omitempty,int"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserTokenUpdateRequestBody contains the data for a user token update request.
|
||||||
|
type UserTokenUpdateRequestBody UserTokenCreateRequestBody
|
||||||
|
|
||||||
|
// UserTokenCreateResponseBody contains the body from a user token create response.
|
||||||
|
type UserTokenCreateResponseBody struct {
|
||||||
|
Data *UserTokenCreateResponseData `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserTokenCreateResponseData contains the data from a user token create response.
|
||||||
|
type UserTokenCreateResponseData struct {
|
||||||
|
// The full token id, format "<userid>!<tokenid>"
|
||||||
|
FullTokenID string `json:"full-tokenid"`
|
||||||
|
Info UserTokenGetResponseData `json:"info"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserTokenGetResponseBody contains the body from a user token get response.
|
||||||
|
type UserTokenGetResponseBody struct {
|
||||||
|
Data *UserTokenGetResponseData `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserTokenGetResponseData contains the data from a user token get response.
|
||||||
|
type UserTokenGetResponseData struct {
|
||||||
|
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
|
||||||
|
PrivSeparate *types.CustomBool `json:"privsep,omitempty" url:"privsep,omitempty,int"`
|
||||||
|
ExpirationDate *types.CustomInt64 `json:"expire,omitempty" url:"expire,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserTokenListResponseBody contains the body from a user token list response.
|
||||||
|
type UserTokenListResponseBody struct {
|
||||||
|
Data []*UserTokenListResponseData `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserTokenListResponseData contains the data from a user token list response.
|
||||||
|
type UserTokenListResponseData struct {
|
||||||
|
UserTokenGetResponseData
|
||||||
|
|
||||||
|
TokenID string `json:"tokenid"`
|
||||||
|
}
|
@ -45,4 +45,5 @@ import (
|
|||||||
//go:generate cp ../build/docs-gen/resources/virtual_environment_haresource.md ../docs/resources/
|
//go:generate cp ../build/docs-gen/resources/virtual_environment_haresource.md ../docs/resources/
|
||||||
//go:generate cp ../build/docs-gen/resources/virtual_environment_cluster_options.md ../docs/resources/
|
//go:generate cp ../build/docs-gen/resources/virtual_environment_cluster_options.md ../docs/resources/
|
||||||
//go:generate cp ../build/docs-gen/resources/virtual_environment_download_file.md ../docs/resources/
|
//go:generate cp ../build/docs-gen/resources/virtual_environment_download_file.md ../docs/resources/
|
||||||
|
//go:generate cp ../build/docs-gen/resources/virtual_environment_user_token.md ../docs/resources/
|
||||||
//go:generate cp ../build/docs-gen/resources/virtual_environment_vm2.md ../docs/resources/
|
//go:generate cp ../build/docs-gen/resources/virtual_environment_vm2.md ../docs/resources/
|
||||||
|
Loading…
Reference in New Issue
Block a user