github.com/argoproj/argo-cd/v3@v3.2.1/cmd/argocd/commands/headless/headless.go (about) 1 package headless 2 3 import ( 4 "context" 5 "fmt" 6 "net" 7 "os" 8 "sync" 9 "time" 10 11 "github.com/alicebob/miniredis/v2" 12 "github.com/golang/protobuf/ptypes/empty" 13 "github.com/redis/go-redis/v9" 14 log "github.com/sirupsen/logrus" 15 "github.com/spf13/cobra" 16 "github.com/spf13/pflag" 17 corev1 "k8s.io/api/core/v1" 18 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 "k8s.io/apimachinery/pkg/runtime" 20 runtimeUtil "k8s.io/apimachinery/pkg/util/runtime" 21 "k8s.io/client-go/dynamic" 22 "k8s.io/client-go/kubernetes" 23 cache2 "k8s.io/client-go/tools/cache" 24 "k8s.io/client-go/tools/clientcmd" 25 "k8s.io/utils/ptr" 26 "sigs.k8s.io/controller-runtime/pkg/client" 27 28 "github.com/argoproj/argo-cd/v3/cmd/argocd/commands/initialize" 29 "github.com/argoproj/argo-cd/v3/common" 30 "github.com/argoproj/argo-cd/v3/pkg/apiclient" 31 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 32 appclientset "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned" 33 repoapiclient "github.com/argoproj/argo-cd/v3/reposerver/apiclient" 34 "github.com/argoproj/argo-cd/v3/server" 35 servercache "github.com/argoproj/argo-cd/v3/server/cache" 36 "github.com/argoproj/argo-cd/v3/util/cache" 37 appstatecache "github.com/argoproj/argo-cd/v3/util/cache/appstate" 38 "github.com/argoproj/argo-cd/v3/util/cli" 39 utilio "github.com/argoproj/argo-cd/v3/util/io" 40 kubeutil "github.com/argoproj/argo-cd/v3/util/kube" 41 "github.com/argoproj/argo-cd/v3/util/localconfig" 42 ) 43 44 type forwardCacheClient struct { 45 namespace string 46 context string 47 init sync.Once 48 client cache.CacheClient 49 compression cache.RedisCompressionType 50 err error 51 redisHaProxyName string 52 redisName string 53 redisPassword string 54 } 55 56 func (c *forwardCacheClient) doLazy(action func(client cache.CacheClient) error) error { 57 c.init.Do(func() { 58 overrides := clientcmd.ConfigOverrides{ 59 CurrentContext: c.context, 60 } 61 redisHaProxyPodLabelSelector := common.LabelKeyAppName + "=" + c.redisHaProxyName 62 redisPodLabelSelector := common.LabelKeyAppName + "=" + c.redisName 63 redisPort, err := kubeutil.PortForward(6379, c.namespace, &overrides, 64 redisHaProxyPodLabelSelector, redisPodLabelSelector) 65 if err != nil { 66 c.err = err 67 return 68 } 69 70 redisClient := redis.NewClient(&redis.Options{Addr: fmt.Sprintf("localhost:%d", redisPort), Password: c.redisPassword}) 71 c.client = cache.NewRedisCache(redisClient, time.Hour, c.compression) 72 }) 73 if c.err != nil { 74 return c.err 75 } 76 return action(c.client) 77 } 78 79 func (c *forwardCacheClient) Set(item *cache.Item) error { 80 return c.doLazy(func(client cache.CacheClient) error { 81 return client.Set(item) 82 }) 83 } 84 85 func (c *forwardCacheClient) Rename(oldKey string, newKey string, expiration time.Duration) error { 86 return c.doLazy(func(client cache.CacheClient) error { 87 return client.Rename(oldKey, newKey, expiration) 88 }) 89 } 90 91 func (c *forwardCacheClient) Get(key string, obj any) error { 92 return c.doLazy(func(client cache.CacheClient) error { 93 return client.Get(key, obj) 94 }) 95 } 96 97 func (c *forwardCacheClient) Delete(key string) error { 98 return c.doLazy(func(client cache.CacheClient) error { 99 return client.Delete(key) 100 }) 101 } 102 103 func (c *forwardCacheClient) OnUpdated(ctx context.Context, key string, callback func() error) error { 104 return c.doLazy(func(client cache.CacheClient) error { 105 return client.OnUpdated(ctx, key, callback) 106 }) 107 } 108 109 func (c *forwardCacheClient) NotifyUpdated(key string) error { 110 return c.doLazy(func(client cache.CacheClient) error { 111 return client.NotifyUpdated(key) 112 }) 113 } 114 115 type forwardRepoClientset struct { 116 namespace string 117 context string 118 init sync.Once 119 repoClientset repoapiclient.Clientset 120 err error 121 repoServerName string 122 kubeClientset kubernetes.Interface 123 } 124 125 func (c *forwardRepoClientset) NewRepoServerClient() (utilio.Closer, repoapiclient.RepoServerServiceClient, error) { 126 c.init.Do(func() { 127 overrides := clientcmd.ConfigOverrides{ 128 CurrentContext: c.context, 129 } 130 repoServerName := c.repoServerName 131 repoServererviceLabelSelector := common.LabelKeyComponentRepoServer + "=" + common.LabelValueComponentRepoServer 132 repoServerServices, err := c.kubeClientset.CoreV1().Services(c.namespace).List(context.Background(), metav1.ListOptions{LabelSelector: repoServererviceLabelSelector}) 133 if err != nil { 134 c.err = err 135 return 136 } 137 if len(repoServerServices.Items) > 0 { 138 if repoServerServicelabel, ok := repoServerServices.Items[0].Labels[common.LabelKeyAppName]; ok && repoServerServicelabel != "" { 139 repoServerName = repoServerServicelabel 140 } 141 } 142 repoServerPodLabelSelector := common.LabelKeyAppName + "=" + repoServerName 143 repoServerPort, err := kubeutil.PortForward(8081, c.namespace, &overrides, repoServerPodLabelSelector) 144 if err != nil { 145 c.err = err 146 return 147 } 148 c.repoClientset = repoapiclient.NewRepoServerClientset(fmt.Sprintf("localhost:%d", repoServerPort), 60, repoapiclient.TLSConfiguration{ 149 DisableTLS: false, StrictValidation: false, 150 }) 151 }) 152 if c.err != nil { 153 return nil, nil, c.err 154 } 155 return c.repoClientset.NewRepoServerClient() 156 } 157 158 func testAPI(ctx context.Context, clientOpts *apiclient.ClientOptions) error { 159 apiClient, err := apiclient.NewClient(clientOpts) 160 if err != nil { 161 return fmt.Errorf("failed to create API client: %w", err) 162 } 163 closer, versionClient, err := apiClient.NewVersionClient() 164 if err != nil { 165 return fmt.Errorf("failed to create version client: %w", err) 166 } 167 defer utilio.Close(closer) 168 _, err = versionClient.Version(ctx, &empty.Empty{}) 169 if err != nil { 170 return fmt.Errorf("failed to get version: %w", err) 171 } 172 return nil 173 } 174 175 // MaybeStartLocalServer allows executing command in a headless mode. If we're in core mode, starts the Argo CD API 176 // server on the fly and changes provided client options to use started API server port. 177 // 178 // If the clientOpts enables core mode, but the local config does not have core mode enabled, this function will 179 // not start the local server. 180 func MaybeStartLocalServer(ctx context.Context, clientOpts *apiclient.ClientOptions, ctxStr string, port *int, address *string, clientConfig clientcmd.ClientConfig) (func(), error) { 181 if clientConfig == nil { 182 flags := pflag.NewFlagSet("tmp", pflag.ContinueOnError) 183 clientConfig = cli.AddKubectlFlagsToSet(flags) 184 } 185 startInProcessAPI := clientOpts.Core 186 if !startInProcessAPI { 187 // Core mode is enabled on client options. Check the local config to see if we should start the API server. 188 localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath) 189 if err != nil { 190 return nil, fmt.Errorf("error reading local config: %w", err) 191 } 192 if localCfg != nil { 193 configCtx, err := localCfg.ResolveContext(clientOpts.Context) 194 if err != nil { 195 return nil, fmt.Errorf("error resolving context: %w", err) 196 } 197 // There was a local config file, so determine whether core mode is enabled per the config file. 198 startInProcessAPI = configCtx.Server.Core 199 } 200 } 201 // If we're in core mode, start the API server on the fly. 202 if !startInProcessAPI { 203 return nil, nil 204 } 205 206 // get rid of logging error handler 207 runtimeUtil.ErrorHandlers = runtimeUtil.ErrorHandlers[1:] 208 cli.SetLogLevel(log.ErrorLevel.String()) 209 log.SetLevel(log.ErrorLevel) 210 os.Setenv(v1alpha1.EnvVarFakeInClusterConfig, "true") 211 if address == nil { 212 address = ptr.To("localhost") 213 } 214 if port == nil || *port == 0 { 215 addr := *address + ":0" 216 ln, err := net.Listen("tcp", addr) 217 if err != nil { 218 return nil, fmt.Errorf("failed to listen on %q: %w", addr, err) 219 } 220 port = &ln.Addr().(*net.TCPAddr).Port 221 utilio.Close(ln) 222 } 223 224 restConfig, err := clientConfig.ClientConfig() 225 if err != nil { 226 return nil, fmt.Errorf("error creating client config: %w", err) 227 } 228 appClientset, err := appclientset.NewForConfig(restConfig) 229 if err != nil { 230 return nil, fmt.Errorf("error creating app clientset: %w", err) 231 } 232 kubeClientset, err := kubernetes.NewForConfig(restConfig) 233 if err != nil { 234 return nil, fmt.Errorf("error creating kubernetes clientset: %w", err) 235 } 236 237 dynamicClientset, err := dynamic.NewForConfig(restConfig) 238 if err != nil { 239 return nil, fmt.Errorf("error creating kubernetes dynamic clientset: %w", err) 240 } 241 242 scheme := runtime.NewScheme() 243 err = v1alpha1.AddToScheme(scheme) 244 if err != nil { 245 return nil, fmt.Errorf("error adding argo resources to scheme: %w", err) 246 } 247 err = corev1.AddToScheme(scheme) 248 if err != nil { 249 return nil, fmt.Errorf("error adding corev1 resources to scheme: %w", err) 250 } 251 controllerClientset, err := client.New(restConfig, client.Options{ 252 Scheme: scheme, 253 }) 254 if err != nil { 255 return nil, fmt.Errorf("error creating kubernetes controller clientset: %w", err) 256 } 257 controllerClientset = client.NewDryRunClient(controllerClientset) 258 259 namespace, _, err := clientConfig.Namespace() 260 if err != nil { 261 return nil, fmt.Errorf("error getting namespace: %w", err) 262 } 263 264 mr, err := miniredis.Run() 265 if err != nil { 266 return nil, fmt.Errorf("error running miniredis: %w", err) 267 } 268 redisOptions := &redis.Options{Addr: mr.Addr()} 269 if err = common.SetOptionalRedisPasswordFromKubeConfig(ctx, kubeClientset, namespace, redisOptions); err != nil { 270 log.Warnf("Failed to fetch & set redis password for namespace %s: %v", namespace, err) 271 } 272 273 appstateCache := appstatecache.NewCache(cache.NewCache(&forwardCacheClient{namespace: namespace, context: ctxStr, compression: cache.RedisCompressionType(clientOpts.RedisCompression), redisHaProxyName: clientOpts.RedisHaProxyName, redisName: clientOpts.RedisName, redisPassword: redisOptions.Password}), time.Hour) 274 srv := server.NewServer(ctx, server.ArgoCDServerOpts{ 275 EnableGZip: false, 276 Namespace: namespace, 277 ListenPort: *port, 278 AppClientset: appClientset, 279 DisableAuth: true, 280 RedisClient: redis.NewClient(redisOptions), 281 Cache: servercache.NewCache(appstateCache, 0, 0), 282 KubeClientset: kubeClientset, 283 DynamicClientset: dynamicClientset, 284 KubeControllerClientset: controllerClientset, 285 Insecure: true, 286 ListenHost: *address, 287 RepoClientset: &forwardRepoClientset{namespace: namespace, context: ctxStr, repoServerName: clientOpts.RepoServerName, kubeClientset: kubeClientset}, 288 EnableProxyExtension: false, 289 }, server.ApplicationSetOpts{}) 290 srv.Init(ctx) 291 292 lns, err := srv.Listen() 293 if err != nil { 294 return nil, fmt.Errorf("failed to listen: %w", err) 295 } 296 go srv.Run(ctx, lns) 297 clientOpts.ServerAddr = fmt.Sprintf("%s:%d", *address, *port) 298 clientOpts.PlainText = true 299 if !cache2.WaitForCacheSync(ctx.Done(), srv.Initialized) { 300 log.Fatal("Timed out waiting for project cache to sync") 301 } 302 303 tries := 5 304 for i := 0; i < tries; i++ { 305 err = testAPI(ctx, clientOpts) 306 if err == nil { 307 break 308 } 309 time.Sleep(time.Second) 310 } 311 if err != nil { 312 return nil, fmt.Errorf("all retries failed: %w", err) 313 } 314 return srv.Shutdown, nil 315 } 316 317 // NewClientOrDie creates a new API client from a set of config options, or fails fatally if the new client creation fails. 318 func NewClientOrDie(opts *apiclient.ClientOptions, c *cobra.Command) apiclient.Client { 319 ctx := c.Context() 320 321 ctxStr := initialize.RetrieveContextIfChanged(c.Flag("context")) 322 // If we're in core mode, start the API server on the fly and configure the client `opts` to use it. 323 // If we're not in core mode, this function call will do nothing. 324 _, err := MaybeStartLocalServer(ctx, opts, ctxStr, nil, nil, nil) 325 if err != nil { 326 log.Fatal(err) 327 } 328 client, err := apiclient.NewClient(opts) 329 if err != nil { 330 log.Fatal(err) 331 } 332 return client 333 }