Skip to content
Draft
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
40 changes: 40 additions & 0 deletions stackit/internal/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,46 @@ func DiagsToError(diags diag.Diagnostics) error {
return fmt.Errorf("%s", strings.Join(diagsStrings, ";"))
}

// LogTrace logs a trace-level message.
func LogTrace(ctx context.Context, msg string, additionalFields ...map[string]interface{}) {
additionalFields = append(additionalFields, map[string]interface{}{
"x-trace-id": runtime.GetTraceId(ctx),
})
tflog.Trace(ctx, msg, additionalFields...)
}

// LogDebug logs a debug-level message.
func LogDebug(ctx context.Context, msg string, additionalFields ...map[string]interface{}) {
additionalFields = append(additionalFields, map[string]interface{}{
"x-trace-id": runtime.GetTraceId(ctx),
})
tflog.Debug(ctx, msg, additionalFields...)
}

// LogInfo logs an info-level message.
func LogInfo(ctx context.Context, msg string, additionalFields ...map[string]interface{}) {
additionalFields = append(additionalFields, map[string]interface{}{
"x-trace-id": runtime.GetTraceId(ctx),
})
tflog.Info(ctx, msg, additionalFields...)
}

// LogWarn logs a warning-level message.
func LogWarn(ctx context.Context, msg string, additionalFields ...map[string]interface{}) {
additionalFields = append(additionalFields, map[string]interface{}{
"x-trace-id": runtime.GetTraceId(ctx),
})
tflog.Warn(ctx, msg, additionalFields...)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we'll need some linting rule then so only these LogInfo, LogWarn, ... functions are used then, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I'd add a linter to forbid direct tflog.Info|Error|... calls in the services pkg

}

// LogError logs an error-level message.
func LogError(ctx context.Context, msg string, additionalFields ...map[string]interface{}) {
additionalFields = append(additionalFields, map[string]interface{}{
"x-trace-id": runtime.GetTraceId(ctx),
})
tflog.Error(ctx, msg, additionalFields...)
}

// LogAndAddError Logs the error and adds it to the diags
func LogAndAddError(ctx context.Context, diags *diag.Diagnostics, summary, detail string) {
if traceId := runtime.GetTraceId(ctx); traceId != "" {
Expand Down
214 changes: 214 additions & 0 deletions stackit/internal/core/wrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package core

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/resource"
)

// WrapDataSource wraps a data source so its Read method can capture the HTTP
// response used for provider logging.
func WrapDataSource(inner datasource.DataSource) datasource.DataSource {
return &wrappedDataSource{inner: inner}
}

type wrappedDataSource struct {
inner datasource.DataSource
}

var (
_ datasource.DataSource = &wrappedDataSource{}
_ datasource.DataSourceWithConfigure = &wrappedDataSource{}
_ datasource.DataSourceWithConfigValidators = &wrappedDataSource{}
_ datasource.DataSourceWithValidateConfig = &wrappedDataSource{}
)

func (w *wrappedDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
w.inner.Metadata(ctx, req, resp)
}

func (w *wrappedDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
w.inner.Schema(ctx, req, resp)
}

func (w *wrappedDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic,tflogresponse // Framework signature requires values; wrapped method logs the response.
ctx = InitProviderContext(ctx) //nolint:tflogresponse // The wrapped CRUD method logs the response.
w.inner.Read(ctx, req, resp)
}

func (w *wrappedDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if inner, ok := w.inner.(datasource.DataSourceWithConfigure); ok {
inner.Configure(ctx, req, resp)
}
}

func (w *wrappedDataSource) ConfigValidators(ctx context.Context) []datasource.ConfigValidator {
if inner, ok := w.inner.(datasource.DataSourceWithConfigValidators); ok {
return inner.ConfigValidators(ctx)
}

return nil
}

func (w *wrappedDataSource) ValidateConfig(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) {
if inner, ok := w.inner.(datasource.DataSourceWithValidateConfig); ok {
inner.ValidateConfig(ctx, req, resp)
}
}

// WrapResource wraps a resource so its CRUD methods can capture the HTTP
// response used for provider logging.
func WrapResource(inner resource.Resource) resource.Resource {
wrapped := &wrappedResource{inner: inner}
_, hasIdentity := inner.(resource.ResourceWithIdentity)
_, hasUpgradeIdentity := inner.(resource.ResourceWithUpgradeIdentity)

switch {
case hasIdentity && hasUpgradeIdentity:
return &wrappedResourceWithIdentityAndUpgrade{wrappedResourceWithIdentity: wrappedResourceWithIdentity{wrappedResource: wrapped}}
case hasIdentity:
return &wrappedResourceWithIdentity{wrappedResource: wrapped}
case hasUpgradeIdentity:
return &wrappedResourceWithUpgradeIdentity{wrappedResource: wrapped}
default:
return wrapped
}
}

type wrappedResource struct {
inner resource.Resource
}

var (
_ resource.Resource = &wrappedResource{}
_ resource.ResourceWithConfigure = &wrappedResource{}
_ resource.ResourceWithConfigValidators = &wrappedResource{}
_ resource.ResourceWithImportState = &wrappedResource{}
_ resource.ResourceWithModifyPlan = &wrappedResource{}
_ resource.ResourceWithMoveState = &wrappedResource{}
_ resource.ResourceWithUpgradeState = &wrappedResource{}
_ resource.ResourceWithValidateConfig = &wrappedResource{}
)

func (w *wrappedResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
w.inner.Metadata(ctx, req, resp)
}

func (w *wrappedResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
w.inner.Schema(ctx, req, resp)
}

func (w *wrappedResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic,tflogresponse // Framework signature requires values; wrapped method logs the response.
ctx = InitProviderContext(ctx) //nolint:tflogresponse // The wrapped CRUD method logs the response.
w.inner.Create(ctx, req, resp)
}

func (w *wrappedResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic,tflogresponse // Framework signature requires values; wrapped method logs the response.
ctx = InitProviderContext(ctx) //nolint:tflogresponse // The wrapped CRUD method logs the response.
w.inner.Read(ctx, req, resp)
}

func (w *wrappedResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic,tflogresponse // Framework signature requires values; wrapped method logs the response.
ctx = InitProviderContext(ctx) //nolint:tflogresponse // The wrapped CRUD method logs the response.
w.inner.Update(ctx, req, resp)
}

func (w *wrappedResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic,tflogresponse // Framework signature requires values; wrapped method logs the response.
ctx = InitProviderContext(ctx) //nolint:tflogresponse // The wrapped CRUD method logs the response.
w.inner.Delete(ctx, req, resp)
}

func (w *wrappedResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if inner, ok := w.inner.(resource.ResourceWithConfigure); ok {
inner.Configure(ctx, req, resp)
}
}

func (w *wrappedResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator {
if inner, ok := w.inner.(resource.ResourceWithConfigValidators); ok {
return inner.ConfigValidators(ctx)
}

return nil
}

func (w *wrappedResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
if inner, ok := w.inner.(resource.ResourceWithImportState); ok {
inner.ImportState(ctx, req, resp)
return
}

resp.Diagnostics.AddError(
"Resource Import Not Implemented",
"This resource does not support import. Please contact the provider developer for additional information.",
)
}

func (w *wrappedResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // Framework signature requires values.
if inner, ok := w.inner.(resource.ResourceWithModifyPlan); ok {
inner.ModifyPlan(ctx, req, resp)
}
}

func (w *wrappedResource) MoveState(ctx context.Context) []resource.StateMover {
if inner, ok := w.inner.(resource.ResourceWithMoveState); ok {
return inner.MoveState(ctx)
}

return nil
}

func (w *wrappedResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
if inner, ok := w.inner.(resource.ResourceWithUpgradeState); ok {
return inner.UpgradeState(ctx)
}

return nil
}

func (w *wrappedResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
if inner, ok := w.inner.(resource.ResourceWithValidateConfig); ok {
inner.ValidateConfig(ctx, req, resp)
}
}

type wrappedResourceWithIdentity struct {
*wrappedResource
}

var _ resource.ResourceWithIdentity = &wrappedResourceWithIdentity{}

func (w *wrappedResourceWithIdentity) IdentitySchema(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) {
if inner, ok := w.inner.(resource.ResourceWithIdentity); ok {
inner.IdentitySchema(ctx, req, resp)
}
}

type wrappedResourceWithUpgradeIdentity struct {
*wrappedResource
}

var _ resource.ResourceWithUpgradeIdentity = &wrappedResourceWithUpgradeIdentity{}

func (w *wrappedResourceWithUpgradeIdentity) UpgradeIdentity(ctx context.Context) map[int64]resource.IdentityUpgrader {
if inner, ok := w.inner.(resource.ResourceWithUpgradeIdentity); ok {
return inner.UpgradeIdentity(ctx)
}

return nil
}

type wrappedResourceWithIdentityAndUpgrade struct {
wrappedResourceWithIdentity
}

var _ resource.ResourceWithUpgradeIdentity = &wrappedResourceWithIdentityAndUpgrade{}

func (w *wrappedResourceWithIdentityAndUpgrade) UpgradeIdentity(ctx context.Context) map[int64]resource.IdentityUpgrader {
if inner, ok := w.inner.(resource.ResourceWithUpgradeIdentity); ok {
return inner.UpgradeIdentity(ctx)
}

return nil
}
75 changes: 75 additions & 0 deletions stackit/internal/core/wrap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package core

import (
"context"
"testing"

"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/resource"
sdkconfig "github.com/stackitcloud/stackit-sdk-go/core/config"
)

type testDataSource struct {
readContext context.Context
}

func (d *testDataSource) Metadata(context.Context, datasource.MetadataRequest, *datasource.MetadataResponse) {
}
func (d *testDataSource) Schema(context.Context, datasource.SchemaRequest, *datasource.SchemaResponse) {
}
func (d *testDataSource) Read(ctx context.Context, _ datasource.ReadRequest, _ *datasource.ReadResponse) { //nolint:gocritic // Framework signature requires values.
d.readContext = ctx
}

type testResource struct {
contexts map[string]context.Context
}

func (r *testResource) Metadata(context.Context, resource.MetadataRequest, *resource.MetadataResponse) {
}
func (r *testResource) Schema(context.Context, resource.SchemaRequest, *resource.SchemaResponse) {}
func (r *testResource) Create(ctx context.Context, _ resource.CreateRequest, _ *resource.CreateResponse) { //nolint:gocritic // Framework signature requires values.
r.contexts["create"] = ctx
}
func (r *testResource) Read(ctx context.Context, _ resource.ReadRequest, _ *resource.ReadResponse) { //nolint:gocritic // Framework signature requires values.
r.contexts["read"] = ctx
}
func (r *testResource) Update(ctx context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { //nolint:gocritic // Framework signature requires values.
r.contexts["update"] = ctx
}
func (r *testResource) Delete(ctx context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) { //nolint:gocritic // Framework signature requires values.
r.contexts["delete"] = ctx
}

func TestWrapDataSourceReadInitializesProviderContext(t *testing.T) {
inner := &testDataSource{}
wrapped := WrapDataSource(inner)

wrapped.Read(context.Background(), datasource.ReadRequest{}, &datasource.ReadResponse{})

if inner.readContext.Value(sdkconfig.ContextHTTPResponse) == nil {
t.Error("Read context does not capture HTTP responses")
}
}

func TestWrapResourceDoesNotAddIdentitySupport(t *testing.T) {
if _, ok := WrapResource(&testResource{}).(resource.ResourceWithIdentity); ok {
t.Error("wrapper must not add identity support when the resource does not implement it")
}
}

func TestWrapResourceCRUDInitializesProviderContext(t *testing.T) {
inner := &testResource{contexts: make(map[string]context.Context)}
wrapped := WrapResource(inner)

wrapped.Create(context.Background(), resource.CreateRequest{}, &resource.CreateResponse{})
wrapped.Read(context.Background(), resource.ReadRequest{}, &resource.ReadResponse{})
wrapped.Update(context.Background(), resource.UpdateRequest{}, &resource.UpdateResponse{})
wrapped.Delete(context.Background(), resource.DeleteRequest{}, &resource.DeleteResponse{})

for _, operation := range []string{"create", "read", "update", "delete"} {
if inner.contexts[operation].Value(sdkconfig.ContextHTTPResponse) == nil {
t.Errorf("%s context does not capture HTTP responses", operation)
}
}
}
12 changes: 6 additions & 6 deletions stackit/internal/services/iaas/machinetype/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
}

// NewMachineTypeDataSource instantiates the data source
func NewMachineTypeDataSource() datasource.DataSource {
return &machineTypeDataSource{}
func NewMachineTypeDataSource(clientFactory clientutils.ClientFactory) datasource.DataSource {
return &machineTypeDataSource{clientFactory: clientFactory}
}

type machineTypeDataSource struct {
Expand Down Expand Up @@ -76,7 +76,7 @@
return
}

tflog.Info(ctx, "IAAS client configured")
core.LogInfo(ctx, "IAAS client configured")
}

func (d *machineTypeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
Expand Down Expand Up @@ -158,20 +158,20 @@
region := d.providerData.GetRegionWithOverride(model.Region)
sortAscending := model.SortAscending.ValueBool()

ctx = core.InitProviderContext(ctx)
//ctx = core.InitProviderContext(ctx)

Check failure on line 161 in stackit/internal/services/iaas/machinetype/datasource.go

View workflow job for this annotation

GitHub Actions / CI

commentFormatting: put a space between `//` and comment text (gocritic)

ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "filter_is_null", model.Filter.IsNull())
ctx = tflog.SetField(ctx, "filter_is_unknown", model.Filter.IsUnknown())

listMachineTypeReq := d.client.ListMachineTypes(ctx, projectId, region)

Check failure on line 168 in stackit/internal/services/iaas/machinetype/datasource.go

View workflow job for this annotation

GitHub Actions / CI

tfctxinit: call to github.com/stackitcloud/stackit-sdk-go must happen AFTER github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core.InitProviderContext is called in Read (tfctxinit)

if !model.Filter.IsNull() && !model.Filter.IsUnknown() && strings.TrimSpace(model.Filter.ValueString()) != "" {
listMachineTypeReq = listMachineTypeReq.Filter(strings.TrimSpace(model.Filter.ValueString()))

Check failure on line 171 in stackit/internal/services/iaas/machinetype/datasource.go

View workflow job for this annotation

GitHub Actions / CI

tfctxinit: call to github.com/stackitcloud/stackit-sdk-go must happen AFTER github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core.InitProviderContext is called in Read (tfctxinit)
}

apiResp, err := listMachineTypeReq.Execute()

Check failure on line 174 in stackit/internal/services/iaas/machinetype/datasource.go

View workflow job for this annotation

GitHub Actions / CI

tfctxinit: call to github.com/stackitcloud/stackit-sdk-go must happen AFTER github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core.InitProviderContext is called in Read (tfctxinit)
if err != nil {
utils.LogError(ctx, &resp.Diagnostics, err, "Failed to read machine types",
fmt.Sprintf("Unable to retrieve machine types for project %q %s.", projectId, err),
Expand All @@ -183,7 +183,7 @@
return
}

ctx = core.LogResponse(ctx)
//ctx = core.LogResponse(ctx)

Check failure on line 186 in stackit/internal/services/iaas/machinetype/datasource.go

View workflow job for this annotation

GitHub Actions / CI

commentFormatting: put a space between `//` and comment text (gocritic)

if len(apiResp.Items) == 0 {
core.LogAndAddWarning(ctx, &resp.Diagnostics, "No machine types found", "No matching machine types.")
Expand Down Expand Up @@ -211,7 +211,7 @@
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Successfully read machine type")
core.LogInfo(ctx, "Successfully read machine type")
}

func mapDataSourceFields(ctx context.Context, machineType *iaas.MachineType, model *DataSourceModel, region string) error {
Expand Down
Loading
Loading