Implement object storage credential (#79)

* Implement credential resource

* Implement test

* Fix test

* Implement data source

* Implement credential acc test

* Add objectstorage credential

* Add test for enableProject

* Add readCredentials test

* Removed unnecessary test case

* Generate docs

* Fix lint

* Fix field name

* Readd credentials group checks

* Fix comment

* Fix comment

* Remove auth

---------

Co-authored-by: Henrique Santos <henrique.santos@freiheit.com>
This commit is contained in:
Henrique Santos 2023-10-12 14:42:31 +01:00 committed by GitHub
parent f9b245ffb4
commit 0fb7d0d793
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1096 additions and 0 deletions

View file

@ -0,0 +1,149 @@
package objectstorage
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/services/objectstorage"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &credentialDataSource{}
)
// NewCredentialDataSource is a helper function to simplify the provider implementation.
func NewCredentialDataSource() datasource.DataSource {
return &credentialDataSource{}
}
// credentialDataSource is the resource implementation.
type credentialDataSource struct {
client *objectstorage.APIClient
}
// Metadata returns the resource type name.
func (r *credentialDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_objectstorage_credential"
}
// Configure adds the provider configured client to the datasource.
func (r *credentialDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}
providerData, ok := req.ProviderData.(core.ProviderData)
if !ok {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
return
}
var apiClient *objectstorage.APIClient
var err error
if providerData.PostgreSQLCustomEndpoint != "" {
apiClient, err = objectstorage.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithEndpoint(providerData.PostgreSQLCustomEndpoint),
)
} else {
apiClient, err = objectstorage.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithRegion(providerData.Region),
)
}
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err))
return
}
r.client = apiClient
tflog.Info(ctx, "ObjectStorage credential client configured")
}
// Schema defines the schema for the datasource.
func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
descriptions := map[string]string{
"main": "ObjectStorage credential data source schema.",
"id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`credentials_group_id`,`credential_id`\".",
"credential_id": "The credential ID.",
"credentials_group_id": "The credential group ID.",
"project_id": "STACKIT Project ID to which the credential group is associated.",
}
resp.Schema = schema.Schema{
Description: descriptions["main"],
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: descriptions["id"],
Computed: true,
},
"credential_id": schema.StringAttribute{
Description: descriptions["credential_id"],
Required: true,
},
"credentials_group_id": schema.StringAttribute{
Description: descriptions["credentials_group_id"],
Required: true,
},
"project_id": schema.StringAttribute{
Description: descriptions["project_id"],
Required: true,
},
"name": schema.StringAttribute{
Computed: true,
},
"access_key": schema.StringAttribute{
Computed: true,
},
"secret_access_key": schema.StringAttribute{
Computed: true,
Sensitive: true,
},
"expiration_timestamp": schema.StringAttribute{
CustomType: timetypes.RFC3339Type{},
Required: true,
},
},
}
}
// Read refreshes the Terraform state with the latest data.
func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
credentialsGroupId := model.CredentialsGroupId.ValueString()
credentialId := model.CredentialId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
ctx = tflog.SetField(ctx, "credential_id", credentialId)
err := readCredentials(ctx, &model, r.client)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Finding credential: %v", err))
return
}
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "ObjectStorage credential read")
}

View file

@ -0,0 +1,421 @@
package objectstorage
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
"github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
"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/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/services/objectstorage"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &credentialResource{}
_ resource.ResourceWithConfigure = &credentialResource{}
_ resource.ResourceWithImportState = &credentialResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
CredentialId types.String `tfsdk:"credential_id"`
CredentialsGroupId types.String `tfsdk:"credentials_group_id"`
ProjectId types.String `tfsdk:"project_id"`
Name types.String `tfsdk:"name"`
AccessKey types.String `tfsdk:"access_key"`
SecretAccessKey types.String `tfsdk:"secret_access_key"`
ExpirationTimestamp timetypes.RFC3339 `tfsdk:"expiration_timestamp"`
}
// NewCredentialResource is a helper function to simplify the provider implementation.
func NewCredentialResource() resource.Resource {
return &credentialResource{}
}
// credentialResource is the resource implementation.
type credentialResource struct {
client *objectstorage.APIClient
}
// Metadata returns the resource type name.
func (r *credentialResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_objectstorage_credential"
}
// Configure adds the provider configured client to the resource.
func (r *credentialResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}
providerData, ok := req.ProviderData.(core.ProviderData)
if !ok {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
return
}
var apiClient *objectstorage.APIClient
var err error
if providerData.PostgreSQLCustomEndpoint != "" {
apiClient, err = objectstorage.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithEndpoint(providerData.PostgreSQLCustomEndpoint),
)
} else {
apiClient, err = objectstorage.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithRegion(providerData.Region),
)
}
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err))
return
}
r.client = apiClient
tflog.Info(ctx, "ObjectStorage credential client configured")
}
// Schema defines the schema for the resource.
func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
descriptions := map[string]string{
"main": "ObjectStorage credential resource schema.",
"id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`credentials_group_id`,`credential_id`\".",
"credential_id": "The credential ID.",
"credentials_group_id": "The credential group ID.",
"project_id": "STACKIT Project ID to which the credential group is associated.",
}
resp.Schema = schema.Schema{
Description: descriptions["main"],
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: descriptions["id"],
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"credential_id": schema.StringAttribute{
Description: descriptions["credential_id"],
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"credentials_group_id": schema.StringAttribute{
Description: descriptions["credentials_group_id"],
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"project_id": schema.StringAttribute{
Description: descriptions["project_id"],
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
stringplanmodifier.UseStateForUnknown(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"name": schema.StringAttribute{
Computed: true,
},
"access_key": schema.StringAttribute{
Computed: true,
},
"secret_access_key": schema.StringAttribute{
Computed: true,
Sensitive: true,
},
"expiration_timestamp": schema.StringAttribute{
CustomType: timetypes.RFC3339Type{},
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
}
}
// Create creates the resource and sets the initial Terraform state.
func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
credentialsGroupId := model.CredentialsGroupId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
// Handle project init
err := enableProject(ctx, &model, r.client)
if resp.Diagnostics.HasError() {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Enabling object storage project before creation: %v", err))
return
}
// Generate API request body from model
payload, err := toCreatePayload(&model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Creating API payload: %v", err))
return
}
// Create new credential
credentialResp, err := r.client.CreateAccessKey(ctx, projectId).CredentialsGroup(credentialsGroupId).CreateAccessKeyPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err))
return
}
if credentialResp.KeyId == nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", "Got empty credential id")
return
}
credentialId := *credentialResp.KeyId
ctx = tflog.SetField(ctx, "credential_id", credentialId)
// Map response body to schema
err = mapFields(credentialResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err))
return
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "ObjectStorage credential created")
}
// Read refreshes the Terraform state with the latest data.
func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
credentialsGroupId := model.CredentialsGroupId.ValueString()
credentialId := model.CredentialId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
ctx = tflog.SetField(ctx, "credential_id", credentialId)
err := readCredentials(ctx, &model, r.client)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Finding credential: %v", err))
return
}
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "ObjectStorage credential read")
}
// Update updates the resource and sets the updated Terraform state on success.
func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
// Update shouldn't be called
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credential", "Credential can't be updated")
}
// Delete deletes the resource and removes the Terraform state on success.
func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
credentialsGroupId := model.CredentialsGroupId.ValueString()
credentialId := model.CredentialId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
ctx = tflog.SetField(ctx, "credential_id", credentialId)
// Delete existing credential
_, err := r.client.DeleteAccessKey(ctx, projectId, credentialId).CredentialsGroup(credentialsGroupId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err))
}
tflog.Info(ctx, "ObjectStorage credential deleted")
}
// ImportState imports a resource into the Terraform state on success.
// The expected format of the resource import identifier is: project_id,credentials_group_id,credential_id
func (r *credentialResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing credential",
fmt.Sprintf("Expected import identifier with format [project_id],[credentials_group_id],[credential_id], got %q", req.ID),
)
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("credentials_group_id"), idParts[1])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("credential_id"), idParts[2])...)
tflog.Info(ctx, "ObjectStorage credential state imported")
}
type objectStorageClient interface {
CreateProjectExecute(ctx context.Context, projectId string) (*objectstorage.GetProjectResponse, error)
}
// enableProject enables object storage for the specified project. If the project is already enabled, nothing happens
func enableProject(ctx context.Context, model *Model, client objectStorageClient) error {
projectId := model.ProjectId.ValueString()
// From the object storage OAS: Creation will also be successful if the project is already enabled, but will not create a duplicate
_, err := client.CreateProjectExecute(ctx, projectId)
if err != nil {
return fmt.Errorf("failed to create object storage project: %w", err)
}
return nil
}
func toCreatePayload(model *Model) (*objectstorage.CreateAccessKeyPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
if model.ExpirationTimestamp.IsNull() || model.ExpirationTimestamp.IsUnknown() {
return &objectstorage.CreateAccessKeyPayload{}, nil
}
expirationTimestamp, diags := model.ExpirationTimestamp.ValueRFC3339Time()
if diags.HasError() {
return nil, fmt.Errorf("unable to fecth expiration timestamp: %w", core.DiagsToError(diags))
}
return &objectstorage.CreateAccessKeyPayload{
Expires: &expirationTimestamp,
}, nil
}
func mapFields(credentialResp *objectstorage.CreateAccessKeyResponse, model *Model) error {
if credentialResp == nil {
return fmt.Errorf("response input is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
var credentialId string
if model.CredentialId.ValueString() != "" {
credentialId = model.CredentialId.ValueString()
} else if credentialResp.KeyId != nil {
credentialId = *credentialResp.KeyId
} else {
return fmt.Errorf("credential id not present")
}
var diags diag.Diagnostics
idParts := []string{
model.ProjectId.ValueString(),
model.CredentialsGroupId.ValueString(),
credentialId,
}
model.Id = types.StringValue(
strings.Join(idParts, core.Separator),
)
model.CredentialId = types.StringValue(credentialId)
model.Name = types.StringPointerValue(credentialResp.DisplayName)
model.AccessKey = types.StringPointerValue(credentialResp.AccessKey)
model.SecretAccessKey = types.StringPointerValue(credentialResp.SecretAccessKey)
model.ExpirationTimestamp, diags = timetypes.NewRFC3339PointerValue(credentialResp.Expires)
if diags.HasError() {
return fmt.Errorf("parsing expiration timestamp: %w", core.DiagsToError(diags))
}
return nil
}
// readCredentials gets all the existing credentials for the specified credentials group,
// finds the credential that is being read and updates the state.
// If the credential cannot be found, it throws an error
func readCredentials(ctx context.Context, model *Model, client *objectstorage.APIClient) error {
projectId := model.ProjectId.ValueString()
credentialsGroupId := model.CredentialsGroupId.ValueString()
credentialId := model.CredentialId.ValueString()
credentialsGroupResp, err := client.GetAccessKeys(ctx, projectId).CredentialsGroup(credentialsGroupId).Execute()
if err != nil {
return fmt.Errorf("getting credentials groups: %w", err)
}
if credentialsGroupResp == nil {
return fmt.Errorf("getting credentials groups: nil response")
}
foundCredential := false
for _, credential := range *credentialsGroupResp.AccessKeys {
if credential.KeyId == nil || *credential.KeyId != credentialId {
continue
}
foundCredential = true
var diags diag.Diagnostics
idParts := []string{
projectId,
credentialsGroupId,
credentialId,
}
model.Id = types.StringValue(
strings.Join(idParts, core.Separator),
)
model.Name = types.StringPointerValue(credential.DisplayName)
model.ExpirationTimestamp, diags = timetypes.NewRFC3339PointerValue(credential.Expires)
if diags.HasError() {
return fmt.Errorf("parsing expiration timestamp: %w", core.DiagsToError(diags))
}
break
}
if !foundCredential {
return fmt.Errorf("credential could not be found")
}
return nil
}

View file

@ -0,0 +1,374 @@
package objectstorage
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/objectstorage"
)
type objectStorageClientMocked struct {
returnError bool
}
func (c *objectStorageClientMocked) CreateProjectExecute(_ context.Context, projectId string) (*objectstorage.GetProjectResponse, error) {
if c.returnError {
return nil, fmt.Errorf("create project failed")
}
return &objectstorage.GetProjectResponse{
Project: utils.Ptr(projectId),
}, nil
}
func TestMapFields(t *testing.T) {
timeValue := time.Now()
tests := []struct {
description string
input *objectstorage.CreateAccessKeyResponse
expected Model
isValid bool
}{
{
"default_values",
&objectstorage.CreateAccessKeyResponse{},
Model{
Id: types.StringValue("pid,cgid,cid"),
ProjectId: types.StringValue("pid"),
CredentialsGroupId: types.StringValue("cgid"),
CredentialId: types.StringValue("cid"),
Name: types.StringNull(),
AccessKey: types.StringNull(),
SecretAccessKey: types.StringNull(),
ExpirationTimestamp: timetypes.NewRFC3339Null(),
},
true,
},
{
"simple_values",
&objectstorage.CreateAccessKeyResponse{
AccessKey: utils.Ptr("key"),
DisplayName: utils.Ptr("name"),
Expires: utils.Ptr(timeValue.Format(time.RFC3339)),
SecretAccessKey: utils.Ptr("secret-key"),
},
Model{
Id: types.StringValue("pid,cgid,cid"),
ProjectId: types.StringValue("pid"),
CredentialsGroupId: types.StringValue("cgid"),
CredentialId: types.StringValue("cid"),
Name: types.StringValue("name"),
AccessKey: types.StringValue("key"),
SecretAccessKey: types.StringValue("secret-key"),
ExpirationTimestamp: timetypes.NewRFC3339TimeValue(timeValue),
},
true,
},
{
"empty_strings",
&objectstorage.CreateAccessKeyResponse{
AccessKey: utils.Ptr(""),
DisplayName: utils.Ptr(""),
SecretAccessKey: utils.Ptr(""),
},
Model{
Id: types.StringValue("pid,cgid,cid"),
ProjectId: types.StringValue("pid"),
CredentialsGroupId: types.StringValue("cgid"),
CredentialId: types.StringValue("cid"),
Name: types.StringValue(""),
AccessKey: types.StringValue(""),
SecretAccessKey: types.StringValue(""),
ExpirationTimestamp: timetypes.NewRFC3339Null(),
},
true,
},
{
"nil_response",
nil,
Model{},
false,
},
{
"bad_time",
&objectstorage.CreateAccessKeyResponse{
Expires: utils.Ptr("foo-bar"),
},
Model{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
model := &Model{
ProjectId: tt.expected.ProjectId,
CredentialsGroupId: tt.expected.CredentialsGroupId,
CredentialId: tt.expected.CredentialId,
}
err := mapFields(tt.input, model)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
if tt.isValid && err != nil {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(model, &tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
})
}
}
func TestEnableProject(t *testing.T) {
tests := []struct {
description string
expected Model
enableFails bool
isValid bool
}{
{
"default_values",
Model{
Id: types.StringValue("pid,cgid,cid"),
ProjectId: types.StringValue("pid"),
CredentialsGroupId: types.StringValue("cgid"),
CredentialId: types.StringValue("cid"),
Name: types.StringNull(),
AccessKey: types.StringNull(),
SecretAccessKey: types.StringNull(),
ExpirationTimestamp: timetypes.NewRFC3339Null(),
},
false,
true,
},
{
"error_response",
Model{
Id: types.StringValue("pid,cgid,cid"),
ProjectId: types.StringValue("pid"),
CredentialsGroupId: types.StringValue("cgid"),
CredentialId: types.StringValue("cid"),
Name: types.StringNull(),
AccessKey: types.StringNull(),
SecretAccessKey: types.StringNull(),
ExpirationTimestamp: timetypes.NewRFC3339Null(),
},
true,
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
client := &objectStorageClientMocked{
returnError: tt.enableFails,
}
model := &Model{
ProjectId: tt.expected.ProjectId,
CredentialsGroupId: tt.expected.CredentialsGroupId,
CredentialId: tt.expected.CredentialId,
}
err := enableProject(context.Background(), model, client)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
if tt.isValid && err != nil {
t.Fatalf("Should not have failed: %v", err)
}
})
}
}
func TestReadCredentials(t *testing.T) {
timeValue := time.Now()
tests := []struct {
description string
mockedResp *objectstorage.GetAccessKeysResponse
expected Model
getCredentialsFails bool
isValid bool
}{
{
"default_values",
&objectstorage.GetAccessKeysResponse{
AccessKeys: &[]objectstorage.AccessKey{
{
KeyId: utils.Ptr("foo-cid"),
},
{
KeyId: utils.Ptr("bar-cid"),
},
{
KeyId: utils.Ptr("cid"),
},
},
},
Model{
Id: types.StringValue("pid,cgid,cid"),
ProjectId: types.StringValue("pid"),
CredentialsGroupId: types.StringValue("cgid"),
CredentialId: types.StringValue("cid"),
Name: types.StringNull(),
AccessKey: types.StringNull(),
SecretAccessKey: types.StringNull(),
ExpirationTimestamp: timetypes.NewRFC3339Null(),
},
false,
true,
},
{
"simple_values",
&objectstorage.GetAccessKeysResponse{
AccessKeys: &[]objectstorage.AccessKey{
{
KeyId: utils.Ptr("foo-cid"),
DisplayName: utils.Ptr("foo-name"),
Expires: utils.Ptr(timeValue.Add(time.Hour).Format(time.RFC3339)),
},
{
KeyId: utils.Ptr("bar-cid"),
DisplayName: utils.Ptr("bar-name"),
Expires: utils.Ptr(timeValue.Add(time.Minute).Format(time.RFC3339)),
},
{
KeyId: utils.Ptr("cid"),
DisplayName: utils.Ptr("name"),
Expires: utils.Ptr(timeValue.Format(time.RFC3339)),
},
},
},
Model{
Id: types.StringValue("pid,cgid,cid"),
ProjectId: types.StringValue("pid"),
CredentialsGroupId: types.StringValue("cgid"),
CredentialId: types.StringValue("cid"),
Name: types.StringValue("name"),
AccessKey: types.StringNull(),
SecretAccessKey: types.StringNull(),
ExpirationTimestamp: timetypes.NewRFC3339TimeValue(timeValue),
},
false,
true,
},
{
"empty_credentials",
&objectstorage.GetAccessKeysResponse{
AccessKeys: &[]objectstorage.AccessKey{},
},
Model{},
false,
false,
},
{
"nil_response",
nil,
Model{},
false,
false,
},
{
"non_matching_credential",
&objectstorage.GetAccessKeysResponse{
AccessKeys: &[]objectstorage.AccessKey{
{
KeyId: utils.Ptr("foo-cid"),
DisplayName: utils.Ptr("foo-name"),
Expires: utils.Ptr(timeValue.Add(time.Hour).Format(time.RFC3339)),
},
{
KeyId: utils.Ptr("bar-cid"),
DisplayName: utils.Ptr("bar-name"),
Expires: utils.Ptr(timeValue.Add(time.Minute).Format(time.RFC3339)),
},
},
},
Model{},
false,
false,
},
{
"error_response",
&objectstorage.GetAccessKeysResponse{
AccessKeys: &[]objectstorage.AccessKey{
{
KeyId: utils.Ptr("cid"),
DisplayName: utils.Ptr("name"),
Expires: utils.Ptr(timeValue.Format(time.RFC3339)),
},
},
},
Model{},
true,
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
mockedRespBytes, err := json.Marshal(tt.mockedResp)
if err != nil {
t.Fatalf("Failed to marshal mocked response: %v", err)
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if tt.getCredentialsFails {
w.WriteHeader(http.StatusBadGateway)
w.Header().Set("Content-Type", "application/json")
_, err := w.Write([]byte("{\"message\": \"Something bad happened\""))
if err != nil {
t.Errorf("Failed to write bad response: %v", err)
}
return
}
_, err := w.Write(mockedRespBytes)
if err != nil {
t.Errorf("Failed to write response: %v", err)
}
})
mockedServer := httptest.NewServer(handler)
defer mockedServer.Close()
client, err := objectstorage.NewAPIClient(
config.WithoutAuthentication(),
config.WithEndpoint(mockedServer.URL),
)
if err != nil {
t.Fatalf("Failed to initialize client: %v", err)
}
model := &Model{
ProjectId: tt.expected.ProjectId,
CredentialsGroupId: tt.expected.CredentialsGroupId,
CredentialId: tt.expected.CredentialId,
}
err = readCredentials(context.Background(), model, client)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
if tt.isValid && err != nil {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(model, &tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
})
}
}

View file

@ -30,6 +30,11 @@ var credentialsGroupResource = map[string]string{
"name": fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)),
}
// Credential resource data
var credentialResource = map[string]string{
"expiration_timestamp": "2345-06-07T08:09:10.110Z",
}
func resourceConfig() string {
return fmt.Sprintf(`
%s
@ -43,12 +48,19 @@ func resourceConfig() string {
project_id = "%s"
name = "%s"
}
resource "stackit_objectstorage_credential" "credential" {
project_id = stackit_objectstorage_credential.credentials_group.project_id
credentials_group_id = stackit_objectstorage_credential.credentials_group.credentials_group_id
expiration_timestamp = "%s"
}
`,
testutil.ObjectStorageProviderConfig(),
bucketResource["project_id"],
bucketResource["bucket_name"],
credentialsGroupResource["project_id"],
credentialsGroupResource["name"],
credentialResource["expiration_timestamp"],
)
}
@ -73,6 +85,21 @@ func TestAccObjectStorageResource(t *testing.T) {
resource.TestCheckResourceAttr("stackit_objectstorage_credentials_group.credentials_group", "name", credentialsGroupResource["name"]),
resource.TestCheckResourceAttrSet("stackit_objectstorage_credentials_group.credentials_group", "credentials_group_id"),
resource.TestCheckResourceAttrSet("stackit_objectstorage_credentials_group.credentials_group", "urn"),
// Credential data
resource.TestCheckResourceAttrPair(
"stackit_objectstorage_credential.credential", "project_id",
"stackit_objectstorage_credentials_group.credentials_group", "project_id",
),
resource.TestCheckResourceAttrPair(
"stackit_objectstorage_credential.credential", "credentials_group_id",
"stackit_objectstorage_credentials_group.credentials_group", "credentials_group_id",
),
resource.TestCheckResourceAttrSet("stackit_objectstorage_credential.credential", "credential_id"),
resource.TestCheckResourceAttr("stackit_objectstorage_credential.credential", "expiration_timestamp", credentialResource["expiration_timestamp"]),
resource.TestCheckResourceAttrSet("stackit_objectstorage_credential.credential", "name"),
resource.TestCheckResourceAttrSet("stackit_objectstorage_credential.credential", "access_key"),
resource.TestCheckResourceAttrSet("stackit_objectstorage_credential.credential", "secret_access_key"),
),
},
// Data source
@ -88,6 +115,12 @@ func TestAccObjectStorageResource(t *testing.T) {
data "stackit_objectstorage_credentials_group" "credentials_group" {
project_id = stackit_objectstorage_credentials_group.credentials_group.project_id
credentials_group_id = stackit_objectstorage_credentials_group.credentials_group.credentials_group_id
}
data "stackit_objectstorage_credential" "credential" {
project_id = stackit_objectstorage_credential.credential.project_id
credentials_group_id = stackit_objectstorage_credential.credential.credentials_group_id
credential_id = stackit_objectstorage_credential.credential.credential_id
}`,
resourceConfig(),
),
@ -121,6 +154,36 @@ func TestAccObjectStorageResource(t *testing.T) {
"stackit_objectstorage_credentials_group.credentials_group", "urn",
"data.stackit_objectstorage_credentials_group.credentials_group", "urn",
),
// Credential data
resource.TestCheckResourceAttrPair(
"stackit_objectstorage_credential.credential", "project_id",
"data.stackit_objectstorage_credential.credential", "project_id",
),
resource.TestCheckResourceAttrPair(
"stackit_objectstorage_credential.credential", "credentials_group_id",
"data.stackit_objectstorage_credential.credential", "credentials_group_id",
),
resource.TestCheckResourceAttrPair(
"stackit_objectstorage_credential.credential", "credential_id",
"data.stackit_objectstorage_credential.credential", "credential_id",
),
resource.TestCheckResourceAttrPair(
"stackit_objectstorage_credential.credential", "access_key",
"data.stackit_objectstorage_credential.credential", "access_key",
),
resource.TestCheckResourceAttrPair(
"stackit_objectstorage_credential.credential", "secret_access_key",
"data.stackit_objectstorage_credential.credential", "secret_access_key",
),
resource.TestCheckResourceAttrPair(
"stackit_objectstorage_credential.credential", "name",
"data.stackit_objectstorage_credential.credential", "name",
),
resource.TestCheckResourceAttrPair(
"stackit_objectstorage_credential.credential", "expiration_timestamp",
"data.stackit_objectstorage_credential.credential", "expiration_timestamp",
),
),
},
// Import
@ -141,6 +204,26 @@ func TestAccObjectStorageResource(t *testing.T) {
ImportState: true,
ImportStateVerify: true,
},
{
ResourceName: "stackit_objectstorage_credential.credential",
ImportStateIdFunc: func(s *terraform.State) (string, error) {
r, ok := s.RootModule().Resources["stackit_objectstorage_credential.credential"]
if !ok {
return "", fmt.Errorf("couldn't find resource stackit_objectstorage_credential.credential")
}
credentialsGroupId, ok := r.Primary.Attributes["credentials_group_id"]
if !ok {
return "", fmt.Errorf("couldn't find attribute credentials_group_id")
}
credentialId, ok := r.Primary.Attributes["credential_id"]
if !ok {
return "", fmt.Errorf("couldn't find attribute credential_id")
}
return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, credentialsGroupId, credentialId), nil
},
ImportState: true,
ImportStateVerify: true,
},
// Deletion is done by the framework implicitly
},
})

View file

@ -19,6 +19,7 @@ import (
mariaDBCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mariadb/credential"
mariaDBInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mariadb/instance"
objectStorageBucket "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/bucket"
objecStorageCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/credential"
objecStorageCredentialsGroup "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/credentialsgroup"
openSearchCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/opensearch/credential"
openSearchInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/opensearch/instance"
@ -315,6 +316,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
mariaDBCredential.NewCredentialDataSource,
objectStorageBucket.NewBucketDataSource,
objecStorageCredentialsGroup.NewCredentialsGroupDataSource,
objecStorageCredential.NewCredentialDataSource,
openSearchInstance.NewInstanceDataSource,
openSearchCredential.NewCredentialDataSource,
rabbitMQInstance.NewInstanceDataSource,
@ -344,6 +346,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
mariaDBCredential.NewCredentialResource,
objectStorageBucket.NewBucketResource,
objecStorageCredentialsGroup.NewCredentialsGroupResource,
objecStorageCredential.NewCredentialResource,
openSearchInstance.NewInstanceResource,
openSearchCredential.NewCredentialResource,
rabbitMQInstance.NewInstanceResource,