Skip to content

sfs: provider panics with a nil pointer dereference when the API returns an undecodable response #1743

Open

Description

@devpie

Description

The SFS resources and data sources crash the provider process with a nil pointer dereference when an API call
returns a response that the SDK decodes to nil. Terraform reports it as The plugin encountered an error, and failed to respond to the plugin6.(*GRPCProvider).ReadResource call, so the operator gets a plugin crash instead of
a diagnostic.

Two routes lead to a nil response, neither of which requires anything unusual from the SFS API:

1. A 2xx answer with an empty body. The generated SDK decoder returns early for an empty body
(v1api/client.go):

func (c *APIClient) decode(v interface{}, b []byte, contentType string) (err error) {
	if len(b) == 0 {
		return nil
	}

localVarReturnValue therefore stays nil and Execute() returns (nil, nil).

2. A single retryable gateway error during a wait. WaiterHelper.Wait returns true for waitFinished on any
fetch error, handleError swallows a 502 or 504 and returns a nil error, and the loop then returns the nil response
because it is already done — the retry never happens. Reported separately as
stackitcloud/stackit-sdk-go#11084 with a runnable reproduction.

The call sites check only the returned error. Several of the checks that are meant to validate a response are
themselves the dereference — they test x.Field == nil without first testing x == nil:

// stackit/internal/services/sfs/resourcepool/resource.go, v0.113.0
if response.ResourcePool == nil || response.ResourcePool.Id == nil {   // panics when response is nil

and in both create paths the wait handler result is read by tflog.SetField one line before its guard.

Steps to reproduce

A plain Terraform configuration cannot trigger this deterministically — it needs the API to answer a GET with an
empty body or a single 502, which is not something a config can provoke. The configuration involved is unremarkable:

resource "stackit_sfs_resource_pool" "this" {
  project_id        = var.project_id
  name              = "sfs-pool"
  availability_zone = "eu01-m"
  performance_class = "Standard"
  size_gigabytes    = 512
  ip_acl            = ["192.168.2.0/24"]
}

The crash reproduces deterministically as a unit test against a mock server. Add this to
stackit/internal/services/sfs/sfs_test.go and run
go test ./stackit/internal/services/sfs/ -run TestSfsResourcePoolReadHandlesEmptyResponse:

func TestSfsResourcePoolReadHandlesEmptyResponse(t *testing.T) {
	projectId := uuid.NewString()
	resourcePoolId := uuid.NewString()
	const region = "eu01"

	s := testutil.NewMockServer(t)
	defer s.Server.Close()
	tfConfig := fmt.Sprintf(`
provider "stackit" {
	default_region = "%s"
	sfs_custom_endpoint = "%s"
	service_account_token = "mock-server-needs-no-auth"
	enable_beta_resources = true
}
resource "stackit_sfs_resource_pool" "resourcepool" {
  project_id        = "%s"
  name              = "sfs-instance"
  availability_zone = "eu01-m"
  performance_class = "Standard"
  size_gigabytes    = 512
  ip_acl            = ["192.168.2.0/24"]
}
`, region, s.Server.URL, projectId)

	resource.UnitTest(t, resource.TestCase{
		ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
		Steps: []resource.TestStep{
			{
				// Fail the create wait so the IDs are in state and the next step can refresh them.
				PreConfig: func() {
					s.Reset(
						testutil.MockResponse{
							Description: "create resource pool",
							ToJsonBody: sfs.CreateResourcePoolResponse{
								ResourcePool: &sfs.ResourcePool{Id: new(resourcePoolId)},
							},
						},
						testutil.MockResponse{Description: "failing waiter", StatusCode: http.StatusInternalServerError},
					)
				},
				Config:      tfConfig,
				ExpectError: regexp.MustCompile("Error creating resource pool"),
			},
			{
				PreConfig: func() {
					s.Reset(
						testutil.MockResponse{Description: "refresh with an empty body", StatusCode: http.StatusOK},
						testutil.MockResponse{Description: "delete", StatusCode: http.StatusAccepted},
						testutil.MockResponse{Description: "delete waiter", StatusCode: http.StatusNotFound},
					)
				},
				RefreshState: true,
				ExpectError:  regexp.MustCompile("Error reading resource pool"),
			},
		},
	})
}

Actual behavior

The provider process panics:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x105620e48]

github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sfs/resourcepool.(*resourcePoolResource).Read(...)
	stackit/internal/services/sfs/resourcepool/resource.go:375
github.com/hashicorp/terraform-plugin-framework/internal/fwserver.(*Server).ReadResource(...)
	terraform-plugin-framework@v1.19.0/internal/fwserver/server_readresource.go:156

Line 375 is err = mapFields(ctx, region, response.ResourcePool, &model); on the released v0.113.0 it is line 372.

Affected sites, all of them reachable by one of the two routes above:

file method
sfs/resourcepool/resource.go Create (wait result, post-create GET), Read, Update (PATCH response, wait result)
sfs/resourcepool/datasource.go Read
sfs/share/resource.go Create (POST response, wait result, post-create GET), Read, Update (PATCH response, wait result)
sfs/share/datasource.go Read
sfs/snapshots/datasource.go Read

export-policy and snapshot-policy already guard their responses, project-lock uses nil-receiver-safe getters,
and every SFS Delete discards the response, so the delete waiter's legitimate nil return on a 404 is harmless.

Expected behavior

A response the SDK could not decode should produce a Terraform diagnostic, not a plugin crash. The package already
has the right shape for this in resourcepool/resource.go — one combined condition covering the response and the
fields actually used:

if resourcePool == nil || resourcePool.ResourcePool == nil || resourcePool.ResourcePool.Id == nil {
	core.LogAndAddError(ctx, &resp.Diagnostics, "error creating resource pool", "Calling API: Incomplete response (id missing)")
	return
}

Environment

  • OS: macOS 15 (darwin/arm64)
  • Terraform version (see terraform --version): reproduced through terraform-plugin-testing v1.16.0; not version-specific
  • Version of the STACKIT Terraform provider: v0.113.0, and main at a7d114b

Additional information

A fix is open as #1741. It guards the call sites so the provider reports a diagnostic; the underlying
(nil, nil) return from the wait handler is stackitcloud/stackit-sdk-go#11084.

Activity

  1. devpie commented on Sep 7, 2026

    @devpie
    ContributorAuthor

    This is not an SFS-only problem, and the sweep turned up one crash that is plainer than any of them.

    I ran NilAway over main (a7d114b), one service at a time. The raw output is
    3122 findings, but that number is misleading in two ways: the same site is reported once per service that pulls it in,
    and 94% of the raw hits come from acceptance-test code indexing config.Variables maps, all funnelling into a single
    helper in internal/testutil. Deduplicated and split:

    count
    unique sites 181
    in test code 51
    in production code 130

    The plainest finding first

    stackit/internal/services/iaas/server/resource.go:840 dereferences a server that may be nil, because the call that
    produced it never returns on error:

    // :825
    if server, err = r.client.DefaultAPI.GetServer(ctx, projectId, region, serverId).Execute(); err != nil {
        core.LogAndAddError(ctx, &resp.Diagnostics, "Error retrieving server state", ...)
    }   // no return
    
    // :840
    if err := updateServerStatus(ctx, r.client.DefaultAPI, server.Status, &model, region); err != nil {

    Any failing GetServer — a 500, a timeout, a transient network error — logs a diagnostic and then panics on
    server.Status, so the operator gets a plugin crash instead of the error message that was just recorded. This needs no
    assumption about response bodies.

    The identical missing return sits at :577 in the create path, but there it is saved by accident: mapFields
    rejects a nil response at :949 and the caller returns at :583-585. So :588, which NilAway also flags, is a false
    positive.

    Filed separately as #1746 with a fix in #1747, since it is a different defect from the one this issue describes.

    The bulk of it

    Of the 130 production sites:

    class count
    SDK Execute() returning (nil, nil), dereferenced unchecked 97
    a function returns nil — 11 provider functions, 6 generated SDK models (GetActualInstance()) 17
    unassigned variable 12
    other, including the two iaas/server sites above 4

    These buckets follow NilAway's message wording rather than the underlying mechanism; grouped by mechanism the SDK's
    share is larger than the first row alone suggests.

    The 97 are the class this issue is about. They span 34 of the 43 service directories under
    stackit/internal/services/ — 33 distinct STACKIT services, since iaas and iaasalpha are the stable and the alpha
    API of the same one: iaas 17, observability 7, sfs 6, albwaf 4, scf 4, dns 3, loadbalancer 3, logme 3,
    and 25 more. A site counts here when NilAway traces the nil back to the generated client's unassigned
    localVarReturnValue; the two iaas/server sites are excluded because it flags them only as an unguarded Execute()
    result.

    What that means for #1741: it covers the SFS subset only. Measured with the same tool, SFS production code goes
    from 6 sites to 0 with that branch applied, and the remaining 91 are untouched — the commit changes six files, all
    under stackit/internal/services/sfs/, and no shared package or dependency.

    A caveat I want to state plainly: the 97 are candidates, not confirmed crashes. NilAway does produce false
    positives — iaas/server/resource.go:588 above is one — and "the response can be nil" is formally always true here.
    The real question per site is whether that particular endpoint can answer 2xx with a body the generated decoder turns
    into nil.

    I reproduced two, both by pointing the provider at a mock server answering 200 with an empty body: the SFS share
    Create (row 80, share/resource.go:246) and the SFS resource pool Read described above. Both panic on main. The
    Read is not a row of its own here — NilAway reports resourcepool/resource.go:375 as a second dereference point of
    the same nil source as row 77. That leaves 96 rows I have not reproduced, and forcing an empty body from a mock does
    not show that any real endpoint sends one.

    On the other nil route: the wait handlers can also hand back (nil, nil), reported as
    stackitcloud/stackit-sdk-go#11084. If retrying 5xx responses moves into the SDK's HTTP client instead of living in the
    wait handlers, that route closes by itself — the handler would no longer swallow an error and then fall through to its
    done branch. It would not touch the 97 here, which come from the empty-body path in decode().

    Ways forward

    1. Per site, service by service, in the shape of fix(sfs): guard against nil responses from the API #1741: one combined x == nil guard covering the response and the
      fields actually used, matching the pattern already used elsewhere in the codebase.
    2. Closer to the source. All 97 trace back to the generated client's decode(), which returns early for an empty
      body and leaves the return value nil on the success path. Whether that should change in the SDK rather than in 97
      call sites is your call — it would be a behaviour change for every consumer, so I did not assume it.

    Before doing 1 at scale I would sample 10–15 sites and check how many are real, so the effort goes where it pays. I am
    happy to do any of these.

    All 97 sites (file:line, SDK call, dereferenced field)
    # file:line SDK call field
    1 stackit/internal/services/alb/applicationloadbalancer/resource.go:1180 CreateLoadBalancerExecute() Name
    2 stackit/internal/services/alb/applicationloadbalancer/resource.go:1302 UpdateLoadBalancerExecute() Name
    3 stackit/internal/services/albcertificates/certificate/resource.go:241 CreateCertificateExecute() Id
    4 stackit/internal/services/albwaf/custom_rule_group/resource.go:413 CreateCustomRuleGroupExecute() Name
    5 stackit/internal/services/albwaf/managed_rule_set/resource.go:300 CreateManagedRuleSetExecute() Name
    6 stackit/internal/services/albwaf/waf_configuration/resource.go:237 CreateWAFExecute() Name
    7 stackit/internal/services/albwaf/waf_configuration/resource.go:369 UpdateWAFExecute() Name
    8 stackit/internal/services/cdn/distribution/datasource.go:417 GetDistributionExecute() Distribution
    9 stackit/internal/services/cdn/distribution/resource.go:813 CreateDistributionExecute() Distribution
    10 stackit/internal/services/dns/recordset/resource.go:238 CreateRecordSetExecute() Rrset
    11 stackit/internal/services/dns/zone/datasource.go:261 ListZonesExecute() TotalItems
    12 stackit/internal/services/dns/zone/resource.go:337 CreateZoneExecute() Zone
    13 stackit/internal/services/edgecloud/plans/datasource.go:163 ListPlansProjectExecute() ValidPlans
    14 stackit/internal/services/git/instance/resource.go:223 CreateInstanceExecute() Id
    15 stackit/internal/services/iaas/affinitygroup/resource.go:221 CreateAffinityGroupExecute() Id
    16 stackit/internal/services/iaas/image/resource.go:447 CreateImageExecute() Id
    17 stackit/internal/services/iaas/imagev2/datasource.go:433 ListImagesExecute() Items
    18 stackit/internal/services/iaas/machinetype/datasource.go:186 ListMachineTypesExecute() Items
    19 stackit/internal/services/iaas/network/resource.go:491 CreateNetworkExecute() Id
    20 stackit/internal/services/iaas/networkarea/resource.go:179 CreateNetworkAreaExecute() Id
    21 stackit/internal/services/iaas/networkarea/resource.go:340 ListNetworkAreaRegionsExecute() Regions
    22 stackit/internal/services/iaas/networkarearegion/resource.go:702 ListNetworkAreaRangesExecute() Items
    23 stackit/internal/services/iaas/networkarearoute/resource.go:366 CreateNetworkAreaRouteExecute() Items
    24 stackit/internal/services/iaas/networkinterface/resource.go:310 CreateNicExecute() Id
    25 stackit/internal/services/iaas/publicip/resource.go:216 CreatePublicIPExecute() Id
    26 stackit/internal/services/iaas/securitygroup/resource.go:233 CreateSecurityGroupExecute() Id
    27 stackit/internal/services/iaas/securitygrouprule/resource.go:493 CreateSecurityGroupRuleExecute() Id
    28 stackit/internal/services/iaas/server/resource.go:552 CreateServerExecute() Id
    29 stackit/internal/services/iaas/server/resource.go:775 UpdateServerExecute() MachineType
    30 stackit/internal/services/iaas/volume/resource.go:479 CreateVolumeExecute() Id
    31 stackit/internal/services/iaas/volume/resource.go:611 UpdateVolumeExecute() Size
    32 stackit/internal/services/iaasalpha/vpcroutingtable/resource.go:259 AddVPCRoutingTableExecute() Id
    33 stackit/internal/services/intake/runner/resource.go:247 CreateIntakeRunnerExecute() Id
    34 stackit/internal/services/loadbalancer/loadbalancer/resource.go:785 CreateLoadBalancerExecute() Name
    35 stackit/internal/services/loadbalancer/observability-credential/resource.go:217 CreateCredentialsExecute() Credential
    36 stackit/internal/services/loadbalancer/observability-credential/resource.go:275 GetCredentialsExecute() Credential
    37 stackit/internal/services/logme/credential/resource.go:228 CreateCredentialsExecute() Id
    38 stackit/internal/services/logme/instance/resource.go:478 CreateInstanceExecute() InstanceId
    39 stackit/internal/services/logme/instance/resource.go:982 ListOfferingsExecute() Offerings
    40 stackit/internal/services/logs/accesstoken/resource.go:290 CreateAccessTokenExecute() Id
    41 stackit/internal/services/mariadb/credential/resource.go:254 CreateCredentialsExecute() Id
    42 stackit/internal/services/mariadb/instance/resource.go:794 ListOfferingsExecute() Offerings
    43 stackit/internal/services/modelserving/token/resource.go:307 CreateTokenExecute() Token
    44 stackit/internal/services/mongodbflex/instance/resource.go:1085 ListFlavorsExecute() Flavors
    45 stackit/internal/services/objectstorage/credential/resource.go:305 CreateAccessKeyExecute() KeyId
    46 stackit/internal/services/observability/alertgroup/datasource.go:169 GetAlertgroupExecute() Data
    47 stackit/internal/services/observability/alertgroup/resource.go:329 CreateAlertgroupsExecute() Data
    48 stackit/internal/services/observability/instance/resource.go:1033 CreateInstanceExecute() InstanceId
    49 stackit/internal/services/observability/instance/resource.go:2564 ListPlansExecute() Plans
    50 stackit/internal/services/observability/log-alertgroup/datasource.go:165 GetLogsAlertgroupExecute() Data
    51 stackit/internal/services/observability/log-alertgroup/resource.go:276 CreateLogsAlertgroupsExecute() Data
    52 stackit/internal/services/observability/scrapeconfig/datasource.go:219 GetScrapeConfigExecute() Data
    53 stackit/internal/services/opensearch/credential/resource.go:254 CreateCredentialsExecute() Id
    54 stackit/internal/services/opensearch/instance/resource.go:416 CreateInstanceExecute() InstanceId
    55 stackit/internal/services/opensearch/instance/resource.go:864 ListOfferingsExecute() Offerings
    56 stackit/internal/services/postgresflex/instance/datasource.go:270 GetInstanceExecute() FlavorId
    57 stackit/internal/services/rabbitmq/credential/resource.go:268 CreateCredentialsExecute() Id
    58 stackit/internal/services/rabbitmq/instance/resource.go:412 CreateInstanceExecute() InstanceId
    59 stackit/internal/services/rabbitmq/instance/resource.go:875 ListOfferingsExecute() Offerings
    60 stackit/internal/services/redis/credential/resource.go:257 CreateCredentialsExecute() Id
    61 stackit/internal/services/redis/instance/resource.go:483 CreateInstanceExecute() InstanceId
    62 stackit/internal/services/redis/instance/resource.go:953 ListOfferingsExecute() Offerings
    63 stackit/internal/services/resourcemanager/folder/resource.go:218 CreateFolderExecute() ContainerId
    64 stackit/internal/services/resourcemanager/project/resource.go:222 CreateProjectExecute() ContainerId
    65 stackit/internal/services/scf/organization/resource.go:262 CreateOrganizationExecute() Guid
    66 stackit/internal/services/scf/organization/resource.go:280 ApplyOrganizationQuotaExecute() QuotaId
    67 stackit/internal/services/scf/organization/resource.go:424 GetOrganizationExecute() QuotaId
    68 stackit/internal/services/scf/organizationmanager/resource.go:241 CreateOrgManagerExecute() Guid
    69 stackit/internal/services/secretsmanager/instance/resource.go:216 CreateInstanceExecute() Id
    70 stackit/internal/services/secretsmanager/instance/resource.go:560 ListACLsExecute() Acls
    71 stackit/internal/services/secretsmanager/user/resource.go:206 CreateUserExecute() Id
    72 stackit/internal/services/serverbackup/schedule/resource.go:280 CreateBackupScheduleExecute() Id
    73 stackit/internal/services/serverbackup/schedule/resource.go:560 ListBackupsExecute() Items
    74 stackit/internal/services/serverupdate/schedule/resource.go:257 CreateUpdateScheduleExecute() Id
    75 stackit/internal/services/serviceaccount/account/datasource.go:135 ListServiceAccountsExecute() Items
    76 stackit/internal/services/serviceaccount/accounts/datasource.go:178 ListServiceAccountsExecute() Items
    77 stackit/internal/services/sfs/resourcepool/datasource.go:118 GetResourcePoolExecute() ResourcePool
    78 stackit/internal/services/sfs/resourcepool/resource.go:441 UpdateResourcePoolExecute() ResourcePool
    79 stackit/internal/services/sfs/share/datasource.go:112 GetShareExecute() Share
    80 stackit/internal/services/sfs/share/resource.go:246 CreateShareExecute() Share
    81 stackit/internal/services/sfs/share/resource.go:406 UpdateShareExecute() Share
    82 stackit/internal/services/sfs/snapshots/datasource.go:128 ListResourcePoolSnapshotsExecute() ResourcePoolSnapshots
    83 stackit/internal/services/ske/cluster/resource.go:1060 ListProviderOptionsExecute() KubernetesVersions
    84 stackit/internal/services/ske/kubeconfig/resource.go:496 GetClusterExecute() Status
    85 stackit/internal/services/ske/kubeconfig/resource.go:514 GetClusterExecute() Status
    86 stackit/internal/services/sqlserverflex/flavors/datasource.go:195 ListFlavorsExecute() Pagination
    87 stackit/internal/services/sqlserverflex/instance/datasource.go:287 GetInstanceExecute() FlavorId
    88 stackit/internal/services/sqlserverflex/instance/resource.go:736 CreateInstanceExecute() Id
    89 stackit/internal/services/telemetryrouter/accesstoken/resource.go:399 UpdateAccessTokenExecute() Id
    90 stackit/internal/services/telemetryrouter/destination/resource.go:690 UpdateDestinationExecute() Id
    91 stackit/internal/services/telemetryrouter/instance/resource.go:429 UpdateTelemetryRouterExecute() Id
    92 stackit/internal/services/valkey/credential/resource.go:256 CreateCredentialsExecute() Id
    93 stackit/internal/services/valkey/instance/resource.go:480 CreateInstanceExecute() InstanceId
    94 stackit/internal/services/valkey/instance/resource.go:944 ListOfferingsExecute() Offerings
    95 stackit/internal/services/vpn/connection/datasource.go:290 GetGatewayConnectionExecute() -
    96 stackit/internal/services/vpn/connection/resource.go:1024 CreateGatewayConnectionExecute() -
    97 stackit/internal/services/vpn/gateway/resource.go:332 CreateGatewayExecute() Id
  2. SerseusWasTaken commented on Sep 7, 2026

    @SerseusWasTaken
    Contributor

    Thanks @devpie for reporting this issue.

    I have created a internal tracking issue and we will be taking a look at this in the near future.

    Kind regards,
    Jonas from the STACKIT Developer Tools team

  3. veriton-dev commented on Sep 7, 2026

    @veriton-dev

    This class of crash is usually a missing null-guard at the resource Read boundary, not deep in the SDK.

    Pattern that works for generated clients:

    1. After every SDK call, treat resp == nil / empty 2xx body as a first-class outcome — never dereference fields.
    2. Map that to a Terraform diagnostic (Error with the HTTP status + path), not a plugin panic.
    3. For ReadResource/ReadDataSource, prefer RemoveResourceFromState (or equivalent) when the API says “gone/empty” only if that is the real semantics; otherwise return a hard error so apply stops cleanly.
    4. Add two regression tests with a mock transport:
      • 2xx + empty body
      • 2xx + undecodable JSON
        Assert: provider process stays alive + diagnostic string contains status/path.

    If you want a tight PR for the SFS Read path only, I can take a paid micro-pass ($1–$3 findings+diff, pay-after-delivery on first orders): acer-openclaw@agentmail.to · https://veriton-dev.github.io/veriton-micro-dev/

  4. devpie commented on Sep 8, 2026

    @devpie
    ContributorAuthor

    I sampled the 97 to find out how many are real, before proposing work on the rest.

    Method. 14 sites, stratified rather than the first 14 of the table: 14 distinct services and all four call
    kinds (4 create, 4 get, 4 list, 2 update). Each site was traced from the Execute() to the dereference, following
    into every function called in between, and each verdict was then checked by two independent reviewers whose brief was
    to overturn it. Row 78, which #1741 already fixes, went in as a control.

    Result: 13 of 14 are real, 0 are prevented by a downstream guard.

    The fourteenth, scf/organization/resource.go:424 (row 67), is unresolved rather than clean. The objection raised
    against it was that the v1api getters are nil-receiver-safe, which is true for org.GetName() at :399 and
    org.GetQuotaId() at :415 — but :424 is org.QuotaId = applyOrgQuota.QuotaId, a direct field write that no getter
    protects. The path that leaves org nil at that point is convoluted, and I have not settled it either way.

    Not one of the 14 turned out to be prevented by something downstream. That is structural rather than luck: the
    "saved" shape needs the dereference to sit after a nil-rejecting mapFields, and in this class it sits before —
    the field is read to build the state entry or the follow-up request. iaas/server/resource.go:588, which I cited as
    a false positive in my previous comment, is the exception, not the pattern.

    A correction to my previous comment

    I wrote there that an empty body on a 2xx would be "a server fault, not normal protocol". That understates it. For at
    least alb, albcertificates and albwaf the success schema is declared but carries no required array, so a
    perfectly spec-conformant 200 that simply omits the field crashes the provider too. alb/applicationloadbalancer/resource.go:1180
    shows why:

    createResp, err := r.client.DefaultAPI.CreateLoadBalancer(ctx, projectId, region).CreateLoadBalancerPayload(*payload).Execute()
    if err != nil { ...; return }
    ...
    ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
    	"project_id": projectId,
    	"region":     region,
    	"name":       *createResp.Name,     // :1180
    })

    That is two dereferences on one line. createResp nil panics, and createResp non-nil with Name nil panics as
    well. The second needs no misbehaving server at all.

    Why it matters beyond the panic

    In the create paths the dereference happens before the state write — SetAndLogStateFields is on the following
    line at best, and in the case above the crashing expression is an argument to it. So the resource the API has just
    created never reaches Terraform state. The user gets a plugin crash, no diagnostic, and an orphaned resource that the
    next apply collides with.

    That is the same failure shape as the SFS pool in #1737 that started this, and it is reachable in ~90 places.

    Suggestion

    On this evidence I would not sample further; the class is real. Two things follow:

    1. iaas/server/resource.go:840 (iaas: stackit_server panics when GetServer fails during update #1746, fix in fix(iaas): return after a failing GetServer in the server resource #1747) is unrelated to the 97 but the same crash shape, and it is a
      one-line fix.
    2. For the 97 themselves, doing them per service in the shape of fix(sfs): guard against nil responses from the API #1741 means the same guard and the same two-sentence
      diagnostic roughly ninety times. At that volume a small shared helper in internal/core is probably worth more
      than ninety inline copies — but that is a judgement about your codebase, not mine, so I would rather ask than
      assume. Say which you prefer and I will start.

    Happy to hand over the per-site analyses for the 14 if that is useful for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions