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  }