Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add trust token resource #512

Merged
merged 3 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions docs/resources/trust_token.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# lxd_trust_token
MusicDin marked this conversation as resolved.
Show resolved Hide resolved

The `lxd_trust_token` resource allows you to request a new trust token.

~> **Note:** The LXD trust token resource cannot be used for the initial authentication
with the LXD server because LXD Terraform provider needs to be authenticated in order
to request trust tokens for other clients.

## Example Usage

```hcl
resource "lxd_trust_token" "token1" {
name = "mytoken"
}

output "token" {
value = lxd_trust_token.token1.token
}
```

## Argument Reference

* `name` - **Required** - Name of the token.

* `projects` - *Optional* - List of projects to restrict the token to.

* `remote` - *Optional* - The remote in which the resource will be created. If not provided,
the provider's default remote will be used.

* `trigger` - *Optional* - When to trigger the token generation. Possible values are `once` and `always` (if missing). Defaults to `once`.

## Attribute Reference

The following attributes are exported:

* `token` - The generated token.

* `expires_at` - Time at which the trust token expires. If token expiry is configured, the value will be in format `YYYY/MM/DD hh:mm TZ`.

## Trust token expiry

~> **Warning:** The provider is unable to differentiate between an expired and a consumed token.
If token generation is set to `once` and the token expires, it will not be regenerated.

Trust token expiry is defined in the server's configuration (`core.remote_token_expiry`).
If the setting is configured, `expires_at` attribute will be populated, otherwise, it will be empty.

For example, to set the token expiry to 20 minutes, run the following command:
```
lxc config set core.remote_token_expiry=20M
```

If trigger is set to `once` the token will not be regenerated on subsequent plan applies.
By setting the trigger to `always` ensures that the token is always present, and will be regenerated if missing.

## Notes

* Token's unique identifier is the operation ID and not the token name. Therefore, multiple tokens can exist with the same name.

* See the LXD [documentation](https://documentation.ubuntu.com/lxd/en/latest/authentication/#authentication-token) for more information on trust tokens.
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ func (p *LxdProvider) Resources(_ context.Context) []func() resource.Resource {
storage.NewStorageVolumeResource,
storage.NewStorageVolumeCopyResource,
truststore.NewTrustCertificateResource,
truststore.NewTrustTokenResource,
}
}

Expand Down
304 changes: 304 additions & 0 deletions internal/truststore/trust_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
package truststore

import (
"context"
"fmt"

lxd "github.com/canonical/lxd/client"
"github.com/canonical/lxd/shared/api"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"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/terraform-lxd/terraform-provider-lxd/internal/errors"
provider_config "github.com/terraform-lxd/terraform-provider-lxd/internal/provider-config"
)

type TrustTokenModel struct {
Name types.String `tfsdk:"name"`
Projects types.List `tfsdk:"projects"`
Remote types.String `tfsdk:"remote"`
Trigger types.String `tfsdk:"trigger"`

// Computed.
Token types.String `tfsdk:"token"`
ExpiresAt types.String `tfsdk:"expires_at"`
OperationID types.String `tfsdk:"operation_id"`
}

// TrustTokenResource represent LXD trust token resource.
type TrustTokenResource struct {
provider *provider_config.LxdProviderConfig
}

// NewTrustTokenResource returns a new trust token resource.
func NewTrustTokenResource() resource.Resource {
return &TrustTokenResource{}
}

func (r TrustTokenResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = fmt.Sprintf("%s_trust_token", req.ProviderTypeName)
}

func (r TrustTokenResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
Description: "Name of the token.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},

"projects": schema.ListAttribute{
Optional: true,
Computed: true,
Description: "List of projects to restrict the token to. By default, no restriction applies.",
Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})),
ElementType: types.StringType,
PlanModifiers: []planmodifier.List{
listplanmodifier.RequiresReplace(),
},
},

"remote": schema.StringAttribute{
Optional: true,
Description: "The remote in which the trust token is created. If not provided, the provider's default remote is used.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},

"trigger": schema.StringAttribute{
Optional: true,
Computed: true,
Default: stringdefault.StaticString("once"),
Description: "When to trigger the token generation. Possible values are once and always (if missing).",
Validators: []validator.String{
stringvalidator.OneOf("once", "always"),
},
PlanModifiers: []planmodifier.String{
// If trigger is changed, replace the resource to
// ensure that the update is never triggered.
stringplanmodifier.RequiresReplace(),
},
},

// Computed.
"token": schema.StringAttribute{
Computed: true,
Description: "Generated trust token.",
},

"expires_at": schema.StringAttribute{
Computed: true,
Description: "Time when trust token will expire.",
},

// Operation ID is used to find the created token. We can not rely on
// the token name, as there can be multiple tokens with the same name.
"operation_id": schema.StringAttribute{
Computed: true,
},
},
}
}

func (r *TrustTokenResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
data := req.ProviderData
if data == nil {
return
}

provider, ok := data.(*provider_config.LxdProviderConfig)
if !ok {
resp.Diagnostics.Append(errors.NewProviderDataTypeError(req.ProviderData))
return
}

r.provider = provider
}

func (r TrustTokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan TrustTokenModel

diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

remote := plan.Remote.ValueString()
server, err := r.provider.InstanceServer(remote, "default", "")
if err != nil {
resp.Diagnostics.Append(errors.NewInstanceServerError(err))
return
}

tokenName := plan.Name.ValueString()

// Get list of project to restrict the token to.
tokenProjects, diags := ToProjectList(ctx, plan.Projects)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

// Create new token.
tokenPost := api.CertificatesPost{
Name: tokenName,
Type: "client",
Token: true,
Projects: tokenProjects,
Restricted: len(tokenProjects) > 0,
}

op, err := server.CreateCertificateToken(tokenPost)
if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf("Failed to create trust token %q", tokenName), err.Error())
return
}

opAPI := op.Get()
token, err := opAPI.ToCertificateAddToken()
if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf("Failed to convert operation into trust token: %q", tokenName), err.Error())
return
}

plan.Token = types.StringValue(token.String())
plan.ExpiresAt = types.StringValue(token.ExpiresAt.Format("2006/01/02 15:04 MST"))
plan.OperationID = types.StringValue(opAPI.ID)

// Update Terraform state.
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

func (r TrustTokenResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) {
// Not supported.
}

func (r TrustTokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state TrustTokenModel

diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

remote := state.Remote.ValueString()
server, err := r.provider.InstanceServer(remote, "default", "")
if err != nil {
resp.Diagnostics.Append(errors.NewInstanceServerError(err))
return
}

opID := state.OperationID.ValueString()
tokenName := state.Name.ValueString()

_, token, err := getTrustToken(server, opID)
if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf("Failed to retrieve trust token %q", tokenName), err.Error())
return
}

// If token is found, update the token.
//
// Otherwise, decide based on the trigger value:
// - always: Remove it from the state to trigger generation of a new token.
// - once: Leave the state as is to prevent changes in the Terraform plan.
if token != nil {
state.Name = types.StringValue(token.ClientName)
state.Token = types.StringValue(token.String())
state.ExpiresAt = types.StringValue(token.ExpiresAt.Format("2006/01/02 15:04 MST"))
} else if state.Trigger.ValueString() == "always" {
resp.State.RemoveResource(ctx)
return
}

// Update Terraform state.
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

func (r TrustTokenResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state TrustTokenModel

diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

remote := state.Remote.ValueString()
server, err := r.provider.InstanceServer(remote, "default", "")
if err != nil {
resp.Diagnostics.Append(errors.NewInstanceServerError(err))
return
}

tokenName := state.Token.ValueString()
opID := state.OperationID.ValueString()

op, _, err := getTrustToken(server, opID)
if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf("Failed to retrieve trust token %q", tokenName), err.Error())
return
}

// Remove the operation if found. Otherwise, the token no longer exists.
if op != nil {
err = server.DeleteOperation(op.ID)
if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf("Failed to remove trust token %q", tokenName), err.Error())
return
}
}
}

// getTrustToken returns a trust token operation and parsed trust token, if found.
// If token operation is not found, no error is returned. Instead, nil operation
// and nil trust token are returned.
func getTrustToken(server lxd.InstanceServer, opID string) (*api.Operation, *api.CertificateAddToken, error) {
// Get all operations.
ops, err := server.GetOperations()
if err != nil {
return nil, nil, err
}

for _, op := range ops {
if op.ID != opID {
// Skip operations that do not match the given ID.
continue
}

if op.Class != api.OperationClassToken {
// Operation must be of type OperationClassToken.
break
}

if op.StatusCode != api.Running {
// Tokens are single use. If token is cancelled but not deleted yet
// its not available.
break
}

token, err := op.ToCertificateAddToken()
if err != nil {
// Operation is not a valid certificate add token operation.
break
}

return &op, token, nil
}

return nil, nil, nil
}
Loading