Skip to content

[BUG] - every vultr_instance read fails: VPC 2.0 endpoint returns 404, and the error is fatal #753

Description

@slabinfo

Summary

vultr_instance's Read unconditionally fetches VPC 2.0 membership and treats any error as fatal. Since the VPC 2.0 API endpoint now returns 404, every read of a vultr_instance fails — which means every apply, refresh and destroy fails — even for configurations that never mention VPC at all.

Error: error getting list of attached VPCs 2.0: {"error":"Not found.","status":404}

  with module.edge["…"].vultr_instance.edge,
  on main.tf line …, in resource "vultr_instance" "edge":

Environment

  • provider vultr/vultr v2.32.0 (newest 2.x; installed via OpenTofu 1.12.4)
  • account has plain VPC only — no VPC 2.0

Endpoint behaviour with a valid, root-ACL API key:

request result
GET /v2/account 200
GET /v2/instances 200
GET /v2/vpcs 200 {"vpcs":[]}
GET /v2/vpc2 404 {"error":"Not found.","status":404}

Why this is severe rather than cosmetic

Terraform reads back after create and before destroy, so the create path leaves real state behind:

  1. apply creates the instance successfully
  2. the follow-up read fails on the VPC 2.0 call
  3. the apply errors, so the instance is never recorded in state
  4. Vultr is billing a server Terraform doesn't know exists — and the next apply creates another

I hit exactly that: two identical live instances billing, neither in state. destroy is affected too, since it reads first — so a plain vultr_instance can be neither reliably created nor reliably removed.

The value is discarded anyway

This is what makes it look like a clear bug rather than a config problem. In resource_vultr_instance.go the fetch is unconditional:

vpcs, err := getVPCs(client, d.Id())
if err != nil {
	return diag.Errorf("%s", err.Error())
}

vpc2s, err := getVPC2s(client, d.Id())   // always runs
if err != nil {
	return diag.Errorf("%s", err.Error()) // always fatal
}

…but the result is only used if the practitioner configured vpc2_ids:

if _, vpc2Update := d.GetOk("vpc2_ids"); vpc2Update {
	if err := d.Set("vpc2_ids", vpc2s); err != nil { … }
}

So for any config without vpc2_ids — mine sets no VPC fields whatsoever — the provider fails the entire read over a value it is about to throw away. There is no configuration that avoids it, because the call is in Read, not in a vultr_vpc2 resource.

Suggested fix

Treat "the VPC 2.0 endpoint is gone" as no VPC 2.0 attachments, which seems the truthful answer for a retired product, and matches the 404 idiom already used in resource_vultr_firewall_group.go:60:

// vultr/instances.go
func vpc2Gone(err error) bool {
	return strings.Contains(err.Error(), `"status":404`)
}

applied in both getVPC2s (vultr/instances.go:35) and getBareMetalServerVPC2s (vultr/instances.go:60):

if err != nil {
	if vpc2Gone(err) {
		return nil, nil
	}
	return nil, fmt.Errorf("error getting list of attached VPCs 2.0: %v", err)
}

I'm running exactly this as a local build and it resolves the problem: apply creates an instance and destroy removes it, both clean. Happy to open a PR if the approach looks right — a typed status check would be nicer than string matching if govultr exposes one.

Related: VPC2 elements were marked deprecated in v2.25.0, but only the schema fields were deprecated; the read path still calls the endpoint. master is unchanged as of today.

One caveat, in case it helps triage

I can't fully explain the timing, and it may point at a partial rollout rather than a clean cutover: earlier the same day, with byte-identical configuration and the same provider version, one apply and its destroy completed successfully. Every attempt afterwards failed as above. I also can't find other reports of this, which is surprising for something that would break every vultr_instance read globally — so it may only affect accounts where VPC 2.0 has already been withdrawn.

Reproduction

Any minimal instance is enough — no VPC configuration involved:

terraform {
  required_providers {
    vultr = { source = "vultr/vultr", version = "~> 2.0" }
  }
}

resource "vultr_instance" "example" {
  region = "fra"
  plan   = "vhp-2c-4gb-amd"
  os_id  = 2284
}

tofu apply → the instance is created, then the apply fails on the VPC 2.0 read.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions