Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions docs/operator-guide/nautobotop.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ metadata:
namespace: nautobotop
type: Opaque
stringData:
username: admin
token: your-nautobot-api-token
```

Expand Down Expand Up @@ -90,7 +89,6 @@ spec:
nautobotSecretRef:
name: nautobot-token
namespace: nautobotop
usernameKey: username
tokenKey: token

nautobotServiceRef:
Expand Down Expand Up @@ -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 |
Expand Down
8 changes: 0 additions & 8 deletions go/nautobotop/api/v1alpha1/secret_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 18 additions & 18 deletions go/nautobotop/internal/controller/nautobot_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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.
Expand Down
14 changes: 11 additions & 3 deletions go/nautobotop/internal/nautobot/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -33,17 +35,22 @@ 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).
SetCommonRetryCount(3).
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{
{
Expand All @@ -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
}

Expand Down
44 changes: 44 additions & 0 deletions go/nautobotop/internal/nautobot/client/user.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading