@@ -16,9 +16,13 @@ package reconciler
1616
1717import (
1818 "context"
19+ "errors"
1920 "fmt"
21+ "hash/fnv"
2022
2123 corev1 "k8s.io/api/core/v1"
24+ apierrors "k8s.io/apimachinery/pkg/api/errors"
25+ "k8s.io/apimachinery/pkg/util/rand"
2226 "sigs.k8s.io/controller-runtime/pkg/client"
2327
2428 "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/controller/atlas"
@@ -29,8 +33,29 @@ const (
2933 orgIDKey = "orgId"
3034 publicAPIKey = "publicApiKey"
3135 privateAPIKey = "privateApiKey"
36+
37+ clientIDKey = "clientId"
38+ clientSecretKey = "clientSecret"
39+
40+ accessTokenKey = "accessToken"
41+ credentialsHashKey = "credentialsHash"
42+ accessTokenSecretPrefix = "atlas-access-token-"
3243)
3344
45+ // CredentialsHash returns a deterministic, non-cryptographic fingerprint of
46+ // the (clientID, clientSecret) pair. The service-account-token controller
47+ // stores this fingerprint on the Access Token Secret under credentialsHashKey
48+ // so any component reading the Secret can detect that the source credentials
49+ // have been rotated since the cached bearer token was issued. The nul
50+ // separator disambiguates ("ab","c") from ("a","bc").
51+ func CredentialsHash (clientID , clientSecret string ) (string , error ) {
52+ h := fnv .New64a ()
53+ if _ , err := h .Write ([]byte (clientID + "\x00 " + clientSecret )); err != nil {
54+ return "" , fmt .Errorf ("failed to compute credentials hash: %w" , err )
55+ }
56+ return fmt .Sprint (h .Sum64 ()), nil
57+ }
58+
3459func (r * AtlasReconciler ) ResolveConnectionConfig (ctx context.Context , referrer project.ProjectReferrerObject ) (* atlas.ConnectionConfig , error ) {
3560 connectionSecret := r .connectionSecretRef (referrer )
3661 if connectionSecret != nil && connectionSecret .Name != "" {
@@ -78,49 +103,136 @@ func GetConnectionConfig(ctx context.Context, k8sClient client.Client, secretRef
78103 return nil , fmt .Errorf ("failed to read Atlas API credentials from the secret %s: %w" , secretRef .String (), err )
79104 }
80105
81- cfg := & atlas.ConnectionConfig {
106+ if err := validateConnectionSecret (secret ); err != nil {
107+ return nil , fmt .Errorf ("invalid connection secret %s: %w" , secretRef , err )
108+ }
109+
110+ if isServiceAccountCredentials (secret ) {
111+ bearerToken , err := getServiceAccountAccessToken (ctx , k8sClient , secret )
112+ if err != nil {
113+ return nil , err
114+ }
115+
116+ return & atlas.ConnectionConfig {
117+ OrgID : string (secret .Data [orgIDKey ]),
118+ Credentials : & atlas.Credentials {
119+ ServiceAccount : & atlas.ServiceAccountToken {
120+ BearerToken : bearerToken ,
121+ },
122+ },
123+ }, nil
124+ }
125+
126+ return & atlas.ConnectionConfig {
82127 OrgID : string (secret .Data [orgIDKey ]),
83128 Credentials : & atlas.Credentials {
84129 APIKeys : & atlas.APIKeys {
85130 PublicKey : string (secret .Data [publicAPIKey ]),
86131 PrivateKey : string (secret .Data [privateAPIKey ]),
87132 },
88133 },
134+ }, nil
135+ }
136+
137+ // DeriveAccessTokenSecretName returns the deterministic name of the Access Token Secret for a given Connection Secret.
138+ // The Connection Secret name is included literally for operator debuggability; it is truncated when the total
139+ // exceeds the Kubernetes 253-character DNS-subdomain limit.
140+ func DeriveAccessTokenSecretName (namespace , connectionSecretName string ) (string , error ) {
141+ hasher := fnv .New64a ()
142+ _ , err := hasher .Write ([]byte (namespace + "/" + connectionSecretName ))
143+ if err != nil {
144+ return "" , fmt .Errorf ("failed to compute hash for access token secret name: %w" , err )
89145 }
146+ hash := rand .SafeEncodeString (fmt .Sprint (hasher .Sum64 ()))
90147
91- if missingFields , valid := validate (cfg ); ! valid {
92- return nil , fmt .Errorf ("the following fields are missing in the secret %v: %v" , secretRef , missingFields )
148+ const k8sNameLimit = 253
149+ maxNameLen := k8sNameLimit - len (accessTokenSecretPrefix ) - 1 - len (hash )
150+ name := connectionSecretName
151+ if len (name ) > maxNameLen {
152+ name = name [:maxNameLen ]
93153 }
94154
95- return cfg , nil
155+ return accessTokenSecretPrefix + name + "-" + hash , nil
96156}
97157
98- func validate (cfg * atlas.ConnectionConfig ) ([]string , bool ) {
99- missingFields := make ([]string , 0 , 3 )
158+ func getServiceAccountAccessToken (ctx context.Context , k8sClient client.Client , secret * corev1.Secret ) (string , error ) {
159+ tokenSecretName , err := DeriveAccessTokenSecretName (secret .Namespace , secret .Name )
160+ if err != nil {
161+ return "" , err
162+ }
163+ tokenRef := client.ObjectKey {Namespace : secret .Namespace , Name : tokenSecretName }
100164
101- if cfg == nil {
102- return []string {orgIDKey , publicAPIKey , privateAPIKey }, false
165+ tokenSecret := & corev1.Secret {}
166+ if err := k8sClient .Get (ctx , tokenRef , tokenSecret ); err != nil {
167+ if apierrors .IsNotFound (err ) {
168+ return "" , fmt .Errorf ("access token secret %s does not exist yet" , tokenRef .String ())
169+ }
170+ return "" , fmt .Errorf ("failed to read access token secret %s: %w" , tokenRef .String (), err )
103171 }
104172
105- if cfg .OrgID == "" {
106- missingFields = append (missingFields , orgIDKey )
173+ // Guard against a stale cached token — if the credential Secret was
174+ // rotated since the token was issued, the service-account-token controller
175+ // may not have caught up yet. Returning an error prompts the downstream
176+ // reconciler to retry rather than hitting Atlas with revoked credentials.
177+ currentHash , err := CredentialsHash (string (secret .Data [clientIDKey ]), string (secret .Data [clientSecretKey ]))
178+ if err != nil {
179+ return "" , err
107180 }
181+ if string (tokenSecret .Data [credentialsHashKey ]) != currentHash {
182+ return "" , fmt .Errorf ("access token secret %s is stale (credentials rotated); waiting for the service-account-token controller to refresh" , tokenRef .String ())
183+ }
184+
185+ bearerToken := string (tokenSecret .Data [accessTokenKey ])
186+ if bearerToken == "" {
187+ return "" , fmt .Errorf ("access token secret %s has an empty accessToken field" , tokenRef .String ())
188+ }
189+
190+ return bearerToken , nil
191+ }
192+
193+ func isServiceAccountCredentials (credentials * corev1.Secret ) bool {
194+ clientID := credentials .Data [clientIDKey ]
195+ clientSecret := credentials .Data [clientSecretKey ]
108196
109- if cfg .Credentials == nil || cfg .Credentials .APIKeys == nil {
110- return append (missingFields , []string {publicAPIKey , privateAPIKey }... ), false
197+ return len (clientID ) > 0 && len (clientSecret ) > 0
198+ }
199+
200+ func validateConnectionSecret (secret * corev1.Secret ) error {
201+ hasAnyAPIKey := len (secret .Data [publicAPIKey ]) > 0 || len (secret .Data [privateAPIKey ]) > 0
202+ hasAnySA := len (secret .Data [clientIDKey ]) > 0 || len (secret .Data [clientSecretKey ]) > 0
203+
204+ if hasAnyAPIKey && hasAnySA {
205+ return errors .New ("secret contains both API key and service account credentials; only one type is allowed" )
111206 }
112207
113- if cfg .Credentials .APIKeys .PublicKey == "" {
114- missingFields = append (missingFields , publicAPIKey )
208+ var missingFields []string
209+
210+ if len (secret .Data [orgIDKey ]) == 0 {
211+ missingFields = append (missingFields , orgIDKey )
115212 }
116213
117- if cfg .Credentials .APIKeys .PrivateKey == "" {
118- missingFields = append (missingFields , privateAPIKey )
214+ if hasAnyAPIKey {
215+ if len (secret .Data [publicAPIKey ]) == 0 {
216+ missingFields = append (missingFields , publicAPIKey )
217+ }
218+ if len (secret .Data [privateAPIKey ]) == 0 {
219+ missingFields = append (missingFields , privateAPIKey )
220+ }
221+ } else if hasAnySA {
222+ if len (secret .Data [clientIDKey ]) == 0 {
223+ missingFields = append (missingFields , clientIDKey )
224+ }
225+ if len (secret .Data [clientSecretKey ]) == 0 {
226+ missingFields = append (missingFields , clientSecretKey )
227+ }
228+ } else {
229+ //By default, we are expecting API keys
230+ missingFields = append (missingFields , publicAPIKey , privateAPIKey )
119231 }
120232
121233 if len (missingFields ) > 0 {
122- return missingFields , false
234+ return fmt . Errorf ( "missing required fields: %v" , missingFields )
123235 }
124236
125- return nil , true
237+ return nil
126238}
0 commit comments