diff --git a/linode/obj/bucket_accessor.go b/linode/obj/bucket_accessor.go new file mode 100644 index 000000000..785f107b2 --- /dev/null +++ b/linode/obj/bucket_accessor.go @@ -0,0 +1,80 @@ +package obj + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v3/linode/helper" +) + +// BucketAccessor provides the minimal information needed to resolve Object Storage +// access keys for an operation. +// +// Implementations typically come from either a resource plan/state model or a +// data model that can supply: +// - explicit access keys on the resource, or +// - a bucket label + region/cluster so temporary keys can be created. +type BucketAccessor interface { + ObjectStorageKeys() ObjectKeys + BucketLabel() string + RegionOrCluster(context.Context, *diag.Diagnostics) string +} + +// GetObjectStorageKeys resolves Object Storage access keys used by OBJ resources. +// +// Resolution order: +// 1. Keys specified on the resource itself. +// 2. Keys specified in provider configuration (obj_access_key/obj_secret_key). +// 3. If enabled, temporary keys created via the Linode API (obj_use_temp_keys). +// +// When temporary keys are created, a non-nil teardown function is returned to +// delete them after the operation. +func GetObjectStorageKeys( + ctx context.Context, + data BucketAccessor, + client *linodego.Client, + config *helper.FrameworkProviderModel, + permissions string, + endpointType *linodego.ObjectStorageEndpointType, + diags *diag.Diagnostics, +) (*ObjectKeys, func()) { + result := data.ObjectStorageKeys() + if result.Ok() { + return &result, nil + } + + result.AccessKey = config.ObjAccessKey.ValueString() + result.SecretKey = config.ObjSecretKey.ValueString() + if result.Ok() { + return &result, nil + } + + if config.ObjUseTempKeys.ValueBool() { + clusterOrRegion := data.RegionOrCluster(ctx, diags) + if diags.HasError() { + return nil, nil + } + + objKey := fwCreateTempKeys(ctx, client, data.BucketLabel(), clusterOrRegion, permissions, endpointType, diags) + if diags.HasError() { + return nil, nil + } + + result.AccessKey = objKey.AccessKey + result.SecretKey = objKey.SecretKey + + teardownTempKeysCleanUp := func() { + cleanUpTempKeys(ctx, client, objKey.ID) + } + + return &result, teardownTempKeysCleanUp + } + + diags.AddError( + "Keys Not Found", + "`access_key` and `secret_key` are Required but not Configured", + ) + + return nil, nil +} diff --git a/linode/obj/framework_models.go b/linode/obj/framework_models.go index ea547336b..a46abadcf 100644 --- a/linode/obj/framework_models.go +++ b/linode/obj/framework_models.go @@ -75,66 +75,13 @@ func (data ResourceModel) getObjectBody(diags *diag.Diagnostics) (body *s3manage return s3manager.ReadSeekCloser(bytes.NewReader(contentBytes)) } -func (data ResourceModel) GetObjectStorageKeys( - ctx context.Context, - client *linodego.Client, - config *helper.FrameworkProviderModel, - permissions string, - endpointType *linodego.ObjectStorageEndpointType, - diags *diag.Diagnostics, -) (*ObjectKeys, func()) { - result := &ObjectKeys{} - - result.AccessKey = data.AccessKey.ValueString() - result.SecretKey = data.SecretKey.ValueString() - - if result.Ok() { - return result, nil - } - - result.AccessKey = config.ObjAccessKey.ValueString() - result.SecretKey = config.ObjSecretKey.ValueString() - - if result.Ok() { - return result, nil - } - - if config.ObjUseTempKeys.ValueBool() { - clusterOrRegion := data.GetRegionOrCluster(ctx, diags) - if diags.HasError() { - return nil, nil - } - - objKey := fwCreateTempKeys(ctx, client, data.Bucket.ValueString(), clusterOrRegion, permissions, nil, diags) - if diags.HasError() { - return nil, nil - } - - result.AccessKey = objKey.AccessKey - result.SecretKey = objKey.SecretKey - - teardownTempKeysCleanUp := func() { - cleanUpTempKeys(ctx, client, objKey.ID) - } - - return result, teardownTempKeysCleanUp - } - - diags.AddError( - "Keys Not Found", - "`access_key` and `secret_key` are Required but not Configured", - ) - - return nil, nil -} - func (plan *ResourceModel) ComputeEndpointIfUnknown(ctx context.Context, client *linodego.Client, diags *diag.Diagnostics) { if !plan.Endpoint.IsUnknown() { return } bucketName := plan.Bucket.ValueString() - regionOrCluster := plan.GetRegionOrCluster(ctx, diags) + regionOrCluster := plan.RegionOrCluster(ctx, diags) if diags.HasError() { return } @@ -161,7 +108,7 @@ func (data *ResourceModel) GenerateObjectStorageObjectID(apply bool, preserveKno return id } -func (data ResourceModel) GetRegionOrCluster(ctx context.Context, diags *diag.Diagnostics) string { +func (data ResourceModel) RegionOrCluster(ctx context.Context, diags *diag.Diagnostics) string { if !data.Region.IsNull() && !data.Region.IsUnknown() { return data.Region.ValueString() } else { @@ -174,6 +121,17 @@ func (data ResourceModel) GetRegionOrCluster(ctx context.Context, diags *diag.Di return data.Cluster.ValueString() } +func (data ResourceModel) ObjectStorageKeys() ObjectKeys { + return ObjectKeys{ + AccessKey: data.AccessKey.ValueString(), + SecretKey: data.SecretKey.ValueString(), + } +} + +func (data ResourceModel) BucketLabel() string { + return data.Bucket.ValueString() +} + func (data *ResourceModel) FlattenObject( obj s3.HeadObjectOutput, preserveKnown bool, ) { diff --git a/linode/obj/framework_resource.go b/linode/obj/framework_resource.go index 6619a5432..fcffedb6e 100644 --- a/linode/obj/framework_resource.go +++ b/linode/obj/framework_resource.go @@ -307,7 +307,7 @@ func (r *Resource) Delete( func populateLogAttributes(ctx context.Context, model ResourceModel, diags *diag.Diagnostics) context.Context { return helper.SetLogFieldBulk(ctx, map[string]any{ "bucket": model.Bucket.ValueString(), - "region_or_cluster": model.GetRegionOrCluster(ctx, diags), + "region_or_cluster": model.RegionOrCluster(ctx, diags), "object_key": model.Key.ValueString(), }) } diff --git a/linode/obj/helpers.go b/linode/obj/helpers.go index 2db005f49..f6e6f0cee 100644 --- a/linode/obj/helpers.go +++ b/linode/obj/helpers.go @@ -40,7 +40,7 @@ func getS3ClientFromModel( endpointType *linodego.ObjectStorageEndpointType, diags *diag.Diagnostics, ) (*s3.Client, func()) { - keys, teardownKeys := data.GetObjectStorageKeys(ctx, client, config, permission, endpointType, diags) + keys, teardownKeys := GetObjectStorageKeys(ctx, data, client, config, permission, endpointType, diags) if diags.HasError() { return nil, teardownKeys } diff --git a/linode/objbucket/framework_datasource_schema.go b/linode/objbucket/framework_datasource_schema.go index 398abfb9f..5a59d23b6 100644 --- a/linode/objbucket/framework_datasource_schema.go +++ b/linode/objbucket/framework_datasource_schema.go @@ -10,6 +10,14 @@ import ( var frameworkDatasourceSchema = schema.Schema{ Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The id of this bucket.", + Computed: true, + }, + "label": schema.StringAttribute{ + Description: "The name of this bucket.", + Required: true, + }, "cluster": schema.StringAttribute{ Description: "The ID of the Object Storage Cluster this bucket is in.", DeprecationMessage: "The cluster attribute has been deprecated, please consider " + @@ -41,24 +49,11 @@ var frameworkDatasourceSchema = schema.Schema{ Description: "The S3 endpoint URL of the bucket, based on the `endpoint_type` and `region`.", Computed: true, }, - "created": schema.StringAttribute{ - Description: "When this bucket was created.", - CustomType: timetypes.RFC3339Type{}, - Computed: true, - }, "hostname": schema.StringAttribute{ Description: "The hostname where this bucket can be accessed." + "This hostname can be accessed through a browser if the bucket is made public.", Computed: true, }, - "id": schema.StringAttribute{ - Description: "The id of this bucket.", - Computed: true, - }, - "label": schema.StringAttribute{ - Description: "The name of this bucket.", - Required: true, - }, "objects": schema.Int64Attribute{ Description: "The number of objects stored in this bucket.", Computed: true, @@ -67,5 +62,10 @@ var frameworkDatasourceSchema = schema.Schema{ Description: "The size of the bucket in bytes.", Computed: true, }, + "created": schema.StringAttribute{ + Description: "When this bucket was created.", + CustomType: timetypes.RFC3339Type{}, + Computed: true, + }, }, } diff --git a/linode/objbucket/framework_models.go b/linode/objbucket/framework_models.go index 9486a5525..7c0f4a463 100644 --- a/linode/objbucket/framework_models.go +++ b/linode/objbucket/framework_models.go @@ -21,6 +21,21 @@ type BaseModel struct { Created timetypes.RFC3339 `tfsdk:"created"` } +func (data BaseModel) RegionOrCluster() (regionOrCluster string) { + if data.Region.ValueString() != "" { + regionOrCluster = data.Region.ValueString() + } else { + regionOrCluster = data.Cluster.ValueString() + } + return +} + +func (data BaseModel) BucketLabel() (label string) { + // Label is a required attribute, so there is no need + // to check whether it is null or unknown. + return data.Label.ValueString() +} + type DataSourceModel struct { BaseModel }