Skip to content

fix: mongodbatlas_privatelink_endpoint creation - #86

Closed
dudu wants to merge 1 commit into
crossplane-contrib:mainfrom
dudu:main
Closed

fix: mongodbatlas_privatelink_endpoint creation#86
dudu wants to merge 1 commit into
crossplane-contrib:mainfrom
dudu:main

Conversation

@dudu

@dudu dudu commented May 6, 2026

Copy link
Copy Markdown
Contributor

Error

mongodbatlas_privatelink_endpointgroupId is empty Bug

Reported error

When applying any privateendpoint.mongodbatlas.crossplane.io/v1alpha1 Resource
(directly or via a Composition that embeds one), the resource gets stuck on the
first reconcile with:

observe failed: cannot run refresh: refresh failed: error reading MongoDB Private
Endpoints Connection(): groupId is empty and must be specified:

The resource never transitions past Observe, so Create is never attempted from
the Crossplane side. Manually issuing the equivalent Atlas REST call
(POST /api/atlas/v1.0/groups/{groupId}/privateEndpoint/endpointService) with
the same credentials returns 201 Created, confirming the API key,
permissions, project and region are all valid — the failure is entirely
inside the Crossplane provider.

The same symptom applies to mongodbatlas_privatelink_endpoint_service (Kind
Service).

Reproduction

apiVersion: privateendpoint.mongodbatlas.crossplane.io/v1alpha1
kind: Resource
metadata:
  name: debug-privatelink-aws
spec:
  managementPolicies: ["*"]
  forProvider:
    projectId: xxx   # any valid Atlas project ID
    providerName: AWS
    region: US_EAST_1
  providerConfigRef:
    name: default

Apply, then inspect the Terraform workspace inside the provider pod
(/tmp/<resource-uid>/):

  • main.tf.json is correctproject_id, provider_name, region are
    all populated.
  • terraform.tfstate contains:
    { "id": "xxx--AWS-US_EAST_1",
      "project_id": "xxx",
      "provider_name": "AWS",
      "region": "US_EAST_1" }
    The id is plaintext and is missing private_link_id entirely.

Root cause

The upstream Terraform provider (mongodb/mongodbatlas v2.10.0) encodes the
state id for mongodbatlas_privatelink_endpoint as a
base64-encoded URL query string containing
project_id, private_link_id, provider_name, and region.
Its Read function calls DecodeStateID(d.Id()) on every refresh:

func DecodeStateID(stateID string) map[string]string {
    decode, _ := base64.StdEncoding.DecodeString(stateID)
    decoded, _ := url.ParseQuery(string(decode))
    result := map[string]string{}
    for key := range decoded {
        result[key] = decoded.Get(key)
    }
    return result
}

This Crossplane provider's resource configurator overrides the upjet defaults
with a plaintext, dash-separated ID format:

// config/cluster/privateendpoint/config.go (and the namespaced mirror)
p.AddResourceConfigurator("mongodbatlas_privatelink_endpoint", func(r *config.Resource) {
    ...
    r.ExternalName.GetIDFn = common.GetIDFromParamsAndExternalName(
        "-", 1, "project_id", "provider_name", "region")
    r.ExternalName.GetExternalNameFn = common.ExternalNameFromID("-", 1, 2)
})

mongodbatlas_privatelink_endpoint is registered in
config/external_name.go with identifierFromProvider(), which sets
config.IdentifierFromProvider (DisableNameInitializer: true). On the first
reconcile the crossplane.io/external-name annotation is therefore empty, so
GetIDFromParamsAndExternalName builds:

{project_id}-{empty}-{provider_name}-{region}
= xxx--AWS-US_EAST_1

upjet writes that value into the Terraform state's id field before invoking
the upstream provider. On the next refresh the upstream Read function calls
DecodeStateID, which fails because the input isn't valid base64 — the
returned map is empty, every field including project_id resolves to "",
and the SDK call

conn.PrivateEndpoints.Get(ctx, ids["project_id"], ids["provider_name"], ids["private_link_id"])

becomes Get(ctx, "", "", ""). The Atlas SDK validation fires and returns
groupId is empty and must be specified.

The same mismatch exists for mongodbatlas_privatelink_endpoint_service
(---separated plaintext on our side, base64 query string upstream).

Solution

Drop the custom GetIDFn / GetExternalNameFn overrides for both resources
and rely on IdentifierFromProvider defaults
(GetIDFn = ExternalNameAsID, GetExternalNameFn = IDAsExternalName).

Signed-off-by: Eduardo Nascimento <eduardo.onascimento@stone.com.br>
@fernandezcuesta

fernandezcuesta commented May 6, 2026

Copy link
Copy Markdown
Collaborator

Hi, thanks for raising this issue.
For a bunch of other resources we addressed it differently, see #80

@dudu dudu closed this May 7, 2026
@dudu

dudu commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

@fernandezcuesta created another version on #87

@dudu dudu reopened this May 7, 2026
@fernandezcuesta

Copy link
Copy Markdown
Collaborator

@dudu closing this together with #87. Thanks!

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.

2 participants