Skip to content
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
4 changes: 1 addition & 3 deletions docs/data-sources/objectstorage_credential.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ ObjectStorage credential data source schema. Must have a `region` specified in t
## Example Usage

```terraform
data "stackit_objectstorage_credentials_group" "example" {
data "stackit_objectstorage_credential" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
credentials_group_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
credential_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
Expand All @@ -35,8 +35,6 @@ data "stackit_objectstorage_credentials_group" "example" {

### Read-Only

- `access_key` (String)
- `expiration_timestamp` (String)
- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`credentials_group_id`,`credential_id`".
- `name` (String)
- `secret_access_key` (String, Sensitive)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
data "stackit_objectstorage_credentials_group" "example" {
data "stackit_objectstorage_credential" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
credentials_group_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
credential_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
Expand Down
102 changes: 90 additions & 12 deletions stackit/internal/services/objectstorage/credential/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ package objectstorage
import (
"context"
"fmt"
"net/http"
"strings"
"time"

"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"

"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/objectstorage"
)

Expand All @@ -19,6 +24,16 @@ var (
_ datasource.DataSource = &credentialDataSource{}
)

type DataSourceModel 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"`
ExpirationTimestamp types.String `tfsdk:"expiration_timestamp"`
Region types.String `tfsdk:"region"`
}

// NewCredentialDataSource is a helper function to simplify the provider implementation.
func NewCredentialDataSource() datasource.DataSource {
return &credentialDataSource{}
Expand Down Expand Up @@ -104,13 +119,6 @@ func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequ
"name": schema.StringAttribute{
Computed: true,
},
"access_key": schema.StringAttribute{
Computed: true,
},
"secret_access_key": schema.StringAttribute{
Computed: true,
Sensitive: true,
},
"expiration_timestamp": schema.StringAttribute{
Computed: true,
},
Expand All @@ -125,7 +133,7 @@ func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequ

// 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
var model DataSourceModel
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
Expand All @@ -147,17 +155,33 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ
ctx = tflog.SetField(ctx, "credential_id", credentialId)
ctx = tflog.SetField(ctx, "region", region)

found, err := readCredentials(ctx, &model, region, r.client)
credentialsGroupResp, err := r.client.ListAccessKeys(ctx, projectId, region).CredentialsGroup(credentialsGroupId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Finding credential: %v", err))
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
if ok && oapiErr.StatusCode == http.StatusNotFound {
resp.State.RemoveResource(ctx)
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Calling API: %v", err))
return
}
if credentialsGroupResp == nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Response is nil: %v", err))
return
}
if !found {
resp.State.RemoveResource(ctx)

credential := findCredential(*credentialsGroupResp, credentialId)
if credential == nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", "Credential not found")
return
}

err = mapDataSourceFields(credential, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err))
return
}

// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
Expand All @@ -166,3 +190,57 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ
}
tflog.Info(ctx, "ObjectStorage credential read")
}

func mapDataSourceFields(credentialResp *objectstorage.AccessKey, model *DataSourceModel, region string) 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")
}

if credentialResp.Expires == nil {
model.ExpirationTimestamp = types.StringNull()
} else {
// Harmonize the timestamp format
// Eg. "2027-01-02T03:04:05.000Z" = "2027-01-02T03:04:05Z"
expirationTimestamp, err := time.Parse(time.RFC3339, *credentialResp.Expires)
if err != nil {
return fmt.Errorf("unable to parse payload expiration timestamp '%v': %w", *credentialResp.Expires, err)
}
model.ExpirationTimestamp = types.StringValue(expirationTimestamp.Format(time.RFC3339))
}

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.Region = types.StringValue(region)
return nil
}

// Returns the access key if found otherwise nil
func findCredential(credentialsGroupResp objectstorage.ListAccessKeysResponse, credentialId string) *objectstorage.AccessKey {
for _, credential := range *credentialsGroupResp.AccessKeys {
if credential.KeyId == nil || *credential.KeyId != credentialId {
continue
}
return &credential
}
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package objectstorage

import (
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/objectstorage"
)

func TestMapDatasourceFields(t *testing.T) {
now := time.Now()

tests := []struct {
description string
input *objectstorage.AccessKey
expected DataSourceModel
isValid bool
}{
{
"default_values",
&objectstorage.AccessKey{},
DataSourceModel{
Id: types.StringValue("pid,cgid,cid"),
ProjectId: types.StringValue("pid"),
CredentialsGroupId: types.StringValue("cgid"),
CredentialId: types.StringValue("cid"),
Name: types.StringNull(),
ExpirationTimestamp: types.StringNull(),
Region: types.StringValue("eu01"),
},
true,
},
{
"simple_values",
&objectstorage.AccessKey{
DisplayName: utils.Ptr("name"),
Expires: utils.Ptr(now.Format(time.RFC3339)),
},
DataSourceModel{
Id: types.StringValue("pid,cgid,cid"),
ProjectId: types.StringValue("pid"),
CredentialsGroupId: types.StringValue("cgid"),
CredentialId: types.StringValue("cid"),
Name: types.StringValue("name"),
ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)),
Region: types.StringValue("eu01"),
},
true,
},
{
"empty_strings",
&objectstorage.AccessKey{
DisplayName: utils.Ptr(""),
},
DataSourceModel{
Id: types.StringValue("pid,cgid,cid"),
ProjectId: types.StringValue("pid"),
CredentialsGroupId: types.StringValue("cgid"),
CredentialId: types.StringValue("cid"),
Name: types.StringValue(""),
ExpirationTimestamp: types.StringNull(),
Region: types.StringValue("eu01"),
},
true,
},
{
"expiration_timestamp_with_fractional_seconds",
&objectstorage.AccessKey{
Expires: utils.Ptr(now.Format(time.RFC3339Nano)),
},
DataSourceModel{
Id: types.StringValue("pid,cgid,cid"),
ProjectId: types.StringValue("pid"),
CredentialsGroupId: types.StringValue("cgid"),
CredentialId: types.StringValue("cid"),
Name: types.StringNull(),
ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)),
Region: types.StringValue("eu01"),
},
true,
},
{
"nil_response",
nil,
DataSourceModel{},
false,
},
{
"bad_time",
&objectstorage.AccessKey{
Expires: utils.Ptr("foo-bar"),
},
DataSourceModel{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
model := &DataSourceModel{
ProjectId: tt.expected.ProjectId,
CredentialsGroupId: tt.expected.CredentialsGroupId,
CredentialId: tt.expected.CredentialId,
}
err := mapDataSourceFields(tt.input, model, "eu01")
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)
}
}
})
}
}