Skip to content
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