Skip to content

NBResource controller cannot adopt pre-existing NetBird network resources (permanent DuplicateName failure) #363

Description

@jplimack-ai

Version: netbird-operator v0.7.0 (ghcr.io/netbirdio/netbird-operator:v0.7.0), reproduced against main

Location: internal/controller/nbresource_controller.go, handleNetBirdResource(), lines 422-441

// Create/Update upstream network resource
if nbResource.Status.NetworkResourceID == nil && resource == nil {
	resource, err := r.Netbird.Networks.Resources(nbResource.Spec.NetworkID).Create(ctx, api.NetworkResourceRequest{
		Address:     nbResource.Spec.Address,
		Enabled:     true,
		Groups:      groupIDs,
		Description: &networkDescription,
		Name:        nbResource.Spec.Name,
	})

	if err != nil && strings.Contains(err.Error(), "already exists") {
		log.Log.Error(errNetBirdAPI, "network resource with the same name already exists", "err", err)
		nbResource.Status.Conditions = nbv1.NBConditionFalse("DuplicateName", "Resource name already exists")
		return nil, errDuplicateResource
	}

	if err != nil {
		logger.Error(errNetBirdAPI, "error creating resource", "err", err)
		return nil, err
	}

	nbResource.Status.NetworkResourceID = &resource.Id
}

Repro:

  1. Create a network resource named X on network net-1 via the NetBird management API directly (bootstrap script, another NBResource CR that was deleted without owning status.networkResourceID cleanup, or a prior failed apply).
  2. Apply an NBResource CR with spec.name: X, spec.networkID: net-1, and empty status (fresh CR, status.networkResourceID == nil).
  3. Reconcile calls Create(), which returns resource with name X already exists (netbird API error).

Actual: The error is matched by strings.Contains(err.Error(), "already exists"), status.conditions is set to reason=DuplicateName, and errDuplicateResource is returned. Reconcile() (lines 108-111) treats that as a non-error and just requeues after defaultRequeueAfter — forever, since nothing ever looks the existing resource up by name to adopt it. status.networkResourceID is never populated, so handlePolicy() (line 129, gated on status.PolicyName/spec) never runs, and status.tcpPorts/status.udpPorts never propagate to any dependent policy.

Expected: Same adopt pattern already implemented for groups in NBGroupReconciler.syncNetBirdGroup() (internal/controller/nbgroup_controller.go, lines 84-116):

groups, err := r.Netbird.Groups.List(ctx)
...
for _, g := range groups {
	if g.Name == nbGroup.Spec.Name {
		group = &g
	}
}
if nbGroup.Status.GroupID == nil && group == nil {
	// create
} else if nbGroup.Status.GroupID == nil && group != nil {
	logger.Info("NBGroup: Found group with same name on NetBird", "name", nbGroup.Spec.Name, "id", group.Id)
	nbGroup.Status.GroupID = &group.Id
	nbGroup.Status.Conditions = nbv1.NBConditionTrue()
}

NBResource should look up the existing network resource by name (or handle the already exists error by fetching-and-adopting) and populate status.networkResourceID/update it, instead of only handling Create().

Impact: Any NBResource whose name collides with an object already present on the NetBird management API is permanently stuck in a DuplicateName condition with no networkResourceID, no port propagation, and an indefinite requeue loop — the reconcile can never converge.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions