diff --git a/docs/operator-guide/nautobotop.md b/docs/operator-guide/nautobotop.md index f004d2960..257b77e70 100644 --- a/docs/operator-guide/nautobotop.md +++ b/docs/operator-guide/nautobotop.md @@ -61,7 +61,6 @@ metadata: namespace: nautobotop type: Opaque stringData: - username: admin token: your-nautobot-api-token ``` @@ -90,7 +89,6 @@ spec: nautobotSecretRef: name: nautobot-token namespace: nautobotop - usernameKey: username tokenKey: token nautobotServiceRef: @@ -186,7 +184,7 @@ spec: | `requeueAfter` | int | 600 | Seconds between reconciliation attempts | | `syncIntervalSeconds` | int | 172800 | Minimum seconds between full syncs | | `cacheMaxSize` | int | 70000 | Maximum number of entries in the Nautobot object cache | -| `nautobotSecretRef` | SecretKeySelector | | Reference to the Secret holding Nautobot credentials | +| `nautobotSecretRef` | SecretKeySelector | | Reference to the Secret holding the Nautobot API token | | `nautobotServiceRef` | ServiceSelector | | Reference to the Nautobot Kubernetes Service | | `locationTypesRef` | []ConfigMapRef | | ConfigMaps containing location type definitions | | `locationRef` | []ConfigMapRef | | ConfigMaps containing location definitions | diff --git a/go/nautobotop/api/v1alpha1/secret_type.go b/go/nautobotop/api/v1alpha1/secret_type.go index 24dc813a9..a8f79a7c1 100644 --- a/go/nautobotop/api/v1alpha1/secret_type.go +++ b/go/nautobotop/api/v1alpha1/secret_type.go @@ -15,14 +15,6 @@ type SecretKeySelector struct { // +kubebuilder:validation:Pattern:=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ Namespace *string `json:"namespace,omitempty"` - // A UsernameKey in the referenced Secret. - // Some instances of this field may be defaulted, in others it may be required. - // +optional - // +kubebuilder:validation:MinLength:=1 - // +kubebuilder:validation:MaxLength:=253 - // +kubebuilder:validation:Pattern:=^[-._a-zA-Z0-9]+$ - UsernameKey string `json:"usernameKey,omitempty"` - // A key in the referenced Secret. // Some instances of this field may be defaulted, in others it may be required. // +optional diff --git a/go/nautobotop/internal/controller/nautobot_controller.go b/go/nautobotop/internal/controller/nautobot_controller.go index 1685682f2..176a05233 100644 --- a/go/nautobotop/internal/controller/nautobot_controller.go +++ b/go/nautobotop/internal/controller/nautobot_controller.go @@ -45,7 +45,8 @@ import ( // NautobotReconciler reconciles a Nautobot object type NautobotReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + resolvedUsername string } // +kubebuilder:rbac:groups=sync.rax.io,resources=nautobots,verbs=get;list;watch;create;update;patch;delete @@ -143,17 +144,26 @@ func (r *NautobotReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c } // Create Nautobot client - username, token, err := r.getAuthTokenFromSecretRef(ctx, nautobotCR) + token, err := r.getAuthTokenFromSecretRef(ctx, nautobotCR) if err != nil { log.Error(err, "failed to get nautobot auth token") return ctrl.Result{}, err } nautobotURL := fmt.Sprintf("http://%s.%s.svc.cluster.local/api", nautobotCR.Spec.NautobotServiceRef.Name, nautobotCR.Spec.NautobotServiceRef.Namespace) - nautobotClient, err := nbClient.NewNautobotClient(nautobotURL, username, token, nautobotCR.Spec.CacheMaxSize) + nautobotClient, err := nbClient.NewNautobotClient(nautobotURL, token, nautobotCR.Spec.CacheMaxSize) if err != nil { log.Error(err, "failed to create nautobot client") return ctrl.Result{}, err } + if r.resolvedUsername != "" { + nautobotClient.Username = r.resolvedUsername + } else { + if err := nautobotClient.ResolveUsername(ctx); err != nil { + log.Error(err, "failed to resolve nautobot username from token") + return ctrl.Result{}, err + } + r.resolvedUsername = nautobotClient.Username + } if err := nautobotClient.PreLoadCacheForLookup(ctx); err != nil { log.Error(err, "failed to warmup cache") @@ -470,27 +480,17 @@ func (r *NautobotReconciler) syncTenant(ctx context.Context, return nil } -// getAuthTokenFromSecretRef: this will fetch Nautobot auth token from the given refer. -func (r *NautobotReconciler) getAuthTokenFromSecretRef(ctx context.Context, nautobotCR syncv1alpha1.Nautobot) (string, string, error) { - var username, token string +// getAuthTokenFromSecretRef fetches the Nautobot API token from the referenced Kubernetes Secret. +func (r *NautobotReconciler) getAuthTokenFromSecretRef(ctx context.Context, nautobotCR syncv1alpha1.Nautobot) (string, error) { secret := &corev1.Secret{} err := r.Get(ctx, types.NamespacedName{Name: nautobotCR.Spec.NautobotSecretRef.Name, Namespace: *nautobotCR.Spec.NautobotSecretRef.Namespace}, secret) if err != nil { - return "", "", err - } - // Read the secret value - if valBytes, ok := secret.Data[nautobotCR.Spec.NautobotSecretRef.UsernameKey]; ok { - username = string(valBytes) + return "", err } if valBytes, ok := secret.Data[nautobotCR.Spec.NautobotSecretRef.TokenKey]; ok { - token = string(valBytes) - } - - if username != "" || token != "" { - return username, token, nil + return string(valBytes), nil } - - return "", "", fmt.Errorf("secret keys not found in provide secret") + return "", fmt.Errorf("token key %q not found in secret %s", nautobotCR.Spec.NautobotSecretRef.TokenKey, nautobotCR.Spec.NautobotSecretRef.Name) } // SetupWithManager sets up the controller with the Manager. diff --git a/go/nautobotop/internal/nautobot/client/client.go b/go/nautobotop/internal/nautobot/client/client.go index e215386fb..c1c979105 100644 --- a/go/nautobotop/internal/nautobot/client/client.go +++ b/go/nautobotop/internal/nautobot/client/client.go @@ -20,6 +20,8 @@ type NautobotClient struct { Username string Report map[string][]string Cache *cache.Cache + reqClient *req.Client + apiURL string } // AddReport appends one or more lines to the current reconciliation report. @@ -33,7 +35,8 @@ func (n *NautobotClient) AddReport(key string, line ...string) { // apiURL: The base URL of the Nautobot API (e.g., "http://localhost:8000"). // authToken: The API token for authentication. // cacheMaxSize: The maximum size of the cache (0 uses default of 70,000). -func NewNautobotClient(apiURL string, username, authToken string, cacheMaxSize int) (*NautobotClient, error) { +// The username is resolved from the token via ResolveUsername after construction. +func NewNautobotClient(apiURL string, authToken string, cacheMaxSize int) (*NautobotClient, error) { // Configure req client with retry and backoff reqClient := req.C(). SetTimeout(30*time.Second). @@ -41,9 +44,13 @@ func NewNautobotClient(apiURL string, username, authToken string, cacheMaxSize i SetCommonRetryBackoffInterval(1*time.Second, 5*time.Second). SetCommonRetryCondition(func(resp *req.Response, err error) bool { return err != nil || resp.StatusCode >= 500 - }) + }). + SetCommonHeader("Authorization", fmt.Sprintf("Token %s", authToken)) config := nb.NewConfiguration() + // reqClient.GetClient() returns the underlying *http.Client for the SDK to use. + // The SDK applies auth via config.AddDefaultHeader for its own calls. + // Direct calls via reqClient.R() (e.g. ResolveUsername) apply auth via SetCommonHeader above. config.HTTPClient = reqClient.GetClient() config.Servers = nb.ServerConfigurations{ { @@ -58,11 +65,12 @@ func NewNautobotClient(apiURL string, username, authToken string, cacheMaxSize i } return &NautobotClient{ - Username: username, Config: config, APIClient: client, Report: make(map[string][]string), Cache: c, + reqClient: reqClient, + apiURL: apiURL, }, nil } diff --git a/go/nautobotop/internal/nautobot/client/user.go b/go/nautobotop/internal/nautobot/client/user.go new file mode 100644 index 000000000..68cbf77f6 --- /dev/null +++ b/go/nautobotop/internal/nautobot/client/user.go @@ -0,0 +1,44 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" +) + +type tokenListResponse struct { + Count int `json:"count"` + Results []tokenResult `json:"results"` +} + +type tokenResult struct { + User tokenUser `json:"user"` +} + +type tokenUser struct { + Username string `json:"username"` +} + +// ResolveUsername fetches the username associated with the configured API token +// from Nautobot's /api/users/tokens/ endpoint and stores it in n.Username. +func (n *NautobotClient) ResolveUsername(ctx context.Context) error { + url := fmt.Sprintf("%s/users/tokens/", n.apiURL) + resp, err := n.reqClient.R().SetContext(ctx).Get(url) + if err != nil { + return fmt.Errorf("failed to call users/tokens API: %w", err) + } + if resp.StatusCode != 200 { + return fmt.Errorf("users/tokens API returned status %d", resp.StatusCode) + } + + var result tokenListResponse + if err := json.Unmarshal(resp.Bytes(), &result); err != nil { + return fmt.Errorf("failed to parse users/tokens response: %w", err) + } + if result.Count == 0 || len(result.Results) == 0 { + return fmt.Errorf("users/tokens API returned no tokens") + } + + n.Username = result.Results[0].User.Username + return nil +}