|
15 | 15 | package cluster |
16 | 16 |
|
17 | 17 | import ( |
| 18 | + "context" |
| 19 | + "crypto/x509" |
18 | 20 | "errors" |
19 | 21 | "fmt" |
20 | 22 | "time" |
21 | 23 |
|
| 24 | + "github.com/argoproj-labs/argocd-agent/internal/config" |
22 | 25 | "github.com/argoproj-labs/argocd-agent/internal/event" |
| 26 | + "github.com/argoproj-labs/argocd-agent/internal/tlsutil" |
23 | 27 | "github.com/redis/go-redis/v9" |
24 | 28 | "github.com/redis/go-redis/v9/maintnotifications" |
25 | 29 | "github.com/sirupsen/logrus" |
26 | 30 |
|
27 | 31 | appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" |
28 | 32 | cacheutil "github.com/argoproj/argo-cd/v3/util/cache" |
29 | 33 | appstatecache "github.com/argoproj/argo-cd/v3/util/cache/appstate" |
| 34 | + v1 "k8s.io/api/core/v1" |
| 35 | + apierrors "k8s.io/apimachinery/pkg/api/errors" |
30 | 36 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| 37 | + "k8s.io/client-go/kubernetes" |
31 | 38 | ) |
32 | 39 |
|
| 40 | +const LabelKeySelfRegisteredCluster = "argocd-agent.argoproj-labs.io/self-registered-cluster" |
| 41 | + |
33 | 42 | // SetAgentConnectionStatus updates cluster info with connection state and time in mapped cluster at principal. |
34 | 43 | // This is called when the agent is connected or disconnected with the principal. |
35 | 44 | func (m *Manager) SetAgentConnectionStatus(agentName, status appv1.ConnectionStatus, modifiedAt time.Time) { |
@@ -181,3 +190,101 @@ func NewClusterCacheInstance(redisAddress, redisPassword string, redisCompressio |
181 | 190 |
|
182 | 191 | return clusterCache, nil |
183 | 192 | } |
| 193 | + |
| 194 | +// CreateCluster creates a cluster secret for an agent's cluster on the principal. |
| 195 | +func CreateCluster(ctx context.Context, kubeclient kubernetes.Interface, namespace, agentName, resourceProxyAddress string) error { |
| 196 | + |
| 197 | + // Generate client certificate signed by principal's CA. |
| 198 | + clientCert, clientKey, caData, err := generateAgentClientCert(ctx, kubeclient, namespace, agentName, config.SecretNamePrincipalCA) |
| 199 | + if err != nil { |
| 200 | + return fmt.Errorf("could not generate client certificate: %w", err) |
| 201 | + } |
| 202 | + |
| 203 | + // Note: this structure has to be same as manual creation done by `argocd-agentctl agent create <agent_name>` |
| 204 | + cluster := &appv1.Cluster{ |
| 205 | + Server: fmt.Sprintf("https://%s?agentName=%s", resourceProxyAddress, agentName), |
| 206 | + Name: agentName, |
| 207 | + Labels: map[string]string{ |
| 208 | + LabelKeyClusterAgentMapping: agentName, |
| 209 | + LabelKeySelfRegisteredCluster: "true", |
| 210 | + }, |
| 211 | + Config: appv1.ClusterConfig{ |
| 212 | + TLSClientConfig: appv1.TLSClientConfig{ |
| 213 | + CertData: []byte(clientCert), |
| 214 | + KeyData: []byte(clientKey), |
| 215 | + CAData: []byte(caData), |
| 216 | + }, |
| 217 | + }, |
| 218 | + } |
| 219 | + |
| 220 | + // Convert cluster object to Kubernetes secret object |
| 221 | + secret := &v1.Secret{ |
| 222 | + ObjectMeta: metav1.ObjectMeta{ |
| 223 | + Name: getClusterSecretName(agentName), |
| 224 | + Namespace: namespace, |
| 225 | + }, |
| 226 | + } |
| 227 | + if err := ClusterToSecret(cluster, secret); err != nil { |
| 228 | + return fmt.Errorf("could not convert cluster to secret: %w", err) |
| 229 | + } |
| 230 | + |
| 231 | + // Create the secret to register the agent's cluster. |
| 232 | + // Handle AlreadyExists as success to make this idempotent and avoid TOCTOU race |
| 233 | + // conditions where concurrent registrations both pass the existence check. |
| 234 | + if _, err = kubeclient.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{}); err != nil { |
| 235 | + if apierrors.IsAlreadyExists(err) { |
| 236 | + return nil |
| 237 | + } |
| 238 | + return fmt.Errorf("could not create cluster secret to register agent's cluster: %w", err) |
| 239 | + } |
| 240 | + |
| 241 | + return nil |
| 242 | +} |
| 243 | + |
| 244 | +// ClusterSecretExists checks if a cluster secret exists for the given agent. |
| 245 | +func ClusterSecretExists(ctx context.Context, kubeclient kubernetes.Interface, namespace, agentName string) (bool, error) { |
| 246 | + if _, err := kubeclient.CoreV1().Secrets(namespace).Get(ctx, getClusterSecretName(agentName), metav1.GetOptions{}); err != nil { |
| 247 | + if apierrors.IsNotFound(err) { |
| 248 | + return false, nil |
| 249 | + } |
| 250 | + return false, err |
| 251 | + } |
| 252 | + return true, nil |
| 253 | +} |
| 254 | + |
| 255 | +func generateAgentClientCert(ctx context.Context, kubeclient kubernetes.Interface, namespace, agentName, caSecretName string) (clientCert, clientKey, caData string, err error) { |
| 256 | + |
| 257 | + // Read the CA certificate from the principal's CA secret |
| 258 | + tlsCert, err := tlsutil.TLSCertFromSecret(ctx, kubeclient, namespace, caSecretName) |
| 259 | + if err != nil { |
| 260 | + err = fmt.Errorf("could not read CA secret: %w", err) |
| 261 | + return |
| 262 | + } |
| 263 | + |
| 264 | + // Parse CA certificate from PEM format |
| 265 | + signerCert, err := x509.ParseCertificate(tlsCert.Certificate[0]) |
| 266 | + if err != nil { |
| 267 | + err = fmt.Errorf("could not parse CA certificate: %w", err) |
| 268 | + return |
| 269 | + } |
| 270 | + |
| 271 | + // Generate a client cert with agent name as CN and sign it with the CA's cert and key |
| 272 | + clientCert, clientKey, err = tlsutil.GenerateClientCertificate(agentName, signerCert, tlsCert.PrivateKey) |
| 273 | + if err != nil { |
| 274 | + err = fmt.Errorf("could not create client cert: %w", err) |
| 275 | + return |
| 276 | + } |
| 277 | + |
| 278 | + // Convert CA certificate to PEM format |
| 279 | + caData, err = tlsutil.CertDataToPEM(tlsCert.Certificate[0]) |
| 280 | + if err != nil { |
| 281 | + err = fmt.Errorf("could not convert CA certificate to PEM format: %w", err) |
| 282 | + return |
| 283 | + } |
| 284 | + |
| 285 | + return |
| 286 | +} |
| 287 | + |
| 288 | +func getClusterSecretName(agentName string) string { |
| 289 | + return "cluster-" + agentName |
| 290 | +} |
0 commit comments