diff --git a/USERS.md b/USERS.md index 60dd3b881c10b..8d2de3f04466d 100644 --- a/USERS.md +++ b/USERS.md @@ -219,6 +219,7 @@ Currently, the following organizations are **officially** using Argo CD: 1. [PGS](https://www.pgs.com) 1. [Pigment](https://www.gopigment.com/) 1. [Pipefy](https://www.pipefy.com/) +1. [Pipekit](https://www.pipekit.io) 1. [Pismo](https://pismo.io/) 1. [Platform9 Systems](https://platform9.com/) 1. [Polarpoint.io](https://polarpoint.io) diff --git a/cmd/argocd/commands/app.go b/cmd/argocd/commands/app.go index 0a54c517ca696..14346312f6326 100644 --- a/cmd/argocd/commands/app.go +++ b/cmd/argocd/commands/app.go @@ -45,6 +45,7 @@ import ( "github.com/argoproj/argo-cd/v2/util/argo" argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff" "github.com/argoproj/argo-cd/v2/util/cli" + "github.com/argoproj/argo-cd/v2/util/app/discovery" "github.com/argoproj/argo-cd/v2/util/errors" "github.com/argoproj/argo-cd/v2/util/git" "github.com/argoproj/argo-cd/v2/util/grpc" @@ -318,7 +319,7 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com var command = &cobra.Command{ Use: "get APPNAME", Short: "Get application details", - Example: templates.Examples(` + Example: templates.Examples(` # Get basic details about the application "my-app" in wide format argocd app get my-app -o wide @@ -342,7 +343,7 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com # Get application details and display them in a tree format argocd app get my-app --output tree - + # Get application details and display them in a detailed tree format argocd app get my-app --output tree=detailed `), @@ -431,7 +432,7 @@ func NewApplicationLogsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co var command = &cobra.Command{ Use: "logs APPNAME", Short: "Get logs of application pods", - Example: templates.Examples(` + Example: templates.Examples(` # Get logs of pods associated with the application "my-app" argocd app logs my-app @@ -716,7 +717,7 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com var command = &cobra.Command{ Use: "set APPNAME", Short: "Set application parameters", - Example: templates.Examples(` + Example: templates.Examples(` # Set application parameters for the application "my-app" argocd app set my-app --parameter key1=value1 --parameter key2=value2 @@ -992,7 +993,7 @@ func getLocalObjects(ctx context.Context, app *argoappv1.Application, proj *argo func getLocalObjectsString(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, local, localRepoRoot, appLabelKey, kubeVersion string, apiVersions []string, kustomizeOptions *argoappv1.KustomizeOptions, trackingMethod string) []string { source := app.Spec.GetSource() - res, err := repository.GenerateManifests(ctx, local, localRepoRoot, source.TargetRevision, &repoapiclient.ManifestRequest{ + res, err := repository.GenerateManifests(ctx, discovery.NewNoServices(), local, localRepoRoot, source.TargetRevision, &repoapiclient.ManifestRequest{ Repo: &argoappv1.Repository{Repo: source.RepoURL}, AppLabelKey: appLabelKey, AppName: app.Name, diff --git a/cmpserver/apiclient/clientset.go b/cmpserver/apiclient/clientset.go index 025625ff8092e..d27c08633bf40 100644 --- a/cmpserver/apiclient/clientset.go +++ b/cmpserver/apiclient/clientset.go @@ -24,19 +24,53 @@ type Clientset interface { NewConfigManagementPluginClient() (io.Closer, ConfigManagementPluginServiceClient, error) } +type ClientType int + +const ( + Sidecar ClientType = iota + Service +) + +func (ct *ClientType) addrType() string { + switch *ct { + case Sidecar: + return "unix" + case Service: + return "tcp" + default: + return "" + } +} + +func (ct *ClientType) String() string { + switch *ct { + case Sidecar: + return "sidecar" + case Service: + return "service" + default: + return "unknown" + } +} + type clientSet struct { - address string + address string + clientType ClientType +} + +func (c *clientSet) addrType() string { + return c.clientType.addrType() } func (c *clientSet) NewConfigManagementPluginClient() (io.Closer, ConfigManagementPluginServiceClient, error) { - conn, err := NewConnection(c.address) + conn, err := c.newConnection() if err != nil { return nil, nil, err } return conn, NewConfigManagementPluginServiceClient(conn), nil } -func NewConnection(address string) (*grpc.ClientConn, error) { +func (c *clientSet) newConnection() (*grpc.ClientConn, error) { retryOpts := []grpc_retry.CallOption{ grpc_retry.WithMax(3), grpc_retry.WithBackoff(grpc_retry.BackoffLinear(1000 * time.Millisecond)), @@ -51,15 +85,15 @@ func NewConnection(address string) (*grpc.ClientConn, error) { } dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) - conn, err := grpc_util.BlockingDial(context.Background(), "unix", address, nil, dialOpts...) + conn, err := grpc_util.BlockingDial(context.Background(), c.addrType(), c.address, nil, dialOpts...) if err != nil { - log.Errorf("Unable to connect to config management plugin service with address %s", address) + log.Errorf("Unable to connect to config management plugin with address %s (type %s)", c.address, c.clientType.String()) return nil, err } return conn, nil } // NewConfigManagementPluginClientSet creates new instance of config management plugin server Clientset -func NewConfigManagementPluginClientSet(address string) Clientset { - return &clientSet{address: address} +func NewConfigManagementPluginClientSet(address string, clientType ClientType) Clientset { + return &clientSet{address: address, clientType: clientType} } diff --git a/cmpserver/plugin/config.go b/cmpserver/plugin/config.go index faa718ff9fd2e..ec6886cdee4c0 100644 --- a/cmpserver/plugin/config.go +++ b/cmpserver/plugin/config.go @@ -23,6 +23,7 @@ type PluginConfig struct { type PluginConfigSpec struct { Version string `json:"version"` + ListenAddress string `json:"listenAddress,omitempty"` Init Command `json:"init,omitempty"` Generate Command `json:"generate"` Discover Discover `json:"discover"` @@ -93,13 +94,17 @@ func ValidatePluginConfig(config PluginConfig) error { return nil } -func (cfg *PluginConfig) Address() string { +// Returns the listen address and whether this is a tcp or unix address +func (cfg *PluginConfig) Address() (string, string) { var address string + if cfg.Spec.ListenAddress != "" { + return cfg.Spec.ListenAddress, `tcp` + } pluginSockFilePath := common.GetPluginSockFilePath() if cfg.Spec.Version != "" { address = fmt.Sprintf("%s/%s-%s.sock", pluginSockFilePath, cfg.Metadata.Name, cfg.Spec.Version) } else { address = fmt.Sprintf("%s/%s.sock", pluginSockFilePath, cfg.Metadata.Name) } - return address + return address, `unix` } diff --git a/cmpserver/plugin/config_test.go b/cmpserver/plugin/config_test.go index 9e22dab1d3741..8431bb9d39c42 100644 --- a/cmpserver/plugin/config_test.go +++ b/cmpserver/plugin/config_test.go @@ -170,9 +170,10 @@ spec: func Test_PluginConfig_Address(t *testing.T) { testCases := []struct { - name string - config *PluginConfig - expected string + name string + config *PluginConfig + expected string + expectedType string }{ { name: "no version specified", @@ -184,7 +185,8 @@ func Test_PluginConfig_Address(t *testing.T) { Name: "name", }, }, - expected: "name", + expected: "name", + expectedType: "unix", }, { name: "version specified", @@ -199,7 +201,25 @@ func Test_PluginConfig_Address(t *testing.T) { Version: "version", }, }, - expected: "name-version", + expected: "name-version", + expectedType: "unix", + }, + { + name: "version specified", + config: &PluginConfig{ + TypeMeta: v1.TypeMeta{ + Kind: ConfigManagementPluginKind, + }, + Metadata: v1.ObjectMeta{ + Name: "name", + }, + Spec: PluginConfigSpec{ + Version: "version", + ListenAddress: "0.0.0.0:8080", + }, + }, + expected: "0.0.0.0:8080", + expectedType: "tcp", }, } @@ -207,9 +227,14 @@ func Test_PluginConfig_Address(t *testing.T) { tcc := tc t.Run(tcc.name, func(t *testing.T) { t.Parallel() - actual := tcc.config.Address() - expectedAddress := fmt.Sprintf("%s/%s.sock", common.GetPluginSockFilePath(), tcc.expected) - assert.Equal(t, expectedAddress, actual) + actual, addrType := tcc.config.Address() + assert.Equal(t, tcc.expectedType, addrType) + if tcc.expectedType == `unix` { + expectedAddress := fmt.Sprintf("%s/%s.sock", common.GetPluginSockFilePath(), tcc.expected) + assert.Equal(t, expectedAddress, actual) + } else { + assert.Equal(t, tcc.expected, actual) + } }) } } diff --git a/cmpserver/server.go b/cmpserver/server.go index 1d07e531394d3..d6bdd3103f0c5 100644 --- a/cmpserver/server.go +++ b/cmpserver/server.go @@ -83,13 +83,17 @@ func (a *ArgoCDCMPServer) Run() { config := a.initConstants.PluginConfig // Listen on the socket address - _ = os.Remove(config.Address()) - listener, err := net.Listen("unix", config.Address()) + address, addressType := config.Address() + if addressType == `unix` { + _ = os.Remove(address) + } + listener, err := net.Listen(addressType, address) + errors.CheckError(err) log.Infof("argocd-cmp-server %s serving on %s", common.GetVersion(), listener.Addr()) signal.Notify(a.stopCh, syscall.SIGINT, syscall.SIGTERM) - go a.Shutdown(config.Address()) + go a.Shutdown(address) grpcServer, err := a.CreateGRPC() errors.CheckError(err) diff --git a/docs/operator-manual/config-management-plugins.md b/docs/operator-manual/config-management-plugins.md index 7c86075ff2f7f..90f04f4e7da36 100644 --- a/docs/operator-manual/config-management-plugins.md +++ b/docs/operator-manual/config-management-plugins.md @@ -1,4 +1,3 @@ - # Config Management Plugins Argo CD's "native" config management tools are Helm, Jsonnet, and Kustomize. If you want to use a different config @@ -19,9 +18,10 @@ The following sections will describe how to create, install, and use plugins. Ch ## Installing a config management plugin -### Sidecar plugin +### Sidecar or services plugin -An operator can configure a plugin tool via a sidecar to repo-server. The following changes are required to configure a new plugin: +An operator can configure a plugin tool via a sidecar to the repo-server, or as a Kubernetes service in the same cluster +as Argo CD. The following changes are required to configure a new plugin: #### Write the plugin configuration file @@ -37,6 +37,10 @@ spec: # The version of your plugin. Optional. If specified, the Application's spec.source.plugin.name field # must be -. version: v1.0 + # The listen address is where your plugin server will listen for connections. Optional. If not given it will listen + # on a unix socket, and this is the correct configuration for a sidecar plugin. + # To run as a service configure this to listen appropriately for incoming connections and specify a port number. + listenAddress: 0.0.0.0:8080 # The init command runs in the Application source directory at the beginning of each manifest generation. The init # command can output anything. A non-zero status code will fail manifest generation. init: @@ -54,18 +58,18 @@ spec: - | echo "{\"kind\": \"ConfigMap\", \"apiVersion\": \"v1\", \"metadata\": { \"name\": \"$ARGOCD_APP_NAME\", \"namespace\": \"$ARGOCD_APP_NAMESPACE\", \"annotations\": {\"Foo\": \"$ARGOCD_ENV_FOO\", \"KubeVersion\": \"$KUBE_VERSION\", \"KubeApiVersion\": \"$KUBE_API_VERSIONS\",\"Bar\": \"baz\"}}}" # The discovery config is applied to a repository. If every configured discovery tool matches, then the plugin may be - # used to generate manifests for Applications using the repository. If the discovery config is omitted then the plugin - # will not match any application but can still be invoked explicitly by specifying the plugin name in the app spec. - # Only one of fileName, find.glob, or find.command should be specified. If multiple are specified then only the + # used to generate manifests for Applications using the repository. If the discovery config is omitted then the plugin + # will not match any application but can still be invoked explicitly by specifying the plugin name in the app spec. + # Only one of fileName, find.glob, or find.command should be specified. If multiple are specified then only the # first (in that order) is evaluated. discover: - # fileName is a glob pattern (https://pkg.go.dev/path/filepath#Glob) that is applied to the Application's source + # fileName is a glob pattern (https://pkg.go.dev/path/filepath#Glob) that is applied to the Application's source # directory. If there is a match, this plugin may be used for the Application. fileName: "./subdir/s*.yaml" find: - # This does the same thing as fileName, but it supports double-start (nested directory) glob patterns. + # This does the same thing as fileName, but it supports double-star (nested directory) glob patterns. glob: "**/Chart.yaml" - # The find command runs in the repository's root directory. To match, it must exit with status code 0 _and_ + # The find command runs in the repository's root directory. To match, it must exit with status code 0 _and_ # produce non-empty output to standard out. command: [sh, -c, find . -name env.yaml] # The parameters config describes what parameters the UI should display for an Application. It is up to the user to @@ -73,7 +77,7 @@ spec: # inform the "Parameters" tab in the App Details page of the UI. parameters: # Static parameter announcements are sent to the UI for _all_ Applications handled by this plugin. - # Think of the `string`, `array`, and `map` values set here as "defaults". It is up to the plugin author to make + # Think of the `string`, `array`, and `map` values set here as "defaults". It is up to the plugin author to make # sure that these default values actually reflect the plugin's behavior if the user doesn't explicitly set different # values for those parameters. static: @@ -116,13 +120,13 @@ spec: ``` !!! note - While the ConfigManagementPlugin _looks like_ a Kubernetes object, it is not actually a custom resource. + While the ConfigManagementPlugin _looks like_ a Kubernetes object, it is not actually a custom resource. It only follows kubernetes-style spec conventions. The `generate` command must print a valid Kubernetes YAML or JSON object stream to stdout. Both `init` and `generate` commands are executed inside the application source directory. The `discover.fileName` is used as [glob](https://pkg.go.dev/path/filepath#Glob) pattern to determine whether an -application repository is supported by the plugin or not. +application repository is supported by the plugin or not. ```yaml discover: @@ -134,19 +138,20 @@ If `discover.fileName` is not provided, the `discover.find.command` is executed application repository is supported by the plugin or not. The `find` command should return a non-error exit code and produce output to stdout when the application source type is supported. -#### Place the plugin configuration file in the sidecar +#### Place the plugin configuration file in the plugin's runtime environment -Argo CD expects the plugin configuration file to be located at `/home/argocd/cmp-server/config/plugin.yaml` in the sidecar. +Argo CD expects the plugin configuration file to be located at `/home/argocd/cmp-server/config/plugin.yaml` in the sidecar +or pod behind your service. -If you use a custom image for the sidecar, you can add the file directly to that image. +If you use a custom image for the plugin, you can add the file directly to that image. ```dockerfile WORKDIR /home/argocd/cmp-server/config/ COPY plugin.yaml ./ ``` -If you use a stock image for the sidecar or would rather maintain the plugin configuration in a ConfigMap, just nest the -plugin config file in a ConfigMap under the `plugin.yaml` key and mount the ConfigMap in the sidecar (see next section). +If you use a stock image for the plugin or would rather maintain the plugin configuration in a ConfigMap, just nest the +plugin config file in a ConfigMap under the `plugin.yaml` key and mount the ConfigMap in the container (see next section). ```yaml apiVersion: v1 @@ -169,16 +174,17 @@ data: fileName: "./subdir/s*.yaml" ``` -#### Register the plugin sidecar +#### Register the plugin as a sidecar -To install a plugin, patch argocd-repo-server to run the plugin container as a sidecar, with argocd-cmp-server as its -entrypoint. You can use either off-the-shelf or custom-built plugin image as sidecar image. For example: +To install a plugin as a sidecar, patch argocd-repo-server to run the plugin container as a sidecar, with +argocd-cmp-server as its entrypoint. You can use either an off-the-shelf or custom-built plugin image as sidecar image. +For example: ```yaml containers: - name: my-plugin command: [/var/run/argocd/argocd-cmp-server] # Entrypoint should be Argo CD lightweight CMP server i.e. argocd-cmp-server - image: busybox # This can be off-the-shelf or custom-built image + image: busybox # This can be an off-the-shelf or custom-built image securityContext: runAsNonRoot: true runAsUser: 999 @@ -191,7 +197,7 @@ containers: - mountPath: /home/argocd/cmp-server/config/plugin.yaml subPath: plugin.yaml name: my-plugin-config - # Starting with v2.4, do NOT mount the same tmp volume as the repo-server container. The filesystem separation helps + # Starting with v2.4, do NOT mount the same tmp volume as the repo-server container. The filesystem separation helps # mitigate path traversal attacks. - mountPath: /tmp name: cmp-tmp @@ -201,18 +207,172 @@ volumes: name: my-plugin-config - emptyDir: {} name: cmp-tmp -``` +``` !!! important "Double-check these items" 1. Make sure to use `/var/run/argocd/argocd-cmp-server` as an entrypoint. The `argocd-cmp-server` is a lightweight GRPC service that allows Argo CD to interact with the plugin. 2. Make sure that sidecar container is running as user 999. 3. Make sure that plugin configuration file is present at `/home/argocd/cmp-server/config/plugin.yaml`. It can either be volume mapped via configmap or baked into image. +#### Register the plugin as a service + +Argo CD needs to have access to the cluster to discover services. To do this it uses a standard Kubernetes Service Account token to authenticate +with the cluster. It also needs a Role or ClusterRole bound to that Service Account to access with the permissions get, list and watch the namespace(s) +that the services are in. By default (no configuration) the services are looked for in the same namespace as the argocd-repo-server. You can set +`ARGOCD_SERVICE_PLUGINS_NAMESPACE` to the name of a namespace to change it to look in a different namespace. To look in all +namespaces set `ARGOCD_SERVICE_PLUGINS_NAMESPACE` to `*`. You cannot configure multiple specific namespaces, just one namespace or all namespaces. + +!!! Ensure that: + 1. The argocd-repo-server deployment has `automountServiceAccountToken: true` + 2. The argocd-repo-server's service account has a role binding allowing `get`, `list` and `watch` on services. + + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: argocd-repo-server-getsvcs + namespace: argocd +rules: +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: argocd-repo-server-getsvcs + namespace: argocd +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: argocd-repo-server-getsvcs +subjects: +- kind: ServiceAccount + name: argocd-repo-server + namespace: argocd +``` + +or for cluster wide + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: argocd-repo-server-getsvcs +rules: +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: argocd-repo-server-getsvcs +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: argocd-repo-server-getsvcs +subjects: +- kind: ServiceAccount + name: argocd-repo-server + namespace: argocd +``` + +To install a plugin as a service, deploy something running your code. This would usually be a deployment, but Argo CD doesn't +actually care what is behind the service. You can use either an off-the-shelf or custom-built plugin image as sidecar image. This +should follow the same rules as for a sidecar. + +To advertise the service to Argo CD you must provide a service with the label `argocd.argoproj.io/plugin: true`. Each port in the +service will be treated as a separate plugin, and the name of the port is the name of the plugin, which will be used by applications +which specify their plugin by name. + +This might look like: + +```yaml +--- +apiVersion: v1 +kind: Service +metadata: + name: myplugin + namespace: argocd + labels: + argocd.argoproj.io/plugin: "true" +spec: + ports: + - name: myplugin + port: 8080 + protocol: TCP + targetPort: cmp + selector: + k8s-app: myplugin +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + k8s-app: myplugin + name: myplugin + namespace: argocd +spec: + selector: + matchLabels: + k8s-app: myplugin + template: + metadata: + labels: + k8s-app: myplugin + spec: + containers: + - name: myplugin + image: myrepo/myplugin:latest + imagePullPolicy: Always + ports: + - containerPort: 8080 + name: cmp + protocol: TCP +``` + +!!! important "Double-check these items" + 1. Make sure to use `argocd-cmp-server` as an entrypoint from the same version of Argo CD. The `argocd-cmp-server` is a lightweight GRPC service that allows Argo CD to interact with the plugin. + 2. Make sure that sidecar container is running as user 999. + 3. Make sure that plugin configuration file is present at `/home/argocd/cmp-server/config/plugin.yaml`. It can either be volume mapped via configmap or baked into image. + 4. The **service** has the label `argocd.argoproj.io/plugin: "true"` + 5. If you are calling the plugin by name that the **port** name is what you use to call it. + +If you'd like to disable this feature do not provide a service account token. + +#### Choosing between sidecar and service plugins + +If you are debugging your plugin, choose a service to start with. You can then restart the deployments behind the service without +needing to restart Argo CD. You could also easily develop in an in-cluster development environment this way. If you want to develop +locally and your cluster can reach your local machine you could even develop using an ExternalName service. + +For large mono-repos sidecars will continue to work where services will take longer to transfer the state to the plugin. Argo CD +doesn't condone large repos anyway. Because the network traffic between the repo-server and the plugin is over a unix socket it is by +default more secure. + +Services allow independent scaling, even using HPAs, installation and upgrading at runtime, and work more like normal services. +You can add monitoring to both types, but it feels more natural with a service. If you are making your plugin available for public +consumption you can provide separate deployment manifests (yaml/kustomize/helm chart) to allow easier adoption. Plugin repository +downtime is less serious in this case; with a sidecar if any one of the containers in the pod is unavailable then your pod will not +start/restart. You can grant specific cluster permissions to your plugin without granting them to the rest of the repo server. + ### Using environment variables in your plugin Plugin commands have access to -1. The system environment variables of the sidecar +1. The system environment variables of the container 2. [Standard build environment variables](../user-guide/build-environment.md) 3. Variables in the Application spec (References to system and build variables will get interpolated in the variables' values): @@ -226,9 +386,9 @@ Plugin commands have access to value: bar - name: REV value: test-$ARGOCD_APP_REVISION - - Before reaching the `init.command`, `generate.command`, and `discover.find.command` commands, Argo CD prefixes all - user-supplied environment variables (#3 above) with `ARGOCD_ENV_`. This prevents users from directly setting + + Before reaching the `init.command`, `generate.command`, and `discover.find.command` commands, Argo CD prefixes all + user-supplied environment variables (#3 above) with `ARGOCD_ENV_`. This prevents users from directly setting potentially-sensitive environment variables. 4. Parameters in the Application spec: @@ -244,43 +404,43 @@ Plugin commands have access to - name: helm-parameters map: image.tag: v1.2.3 - + The parameters are available as JSON in the `ARGOCD_APP_PARAMETERS` environment variable. The example above would produce this JSON: - + [{"name": "values-files", "array": ["values-dev.yaml"]}, {"name": "helm-parameters", "map": {"image.tag": "v1.2.3"}}] - + !!! note Parameter announcements, even if they specify defaults, are _not_ sent to the plugin in `ARGOCD_APP_PARAMETERS`. Only parameters explicitly set in the Application spec are sent to the plugin. It is up to the plugin to apply the same defaults as the ones announced to the UI. - + The same parameters are also available as individual environment variables. The names of the environment variables follows this convention: - + - name: some-string-param string: some-string-value # PARAM_SOME_STRING_PARAM=some-string-value - + - name: some-array-param value: [item1, item2] # PARAM_SOME_ARRAY_PARAM_0=item1 # PARAM_SOME_ARRAY_PARAM_1=item2 - + - name: some-map-param map: image.tag: v1.2.3 # PARAM_SOME_MAP_PARAM_IMAGE_TAG=v1.2.3 - -!!! warning "Sanitize/escape user input" + +!!! warning "Sanitize/escape user input" As part of Argo CD's manifest generation system, config management plugins are treated with a level of trust. Be sure to escape user input in your plugin to prevent malicious input from causing unwanted behavior. ## Using a config management plugin with an Application You may leave the `name` field -empty in the `plugin` section for the plugin to be automatically matched with the Application based on its discovery rules. If you do mention the name make sure -it is either `-` if version is mentioned in the `ConfigManagementPlugin` spec or else just ``. When name is explicitly +empty in the `plugin` section for the plugin to be automatically matched with the Application based on its discovery rules. If you do mention the name make sure +it is either `-` if version is mentioned in the `ConfigManagementPlugin` spec or else just ``. When name is explicitly specified only that particular plugin will be used iff its discovery pattern/command matches the provided application repo. ```yaml @@ -309,22 +469,22 @@ If you don't need to set any environment variables, you can set an empty plugin !!! important If your CMP command runs too long, the command will be killed, and the UI will show an error. The CMP server - respects the timeouts set by the `server.repo.server.timeout.seconds` and `controller.repo.server.timeout.seconds` + respects the timeouts set by the `server.repo.server.timeout.seconds` and `controller.repo.server.timeout.seconds` items in `argocd-cm`. Increase their values from the default of 60s. Each CMP command will also independently timeout on the `ARGOCD_EXEC_TIMEOUT` set for the CMP sidecar. The default is 90s. So if you increase the repo server timeout greater than 90s, be sure to set `ARGOCD_EXEC_TIMEOUT` on the sidecar. - + !!! note Each Application can only have one config management plugin configured at a time. If you're converting an existing - plugin configured through the `argocd-cm` ConfigMap to a sidecar, make sure to update the plugin name to either `-` - if version was mentioned in the `ConfigManagementPlugin` spec or else just use ``. You can also remove the name altogether + plugin configured through the `argocd-cm` ConfigMap to a sidecar, make sure to update the plugin name to either `-` + if version was mentioned in the `ConfigManagementPlugin` spec or else just use ``. You can also remove the name altogether and let the automatic discovery to identify the plugin. !!! note If a CMP renders blank manfiests, and `prune` is set to `true`, Argo CD will automatically remove resources. CMP plugin authors should ensure errors are part of the exit code. Commonly something like `kustomize build . | cat` won't pass errors because of the pipe. Consider setting `set -o pipefail` so anything piped will pass errors on failure. -## Debugging a CMP +## Debugging a CMP as a sidecar If you are actively developing a sidecar-installed CMP, keep a few things in mind: @@ -338,6 +498,9 @@ If you are actively developing a sidecar-installed CMP, keep a few things in min 4. Verify your sidecar has started properly by viewing the Pod and seeing that two containers are running `kubectl get pod -l app.kubernetes.io/component=repo-server -n argocd` 5. Write log message to stderr and set the `--loglevel=info` flag in the sidecar. This will print everything written to stderr, even on successfull command execution. +## Debugging a CMP as a service + +If you are actively developing a services based CMP then things should be more familiar than as a sidecar. You still need to bear in mind that CMP errors are cached by the repo-server in Redis. Restarting the repo-server Pod will not clear the cache. Always do a "Hard Refresh" when actively developing a CMP so you have the latest output. ### Other Common Errors | Error Message | Cause | @@ -402,7 +565,7 @@ spec: The `lockRepo` key is not relevant for sidecar plugins, because sidecar plugins do not share a single source repo directory when generating manifests. -Next, we need to decide how this yaml is going to be added to the sidecar. We can either bake the yaml directly into the image, or we can mount it from a ConfigMap. +Next, we need to decide how this yaml is going to be added to the sidecar. We can either bake the yaml directly into the image, or we can mount it from a ConfigMap. If using a ConfigMap, our example would look like this: @@ -431,14 +594,14 @@ Then this would be mounted in our plugin sidecar. ### Write discovery rules for your plugin -Sidecar plugins can use either discovery rules or a plugin name to match Applications to plugins. If the discovery rule is omitted +Sidecar plugins can use either discovery rules or a plugin name to match Applications to plugins. If the discovery rule is omitted then you have to explicitly specify the plugin by name in the app spec or else that particular plugin will not match any app. -If you want to use discovery instead of the plugin name to match applications to your plugin, write rules applicable to -your plugin [using the instructions above](#1-write-the-plugin-configuration-file) and add them to your configuration +If you want to use discovery instead of the plugin name to match applications to your plugin, write rules applicable to +your plugin [using the instructions above](#1-write-the-plugin-configuration-file) and add them to your configuration file. -To use the name instead of discovery, update the name in your application manifest to `-` +To use the name instead of discovery, update the name in your application manifest to `-` if version was mentioned in the `ConfigManagementPlugin` spec or else just use ``. For example: ```yaml diff --git a/manifests/base/repo-server/argocd-repo-server-deployment.yaml b/manifests/base/repo-server/argocd-repo-server-deployment.yaml index 907bc80a34e56..f2f84c5c958a3 100644 --- a/manifests/base/repo-server/argocd-repo-server-deployment.yaml +++ b/manifests/base/repo-server/argocd-repo-server-deployment.yaml @@ -16,7 +16,6 @@ spec: app.kubernetes.io/name: argocd-repo-server spec: serviceAccountName: argocd-repo-server - automountServiceAccountToken: false containers: - name: argocd-repo-server image: quay.io/argoproj/argocd:latest diff --git a/manifests/base/repo-server/argocd-repo-server-role-binding.yaml b/manifests/base/repo-server/argocd-repo-server-role-binding.yaml new file mode 100644 index 0000000000000..6c8b4d5a915b7 --- /dev/null +++ b/manifests/base/repo-server/argocd-repo-server-role-binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: argocd-repo-server-getsvcs +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: argocd-repo-server-getsvcs +subjects: +- kind: ServiceAccount + name: argocd-repo-server + namespace: argocd diff --git a/manifests/base/repo-server/argocd-repo-server-role.yaml b/manifests/base/repo-server/argocd-repo-server-role.yaml new file mode 100644 index 0000000000000..8a81bc44edcba --- /dev/null +++ b/manifests/base/repo-server/argocd-repo-server-role.yaml @@ -0,0 +1,13 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: argocd-repo-server-getsvcs +rules: +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch diff --git a/manifests/base/repo-server/kustomization.yaml b/manifests/base/repo-server/kustomization.yaml index 877581a3b79b6..ddb9c892652ba 100644 --- a/manifests/base/repo-server/kustomization.yaml +++ b/manifests/base/repo-server/kustomization.yaml @@ -6,3 +6,5 @@ resources: - argocd-repo-server-deployment.yaml - argocd-repo-server-service.yaml - argocd-repo-server-network-policy.yaml +- argocd-repo-server-role.yaml +- argocd-repo-server-role-binding.yaml diff --git a/manifests/core-install.yaml b/manifests/core-install.yaml index 08d7d972e6362..7f33021d70eec 100644 --- a/manifests/core-install.yaml +++ b/manifests/core-install.yaml @@ -20671,6 +20671,20 @@ rules: - watch --- apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: argocd-repo-server-getsvcs +rules: +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: @@ -20723,6 +20737,19 @@ subjects: name: argocd-applicationset-controller --- apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: argocd-repo-server-getsvcs +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: argocd-repo-server-getsvcs +subjects: +- kind: ServiceAccount + name: argocd-repo-server + namespace: argocd +--- +apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: labels: @@ -21184,7 +21211,6 @@ spec: app.kubernetes.io/part-of: argocd topologyKey: kubernetes.io/hostname weight: 5 - automountServiceAccountToken: false containers: - args: - /usr/local/bin/argocd-repo-server diff --git a/manifests/ha/install.yaml b/manifests/ha/install.yaml index 2029be1e07e12..190cf72c0aba2 100644 --- a/manifests/ha/install.yaml +++ b/manifests/ha/install.yaml @@ -20804,6 +20804,20 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role +metadata: + name: argocd-repo-server-getsvcs +rules: +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role metadata: labels: app.kubernetes.io/component: server @@ -21105,6 +21119,19 @@ subjects: --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding +metadata: + name: argocd-repo-server-getsvcs +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: argocd-repo-server-getsvcs +subjects: +- kind: ServiceAccount + name: argocd-repo-server + namespace: argocd +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding metadata: labels: app.kubernetes.io/component: server @@ -22783,7 +22810,6 @@ spec: matchLabels: app.kubernetes.io/name: argocd-repo-server topologyKey: kubernetes.io/hostname - automountServiceAccountToken: false containers: - args: - /usr/local/bin/argocd-repo-server diff --git a/manifests/ha/namespace-install.yaml b/manifests/ha/namespace-install.yaml index 01a8da2ffd7b9..0596c773ea4ad 100644 --- a/manifests/ha/namespace-install.yaml +++ b/manifests/ha/namespace-install.yaml @@ -282,6 +282,20 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role +metadata: + name: argocd-repo-server-getsvcs +rules: +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role metadata: labels: app.kubernetes.io/component: server @@ -422,6 +436,19 @@ subjects: --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding +metadata: + name: argocd-repo-server-getsvcs +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: argocd-repo-server-getsvcs +subjects: +- kind: ServiceAccount + name: argocd-repo-server + namespace: argocd +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding metadata: labels: app.kubernetes.io/component: server @@ -2049,7 +2076,6 @@ spec: matchLabels: app.kubernetes.io/name: argocd-repo-server topologyKey: kubernetes.io/hostname - automountServiceAccountToken: false containers: - args: - /usr/local/bin/argocd-repo-server diff --git a/manifests/install.yaml b/manifests/install.yaml index 83ac4f903fb7b..57ce324c46f05 100644 --- a/manifests/install.yaml +++ b/manifests/install.yaml @@ -20763,6 +20763,20 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role +metadata: + name: argocd-repo-server-getsvcs +rules: +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role metadata: labels: app.kubernetes.io/component: server @@ -21032,6 +21046,19 @@ subjects: --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding +metadata: + name: argocd-repo-server-getsvcs +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: argocd-repo-server-getsvcs +subjects: +- kind: ServiceAccount + name: argocd-repo-server + namespace: argocd +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding metadata: labels: app.kubernetes.io/component: server @@ -21829,7 +21856,6 @@ spec: app.kubernetes.io/part-of: argocd topologyKey: kubernetes.io/hostname weight: 5 - automountServiceAccountToken: false containers: - args: - /usr/local/bin/argocd-repo-server diff --git a/manifests/namespace-install.yaml b/manifests/namespace-install.yaml index 76301680f195a..dae49681f1e36 100644 --- a/manifests/namespace-install.yaml +++ b/manifests/namespace-install.yaml @@ -241,6 +241,20 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role +metadata: + name: argocd-repo-server-getsvcs +rules: +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role metadata: labels: app.kubernetes.io/component: server @@ -349,6 +363,19 @@ subjects: --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding +metadata: + name: argocd-repo-server-getsvcs +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: argocd-repo-server-getsvcs +subjects: +- kind: ServiceAccount + name: argocd-repo-server + namespace: argocd +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding metadata: labels: app.kubernetes.io/component: server @@ -1095,7 +1122,6 @@ spec: app.kubernetes.io/part-of: argocd topologyKey: kubernetes.io/hostname weight: 5 - automountServiceAccountToken: false containers: - args: - /usr/local/bin/argocd-repo-server diff --git a/reposerver/apiclient/repository.pb.go b/reposerver/apiclient/repository.pb.go index 914a967db3dfc..9fc0f8931ac55 100644 --- a/reposerver/apiclient/repository.pb.go +++ b/reposerver/apiclient/repository.pb.go @@ -2365,7 +2365,7 @@ type RepoServerServiceClient interface { ListRefs(ctx context.Context, in *ListRefsRequest, opts ...grpc.CallOption) (*Refs, error) // ListApps returns a list of apps in the repo ListApps(ctx context.Context, in *ListAppsRequest, opts ...grpc.CallOption) (*AppList, error) - // ListPlugins returns a list of cmp v2 plugins running as sidecar to reposerver + // ListPlugins returns a list of cmp v2 plugins running as sidecar or services to reposerver ListPlugins(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*PluginList, error) // Generate manifest for application in specified repo name and revision GetAppDetails(ctx context.Context, in *RepoServerAppDetailsQuery, opts ...grpc.CallOption) (*RepoAppDetailsResponse, error) @@ -2545,7 +2545,7 @@ type RepoServerServiceServer interface { ListRefs(context.Context, *ListRefsRequest) (*Refs, error) // ListApps returns a list of apps in the repo ListApps(context.Context, *ListAppsRequest) (*AppList, error) - // ListPlugins returns a list of cmp v2 plugins running as sidecar to reposerver + // ListPlugins returns a list of cmp v2 plugins running as sidecar or services to reposerver ListPlugins(context.Context, *emptypb.Empty) (*PluginList, error) // Generate manifest for application in specified repo name and revision GetAppDetails(context.Context, *RepoServerAppDetailsQuery) (*RepoAppDetailsResponse, error) diff --git a/reposerver/repository/repository.go b/reposerver/repository/repository.go index 41f26b1f434b8..60feffcbcda33 100644 --- a/reposerver/repository/repository.go +++ b/reposerver/repository/repository.go @@ -91,6 +91,7 @@ type Service struct { newGitClient func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (git.Client, error) newHelmClient func(repoURL string, creds helm.Creds, enableOci bool, proxy string, opts ...helm.ClientOpts) helm.Client initConstants RepoServerInitConstants + discoverer *discovery.Discoverer // now is usually just time.Now, but may be replaced by unit tests for testing purposes now func() time.Time } @@ -135,6 +136,7 @@ func NewService(metricsServer *metrics.MetricsServer, cache *cache.Cache, initCo gitRepoPaths: gitRandomizedPaths, chartPaths: helmRandomizedPaths, gitRepoInitializer: directoryPermissionInitializer, + discoverer: discovery.NewWithServices(), rootDir: rootDir, } } @@ -220,7 +222,7 @@ func (s *Service) ListApps(ctx context.Context, q *apiclient.ListAppsRequest) (* } defer io.Close(closer) - apps, err := discovery.Discover(ctx, gitClient.Root(), gitClient.Root(), q.EnabledSourceTypes, s.initConstants.CMPTarExcludedGlobs) + apps, err := discovery.Discover(ctx, s.discoverer, gitClient.Root(), gitClient.Root(), q.EnabledSourceTypes, s.initConstants.CMPTarExcludedGlobs) if err != nil { return nil, fmt.Errorf("error discovering applications: %w", err) } @@ -232,24 +234,16 @@ func (s *Service) ListApps(ctx context.Context, q *apiclient.ListAppsRequest) (* return &res, nil } -// ListPlugins lists the contents of a GitHub repo +// ListPlugins returns a list of cmp v2 plugins running as sidecar or services to reposerver func (s *Service) ListPlugins(ctx context.Context, _ *empty.Empty) (*apiclient.PluginList, error) { - pluginSockFilePath := common.GetPluginSockFilePath() - - sockFiles, err := os.ReadDir(pluginSockFilePath) - if err != nil { - return nil, fmt.Errorf("failed to get plugins from dir %v, error=%w", pluginSockFilePath, err) - } - + pluginNames, err := discovery.GetPluginNames(s.discoverer) var plugins []*apiclient.PluginInfo - for _, file := range sockFiles { - if file.Type() == os.ModeSocket { - plugins = append(plugins, &apiclient.PluginInfo{Name: strings.TrimSuffix(file.Name(), ".sock")}) - } + for _, name := range pluginNames { + plugins = append(plugins, &apiclient.PluginInfo{Name: name}) } res := apiclient.PluginList{Items: plugins} - return &res, nil + return &res, err } type operationSettings struct { @@ -794,7 +788,7 @@ func (s *Service) runManifestGenAsync(ctx context.Context, repoRoot, commitSHA, } } - manifestGenResult, err = GenerateManifests(ctx, opContext.appPath, repoRoot, commitSHA, q, false, s.gitCredsStore, s.initConstants.MaxCombinedDirectoryManifestsSize, s.gitRepoPaths, WithCMPTarDoneChannel(ch.tarDoneCh), WithCMPTarExcludedGlobs(s.initConstants.CMPTarExcludedGlobs)) + manifestGenResult, err = GenerateManifests(ctx, s.discoverer, opContext.appPath, repoRoot, commitSHA, q, false, s.gitCredsStore, s.initConstants.MaxCombinedDirectoryManifestsSize, s.gitRepoPaths, WithCMPTarDoneChannel(ch.tarDoneCh), WithCMPTarExcludedGlobs(s.initConstants.CMPTarExcludedGlobs)) } refSourceCommitSHAs := make(map[string]string) if len(repoRefs) > 0 { @@ -1365,13 +1359,13 @@ func WithCMPTarExcludedGlobs(excludedGlobs []string) GenerateManifestOpt { } // GenerateManifests generates manifests from a path. Overrides are applied as a side effect on the given ApplicationSource. -func GenerateManifests(ctx context.Context, appPath, repoRoot, revision string, q *apiclient.ManifestRequest, isLocal bool, gitCredsStore git.CredsStore, maxCombinedManifestQuantity resource.Quantity, gitRepoPaths io.TempPaths, opts ...GenerateManifestOpt) (*apiclient.ManifestResponse, error) { +func GenerateManifests(ctx context.Context, discoverer *discovery.Discoverer, appPath, repoRoot, revision string, q *apiclient.ManifestRequest, isLocal bool, gitCredsStore git.CredsStore, maxCombinedManifestQuantity resource.Quantity, gitRepoPaths io.TempPaths, opts ...GenerateManifestOpt) (*apiclient.ManifestResponse, error) { opt := newGenerateManifestOpt(opts...) var targetObjs []*unstructured.Unstructured resourceTracking := argo.NewResourceTracking() - appSourceType, err := GetAppSourceType(ctx, q.ApplicationSource, appPath, repoRoot, q.AppName, q.EnabledSourceTypes, opt.cmpTarExcludedGlobs) + appSourceType, err := GetAppSourceType(ctx, discoverer, q.ApplicationSource, appPath, repoRoot, q.AppName, q.EnabledSourceTypes, opt.cmpTarExcludedGlobs) if err != nil { return nil, fmt.Errorf("error getting app source type: %w", err) } @@ -1397,9 +1391,9 @@ func GenerateManifests(ctx context.Context, appPath, repoRoot, revision string, pluginName = q.ApplicationSource.Plugin.Name } // if pluginName is provided it has to be `-` or just `` if plugin version is empty - targetObjs, err = runConfigManagementPluginSidecars(ctx, appPath, repoRoot, pluginName, env, q, opt.cmpTarDoneCh, opt.cmpTarExcludedGlobs) + targetObjs, err = runConfigManagementPluginSidecars(ctx, discoverer, appPath, repoRoot, pluginName, env, q, opt.cmpTarDoneCh, opt.cmpTarExcludedGlobs) if err != nil { - err = fmt.Errorf("plugin sidecar failed. %s", err.Error()) + err = fmt.Errorf("plugin sidecar/service failed. %s", err.Error()) } case v1alpha1.ApplicationSourceTypeDirectory: var directory *v1alpha1.ApplicationSourceDirectory @@ -1534,7 +1528,7 @@ func mergeSourceParameters(source *v1alpha1.ApplicationSource, path, appName str } // GetAppSourceType returns explicit application source type or examines a directory and determines its application source type -func GetAppSourceType(ctx context.Context, source *v1alpha1.ApplicationSource, appPath, repoPath, appName string, enableGenerateManifests map[string]bool, tarExcludedGlobs []string) (v1alpha1.ApplicationSourceType, error) { +func GetAppSourceType(ctx context.Context, discoverer *discovery.Discoverer, source *v1alpha1.ApplicationSource, appPath, repoPath, appName string, enableGenerateManifests map[string]bool, tarExcludedGlobs []string) (v1alpha1.ApplicationSourceType, error) { err := mergeSourceParameters(source, appPath, appName) if err != nil { return "", fmt.Errorf("error while parsing source parameters: %v", err) @@ -1551,7 +1545,7 @@ func GetAppSourceType(ctx context.Context, source *v1alpha1.ApplicationSource, a } return *appSourceType, nil } - appType, err := discovery.AppType(ctx, appPath, repoPath, enableGenerateManifests, tarExcludedGlobs) + appType, err := discovery.AppType(ctx, discoverer, appPath, repoPath, enableGenerateManifests, tarExcludedGlobs) if err != nil { return "", fmt.Errorf("error getting app source type: %v", err) } @@ -1898,7 +1892,7 @@ func getPluginParamEnvs(envVars []string, plugin *v1alpha1.ApplicationSourcePlug return env, nil } -func runConfigManagementPluginSidecars(ctx context.Context, appPath, repoPath, pluginName string, envVars *v1alpha1.Env, q *apiclient.ManifestRequest, tarDoneCh chan<- bool, tarExcludedGlobs []string) ([]*unstructured.Unstructured, error) { +func runConfigManagementPluginSidecars(ctx context.Context, discoverer *discovery.Discoverer, appPath, repoPath, pluginName string, envVars *v1alpha1.Env, q *apiclient.ManifestRequest, tarDoneCh chan<- bool, tarExcludedGlobs []string) ([]*unstructured.Unstructured, error) { // compute variables. env, err := getPluginEnvs(envVars, q) if err != nil { @@ -1906,7 +1900,7 @@ func runConfigManagementPluginSidecars(ctx context.Context, appPath, repoPath, p } // detect config management plugin server - conn, cmpClient, err := discovery.DetectConfigManagementPlugin(ctx, appPath, repoPath, pluginName, env, tarExcludedGlobs) + conn, cmpClient, err := discovery.DetectConfigManagementPlugin(ctx, discoverer, appPath, repoPath, pluginName, env, tarExcludedGlobs) if err != nil { return nil, err } @@ -1963,7 +1957,7 @@ func (s *Service) GetAppDetails(ctx context.Context, q *apiclient.RepoServerAppD return err } - appSourceType, err := GetAppSourceType(ctx, q.Source, opContext.appPath, repoRoot, q.AppName, q.EnabledSourceTypes, s.initConstants.CMPTarExcludedGlobs) + appSourceType, err := GetAppSourceType(ctx, s.discoverer, q.Source, opContext.appPath, repoRoot, q.AppName, q.EnabledSourceTypes, s.initConstants.CMPTarExcludedGlobs) if err != nil { return err } @@ -1980,7 +1974,7 @@ func (s *Service) GetAppDetails(ctx context.Context, q *apiclient.RepoServerAppD return err } case v1alpha1.ApplicationSourceTypePlugin: - if err := populatePluginAppDetails(ctx, res, opContext.appPath, repoRoot, q, s.gitCredsStore, s.initConstants.CMPTarExcludedGlobs); err != nil { + if err := populatePluginAppDetails(ctx, s.discoverer, res, opContext.appPath, repoRoot, q, s.gitCredsStore, s.initConstants.CMPTarExcludedGlobs); err != nil { return fmt.Errorf("failed to populate plugin app details: %w", err) } } @@ -2139,7 +2133,7 @@ func populateKustomizeAppDetails(res *apiclient.RepoAppDetailsResponse, q *apicl return nil } -func populatePluginAppDetails(ctx context.Context, res *apiclient.RepoAppDetailsResponse, appPath string, repoPath string, q *apiclient.RepoServerAppDetailsQuery, store git.CredsStore, tarExcludedGlobs []string) error { +func populatePluginAppDetails(ctx context.Context, discoverer *discovery.Discoverer, res *apiclient.RepoAppDetailsResponse, appPath string, repoPath string, q *apiclient.RepoServerAppDetailsQuery, store git.CredsStore, tarExcludedGlobs []string) error { res.Plugin = &apiclient.PluginAppSpec{} envVars := []string{ @@ -2159,7 +2153,7 @@ func populatePluginAppDetails(ctx context.Context, res *apiclient.RepoAppDetails pluginName = q.Source.Plugin.Name } // detect config management plugin server (sidecar) - conn, cmpClient, err := discovery.DetectConfigManagementPlugin(ctx, appPath, repoPath, pluginName, env, tarExcludedGlobs) + conn, cmpClient, err := discovery.DetectConfigManagementPlugin(ctx, discoverer, appPath, repoPath, pluginName, env, tarExcludedGlobs) if err != nil { return fmt.Errorf("failed to detect CMP for app: %w", err) } diff --git a/reposerver/repository/repository.proto b/reposerver/repository/repository.proto index de061122e2586..a35b1eefa1119 100644 --- a/reposerver/repository/repository.proto +++ b/reposerver/repository/repository.proto @@ -283,8 +283,8 @@ service RepoServerService { rpc ListApps(ListAppsRequest) returns (AppList) { } - // ListPlugins returns a list of cmp v2 plugins running as sidecar to reposerver - rpc ListPlugins(google.protobuf.Empty) returns (PluginList) { + // ListPlugins returns a list of cmp v2 plugins running as sidecar or services to reposerver + rpc ListPlugins(google.protobuf.Empty) returns (PluginList) { } // Generate manifest for application in specified repo name and revision diff --git a/reposerver/repository/repository_test.go b/reposerver/repository/repository_test.go index 3f2f74c4e5ae0..0b33673e2d9e5 100644 --- a/reposerver/repository/repository_test.go +++ b/reposerver/repository/repository_test.go @@ -34,6 +34,7 @@ import ( argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/argoproj/argo-cd/v2/reposerver/apiclient" "github.com/argoproj/argo-cd/v2/reposerver/cache" + "github.com/argoproj/argo-cd/v2/util/app/discovery" repositorymocks "github.com/argoproj/argo-cd/v2/reposerver/cache/mocks" "github.com/argoproj/argo-cd/v2/reposerver/metrics" fileutil "github.com/argoproj/argo-cd/v2/test/fixture/path" @@ -199,7 +200,7 @@ func TestGenerateYamlManifestInDir(t *testing.T) { } // update this value if we add/remove manifests - const countOfManifests = 48 + const countOfManifests = 50 res1, err := service.GenerateManifest(context.Background(), &q) @@ -207,7 +208,7 @@ func TestGenerateYamlManifestInDir(t *testing.T) { assert.Equal(t, countOfManifests, len(res1.Manifests)) // this will test concatenated manifests to verify we split YAMLs correctly - res2, err := GenerateManifests(context.Background(), "./testdata/concatenated", "/", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil) + res2, err := GenerateManifests(context.Background(), discovery.NewNoServices(), "./testdata/concatenated", "/", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil) assert.NoError(t, err) assert.Equal(t, 3, len(res2.Manifests)) } @@ -264,7 +265,7 @@ func Test_GenerateManifests_NoOutOfBoundsAccess(t *testing.T) { q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &argoappv1.ApplicationSource{}, ProjectName: "something", ProjectSourceRepos: []string{"*"}} - res, err := GenerateManifests(context.Background(), repoDir, "", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil) + res, err := GenerateManifests(context.Background(), discovery.NewNoServices(), repoDir, "", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil) require.Error(t, err) assert.NotContains(t, err.Error(), mustNotContain) assert.Contains(t, err.Error(), "illegal filepath") @@ -280,7 +281,7 @@ func TestGenerateManifests_MissingSymlinkDestination(t *testing.T) { q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &argoappv1.ApplicationSource{}, ProjectName: "something", ProjectSourceRepos: []string{"*"}} - _, err = GenerateManifests(context.Background(), repoDir, "", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil) + _, err = GenerateManifests(context.Background(), discovery.NewNoServices(), repoDir, "", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil) require.NoError(t, err) } @@ -1448,15 +1449,15 @@ func TestGenerateNullList(t *testing.T) { } func TestIdentifyAppSourceTypeByAppDirWithKustomizations(t *testing.T) { - sourceType, err := GetAppSourceType(context.Background(), &argoappv1.ApplicationSource{}, "./testdata/kustomization_yaml", "./testdata", "testapp", map[string]bool{}, []string{}) + sourceType, err := GetAppSourceType(context.Background(), discovery.NewNoServices(), &argoappv1.ApplicationSource{}, "./testdata/kustomization_yaml", "./testdata", "testapp", map[string]bool{}, []string{}) assert.Nil(t, err) assert.Equal(t, argoappv1.ApplicationSourceTypeKustomize, sourceType) - sourceType, err = GetAppSourceType(context.Background(), &argoappv1.ApplicationSource{}, "./testdata/kustomization_yml", "./testdata", "testapp", map[string]bool{}, []string{}) + sourceType, err = GetAppSourceType(context.Background(), discovery.NewNoServices(), &argoappv1.ApplicationSource{}, "./testdata/kustomization_yml", "./testdata", "testapp", map[string]bool{}, []string{}) assert.Nil(t, err) assert.Equal(t, argoappv1.ApplicationSourceTypeKustomize, sourceType) - sourceType, err = GetAppSourceType(context.Background(), &argoappv1.ApplicationSource{}, "./testdata/Kustomization", "./testdata", "testapp", map[string]bool{}, []string{}) + sourceType, err = GetAppSourceType(context.Background(), discovery.NewNoServices(), &argoappv1.ApplicationSource{}, "./testdata/Kustomization", "./testdata", "testapp", map[string]bool{}, []string{}) assert.Nil(t, err) assert.Equal(t, argoappv1.ApplicationSourceTypeKustomize, sourceType) } @@ -1468,7 +1469,7 @@ func TestGenerateFromUTF16(t *testing.T) { ProjectName: "something", ProjectSourceRepos: []string{"*"}, } - res1, err := GenerateManifests(context.Background(), "./testdata/utf-16", "/", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil) + res1, err := GenerateManifests(context.Background(), discovery.NewNoServices(), "./testdata/utf-16", "/", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil) assert.Nil(t, err) assert.Equal(t, 2, len(res1.Manifests)) } diff --git a/server/settings/settings.go b/server/settings/settings.go index 32f5016419b4b..e4022076ceb10 100644 --- a/server/settings/settings.go +++ b/server/settings/settings.go @@ -163,7 +163,7 @@ func (s *Server) plugins(ctx context.Context) ([]*settingspkg.Plugin, error) { pluginList, err := client.ListPlugins(ctx, &empty.Empty{}) if err != nil { - return nil, fmt.Errorf("failed to list sidecar plugins from reposerver: %w", err) + return nil, fmt.Errorf("failed to list sidecar and service plugins from reposerver: %w", err) } var out []*settingspkg.Plugin diff --git a/util/app/discovery/discovery.go b/util/app/discovery/discovery.go index 21fbe5fd4bf36..d31f7e35a37b8 100644 --- a/util/app/discovery/discovery.go +++ b/util/app/discovery/discovery.go @@ -7,8 +7,6 @@ import ( "path/filepath" "strings" - "github.com/argoproj/argo-cd/v2/util/io/files" - grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry" log "github.com/sirupsen/logrus" @@ -20,6 +18,39 @@ import ( "github.com/argoproj/argo-cd/v2/util/kustomize" ) +type Discoverer struct { + plugins *plugins +} + +// NewWithServices provides a fully functioning discoverer, which is +// expected to be long lived as it run an informer watching for cmp +// plugins as services +func NewWithServices() *Discoverer { + return &Discoverer{ + plugins: newPluginService(), + } +} + +// NewNoServices provides a discoverer which will not watch services +// but otherwise works fully. For use in the CLI and testing. +func NewNoServices() *Discoverer { + return &Discoverer{ + plugins: noServicesPluginService(), + } +} + +func GetPluginNames(discoverer *Discoverer) ([]string, error) { + plugins, err := discoverer.plugins.getAllPlugins() + pluginNames := make([]string, len(plugins)) + if err != nil { + return pluginNames, err + } + for i := range plugins { + pluginNames[i] = plugins[i].name + } + return pluginNames, nil +} + func IsManifestGenerationEnabled(sourceType v1alpha1.ApplicationSourceType, enableGenerateManifests map[string]bool) bool { if enableGenerateManifests == nil { return true @@ -31,11 +62,11 @@ func IsManifestGenerationEnabled(sourceType v1alpha1.ApplicationSourceType, enab return enabled } -func Discover(ctx context.Context, appPath, repoPath string, enableGenerateManifests map[string]bool, tarExcludedGlobs []string) (map[string]string, error) { +func Discover(ctx context.Context, discoverer *Discoverer, appPath, repoPath string, enableGenerateManifests map[string]bool, tarExcludedGlobs []string) (map[string]string, error) { apps := make(map[string]string) - // Check if it is CMP - conn, _, err := DetectConfigManagementPlugin(ctx, appPath, repoPath, "", []string{}, tarExcludedGlobs) + // Check if it is CMP. When running from the CLI this won't work, so we don't try. + conn, _, err := DetectConfigManagementPlugin(ctx, discoverer, appPath, repoPath, "", []string{}, tarExcludedGlobs) if err == nil { // Found CMP io.Close(conn) @@ -67,8 +98,8 @@ func Discover(ctx context.Context, appPath, repoPath string, enableGenerateManif return apps, err } -func AppType(ctx context.Context, appPath, repoPath string, enableGenerateManifests map[string]bool, tarExcludedGlobs []string) (string, error) { - apps, err := Discover(ctx, appPath, repoPath, enableGenerateManifests, tarExcludedGlobs) +func AppType(ctx context.Context, discoverer *Discoverer, appPath, repoPath string, enableGenerateManifests map[string]bool, tarExcludedGlobs []string) (string, error) { + apps, err := Discover(ctx, discoverer, appPath, repoPath, enableGenerateManifests, tarExcludedGlobs) if err != nil { return "", err } @@ -85,34 +116,33 @@ func AppType(ctx context.Context, appPath, repoPath string, enableGenerateManife // check cmpSupports() // if supported return conn for the cmp-server -func DetectConfigManagementPlugin(ctx context.Context, appPath, repoPath, pluginName string, env []string, tarExcludedGlobs []string) (io.Closer, pluginclient.ConfigManagementPluginServiceClient, error) { +func DetectConfigManagementPlugin(ctx context.Context, discoverer *Discoverer, appPath, repoPath, pluginName string, env []string, tarExcludedGlobs []string) (io.Closer, pluginclient.ConfigManagementPluginServiceClient, error) { var conn io.Closer var cmpClient pluginclient.ConfigManagementPluginServiceClient var connFound bool - pluginSockFilePath := common.GetPluginSockFilePath() - log.WithFields(log.Fields{ - common.SecurityField: common.SecurityLow, - common.SecurityCWEField: common.SecurityCWEMissingReleaseOfFileDescriptor, - }).Debugf("pluginSockFilePath is: %s", pluginSockFilePath) - if pluginName != "" { // check if the given plugin supports the repo - conn, cmpClient, connFound = cmpSupports(ctx, pluginSockFilePath, appPath, repoPath, fmt.Sprintf("%v.sock", pluginName), env, tarExcludedGlobs, true) - if !connFound { + plugin, err := discoverer.plugins.getPluginByName(pluginName) + if plugin == nil { return nil, nil, fmt.Errorf("couldn't find cmp-server plugin with name %q supporting the given repository", pluginName) } + if err != nil { + return nil, nil, fmt.Errorf("failed to find cmp-server plugin with name %q, error=%w", pluginName, err) + } + conn, cmpClient, connFound = cmpSupports(ctx, appPath, repoPath, plugin, env, tarExcludedGlobs, true) + if !connFound { + return nil, nil, fmt.Errorf("named cmp service %q not reachable", pluginName) + } } else { - fileList, err := os.ReadDir(pluginSockFilePath) + plugins, err := discoverer.plugins.getAllPlugins() if err != nil { - return nil, nil, fmt.Errorf("Failed to list all plugins in dir, error=%w", err) + return nil, nil, fmt.Errorf("failed to list all plugins, error=%w", err) } - for _, file := range fileList { - if file.Type() == os.ModeSocket { - conn, cmpClient, connFound = cmpSupports(ctx, pluginSockFilePath, appPath, repoPath, file.Name(), env, tarExcludedGlobs, false) - if connFound { - break - } + for _, plugin := range plugins { + conn, cmpClient, connFound = cmpSupports(ctx, appPath, repoPath, plugin, env, tarExcludedGlobs, false) + if connFound { + break } } if !connFound { @@ -142,26 +172,15 @@ func matchRepositoryCMP(ctx context.Context, appPath, repoPath string, client pl return resp.GetIsSupported(), resp.GetIsDiscoveryEnabled(), nil } -func cmpSupports(ctx context.Context, pluginSockFilePath, appPath, repoPath, fileName string, env []string, tarExcludedGlobs []string, namedPlugin bool) (io.Closer, pluginclient.ConfigManagementPluginServiceClient, bool) { - absPluginSockFilePath, err := filepath.Abs(pluginSockFilePath) - if err != nil { - log.Errorf("error getting absolute path for plugin socket dir %v, %v", pluginSockFilePath, err) - return nil, nil, false - } - address := filepath.Join(absPluginSockFilePath, fileName) - if !files.Inbound(address, absPluginSockFilePath) { - log.Errorf("invalid socket file path, %v is outside plugin socket dir %v", fileName, pluginSockFilePath) - return nil, nil, false - } - - cmpclientset := pluginclient.NewConfigManagementPluginClientSet(address) +func cmpSupports(ctx context.Context, appPath, repoPath string, plugin *plugin, env []string, tarExcludedGlobs []string, namedPlugin bool) (io.Closer, pluginclient.ConfigManagementPluginServiceClient, bool) { + cmpclientset := pluginclient.NewConfigManagementPluginClientSet(plugin.address, plugin.pluginType.clientSetType()) conn, cmpClient, err := cmpclientset.NewConfigManagementPluginClient() if err != nil { log.WithFields(log.Fields{ common.SecurityField: common.SecurityMedium, common.SecurityCWEField: common.SecurityCWEMissingReleaseOfFileDescriptor, - }).Errorf("error dialing to cmp-server for plugin %s, %v", fileName, err) + }).Errorf("error dialing to cmp-server for plugin %s, %v", plugin.address, err) return nil, nil, false } @@ -183,7 +202,7 @@ func cmpSupports(ctx context.Context, pluginSockFilePath, appPath, repoPath, fil log.WithFields(log.Fields{ common.SecurityField: common.SecurityLow, common.SecurityCWEField: common.SecurityCWEMissingReleaseOfFileDescriptor, - }).Debugf("Reponse from socket file %s does not support %v", fileName, repoPath) + }).Debugf("Reponse from socket file %s does not support %v", plugin.address, repoPath) io.Close(conn) return nil, nil, false } diff --git a/util/app/discovery/discovery_test.go b/util/app/discovery/discovery_test.go index 54eb30aff4fd1..c7f088935a81f 100644 --- a/util/app/discovery/discovery_test.go +++ b/util/app/discovery/discovery_test.go @@ -10,7 +10,7 @@ import ( ) func TestDiscover(t *testing.T) { - apps, err := Discover(context.Background(), "./testdata", "./testdata", map[string]bool{}, []string{}) + apps, err := Discover(context.Background(), NewNoServices(), "./testdata", "./testdata", map[string]bool{}, []string{}) assert.NoError(t, err) assert.Equal(t, map[string]string{ "foo": "Kustomize", @@ -19,15 +19,15 @@ func TestDiscover(t *testing.T) { } func TestAppType(t *testing.T) { - appType, err := AppType(context.Background(), "./testdata/foo", "./testdata", map[string]bool{}, []string{}) + appType, err := AppType(context.Background(), NewNoServices(), "./testdata/foo", "./testdata", map[string]bool{}, []string{}) assert.NoError(t, err) assert.Equal(t, "Kustomize", appType) - appType, err = AppType(context.Background(), "./testdata/baz", "./testdata", map[string]bool{}, []string{}) + appType, err = AppType(context.Background(), NewNoServices(), "./testdata/baz", "./testdata", map[string]bool{}, []string{}) assert.NoError(t, err) assert.Equal(t, "Helm", appType) - appType, err = AppType(context.Background(), "./testdata", "./testdata", map[string]bool{}, []string{}) + appType, err = AppType(context.Background(), NewNoServices(), "./testdata", "./testdata", map[string]bool{}, []string{}) assert.NoError(t, err) assert.Equal(t, "Directory", appType) } @@ -37,15 +37,15 @@ func TestAppType_Disabled(t *testing.T) { string(v1alpha1.ApplicationSourceTypeKustomize): false, string(v1alpha1.ApplicationSourceTypeHelm): false, } - appType, err := AppType(context.Background(), "./testdata/foo", "./testdata", enableManifestGeneration, []string{}) + appType, err := AppType(context.Background(), NewNoServices(), "./testdata/foo", "./testdata", enableManifestGeneration, []string{}) assert.NoError(t, err) assert.Equal(t, "Directory", appType) - appType, err = AppType(context.Background(), "./testdata/baz", "./testdata", enableManifestGeneration, []string{}) + appType, err = AppType(context.Background(), NewNoServices(), "./testdata/baz", "./testdata", enableManifestGeneration, []string{}) assert.NoError(t, err) assert.Equal(t, "Directory", appType) - appType, err = AppType(context.Background(), "./testdata", "./testdata", enableManifestGeneration, []string{}) + appType, err = AppType(context.Background(), NewNoServices(), "./testdata", "./testdata", enableManifestGeneration, []string{}) assert.NoError(t, err) assert.Equal(t, "Directory", appType) } diff --git a/util/app/discovery/plugin.go b/util/app/discovery/plugin.go new file mode 100644 index 0000000000000..676552dc696f4 --- /dev/null +++ b/util/app/discovery/plugin.go @@ -0,0 +1,136 @@ +package discovery + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" + + pluginclient "github.com/argoproj/argo-cd/v2/cmpserver/apiclient" + "github.com/argoproj/argo-cd/v2/common" + log "github.com/sirupsen/logrus" + informerscorev1 "k8s.io/client-go/informers/core/v1" + "k8s.io/client-go/kubernetes" + "sigs.k8s.io/controller-runtime/pkg/client/config" +) + +type pluginType int + +const ( + sidecar pluginType = iota + service +) + +type plugins struct { + servicePlugins []*plugin + serviceMutex sync.RWMutex + informer *informerscorev1.ServiceInformer +} + +type plugin struct { + name string + pluginType pluginType + address string + owner string +} + +func (p *pluginType) clientSetType() pluginclient.ClientType { + switch *p { + case sidecar: + return pluginclient.Sidecar + case service: + return pluginclient.Service + default: + log.Debugf("Unexpected pluginType %d", *p) + return pluginclient.Sidecar + } +} + +func kubernetesClient() (*kubernetes.Clientset, error) { + cfg, err := config.GetConfig() + if err != nil { + return nil, err + } + return kubernetes.NewForConfig(cfg) +} + +func newPluginService() *plugins { + ps := plugins{ + servicePlugins: make([]*plugin, 0), + } + c, err := kubernetesClient() + if err == nil { + go ps.serviceWatcher(c) + } else { + // This is fine if this is what the user wants + log.Warnf("Unable to uses plugins from services, ensure service account token is mounted (%s)", err) + } + return &ps +} + +func noServicesPluginService() *plugins { + return &plugins{ + servicePlugins: make([]*plugin, 0), + } +} + +func (p *plugins) getServicePlugins() ([]*plugin, error) { + return p.servicePlugins, nil +} + +func (_ *plugins) getSidecarPlugins() ([]*plugin, error) { + plugins := make([]*plugin, 0) + pluginSockFilePath := common.GetPluginSockFilePath() + log.WithFields(log.Fields{ + common.SecurityField: common.SecurityLow, + common.SecurityCWEField: common.SecurityCWEMissingReleaseOfFileDescriptor, + }).Debugf("pluginSockFilePath is: %s", pluginSockFilePath) + + fileList, err := os.ReadDir(pluginSockFilePath) + if err != nil { + return nil, fmt.Errorf("Failed to list all plugins in dir, error=%w", err) + } + for _, file := range fileList { + if file.Type() == os.ModeSocket { + name, foundSock := strings.CutSuffix(file.Name(), `.sock`) + if foundSock { + plugins = append(plugins, &plugin{ + name: name, + pluginType: sidecar, + address: filepath.Join(pluginSockFilePath, file.Name()), + owner: file.Name(), + }) + } + } + } + return plugins, nil +} + +func (p *plugins) getAllPlugins() ([]*plugin, error) { + p.serviceMutex.RLock() + defer p.serviceMutex.RUnlock() + servicePlugins, err := p.getServicePlugins() + if err != nil { + return nil, err + } + sidecarPlugins, err := p.getSidecarPlugins() + if err != nil { + return nil, err + } + return append(sidecarPlugins, servicePlugins...), nil +} + +// Gets a plugin by name or returns nil if no such plugin +func (p *plugins) getPluginByName(name string) (*plugin, error) { + plugins, err := p.getAllPlugins() + if err != nil { + return nil, err + } + for _, plugin := range plugins { + if name == plugin.name { + return plugin, nil + } + } + return nil, nil +} diff --git a/util/app/discovery/services.go b/util/app/discovery/services.go new file mode 100644 index 0000000000000..64773afcd4561 --- /dev/null +++ b/util/app/discovery/services.go @@ -0,0 +1,121 @@ +package discovery + +import ( + "context" + "fmt" + "os" + "os/signal" + "slices" + "time" + + log "github.com/sirupsen/logrus" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/clientcmd" +) + +func (s *plugins) namespace() string { + ns := os.Getenv("ARGOCD_SERVICE_PLUGINS_NAMESPACE") + + switch ns { + case `*`: + return corev1.NamespaceAll + case "": + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + configOverrides := &clientcmd.ConfigOverrides{} + kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) + namespace, _, err := kubeConfig.Namespace() + if err != nil { + log.Warnf("Error getting default namespace %s. Got %s", err, namespace) + } + return namespace + default: + return ns + } +} + +func (s *plugins) serviceWatcher(c *kubernetes.Clientset) { + // labelOptions := informers.WithTweakListOptions() + factory := informers.NewFilteredSharedInformerFactory(c, 10*time.Minute, s.namespace(), + func(opts *metav1.ListOptions) { + opts.LabelSelector = "argocd.argoproj.io/plugin=true" + }) + + informer := factory.Core().V1().Services() + s.informer = &informer + + _, err := informer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: s.svcAdd, + UpdateFunc: s.svcUpdate, + DeleteFunc: s.svcDelete, + }, + ) + if err != nil { + log.Errorf("Failed to initialise plugin services watcher, plugins as services will not work: %s", err) + return + } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + informer.Informer().Run(ctx.Done()) +} + +func (s *plugins) svcAdd(obj interface{}) { + svc := obj.(*corev1.Service) + log.Infof("Adding plugin service %s", namespacedName(svc)) + s.serviceMutex.Lock() + defer s.serviceMutex.Unlock() + s.addFromService(svc) +} + +func (s *plugins) svcDelete(obj interface{}) { + svc := obj.(*corev1.Service) + log.Infof("Deleting plugin service %s", namespacedName(svc)) + s.serviceMutex.Lock() + defer s.serviceMutex.Unlock() + s.deleteByOwner(namespacedName(svc).String()) +} + +func (s *plugins) svcUpdate(_ interface{}, new interface{}) { + svc := new.(*corev1.Service) + log.Infof("Updating plugin service %s", namespacedName(svc)) + s.serviceMutex.Lock() + defer s.serviceMutex.Unlock() + // Simple delete all and add them all again logic + s.deleteByOwner(namespacedName(svc).String()) + s.addFromService(svc) +} + +func namespacedName(svc *corev1.Service) types.NamespacedName { + return types.NamespacedName{Name: svc.ObjectMeta.Name, Namespace: svc.ObjectMeta.Namespace} +} + +func (s *plugins) deleteByOwner(owner string) { + // You must have the rw lock to call this + for i, svc := range s.servicePlugins { + if svc.owner == owner { + s.servicePlugins = slices.Delete( + s.servicePlugins, i, i+1) + } + } +} + +func (s *plugins) addFromService(svc *corev1.Service) { + // You must have the rw lock to call this + for _, port := range svc.Spec.Ports { + s.servicePlugins = append(s.servicePlugins, &plugin{ + name: port.Name, + pluginType: service, + address: fmt.Sprintf("%s.%s.svc.cluster.local:%d", + svc.ObjectMeta.Name, svc.ObjectMeta.Namespace, port.Port), + owner: namespacedName(svc).String(), + }) + } +}