ce7cb702218693d08ea6d83464d2457c9250027c
This commit is contained in:
commit
34c5f8963c
2 changed files with 145336 additions and 0 deletions
|
|
@ -0,0 +1,131 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/provider"
|
||||
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/quorate/terraform-provider-versitygw/internal/client"
|
||||
)
|
||||
|
||||
var _ provider.Provider = (*Provider)(nil)
|
||||
|
||||
func New() provider.Provider {
|
||||
return &Provider{}
|
||||
}
|
||||
|
||||
type Provider struct{}
|
||||
|
||||
type providerModel struct {
|
||||
Endpoint types.String `tfsdk:"endpoint"`
|
||||
Region types.String `tfsdk:"region"`
|
||||
AdminAccessKey types.String `tfsdk:"admin_access_key"`
|
||||
AdminSecretKey types.String `tfsdk:"admin_secret_key"`
|
||||
InsecureTLS types.Bool `tfsdk:"insecure_tls"`
|
||||
}
|
||||
|
||||
func (p *Provider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
|
||||
resp.TypeName = "versitygw"
|
||||
}
|
||||
|
||||
func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"endpoint": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "VersityGW admin API endpoint. May also be set with VERSITYGW_ENDPOINT.",
|
||||
},
|
||||
"region": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "SigV4 region. Defaults to us-east-1. May also be set with VERSITYGW_REGION.",
|
||||
},
|
||||
"admin_access_key": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Sensitive: true,
|
||||
Description: "VersityGW admin/root access key. May also be set with VERSITYGW_ADMIN_ACCESS_KEY.",
|
||||
},
|
||||
"admin_secret_key": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Sensitive: true,
|
||||
Description: "VersityGW admin/root secret key. May also be set with VERSITYGW_ADMIN_SECRET_KEY.",
|
||||
},
|
||||
"insecure_tls": schema.BoolAttribute{
|
||||
Optional: true,
|
||||
Description: "Disable TLS certificate verification for the admin endpoint. May also be set with VERSITYGW_INSECURE_TLS=true.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
|
||||
var cfg providerModel
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &cfg)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
endpoint := stringValue(cfg.Endpoint, os.Getenv("VERSITYGW_ENDPOINT"))
|
||||
region := stringValue(cfg.Region, os.Getenv("VERSITYGW_REGION"))
|
||||
if region == "" {
|
||||
region = "us-east-1"
|
||||
}
|
||||
adminAccessKey := stringValue(cfg.AdminAccessKey, os.Getenv("VERSITYGW_ADMIN_ACCESS_KEY"))
|
||||
adminSecretKey := stringValue(cfg.AdminSecretKey, os.Getenv("VERSITYGW_ADMIN_SECRET_KEY"))
|
||||
insecureTLS := boolValue(cfg.InsecureTLS, os.Getenv("VERSITYGW_INSECURE_TLS") == "true")
|
||||
|
||||
if endpoint == "" {
|
||||
resp.Diagnostics.AddAttributeError(path.Root("endpoint"), "Missing VersityGW endpoint", "Set endpoint in provider configuration or VERSITYGW_ENDPOINT.")
|
||||
}
|
||||
if adminAccessKey == "" {
|
||||
resp.Diagnostics.AddAttributeError(path.Root("admin_access_key"), "Missing admin access key", "Set admin_access_key in provider configuration or VERSITYGW_ADMIN_ACCESS_KEY.")
|
||||
}
|
||||
if adminSecretKey == "" {
|
||||
resp.Diagnostics.AddAttributeError(path.Root("admin_secret_key"), "Missing admin secret key", "Set admin_secret_key in provider configuration or VERSITYGW_ADMIN_SECRET_KEY.")
|
||||
}
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
api, err := client.New(client.Config{
|
||||
Endpoint: endpoint,
|
||||
Region: region,
|
||||
AdminAccessKey: adminAccessKey,
|
||||
AdminSecretKey: adminSecretKey,
|
||||
InsecureTLS: insecureTLS,
|
||||
})
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Invalid VersityGW provider configuration", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.ResourceData = api
|
||||
}
|
||||
|
||||
func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
|
||||
return []func() resource.Resource{
|
||||
NewUserResource,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource {
|
||||
return nil
|
||||
}
|
||||
|
||||
func stringValue(value types.String, fallback string) string {
|
||||
if value.IsNull() || value.IsUnknown() {
|
||||
return fallback
|
||||
}
|
||||
return value.ValueString()
|
||||
}
|
||||
|
||||
func boolValue(value types.Bool, fallback bool) bool {
|
||||
if value.IsNull() || value.IsUnknown() {
|
||||
return fallback
|
||||
}
|
||||
return value.ValueBool()
|
||||
}
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/quorate/terraform-provider-versitygw/internal/client"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = (*UserResource)(nil)
|
||||
_ resource.ResourceWithConfigure = (*UserResource)(nil)
|
||||
)
|
||||
|
||||
func NewUserResource() resource.Resource {
|
||||
return &UserResource{}
|
||||
}
|
||||
|
||||
type UserResource struct {
|
||||
client *client.Client
|
||||
}
|
||||
|
||||
type userModel struct {
|
||||
ID types.String `tfsdk:"id"`
|
||||
AccessKeyID types.String `tfsdk:"access_key_id"`
|
||||
SecretAccessKey types.String `tfsdk:"secret_access_key"`
|
||||
Role types.String `tfsdk:"role"`
|
||||
UserID types.Int64 `tfsdk:"user_id"`
|
||||
GroupID types.Int64 `tfsdk:"group_id"`
|
||||
ProjectID types.Int64 `tfsdk:"project_id"`
|
||||
}
|
||||
|
||||
func (r *UserResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_user"
|
||||
}
|
||||
|
||||
func (r *UserResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "VersityGW IAM user managed through the VersityGW admin API.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Computed: true,
|
||||
Description: "User access key ID.",
|
||||
},
|
||||
"access_key_id": schema.StringAttribute{
|
||||
Required: true,
|
||||
Description: "VersityGW username/access key ID.",
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"secret_access_key": schema.StringAttribute{
|
||||
Required: true,
|
||||
Sensitive: true,
|
||||
WriteOnly: true,
|
||||
Description: "VersityGW user secret access key.",
|
||||
},
|
||||
"role": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Description: "VersityGW user role: user, userplus, or admin. Defaults to user.",
|
||||
},
|
||||
"user_id": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Description: "Optional POSIX user ID stored on the VersityGW user.",
|
||||
PlanModifiers: []planmodifier.Int64{
|
||||
int64planmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"group_id": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Description: "Optional POSIX group ID stored on the VersityGW user.",
|
||||
PlanModifiers: []planmodifier.Int64{
|
||||
int64planmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"project_id": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Description: "Optional project ID stored on the VersityGW user.",
|
||||
PlanModifiers: []planmodifier.Int64{
|
||||
int64planmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *UserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
api, ok := req.ProviderData.(*client.Client)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError("Unexpected provider data", fmt.Sprintf("Expected *client.Client, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
r.client = api
|
||||
}
|
||||
|
||||
func (r *UserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan userModel
|
||||
var config userModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if !validateUser(plan, config.SecretAccessKey, &resp.Diagnostics) {
|
||||
return
|
||||
}
|
||||
|
||||
role := valueOrDefault(plan.Role, "user")
|
||||
account := client.Account{
|
||||
Access: plan.AccessKeyID.ValueString(),
|
||||
Secret: config.SecretAccessKey.ValueString(),
|
||||
Role: role,
|
||||
UserID: int64OrZero(plan.UserID),
|
||||
GroupID: int64OrZero(plan.GroupID),
|
||||
ProjectID: int64OrZero(plan.ProjectID),
|
||||
}
|
||||
|
||||
if err := r.client.CreateUser(ctx, account); err != nil {
|
||||
if !client.IsErrorCode(err, "XAdminUserExists") {
|
||||
resp.Diagnostics.AddError("Create VersityGW user failed", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
secret := account.Secret
|
||||
props := client.MutableProps{
|
||||
Secret: &secret,
|
||||
Role: account.Role,
|
||||
UserID: &account.UserID,
|
||||
GroupID: &account.GroupID,
|
||||
ProjectID: &account.ProjectID,
|
||||
}
|
||||
if err := r.client.UpdateUser(ctx, account.Access, props); err != nil {
|
||||
resp.Diagnostics.AddError("Adopt existing VersityGW user failed", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
plan.ID = plan.AccessKeyID
|
||||
plan.Role = types.StringValue(role)
|
||||
plan.UserID = types.Int64Value(account.UserID)
|
||||
plan.GroupID = types.Int64Value(account.GroupID)
|
||||
plan.ProjectID = types.Int64Value(account.ProjectID)
|
||||
plan.SecretAccessKey = types.StringNull()
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *UserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state userModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
account, found, err := r.client.GetUser(ctx, state.AccessKeyID.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Read VersityGW user failed", err.Error())
|
||||
return
|
||||
}
|
||||
if !found {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
state.ID = types.StringValue(account.Access)
|
||||
state.AccessKeyID = types.StringValue(account.Access)
|
||||
state.Role = types.StringValue(account.Role)
|
||||
state.UserID = types.Int64Value(account.UserID)
|
||||
state.GroupID = types.Int64Value(account.GroupID)
|
||||
state.ProjectID = types.Int64Value(account.ProjectID)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
func (r *UserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan userModel
|
||||
var state userModel
|
||||
var config userModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if !validateUser(plan, config.SecretAccessKey, &resp.Diagnostics) {
|
||||
return
|
||||
}
|
||||
|
||||
role := valueOrDefault(plan.Role, "user")
|
||||
secret := config.SecretAccessKey.ValueString()
|
||||
userID := int64OrZero(plan.UserID)
|
||||
groupID := int64OrZero(plan.GroupID)
|
||||
projectID := int64OrZero(plan.ProjectID)
|
||||
|
||||
props := client.MutableProps{
|
||||
Secret: &secret,
|
||||
Role: role,
|
||||
UserID: &userID,
|
||||
GroupID: &groupID,
|
||||
ProjectID: &projectID,
|
||||
}
|
||||
|
||||
if err := r.client.UpdateUser(ctx, state.AccessKeyID.ValueString(), props); err != nil {
|
||||
resp.Diagnostics.AddError("Update VersityGW user failed", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
plan.ID = plan.AccessKeyID
|
||||
plan.Role = types.StringValue(role)
|
||||
plan.UserID = types.Int64Value(userID)
|
||||
plan.GroupID = types.Int64Value(groupID)
|
||||
plan.ProjectID = types.Int64Value(projectID)
|
||||
plan.SecretAccessKey = types.StringNull()
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *UserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state userModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.client.DeleteUser(ctx, state.AccessKeyID.ValueString()); err != nil {
|
||||
resp.Diagnostics.AddError("Delete VersityGW user failed", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func validateUser(model userModel, secretAccessKey types.String, diags interface {
|
||||
AddAttributeError(path.Path, string, string)
|
||||
}) bool {
|
||||
ok := true
|
||||
if model.AccessKeyID.IsNull() || model.AccessKeyID.IsUnknown() || model.AccessKeyID.ValueString() == "" {
|
||||
diags.AddAttributeError(path.Root("access_key_id"), "Invalid access key ID", "access_key_id must be non-empty.")
|
||||
ok = false
|
||||
}
|
||||
if secretAccessKey.IsNull() || secretAccessKey.IsUnknown() || secretAccessKey.ValueString() == "" {
|
||||
diags.AddAttributeError(path.Root("secret_access_key"), "Invalid secret access key", "secret_access_key must be non-empty.")
|
||||
ok = false
|
||||
}
|
||||
role := valueOrDefault(model.Role, "user")
|
||||
if role != "user" && role != "userplus" && role != "admin" {
|
||||
diags.AddAttributeError(path.Root("role"), "Invalid role", "role must be one of: user, userplus, admin.")
|
||||
ok = false
|
||||
}
|
||||
if isNegative(model.UserID) {
|
||||
diags.AddAttributeError(path.Root("user_id"), "Invalid user ID", "user_id must be non-negative.")
|
||||
ok = false
|
||||
}
|
||||
if isNegative(model.GroupID) {
|
||||
diags.AddAttributeError(path.Root("group_id"), "Invalid group ID", "group_id must be non-negative.")
|
||||
ok = false
|
||||
}
|
||||
if isNegative(model.ProjectID) {
|
||||
diags.AddAttributeError(path.Root("project_id"), "Invalid project ID", "project_id must be non-negative.")
|
||||
ok = false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
func valueOrDefault(value types.String, fallback string) string {
|
||||
if value.IsNull() || value.IsUnknown() || value.ValueString() == "" {
|
||||
return fallback
|
||||
}
|
||||
return value.ValueString()
|
||||
}
|
||||
|
||||
func int64OrZero(value types.Int64) int64 {
|
||||
if value.IsNull() || value.IsUnknown() {
|
||||
return 0
|
||||
}
|
||||
return value.ValueInt64()
|
||||
}
|
||||
|
||||
func isNegative(value types.Int64) bool {
|
||||
return !value.IsNull() && !value.IsUnknown() && value.ValueInt64() < 0
|
||||
}
|
||||
Loading…
Reference in a new issue