Skip to content

chore(provider): refactor sdk client creation to allow mock injection - #1663

Open
rubenhoenle wants to merge 7 commits into
mainfrom
mock-refactor
Open

rubenhoenle wants to merge 7 commits into
mainfrom
mock-refactor

Conversation

@rubenhoenle

@rubenhoenle rubenhoenle commented Aug 5, 2026

Copy link
Copy Markdown
Member

Description

Intention

The intention of this refactoring is

  • Being able to inject mocked SDK HTTP clients into resources. This makes unit-testing of resources easier.
    • Old approach (example): In the past we had to start a local mock server and use a custom endpoint to run the SDK requests against it. This doesn't allow for type safety during mocked tests, etc.
  • New approach (example): We can inject a mocked client directly into the resource / datasource. We don't have to spin up mock servers and use custom endpoints. Furthermore we have type safety via our SDK mocks.

Note

Furthermore (and probably more important) this refactoring allows us to roll out automatic retries on e.g. HTTP 429 API errors more easily across the whole Terraform provider with all it's resource and datasource implementations. See #1764 and #1771 for reference.

What changed inside the resource and datasource implementations

Previously our resources looked like this: In each resource in the Configure function a new SDK client was created. The client was created using a util function (boilerplate code which had to implemented for every service, also the tests were copied in boilerplate manner):

// Configure adds the provider configured client to the resource.
func (r *volumeResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
	var ok bool
	r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
	if !ok {
		return
	}

	apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
	if resp.Diagnostics.HasError() {
		return
	}
	r.client = apiClient
	tflog.Info(ctx, "iaas client configured")
}

Now instead we have a pre-populated struct core.ClientCollection which is part of the provider data and includes an instance of each SDK HTTP client.

// Configure adds the provider configured client to the resource.
func (r *volumeResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
	var ok bool
	providerData, clients, ok := core.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
	if !ok {
		return
	}

	r.providerData = providerData
	r.client = clients.IaaSv2Client

	tflog.Info(ctx, "iaas client configured")
}

How this works internally

First of all we have the core.ClientCollection struct which should be passed to every resource and datasource:

type ClientCollection struct {
IaaSv2Client iaasv2.DefaultAPI
IaaSv2AlphaClient iaasv2alpha.DefaultAPI
ResourceManagerClient resourcemanager.DefaultAPI
ModelExperimentsV1Client modelexperiments.DefaultAPI
EdgeV1Client edge.DefaultAPI
DnsV1Client dns.DefaultAPI
CdnV1Client cdn.DefaultAPI
ServerBackupV2Client serverbackup.DefaultAPI
AlbCertificatesV2Client certSdk.DefaultAPI
ServiceEnablementV2Client serviceenablement.DefaultAPI
AlbWafV1CLient albwaf.DefaultAPI
LogsV1Client logs.DefaultAPI
VpnV1Client vpn.DefaultAPI
AuthorizationV2Client authorization.DefaultAPI
PostgresflexV3Client postgresflex.DefaultAPI
AlbV2Client alb.DefaultAPI
SkeV2Client ske.DefaultAPI
SqlServerFlexV3Client sqlserverflex.DefaultAPI
ModelservingV1Client modelserving.DefaultAPI
LogmeV2Client logme.DefaultAPI
OpensearchV2Client opensearch.DefaultAPI
GitV1BetaClient git.DefaultAPI
RedisV2Client redis.DefaultAPI
TelemetryRouterV1Client telemetryrouter.DefaultAPI
TelemetryLinkV1Client telemetrylink.DefaultAPI
ServerUpdateV2Client serverupdate.DefaultAPI
KmsV1Client kms.DefaultAPI
SfsV1Client sfs.DefaultAPI
ServiceAccountV2Client serviceaccount.DefaultAPI
RabbitMqV2Client rabbitmq.DefaultAPI
MongoDbFlexV2Client mongodbflex.DefaultAPI
ObjectStorageV2Client objectstorage.DefaultAPI
MariadbV2Client mariadb.DefaultAPI
ScfV1Client scf.DefaultAPI
LoadbalancerV2Client loadbalancer.DefaultAPI
IntakeV1BetaClient intake.DefaultAPI
DremioV1BetaClient dremio.DefaultAPI
SecretsmanagerV1Client secretsmanager.DefaultAPI
SecretsmanagerV1AlphaClient secretsmanagerV1Alpha.DefaultAPI
ObservabilityV1Client observability.DefaultAPI
}

This client collection has to be initialized somehow with all the SDK clients. Therefore we have the initClientCollection function and the ClientFactory

type ClientFactory interface {
// methods are having the API versions in them here so we can still mix & match API versions just as we need
newAlbCertificatesV2Client() (certificates.DefaultAPI, error)
newAlbV2Client() (alb.DefaultAPI, error)
newAlbWafV1Client() (albwaf.DefaultAPI, error)
newAuthorizationV2Client() (authorization.DefaultAPI, error)
newCdnV1Client() (cdn.DefaultAPI, error)
newDnsV1Client() (dns.DefaultAPI, error)
newDremioV1BetaClient() (dremio.DefaultAPI, error)
newEdgeV1Client() (edge.DefaultAPI, error)
newGitV1BetaClient() (git.DefaultAPI, error)
newIaaSV2AlphaClient() (iaasV2Alpha.DefaultAPI, error)
newIaaSV2Client() (iaasV2.DefaultAPI, error)
newIntakeV1BetaClient() (intake.DefaultAPI, error)
newKmsV1Client() (kms.DefaultAPI, error)
newLoadbalancerV2Client() (loadbalancer.DefaultAPI, error)
newLogmeV2Client() (logme.DefaultAPI, error)
newLogsV1Client() (logs.DefaultAPI, error)
newMariadbV2Client() (mariadb.DefaultAPI, error)
newModelExperimentsV1Client() (modelexperiments.DefaultAPI, error)
newModelServingV1Client() (modelserving.DefaultAPI, error)
newMongoDbFlexV2Client() (mongodbflex.DefaultAPI, error)
newObjectStorageV2Client() (objectstorage.DefaultAPI, error)
newObservabilityV1Client() (observability.DefaultAPI, error)
newOpensearchV2Client() (opensearch.DefaultAPI, error)
newPostgresflexV3Client() (postgresflex.DefaultAPI, error)
newRabbitMqV2Client() (rabbitmq.DefaultAPI, error)
newRedisV2Client() (redis.DefaultAPI, error)
newResourceManagerClient() (resourcemanager.DefaultAPI, error)
newScfV1Client() (scf.DefaultAPI, error)
newSecretsManagerV1AlphaClient() (secretsmanagerV1Alpha.DefaultAPI, error)
newSecretsManagerV1Client() (secretsmanager.DefaultAPI, error)
newServerBackupV2Client() (serverbackup.DefaultAPI, error)
newServerUpdateV2Client() (serverupdate.DefaultAPI, error)
newServiceAccountV2Client() (serviceaccount.DefaultAPI, error)
newServiceEnablementV2Client() (serviceenablementV2.DefaultAPI, error)
newSfsV1Client() (sfs.DefaultAPI, error)
newSkeV2Client() (ske.DefaultAPI, error)
newSqlServerFlexV3Client() (sqlserverflex.DefaultAPI, error)
newTelemetryLinkV1Client() (telemetrylink.DefaultAPI, error)
newTelemetryRouterV1Client() (telemetryrouter.DefaultAPI, error)
newVpnV1Client() (vpn.DefaultAPI, error)
}

func initClientCollection(clientFactory ClientFactory) (*ClientCollection, error) {
var g errgroup.Group
cc := &ClientCollection{}
// initialize clients in parallel
g.Go(func() (err error) { cc.IaaSv2Client, err = clientFactory.newIaaSV2Client(); return err })
g.Go(func() (err error) { cc.EdgeV1Client, err = clientFactory.newEdgeV1Client(); return err })
g.Go(func() (err error) { cc.DnsV1Client, err = clientFactory.newDnsV1Client(); return err })
g.Go(func() (err error) { cc.ServerBackupV2Client, err = clientFactory.newServerBackupV2Client(); return err })
g.Go(func() (err error) { cc.AlbWafV1CLient, err = clientFactory.newAlbWafV1Client(); return err })
g.Go(func() (err error) { cc.LogsV1Client, err = clientFactory.newLogsV1Client(); return err })
g.Go(func() (err error) { cc.VpnV1Client, err = clientFactory.newVpnV1Client(); return err })
g.Go(func() (err error) { cc.IaaSv2AlphaClient, err = clientFactory.newIaaSV2AlphaClient(); return err })
g.Go(func() (err error) { cc.CdnV1Client, err = clientFactory.newCdnV1Client(); return err })
g.Go(func() (err error) { cc.PostgresflexV3Client, err = clientFactory.newPostgresflexV3Client(); return err })
g.Go(func() (err error) { cc.AlbV2Client, err = clientFactory.newAlbV2Client(); return err })
g.Go(func() (err error) { cc.SkeV2Client, err = clientFactory.newSkeV2Client(); return err })
g.Go(func() (err error) { cc.ModelservingV1Client, err = clientFactory.newModelServingV1Client(); return err })
g.Go(func() (err error) { cc.LogmeV2Client, err = clientFactory.newLogmeV2Client(); return err })
g.Go(func() (err error) { cc.OpensearchV2Client, err = clientFactory.newOpensearchV2Client(); return err })
g.Go(func() (err error) { cc.GitV1BetaClient, err = clientFactory.newGitV1BetaClient(); return err })
g.Go(func() (err error) { cc.RedisV2Client, err = clientFactory.newRedisV2Client(); return err })
g.Go(func() (err error) { cc.ServerUpdateV2Client, err = clientFactory.newServerUpdateV2Client(); return err })
g.Go(func() (err error) { cc.KmsV1Client, err = clientFactory.newKmsV1Client(); return err })
g.Go(func() (err error) { cc.SfsV1Client, err = clientFactory.newSfsV1Client(); return err })
g.Go(func() (err error) { cc.RabbitMqV2Client, err = clientFactory.newRabbitMqV2Client(); return err })
g.Go(func() (err error) { cc.MongoDbFlexV2Client, err = clientFactory.newMongoDbFlexV2Client(); return err })
g.Go(func() (err error) { cc.MariadbV2Client, err = clientFactory.newMariadbV2Client(); return err })
g.Go(func() (err error) { cc.ScfV1Client, err = clientFactory.newScfV1Client(); return err })
g.Go(func() (err error) { cc.LoadbalancerV2Client, err = clientFactory.newLoadbalancerV2Client(); return err })
g.Go(func() (err error) { cc.IntakeV1BetaClient, err = clientFactory.newIntakeV1BetaClient(); return err })
g.Go(func() (err error) { cc.DremioV1BetaClient, err = clientFactory.newDremioV1BetaClient(); return err })
g.Go(func() (err error) {
cc.ResourceManagerClient, err = clientFactory.newResourceManagerClient()
return err
})
g.Go(func() (err error) {
cc.ModelExperimentsV1Client, err = clientFactory.newModelExperimentsV1Client()
return err
})
g.Go(func() (err error) {
cc.ServiceEnablementV2Client, err = clientFactory.newServiceEnablementV2Client()
return err
})
g.Go(func() (err error) {
cc.AlbCertificatesV2Client, err = clientFactory.newAlbCertificatesV2Client()
return err
})
g.Go(func() (err error) {
cc.AuthorizationV2Client, err = clientFactory.newAuthorizationV2Client()
return err
})
g.Go(func() (err error) {
cc.SqlServerFlexV3Client, err = clientFactory.newSqlServerFlexV3Client()
return err
})
g.Go(func() (err error) {
cc.TelemetryRouterV1Client, err = clientFactory.newTelemetryRouterV1Client()
return err
})
g.Go(func() (err error) {
cc.TelemetryLinkV1Client, err = clientFactory.newTelemetryLinkV1Client()
return err
})
g.Go(func() (err error) {
cc.ServiceAccountV2Client, err = clientFactory.newServiceAccountV2Client()
return err
})
g.Go(func() (err error) {
cc.ServiceAccountV2Client, err = clientFactory.newServiceAccountV2Client()
return err
})
g.Go(func() (err error) {
cc.ObjectStorageV2Client, err = clientFactory.newObjectStorageV2Client()
return err
})
g.Go(func() (err error) {
cc.SecretsmanagerV1Client, err = clientFactory.newSecretsManagerV1Client()
return err
})
g.Go(func() (err error) {
cc.SecretsmanagerV1AlphaClient, err = clientFactory.newSecretsManagerV1AlphaClient()
return err
})
g.Go(func() (err error) {
cc.ObservabilityV1Client, err = clientFactory.newObservabilityV1Client()
return err
})
// wait for initialization of all clients, handle errors
if err := g.Wait(); err != nil {
return nil, err
}
return cc, nil
}

The ClientFactory interface allows us to have a DefaultClientFactory implementation which is used in regular production use of the STACKIT Terraform provider.

var _ ClientFactory = &DefaultClientFactory{}
type DefaultClientFactory struct {
RoundTripper http.RoundTripper
UserAgent string
CustomEndpoints CustomEndpointConfig
// Deprecated: This should be only used for legacy implementations, not for new ones!
ProviderDefaultRegion string
}
func (f *DefaultClientFactory) defaultConfigOptions(customEndpoint string) []config.ConfigurationOption {
apiClientConfigOptions := []config.ConfigurationOption{
config.WithCustomAuth(f.RoundTripper),
config.WithUserAgent(f.UserAgent),
}
if customEndpoint != "" {
apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(customEndpoint))
}
return apiClientConfigOptions
}
func (f *DefaultClientFactory) newAlbV2Client() (alb.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.ALBCustomEndpoint)
apiClient, err := alb.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newGitV1BetaClient() (git.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.GitCustomEndpoint)
apiClient, err := git.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newIntakeV1BetaClient() (intake.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.IntakeCustomEndpoint)
apiClient, err := intake.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newKmsV1Client() (kms.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.KMSCustomEndpoint)
apiClient, err := kms.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newLoadbalancerV2Client() (loadbalancer.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.LoadBalancerCustomEndpoint)
apiClient, err := loadbalancer.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newLogmeV2Client() (logme.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.LogMeCustomEndpoint)
apiClient, err := logme.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newMariadbV2Client() (mariadb.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.MariaDBCustomEndpoint)
apiClient, err := mariadb.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newModelServingV1Client() (modelserving.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.ModelServingCustomEndpoint)
apiClient, err := modelserving.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newMongoDbFlexV2Client() (mongodbflex.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.MongoDBFlexCustomEndpoint)
apiClient, err := mongodbflex.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
// RetryTransport wraps an underlying RoundTripper to handle HTTP 429s with jitter.
type RetryTransport struct {
Base http.RoundTripper
MaxRetries int
BaseBackoff time.Duration
MaxJitter time.Duration
}
func (t *RetryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
base := t.Base
if base == nil {
base = http.DefaultTransport
}
// Preserve request body for retries if present
var bodyBytes []byte
if req.Body != nil && req.Body != http.NoBody {
var err error
bodyBytes, err = io.ReadAll(req.Body)
if err != nil {
return nil, err
}
err = req.Body.Close()
if err != nil {
return nil, err
}
}
var resp *http.Response
var err error
for attempt := 0; attempt <= t.MaxRetries; attempt++ {
// Re-hydrate the request body on each attempt
if bodyBytes != nil {
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
}
resp, err = base.RoundTrip(req)
// If success or non-429 error, return immediately
if err != nil || resp.StatusCode != http.StatusTooManyRequests {
return resp, err
}
// Stop if max retries reached
if attempt == t.MaxRetries {
break
}
// Calculate base sleep duration (Retry-After or Exponential Backoff)
wait := t.getWaitDuration(resp, attempt)
// Always add random jitter regardless of Retry-After header presence
jitter := time.Duration(rand.Int64N(int64(t.MaxJitter))) //nolint:gosec // only used for jitter
totalWait := wait + jitter
// Drain and close response body before retrying to reuse TCP connections
_, err = io.Copy(io.Discard, resp.Body)
if err != nil {
return nil, err
}
err = resp.Body.Close()
if err != nil {
return nil, err
}
select {
case <-req.Context().Done():
return nil, req.Context().Err()
case <-time.After(totalWait):
}
}
return resp, err
}
func (t *RetryTransport) getWaitDuration(resp *http.Response, attempt int) time.Duration {
if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
// Try parsing as integer seconds
if seconds, err := strconv.Atoi(retryAfter); err == nil {
return time.Duration(seconds) * time.Second
}
// Try parsing as HTTP-Date string
if date, err := http.ParseTime(retryAfter); err == nil {
if d := time.Until(date); d > 0 {
return d
}
}
}
// Fallback to exponential backoff
return t.BaseBackoff * (1 << attempt)
}
func (f *DefaultClientFactory) newObjectStorageV2Client() (objectstorage.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.ObjectStorageCustomEndpoint)
mdlw := func(rt http.RoundTripper) http.RoundTripper {
return &RetryTransport{
Base: rt,
MaxRetries: 3,
BaseBackoff: 1 * time.Second,
MaxJitter: 500 * time.Millisecond, // Always added to wait time
}
}
apiClientConfigOptions = append(apiClientConfigOptions, config.WithMiddleware(mdlw))
apiClient, err := objectstorage.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newOpensearchV2Client() (opensearch.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.OpenSearchCustomEndpoint)
apiClient, err := opensearch.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newRabbitMqV2Client() (rabbitmq.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.RabbitMQCustomEndpoint)
apiClient, err := rabbitmq.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newRedisV2Client() (redis.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.RedisCustomEndpoint)
apiClient, err := redis.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newScfV1Client() (scf.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.ScfCustomEndpoint)
apiClient, err := scf.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newServerUpdateV2Client() (serverupdate.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.ServerUpdateCustomEndpoint)
apiClient, err := serverupdate.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newServiceAccountV2Client() (serviceaccount.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.ServiceAccountCustomEndpoint)
apiClient, err := serviceaccount.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newSfsV1Client() (sfs.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.SfsCustomEndpoint)
apiClient, err := sfs.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newSkeV2Client() (ske.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.SKECustomEndpoint)
apiClient, err := ske.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newSqlServerFlexV3Client() (sqlserverflex.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.SQLServerFlexCustomEndpoint)
apiClient, err := sqlserverflex.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newTelemetryLinkV1Client() (telemetrylink.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.TelemetryLinkCustomEndpoint)
apiClient, err := telemetrylink.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newTelemetryRouterV1Client() (telemetryrouter.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.TelemetryRouterCustomEndpoint)
apiClient, err := telemetryrouter.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newAlbCertificatesV2Client() (certificates.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.ALBCertificatesCustomEndpoint)
apiClient, err := certificates.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newCdnV1Client() (cdn.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.CdnCustomEndpoint)
apiClient, err := cdn.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newIaaSV2AlphaClient() (iaasV2Alpha.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.IaaSCustomEndpoint)
apiClient, err := iaasV2Alpha.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newEdgeV1Client() (edge.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.EdgeCloudCustomEndpoint)
apiClient, err := edge.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newAlbWafV1Client() (albwaf.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.AlbWafCustomEndpoint)
apiClient, err := albwaf.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newAuthorizationV2Client() (authorization.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.AuthorizationCustomEndpoint)
apiClient, err := authorization.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newDnsV1Client() (dns.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.DnsCustomEndpoint)
apiClient, err := dns.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newLogsV1Client() (logs.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.LogsCustomEndpoint)
apiClient, err := logs.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newPostgresflexV3Client() (postgresflex.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.PostgresFlexCustomEndpoint)
apiClient, err := postgresflex.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newServerBackupV2Client() (serverbackup.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.ServerBackupCustomEndpoint)
apiClient, err := serverbackup.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newVpnV1Client() (vpn.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.VpnCustomEndpoint)
apiClient, err := vpn.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newServiceEnablementV2Client() (serviceenablementV2.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.ServiceEnablementCustomEndpoint)
apiClient, err := serviceenablementV2.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newIaaSV2Client() (iaasV2.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.IaaSCustomEndpoint)
apiClient, err := iaasV2.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newResourceManagerClient() (resourcemanager.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.ResourceManagerCustomEndpoint)
apiClient, err := resourcemanager.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newModelExperimentsV1Client() (modelexperiments.DefaultAPI, error) {
apiClientConfigOptions := f.defaultConfigOptions(f.CustomEndpoints.ModelExperimentsCustomEndpoint)
apiClient, err := modelexperiments.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newDremioV1BetaClient() (dremio.DefaultAPI, error) {
apiClientConfigOptions := []config.ConfigurationOption{
config.WithCustomAuth(f.RoundTripper),
config.WithUserAgent(f.UserAgent),
config.WithRegion(f.ProviderDefaultRegion),
}
if f.CustomEndpoints.DremioCustomEndpoint != "" {
apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(f.CustomEndpoints.DremioCustomEndpoint))
}
apiClient, err := dremio.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newSecretsManagerV1Client() (secretsmanager.DefaultAPI, error) {
apiClientConfigOptions := []config.ConfigurationOption{
config.WithCustomAuth(f.RoundTripper),
config.WithUserAgent(f.UserAgent),
}
if f.CustomEndpoints.SecretsManagerCustomEndpoint != "" {
apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(f.CustomEndpoints.SecretsManagerCustomEndpoint))
} else {
apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(f.ProviderDefaultRegion))
}
apiClient, err := secretsmanager.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newSecretsManagerV1AlphaClient() (secretsmanagerV1Alpha.DefaultAPI, error) {
apiClientConfigOptions := []config.ConfigurationOption{
config.WithCustomAuth(f.RoundTripper),
config.WithUserAgent(f.UserAgent),
}
if f.CustomEndpoints.SecretsManagerCustomEndpoint != "" {
apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(f.CustomEndpoints.SecretsManagerCustomEndpoint))
} else {
apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(f.ProviderDefaultRegion))
}
apiClient, err := secretsmanagerV1Alpha.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}
func (f *DefaultClientFactory) newObservabilityV1Client() (observability.DefaultAPI, error) {
apiClientConfigOptions := []config.ConfigurationOption{
config.WithCustomAuth(f.RoundTripper),
config.WithUserAgent(f.UserAgent),
}
if f.CustomEndpoints.ObservabilityCustomEndpoint != "" {
apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(f.CustomEndpoints.ObservabilityCustomEndpoint))
} else {
apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(f.ProviderDefaultRegion))
}
apiClient, err := observability.NewAPIClient(apiClientConfigOptions...)
if err != nil {
return nil, fmt.Errorf("configuring client: %w. This is an error related to the provider configuration, not to the resource configuration", err)
}
return apiClient.DefaultAPI, nil
}

The MockClientFactory is another implementation of the ClientFactory interface and allows us to inject mocked SDK clients into the provider for API-mocked testing of resource and datasource implementations.

var _ ClientFactory = &MockClientFactory{}
type MockClientFactory struct {
AlbCertificatesV2ClientMock certificates.DefaultAPI
AlbV2ClientMock albSdk.DefaultAPI
AlbWafV1ClientMock albWaf.DefaultAPI
AuthorizationV2ClientMock authorization.DefaultAPI
CdnV1ClientMock cdnSdk.DefaultAPI
DnsV1ClientMock dns.DefaultAPI
DremioV2BetaClientMock dremioSdk.DefaultAPI
EdgeV1ClientMock edge.DefaultAPI
GitV1BetaClientMock git.DefaultAPI
IaaSV2ClientMock iaasV2.DefaultAPI
IaasV2AlphaClientMock iaasV2Alpha.DefaultAPI
IntakeV1BetaClientMock intake.DefaultAPI
KmsV1ClientMock kms.DefaultAPI
LoadbalancerV2ClientMock loadbalancer.DefaultAPI
LogmeV2ClientMock logmeSdk.DefaultAPI
LogsV1ClientMock logs.DefaultAPI
MariadbV2ClientMock mariadb.DefaultAPI
ModelExperimentsV1ClientMock modelexperiments.DefaultAPI
ModelServerV1ClientMock modelserving.DefaultAPI
MongoDbFlexV2ClientMock mongodbflex.DefaultAPI
ObjectStorageV2ClientMock objectstorage.DefaultAPI
ObservabilityV1ClientMock observabilitySdk.DefaultAPI
OpensearchV2ClientMock opensearch.DefaultAPI
PostgresflexV3ClientMock postgresflex.DefaultAPI
RabbitMqV2ClientMock rabbitmq.DefaultAPI
RedisV2ClientMock redis.DefaultAPI
ResourceManagerClientMock resourcemanager.DefaultAPI
ScfV1ClientMock scf.DefaultAPI
SecretsManagerV1AlphaClientMock secretsmanagerV1Alpha.DefaultAPI
SecretsManagerV1ClientMock secretsmanager.DefaultAPI
ServerBackupV2ClientMock serverbackup.DefaultAPI
ServerUpdateV2ClientMock serverupdate.DefaultAPI
ServiceAccountV2ClientMock serviceaccount.DefaultAPI
ServiceEnablementV2ClientMock serviceenablementV2.DefaultAPI
SfsV1ClientMock sfs.DefaultAPI
SkeV2ClientMock ske.DefaultAPI
SqlServerFlexV3ClientMock sqlserverflex.DefaultAPI
TelemetryLinkV1ClientMock telemetrylink.DefaultAPI
TelemetryRouterV1ClientMock telemetryrouter.DefaultAPI
VpnV1ClientMock vpn.DefaultAPI
}
func (m *MockClientFactory) newAlbV2Client() (albSdk.DefaultAPI, error) {
if m.AlbV2ClientMock != nil {
return m.AlbV2ClientMock, nil
}
return albSdk.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newDremioV1BetaClient() (dremioSdk.DefaultAPI, error) {
if m.DremioV2BetaClientMock != nil {
return m.DremioV2BetaClientMock, nil
}
return dremioSdk.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newGitV1BetaClient() (git.DefaultAPI, error) {
if m.GitV1BetaClientMock != nil {
return m.GitV1BetaClientMock, nil
}
return git.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newIntakeV1BetaClient() (intake.DefaultAPI, error) {
if m.IntakeV1BetaClientMock != nil {
return m.IntakeV1BetaClientMock, nil
}
return intake.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newKmsV1Client() (kms.DefaultAPI, error) {
if m.KmsV1ClientMock != nil {
return m.KmsV1ClientMock, nil
}
return kms.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newLoadbalancerV2Client() (loadbalancer.DefaultAPI, error) {
if m.LoadbalancerV2ClientMock != nil {
return m.LoadbalancerV2ClientMock, nil
}
return loadbalancer.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newLogmeV2Client() (logmeSdk.DefaultAPI, error) {
if m.LogmeV2ClientMock != nil {
return m.LogmeV2ClientMock, nil
}
return logmeSdk.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newMariadbV2Client() (mariadb.DefaultAPI, error) {
if m.MariadbV2ClientMock != nil {
return m.MariadbV2ClientMock, nil
}
return mariadb.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newModelServingV1Client() (modelserving.DefaultAPI, error) {
if m.ModelServerV1ClientMock != nil {
return m.ModelServerV1ClientMock, nil
}
return modelserving.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newMongoDbFlexV2Client() (mongodbflex.DefaultAPI, error) {
if m.MongoDbFlexV2ClientMock != nil {
return m.MongoDbFlexV2ClientMock, nil
}
return mongodbflex.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newObjectStorageV2Client() (objectstorage.DefaultAPI, error) {
if m.ObjectStorageV2ClientMock != nil {
return m.ObjectStorageV2ClientMock, nil
}
return objectstorage.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newObservabilityV1Client() (observabilitySdk.DefaultAPI, error) {
if m.ObservabilityV1ClientMock != nil {
return m.ObservabilityV1ClientMock, nil
}
return observabilitySdk.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newOpensearchV2Client() (opensearch.DefaultAPI, error) {
if m.OpensearchV2ClientMock != nil {
return m.OpensearchV2ClientMock, nil
}
return opensearch.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newRabbitMqV2Client() (rabbitmq.DefaultAPI, error) {
if m.RabbitMqV2ClientMock != nil {
return m.RabbitMqV2ClientMock, nil
}
return rabbitmq.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newRedisV2Client() (redis.DefaultAPI, error) {
if m.RedisV2ClientMock != nil {
return m.RedisV2ClientMock, nil
}
return redis.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newScfV1Client() (scf.DefaultAPI, error) {
if m.ScfV1ClientMock != nil {
return m.ScfV1ClientMock, nil
}
return scf.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newSecretsManagerV1AlphaClient() (secretsmanagerV1Alpha.DefaultAPI, error) {
if m.SecretsManagerV1AlphaClientMock != nil {
return m.SecretsManagerV1AlphaClientMock, nil
}
return secretsmanagerV1Alpha.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newSecretsManagerV1Client() (secretsmanager.DefaultAPI, error) {
if m.SecretsManagerV1ClientMock != nil {
return m.SecretsManagerV1ClientMock, nil
}
return secretsmanager.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newServerUpdateV2Client() (serverupdate.DefaultAPI, error) {
if m.ServerUpdateV2ClientMock != nil {
return m.ServerUpdateV2ClientMock, nil
}
return serverupdate.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newServiceAccountV2Client() (serviceaccount.DefaultAPI, error) {
if m.ServiceAccountV2ClientMock != nil {
return m.ServiceAccountV2ClientMock, nil
}
return serviceaccount.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newSfsV1Client() (sfs.DefaultAPI, error) {
if m.SfsV1ClientMock != nil {
return m.SfsV1ClientMock, nil
}
return sfs.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newSkeV2Client() (ske.DefaultAPI, error) {
if m.SkeV2ClientMock != nil {
return m.SkeV2ClientMock, nil
}
return ske.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newSqlServerFlexV3Client() (sqlserverflex.DefaultAPI, error) {
if m.SqlServerFlexV3ClientMock != nil {
return m.SqlServerFlexV3ClientMock, nil
}
return sqlserverflex.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newTelemetryLinkV1Client() (telemetrylink.DefaultAPI, error) {
if m.TelemetryLinkV1ClientMock != nil {
return m.TelemetryLinkV1ClientMock, nil
}
return telemetrylink.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newTelemetryRouterV1Client() (telemetryrouter.DefaultAPI, error) {
if m.TelemetryRouterV1ClientMock != nil {
return m.TelemetryRouterV1ClientMock, nil
}
return telemetryrouter.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newAlbCertificatesV2Client() (certificates.DefaultAPI, error) {
if m.AlbCertificatesV2ClientMock != nil {
return m.AlbCertificatesV2ClientMock, nil
}
return certificates.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newCdnV1Client() (cdnSdk.DefaultAPI, error) {
if m.CdnV1ClientMock != nil {
return m.CdnV1ClientMock, nil
}
return cdnSdk.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newIaaSV2AlphaClient() (iaasV2Alpha.DefaultAPI, error) {
if m.IaaSV2ClientMock != nil {
return m.IaasV2AlphaClientMock, nil
}
return iaasV2Alpha.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newEdgeV1Client() (edge.DefaultAPI, error) {
if m.EdgeV1ClientMock != nil {
return m.EdgeV1ClientMock, nil
}
return edge.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newAlbWafV1Client() (albWaf.DefaultAPI, error) {
if m.AlbWafV1ClientMock != nil {
return m.AlbWafV1ClientMock, nil
}
return albWaf.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newAuthorizationV2Client() (authorization.DefaultAPI, error) {
if m.AuthorizationV2ClientMock != nil {
return m.AuthorizationV2ClientMock, nil
}
return authorization.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newDnsV1Client() (dns.DefaultAPI, error) {
if m.DnsV1ClientMock != nil {
return m.DnsV1ClientMock, nil
}
return dns.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newLogsV1Client() (logs.DefaultAPI, error) {
if m.LogsV1ClientMock != nil {
return m.LogsV1ClientMock, nil
}
return logs.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newPostgresflexV3Client() (postgresflex.DefaultAPI, error) {
if m.PostgresflexV3ClientMock != nil {
return m.PostgresflexV3ClientMock, nil
}
return postgresflex.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newServerBackupV2Client() (serverbackup.DefaultAPI, error) {
if m.ServerBackupV2ClientMock != nil {
return m.ServerBackupV2ClientMock, nil
}
return serverbackup.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newVpnV1Client() (vpn.DefaultAPI, error) {
if m.VpnV1ClientMock != nil {
return m.VpnV1ClientMock, nil
}
return vpn.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newServiceEnablementV2Client() (serviceenablementV2.DefaultAPI, error) {
if m.ServiceEnablementV2ClientMock != nil {
return m.ServiceEnablementV2ClientMock, nil
}
return serviceenablementV2.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newIaaSV2Client() (iaasV2.DefaultAPI, error) {
if m.IaaSV2ClientMock != nil {
return m.IaaSV2ClientMock, nil
}
return iaasV2.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newResourceManagerClient() (resourcemanager.DefaultAPI, error) {
if m.ResourceManagerClientMock != nil {
return m.ResourceManagerClientMock, nil
}
return resourcemanager.DefaultAPIServiceMock{}, nil
}
func (m *MockClientFactory) newModelExperimentsV1Client() (modelexperiments.DefaultAPI, error) {
if m.ModelExperimentsV1ClientMock != nil {
return m.ModelExperimentsV1ClientMock, nil
}
return modelexperiments.DefaultAPIServiceMock{}, nil
}

The injection of the mock client factory happens here:

// New is a helper function to simplify provider server and testing implementation.
func New(version string) func() provider.Provider {
return func() provider.Provider {
return &Provider{
version: version,
clientFactory: nil, // nil means the default client factory will be used later
}
}
}
func NewTestProvider(version string, clientFactory core.ClientFactory) func() provider.Provider {
return func() provider.Provider {
return &Provider{
version: version,
clientFactory: clientFactory,
}
}
}

Splitting the provider data into two structs

In Terraform plugin framework you can have one struct which is passed as provider data to each resource and datasource.

In the past this struct looked like this:

type ProviderData struct {
RoundTripper http.RoundTripper
ServiceAccountEmail string
DefaultRegion string
ALBCertificatesCustomEndpoint string
ALBCustomEndpoint string
AlbWafCustomEndpoint string
AuthorizationCustomEndpoint string
AutomationCustomEndpoint string
CdnCustomEndpoint string
DnsCustomEndpoint string
DremioCustomEndpoint string
EdgeCloudCustomEndpoint string
GitCustomEndpoint string
IaaSCustomEndpoint string
IntakeCustomEndpoint string
KMSCustomEndpoint string
LoadBalancerCustomEndpoint string
LogMeCustomEndpoint string
LogsCustomEndpoint string
MariaDBCustomEndpoint string
MongoDBFlexCustomEndpoint string
ModelServingCustomEndpoint string
ModelExperimentsCustomEndpoint string
ObjectStorageCustomEndpoint string
ObservabilityCustomEndpoint string
OpenSearchCustomEndpoint string
PostgresFlexCustomEndpoint string
RabbitMQCustomEndpoint string
RedisCustomEndpoint string
ResourceManagerCustomEndpoint string
ScfCustomEndpoint string
SecretsManagerCustomEndpoint string
SQLServerFlexCustomEndpoint string
ServerBackupCustomEndpoint string
ServerUpdateCustomEndpoint string
SKECustomEndpoint string
ServiceEnablementCustomEndpoint string
SfsCustomEndpoint string
ServiceAccountCustomEndpoint string
TelemetryLinkCustomEndpoint string
TelemetryRouterCustomEndpoint string
ValkeyCustomEndpoint string
VpnCustomEndpoint string
EnableBetaResources bool
Experiments []string
Version string // version of the STACKIT Terraform provider
}

It e.g. included a round tripper for authentication of all the SDK clients which were created using the util functions in each resource/datasource configure method.

RoundTripper http.RoundTripper

config.WithCustomAuth(providerData.RoundTripper),

Since to allow for easy mocking we need to pass pre-populated SDK clients to all resources/datasources in the future we need to add this client to provider data. The provider data struct would grow therefore massively with API clients for every STACKIT service and service version.

At the same we need to keep this provider data struct within our resource/datasource implementation structs to make the region handling possible (using a default_region configured on provider level and allowing our TF users to optionally override it in each resource/datasource configuration).

// volumeResource is the resource implementation.
type volumeResource struct {
client *iaas.APIClient
providerData core.ProviderData
}

Reminder: We can only pass one provider data struct to each resource/datasource. Therefore I decided to split the provider data. The provider data struct passed should consist of two parts: The formerly known provider data (including e.g. the default_region config value or the list of enabled provider experiments) and the client collection.

The provider data struct is meant to be kept in each regional resource/datasource implementation in the same way we did until now. The client collection should only be used in the Configure method of each resource/datasource implementation.

Both structs are tied together using the core.providerDataInternal struct.

type providerDataInternal struct {
// providerData is the public provider data
providerData ProviderData
clients ClientCollection
}

The providerDataInternal struct type isn't exposed by design from the core package. This way the inside the resource and datasource implementations the provider data stays the same like we've previously known it (it even shrinked since we don't need to keep all custom endpoint configs, etc. in it any more).

func ParseProviderData(ctx context.Context, providerData any, diags *diag.Diagnostics) (ProviderData, ClientCollection, bool) {
// Prevent panic if the provider has not been configured.
if providerData == nil {
return ProviderData{}, ClientCollection{}, false
}
stackitProviderDataInternal, ok := providerData.(providerDataInternal)
if !ok {
LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Expected configure type core.providerDataInternal, got %T", providerData))
return ProviderData{}, ClientCollection{}, false
}
return stackitProviderDataInternal.providerData, stackitProviderDataInternal.clients, true
}

Ensuring code quality using automation

Two measures were taken to assert the code quality after this implementation:

  • A tfclientcollection linter was implemented to make sure the core.ClientCollection struct is only consumed from core.ParseProviderData but stored in the resource/datasource implementation structs or passed as a parameter to other functions called by the resource/datasource Configure method implementations.
    • The core.ClientCollection struct can't be package-private in the core package because there's one exception to that: the generic IAM rolebinding resource/datasource implementations.
  • A unit test was added to ensure each client available in the core.ClientCollection struct is actually initialized using the ClientFactory interface in the initClientCollection function: test

Furthermore for testing all the client initialization implementations of the DefaultClientFactory some generic test handler was implemented to get rid of the insane amount of boilerplate code we maintained beforehand.

Checklist

  • Issue was linked above
  • Code format was applied: make fmt
  • Examples were added / adjusted (see examples/ directory)
  • Docs are up-to-date: make generate-docs (will be checked by CI)
  • Unit tests got implemented or updated
  • Acceptance tests got implemented or updated (see e.g. here)
  • Unit tests are passing: make test (will be checked by CI)
  • No linter issues: make lint (will be checked by CI)

@github-actions

Copy link
Copy Markdown

This PR was marked as stale after 7 days of inactivity and will be closed after another 7 days of further inactivity. If this PR should be kept open, just add a comment, remove the stale label or push new commits to it.

@github-actions github-actions Bot added the Stale PR is marked as stale due to inactivity. label Aug 13, 2026
@rubenhoenle rubenhoenle removed the Stale PR is marked as stale due to inactivity. label Aug 17, 2026
@github-actions

Copy link
Copy Markdown

This PR was marked as stale after 7 days of inactivity and will be closed after another 7 days of further inactivity. If this PR should be kept open, just add a comment, remove the stale label or push new commits to it.

@github-actions github-actions Bot added the Stale PR is marked as stale due to inactivity. label Aug 25, 2026
@rubenhoenle rubenhoenle removed the Stale PR is marked as stale due to inactivity. label Sep 1, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

This PR was marked as stale after 7 days of inactivity and will be closed after another 7 days of further inactivity. If this PR should be kept open, just add a comment, remove the stale label or push new commits to it.

@github-actions github-actions Bot added the Stale PR is marked as stale due to inactivity. label Sep 9, 2026
@rubenhoenle rubenhoenle removed the Stale PR is marked as stale due to inactivity. label Sep 9, 2026
@rubenhoenle
rubenhoenle marked this pull request as ready for review September 17, 2026 16:21
@rubenhoenle
rubenhoenle requested a review from a team as a code owner September 17, 2026 16:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant