Skip to content

Commit

Permalink
Enhanced/improved provider configuration options and documentation (#268
Browse files Browse the repository at this point in the history
)

* fix(provider): implement `kubernetes.config_context` options

These were available on the provider configuration but not actually used,

* fix(provider): remove unused `kubernetes` configuration options

These cannot be used by the provider so there's no point in having them here.

* fix(provider): mark `server_addr` as optional

This does not need to be supplied when port forwarding or using local config.

* feat(provider): add `core` to provider configuration

* fix(provider): fix description of `context` option

* fix(provider): improve descriptions for port forwarding configuration

* docs(provider): improve overview and add examples for common provider configuration

* docs: improve wording on port forwarding example

Co-authored-by: Olivier Boukili <boukili.olivier@gmail.com>

* chore: drop `ARGOCD_CONTEXT` var from local test env vars

---------

Co-authored-by: Olivier Boukili <boukili.olivier@gmail.com>
  • Loading branch information
onematchfox and oboukili committed May 6, 2023
1 parent 8e40130 commit 89a6cff
Show file tree
Hide file tree
Showing 8 changed files with 551 additions and 44 deletions.
1 change: 0 additions & 1 deletion GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ ARGOCD_INSECURE?=true
ARGOCD_SERVER?=127.0.0.1:8080
ARGOCD_AUTH_USERNAME?=admin
ARGOCD_AUTH_PASSWORD?=acceptancetesting
ARGOCD_CONTEXT?=kind-argocd
ARGOCD_VERSION?=v2.6.7

export
Expand Down
128 changes: 98 additions & 30 deletions argocd/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import (
"bytes"
"context"
"fmt"
"log"
"net/url"
"sync"

"github.com/argoproj/argo-cd/v2/cmd/argocd/commands/headless"
"github.com/argoproj/argo-cd/v2/pkg/apiclient"
"github.com/argoproj/argo-cd/v2/pkg/apiclient/session"
"github.com/argoproj/argo-cd/v2/util/io"
Expand All @@ -17,6 +17,7 @@ import (
"k8s.io/client-go/tools/clientcmd"

apimachineryschema "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/runtime"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"

// Import to initialize client auth plugins.
Expand All @@ -32,25 +33,34 @@ var tokenMutexClusters = &sync.RWMutex{}
// Used to handle concurrent access to each ArgoCD project
var tokenMutexProjectMap = make(map[string]*sync.RWMutex, 0)

var runtimeErrorHandlers []func(error)

func Provider() *schema.Provider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"server_addr": {
Type: schema.TypeString,
Required: true,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("ARGOCD_SERVER", nil),
Description: "ArgoCD server address with port. Can be set through the `ARGOCD_SERVER` environment variable.",
AtLeastOneOf: []string{
"core",
"port_forward",
"port_forward_with_namespace",
"use_local_config",
},
},
"auth_token": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("ARGOCD_AUTH_TOKEN", nil),
Description: "ArgoCD authentication token, takes precedence over `username`/`password`. Can be set through the `ARGOCD_AUTH_TOKEN` environment variable.",
ConflictsWith: []string{
"username",
"config_path",
"core",
"password",
"use_local_config",
"config_path",
"username",
},
},
"username": {
Expand All @@ -60,10 +70,12 @@ func Provider() *schema.Provider {
Description: "Authentication username. Can be set through the `ARGOCD_AUTH_USERNAME` environment variable.",
ConflictsWith: []string{
"auth_token",
"use_local_config",
"config_path",
"core",
"use_local_config",
},
AtLeastOneOf: []string{
"core",
"password",
"auth_token",
"use_local_config",
Expand All @@ -76,11 +88,13 @@ func Provider() *schema.Provider {
Description: "Authentication password. Can be set through the `ARGOCD_AUTH_PASSWORD` environment variable.",
ConflictsWith: []string{
"auth_token",
"use_local_config",
"config_path",
"core",
"use_local_config",
},
AtLeastOneOf: []string{
"username",
"core",
"auth_token",
"use_local_config",
},
Expand Down Expand Up @@ -110,12 +124,36 @@ func Provider() *schema.Provider {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("ARGOCD_CONTEXT", nil),
Description: "Kubernetes context to load from an existing `.kube/config` file. Can be set through `ARGOCD_CONTEXT` environment variable.",
Description: "Context to choose when using a local ArgoCD config file. Only relevant when `use_local_config`. Can be set through `ARGOCD_CONTEXT` environment variable.",
ConflictsWith: []string{
"core",
"username",
"password",
"auth_token",
},
},
"user_agent": {
Type: schema.TypeString,
Optional: true,
},
"core": {
Type: schema.TypeBool,
Optional: true,
Description: "Configure direct access using Kubernetes API server.\n\n " +
"**Warning**: this feature works by starting a local ArgoCD API server that talks directly to the Kubernetes API using the **current context " +
"in the default kubeconfig** (`~/.kube/config`). This behavior cannot be overridden using either environment variables or the `kubernetes` block " +
"in the provider configuration at present).\n\n If the server fails to start (e.g. your kubeconfig is misconfigured) then the provider will " +
"fail as a result of the `argocd` module forcing it to exit and no logs will be available to help you debug this. The error message will be " +
"similar to\n > `The plugin encountered an error, and failed to respond to the plugin.(*GRPCProvider).ReadResource call. The plugin logs may " +
"contain more details.`\n\n To debug this, you will need to login via the ArgoCD CLI using `argocd login --core` and then running an operation. " +
"E.g. `argocd app list`.",
ConflictsWith: []string{
"auth_token",
"use_local_config",
"password",
"username",
},
},
"grpc_web": {
Type: schema.TypeBool,
Optional: true,
Expand All @@ -131,9 +169,10 @@ func Provider() *schema.Provider {
Optional: true,
Description: "Use the authentication settings found in the local config file. Useful when you have previously logged in using SSO. Conflicts with `auth_token`, `username` and `password`.",
ConflictsWith: []string{
"username",
"password",
"auth_token",
"core",
"password",
"username",
},
},
"config_path": {
Expand All @@ -142,18 +181,21 @@ func Provider() *schema.Provider {
DefaultFunc: schema.EnvDefaultFunc("ARGOCD_CONFIG_PATH", nil),
Description: "Override the default config path of `$HOME/.config/argocd/config`. Only relevant when `use_local_config`. Can be set through the `ARGOCD_CONFIG_PATH` environment variable.",
ConflictsWith: []string{
"username",
"password",
"auth_token",
"core",
"password",
"username",
},
},
"port_forward": {
Type: schema.TypeBool,
Optional: true,
Type: schema.TypeBool,
Description: "Connect to a random argocd-server port using port forwarding.",
Optional: true,
},
"port_forward_with_namespace": {
Type: schema.TypeString,
Optional: true,
Type: schema.TypeString,
Description: "Namespace name which should be used for port forwarding.",
Optional: true,
},
"headers": {
Type: schema.TypeSet,
Expand All @@ -171,7 +213,7 @@ func Provider() *schema.Provider {
Type: schema.TypeList,
MaxItems: 1,
Optional: true,
Description: "Kubernetes configuration.",
Description: "Kubernetes configuration overrides. Only relevant when `port_forward = true` or `port_forward_with_namespace = \"foo\"`. The kubeconfig file that is used can be overridden using the [`KUBECONFIG` environment variable](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#the-kubeconfig-environment-variable)).",
Elem: kubernetesResource(),
},
},
Expand Down Expand Up @@ -267,6 +309,7 @@ func initApiClient(ctx context.Context, d *schema.ResourceData) (apiClient apicl

if _, ok := d.GetOk("kubernetes"); ok {
opts.KubeOverrides = &clientcmd.ConfigOverrides{}

if v, ok := k8sGetOk(d, "insecure"); ok {
opts.KubeOverrides.ClusterInfo.InsecureSkipTLSVerify = v.(bool)
}
Expand All @@ -279,6 +322,25 @@ func initApiClient(ctx context.Context, d *schema.ResourceData) (apiClient apicl
opts.KubeOverrides.AuthInfo.ClientCertificateData = bytes.NewBufferString(v.(string)).Bytes()
}

kubectx, ctxOk := k8sGetOk(d, "config_context")
authInfo, authInfoOk := k8sGetOk(d, "config_context_auth_info")
cluster, clusterOk := k8sGetOk(d, "config_context_cluster")

if ctxOk || authInfoOk || clusterOk {
if ctxOk {
opts.KubeOverrides.CurrentContext = kubectx.(string)
}

opts.KubeOverrides.Context = clientcmdapi.Context{}
if authInfoOk {
opts.KubeOverrides.Context.AuthInfo = authInfo.(string)
}

if clusterOk {
opts.KubeOverrides.Context.Cluster = cluster.(string)
}
}

if v, ok := k8sGetOk(d, "host"); ok {
// Server has to be the complete address of the kubernetes cluster (scheme://hostname:port), not just the hostname,
// because `overrides` are processed too late to be taken into account by `defaultServerUrlFor()`.
Expand Down Expand Up @@ -326,7 +388,6 @@ func initApiClient(ctx context.Context, d *schema.ResourceData) (apiClient apicl
exec.Env = append(exec.Env, clientcmdapi.ExecEnvVar{Name: kk, Value: vv.(string)})
}
} else {
log.Printf("[ERROR] Failed to parse exec")
return nil, fmt.Errorf("failed to parse exec")
}

Expand Down Expand Up @@ -369,6 +430,26 @@ func initApiClient(ctx context.Context, d *schema.ResourceData) (apiClient apicl
}
}

if v, ok := d.Get("core").(bool); ok && v {
opts.ServerAddr = "kubernetes"
opts.Core = true

// HACK: `headless.StartLocalServer` manipulates this global variable
// when starting the local server without checking it's length/contents
// which leads to a panic if called multiple times. So, we need to
// ensure we "reset" it before calling the method.
if runtimeErrorHandlers == nil {
runtimeErrorHandlers = runtime.ErrorHandlers
} else {
runtime.ErrorHandlers = runtimeErrorHandlers
}

err := headless.StartLocalServer(ctx, &opts, "", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to start local server: %w", err)
}
}

return apiclient.NewClient(&opts)
}

Expand Down Expand Up @@ -417,19 +498,6 @@ func kubernetesResource() *schema.Resource {
DefaultFunc: schema.EnvDefaultFunc("KUBE_CLUSTER_CA_CERT_DATA", ""),
Description: "PEM-encoded root certificates bundle for TLS authentication. Can be sourced from `KUBE_CLUSTER_CA_CERT_DATA`.",
},
"config_paths": {
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
Description: "A list of paths to the kube config files. Can be sourced from `KUBE_CONFIG_PATHS`.",
},
"config_path": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("KUBE_CONFIG_PATH", nil),
Description: "Path to the kube config file. Can be sourced from `KUBE_CONFIG_PATH`.",
ConflictsWith: []string{"kubernetes.0.config_paths"},
},
"config_context": {
Type: schema.TypeString,
Optional: true,
Expand Down
89 changes: 80 additions & 9 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,82 @@ The ArgoCD Provider provides lifecycle management of

**NB**: The provider is not concerned with the installation/configuration of
ArgoCD itself. To make use of the provider, you will need to have an existing
ArgoCD installation and, the ArgoCD API server must be
[accessible](https://argo-cd.readthedocs.io/en/stable/getting_started/#3-access-the-argo-cd-api-server)
from where you are running Terraform.
ArgoCD installation.

The correct provider configuration largely depends on whether or not your
ArgoCD API server is exposed or not.

If your ArgoCD API server is exposed, then:
- use `server_addr` along with a `username`/`password` or `auth_token`.
- use `use_local_config` if you have (pre)authenticated via the ArgoCD CLI (E.g.
via SSO using `argocd login --sso`.

If you have not exposed your ArgoCD API server or have not deployed the API
server ([ArgoCD
core](https://argo-cd.readthedocs.io/en/stable/operator-manual/installation/#core)),
see below for options. **Note**: in both these cases, you need sufficient access
to the Kubernetes API to perform any actions.
- use `port_forward_with_namespace` and optionally `kubernetes` configuration
(to temporarily expose the ArgoCD API server using port forwarding) along with
a `username`/`password` or `auth_token`.
- use `core` to run a local ArgoCD API server that communicates directly with
the Kubernetes API. **NB**: When using `core`, take note of the warning in
the docs below.

If you are struggling to determine the correct configuration for the provider or
the provider is behaving strangely and failing to connect for whatever reason,
then we would suggest that you first figure out what combination of parameters
work to log in using the ArgoCD CLI (`argocd login`) and then set the provider
configuration to match what you used in the CLI. See also the ArgoCD [Getting
Started](https://argo-cd.readthedocs.io/en/stable/getting_started/#3-access-the-argo-cd-api-server)
docs.

## Example Usage

```terraform
# Exposed ArgoCD API - authenticated using authentication token.
provider "argocd" {
server_addr = "argocd.local:443"
auth_token = "1234..."
}
# Exposed ArgoCD API - authenticated using `username`/`password`
provider "argocd" {
server_addr = "argocd.local:443"
username = "foo"
password = local.password
}
# Exposed ArgoCD API - (pre)authenticated using local ArgoCD config (e.g. when
# you have previously logged in using SSO).
provider "argocd" {
use_local_config = true
# context = "foo" # Use explicit context from ArgoCD config instead of `current-context`.
}
# Unexposed ArgoCD API - using the current Kubernetes context and
# port-forwarding to temporarily expose ArgoCD API and authenticating using
# `auth_token`.
provider "argocd" {
auth_token = "1234..."
port_forward = true
}
# Unexposed ArgoCD API - using port-forwarding to temporarily expose ArgoCD API
# whilst overriding the current context in kubeconfig.
provider "argocd" {
auth_token = "1234..."
port_forward_with_namespace = "custom-argocd-namespace"
kubernetes {
config_context = "kind-argocd"
}
}
# Unexposed ArgoCD API - using `core` to run ArgoCD server locally and
# communicate directly with the Kubernetes API.
provider "argocd" {
core = true
}
```

<!-- schema generated by tfplugindocs -->
Expand All @@ -34,16 +99,24 @@ provider "argocd" {
- `client_cert_file` (String) Client certificate.
- `client_cert_key` (String) Client certificate key.
- `config_path` (String) Override the default config path of `$HOME/.config/argocd/config`. Only relevant when `use_local_config`. Can be set through the `ARGOCD_CONFIG_PATH` environment variable.
- `context` (String) Kubernetes context to load from an existing `.kube/config` file. Can be set through `ARGOCD_CONTEXT` environment variable.
- `context` (String) Context to choose when using a local ArgoCD config file. Only relevant when `use_local_config`. Can be set through `ARGOCD_CONTEXT` environment variable.
- `core` (Boolean) Configure direct access using Kubernetes API server.

**Warning**: this feature works by starting a local ArgoCD API server that talks directly to the Kubernetes API using the **current context in the default kubeconfig** (`~/.kube/config`). This behavior cannot be overridden using either environment variables or the `kubernetes` block in the provider configuration at present).

If the server fails to start (e.g. your kubeconfig is misconfigured) then the provider will fail as a result of the `argocd` module forcing it to exit and no logs will be available to help you debug this. The error message will be similar to
> `The plugin encountered an error, and failed to respond to the plugin.(*GRPCProvider).ReadResource call. The plugin logs may contain more details.`
To debug this, you will need to login via the ArgoCD CLI using `argocd login --core` and then running an operation. E.g. `argocd app list`.
- `grpc_web` (Boolean) Whether to use gRPC web proxy client. Useful if Argo CD server is behind proxy which does not support HTTP2.
- `grpc_web_root_path` (String) Use the gRPC web proxy client and set the web root, e.g. `argo-cd`. Useful if the Argo CD server is behind a proxy at a non-root path.
- `headers` (Set of String) Additional headers to add to each request to the ArgoCD server.
- `insecure` (Boolean) Whether to skip TLS server certificate. Can be set through the `ARGOCD_INSECURE` environment variable.
- `kubernetes` (Block List, Max: 1) Kubernetes configuration. (see [below for nested schema](#nestedblock--kubernetes))
- `kubernetes` (Block List, Max: 1) Kubernetes configuration overrides. Only relevant when `port_forward = true` or `port_forward_with_namespace = "foo"`. The kubeconfig file that is used can be overridden using the [`KUBECONFIG` environment variable](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#the-kubeconfig-environment-variable)). (see [below for nested schema](#nestedblock--kubernetes))
- `password` (String) Authentication password. Can be set through the `ARGOCD_AUTH_PASSWORD` environment variable.
- `plain_text` (Boolean) Whether to initiate an unencrypted connection to ArgoCD server.
- `port_forward` (Boolean)
- `port_forward_with_namespace` (String)
- `port_forward` (Boolean) Connect to a random argocd-server port using port forwarding.
- `port_forward_with_namespace` (String) Namespace name which should be used for port forwarding.
- `server_addr` (String) ArgoCD server address with port. Can be set through the `ARGOCD_SERVER` environment variable.
- `use_local_config` (Boolean) Use the authentication settings found in the local config file. Useful when you have previously logged in using SSO. Conflicts with `auth_token`, `username` and `password`.
- `user_agent` (String)
Expand All @@ -60,8 +133,6 @@ Optional:
- `config_context` (String) Context to choose from the config file. Can be sourced from `KUBE_CTX`.
- `config_context_auth_info` (String)
- `config_context_cluster` (String)
- `config_path` (String) Path to the kube config file. Can be sourced from `KUBE_CONFIG_PATH`.
- `config_paths` (List of String) A list of paths to the kube config files. Can be sourced from `KUBE_CONFIG_PATHS`.
- `exec` (Block List, Max: 1) Configuration block to use an [exec-based credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins), e.g. call an external command to receive user credentials. (see [below for nested schema](#nestedblock--kubernetes--exec))
- `host` (String) The hostname (in form of URI) of the Kubernetes API. Can be sourced from `KUBE_HOST`.
- `insecure` (Boolean) Whether server should be accessed without verifying the TLS certificate. Can be sourced from `KUBE_INSECURE`.
Expand Down

0 comments on commit 89a6cff

Please sign in to comment.