|
| 1 | +package argocd |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "context" |
| 6 | + "fmt" |
| 7 | + "net/url" |
| 8 | + |
| 9 | + "github.com/argoproj-labs/terraform-provider-argocd/internal/diagnostics" |
| 10 | + "github.com/argoproj/argo-cd/v3/cmd/argocd/commands/headless" |
| 11 | + "github.com/argoproj/argo-cd/v3/pkg/apiclient" |
| 12 | + "github.com/argoproj/argo-cd/v3/pkg/apiclient/session" |
| 13 | + "github.com/argoproj/argo-cd/v3/util/io" |
| 14 | + "github.com/argoproj/argo-cd/v3/util/localconfig" |
| 15 | + "github.com/hashicorp/terraform-plugin-framework/diag" |
| 16 | + "github.com/hashicorp/terraform-plugin-framework/types" |
| 17 | + apimachineryschema "k8s.io/apimachinery/pkg/runtime/schema" |
| 18 | + "k8s.io/apimachinery/pkg/util/runtime" |
| 19 | + "k8s.io/client-go/rest" |
| 20 | + "k8s.io/client-go/tools/clientcmd" |
| 21 | + "k8s.io/client-go/tools/clientcmd/api" |
| 22 | +) |
| 23 | + |
| 24 | +type ArgoCDProviderConfig struct { |
| 25 | + // Configuration for standard login using either with username/password or auth_token |
| 26 | + AuthToken types.String `tfsdk:"auth_token"` |
| 27 | + Username types.String `tfsdk:"username"` |
| 28 | + Password types.String `tfsdk:"password"` |
| 29 | + |
| 30 | + // When using standard login either server address or port forwarding must be used |
| 31 | + ServerAddr types.String `tfsdk:"server_addr"` |
| 32 | + PortForward types.Bool `tfsdk:"port_forward"` |
| 33 | + PortForwardWithNamespace types.String `tfsdk:"port_forward_with_namespace"` |
| 34 | + Kubernetes []Kubernetes `tfsdk:"kubernetes"` |
| 35 | + |
| 36 | + // Run ArgoCD API server locally |
| 37 | + Core types.Bool `tfsdk:"core"` |
| 38 | + |
| 39 | + // Login using credentials from local ArgoCD config file |
| 40 | + UseLocalConfig types.Bool `tfsdk:"use_local_config"` |
| 41 | + ConfigPath types.String `tfsdk:"config_path"` |
| 42 | + Context types.String `tfsdk:"context"` |
| 43 | + |
| 44 | + // Other configuration |
| 45 | + CertFile types.String `tfsdk:"cert_file"` |
| 46 | + ClientCertFile types.String `tfsdk:"client_cert_file"` |
| 47 | + ClientCertKey types.String `tfsdk:"client_cert_key"` |
| 48 | + GRPCWeb types.Bool `tfsdk:"grpc_web"` |
| 49 | + GRPCWebRootPath types.String `tfsdk:"grpc_web_root_path"` |
| 50 | + Headers types.Set `tfsdk:"headers"` |
| 51 | + Insecure types.Bool `tfsdk:"insecure"` |
| 52 | + PlainText types.Bool `tfsdk:"plain_text"` |
| 53 | + UserAgent types.String `tfsdk:"user_agent"` |
| 54 | +} |
| 55 | + |
| 56 | +func (p ArgoCDProviderConfig) getApiClientOptions(ctx context.Context) (*apiclient.ClientOptions, diag.Diagnostics) { |
| 57 | + var diags diag.Diagnostics |
| 58 | + |
| 59 | + opts := &apiclient.ClientOptions{ |
| 60 | + AuthToken: getDefaultString(p.AuthToken, "ARGOCD_AUTH_TOKEN"), |
| 61 | + CertFile: p.CertFile.ValueString(), |
| 62 | + ClientCertFile: p.ClientCertFile.ValueString(), |
| 63 | + ClientCertKeyFile: p.ClientCertKey.ValueString(), |
| 64 | + GRPCWeb: p.GRPCWeb.ValueBool(), |
| 65 | + GRPCWebRootPath: p.GRPCWebRootPath.ValueString(), |
| 66 | + Insecure: getDefaultBool(ctx, p.Insecure, "ARGOCD_INSECURE"), |
| 67 | + PlainText: p.PlainText.ValueBool(), |
| 68 | + PortForward: p.PortForward.ValueBool(), |
| 69 | + PortForwardNamespace: p.PortForwardWithNamespace.ValueString(), |
| 70 | + ServerAddr: getDefaultString(p.ServerAddr, "ARGOCD_SERVER"), |
| 71 | + UserAgent: p.Username.ValueString(), |
| 72 | + } |
| 73 | + |
| 74 | + if !p.Headers.IsNull() { |
| 75 | + var h []string |
| 76 | + |
| 77 | + diags.Append(p.Headers.ElementsAs(ctx, &h, false)...) |
| 78 | + |
| 79 | + opts.Headers = h |
| 80 | + } |
| 81 | + |
| 82 | + coreEnabled, d := p.setCoreOpts(opts) |
| 83 | + |
| 84 | + diags.Append(d...) |
| 85 | + |
| 86 | + localConfigEnabled, d := p.setLocalConfigOpts(opts) |
| 87 | + |
| 88 | + diags.Append(d...) |
| 89 | + |
| 90 | + portForwardingEnabled, d := p.setPortForwardingOpts(ctx, opts) |
| 91 | + |
| 92 | + diags.Append(d...) |
| 93 | + |
| 94 | + username := getDefaultString(p.Username, "ARGOCD_AUTH_USERNAME") |
| 95 | + password := getDefaultString(p.Password, "ARGOCD_AUTH_PASSWORD") |
| 96 | + |
| 97 | + usernameAndPasswordSet := username != "" && password != "" |
| 98 | + |
| 99 | + switch { |
| 100 | + // Provider configuration errors |
| 101 | + case !coreEnabled && !portForwardingEnabled && !localConfigEnabled && opts.ServerAddr == "": |
| 102 | + diags.Append(diagnostics.Error("invalid provider configuration: one of `core,port_forward,port_forward_with_namespace,use_local_config,server_addr` must be specified", nil)...) |
| 103 | + case portForwardingEnabled && opts.AuthToken == "" && !usernameAndPasswordSet: |
| 104 | + diags.Append(diagnostics.Error("invalid provider configuration: either `username/password` or `auth_token` must be specified when port forwarding is enabled", nil)...) |
| 105 | + case opts.ServerAddr != "" && !coreEnabled && opts.AuthToken == "" && !usernameAndPasswordSet: |
| 106 | + diags.Append(diagnostics.Error("invalid provider configuration: either `username/password` or `auth_token` must be specified if `server_addr` is specified", nil)...) |
| 107 | + } |
| 108 | + |
| 109 | + if diags.HasError() { |
| 110 | + return nil, diags |
| 111 | + } |
| 112 | + |
| 113 | + switch { |
| 114 | + // Handle "special" configuration use-cases |
| 115 | + case coreEnabled: |
| 116 | + // HACK: `headless.StartLocalServer` manipulates this global variable |
| 117 | + // when starting the local server without checking it's length/contents |
| 118 | + // which leads to a panic if called multiple times. So, we need to |
| 119 | + // ensure we "reset" it before calling the method. |
| 120 | + if runtimeErrorHandlers == nil { |
| 121 | + runtimeErrorHandlers = runtime.ErrorHandlers |
| 122 | + } else { |
| 123 | + runtime.ErrorHandlers = runtimeErrorHandlers |
| 124 | + } |
| 125 | + |
| 126 | + err := headless.MaybeStartLocalServer(ctx, opts, "", nil, nil, nil) |
| 127 | + if err != nil { |
| 128 | + diags.Append(diagnostics.Error("failed to start local server", err)...) |
| 129 | + return nil, diags |
| 130 | + } |
| 131 | + case opts.ServerAddr != "" && opts.AuthToken == "" && usernameAndPasswordSet: |
| 132 | + apiClient, err := apiclient.NewClient(opts) |
| 133 | + if err != nil { |
| 134 | + diags.Append(diagnostics.Error("failed to create new API client", err)...) |
| 135 | + return nil, diags |
| 136 | + } |
| 137 | + |
| 138 | + closer, sc, err := apiClient.NewSessionClient() |
| 139 | + if err != nil { |
| 140 | + diags.Append(diagnostics.Error("failed to create new session client", err)...) |
| 141 | + return nil, diags |
| 142 | + } |
| 143 | + |
| 144 | + defer io.Close(closer) |
| 145 | + |
| 146 | + sessionOpts := session.SessionCreateRequest{ |
| 147 | + Username: username, |
| 148 | + Password: password, |
| 149 | + } |
| 150 | + |
| 151 | + resp, err := sc.Create(ctx, &sessionOpts) |
| 152 | + if err != nil { |
| 153 | + diags.Append(diagnostics.Error("failed to create new session", err)...) |
| 154 | + return nil, diags |
| 155 | + } |
| 156 | + |
| 157 | + opts.AuthToken = resp.Token |
| 158 | + } |
| 159 | + |
| 160 | + return opts, diags |
| 161 | +} |
| 162 | + |
| 163 | +func (p ArgoCDProviderConfig) setCoreOpts(opts *apiclient.ClientOptions) (bool, diag.Diagnostics) { |
| 164 | + var diags diag.Diagnostics |
| 165 | + |
| 166 | + coreEnabled := p.Core.ValueBool() |
| 167 | + if coreEnabled { |
| 168 | + if opts.ServerAddr != "" { |
| 169 | + diags.AddWarning("`server_addr` is ignored by the provider and overwritten when `core = true`.", "") |
| 170 | + } |
| 171 | + |
| 172 | + opts.ServerAddr = "kubernetes" |
| 173 | + opts.Core = true |
| 174 | + |
| 175 | + if !p.Username.IsNull() { |
| 176 | + diags.AddWarning("`username` is ignored when `core = true`.", "") |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + return coreEnabled, diags |
| 181 | +} |
| 182 | + |
| 183 | +func (p ArgoCDProviderConfig) setLocalConfigOpts(opts *apiclient.ClientOptions) (bool, diag.Diagnostics) { |
| 184 | + var diags diag.Diagnostics |
| 185 | + |
| 186 | + useLocalConfig := p.UseLocalConfig.ValueBool() |
| 187 | + switch useLocalConfig { |
| 188 | + case true: |
| 189 | + if opts.ServerAddr != "" { |
| 190 | + diags.AddWarning("setting `server_addr` alongside `use_local_config = true` is unnecessary and not recommended as this will overwrite the address retrieved from the local ArgoCD context.", "") |
| 191 | + } |
| 192 | + |
| 193 | + if !p.Username.IsNull() { |
| 194 | + diags.AddWarning("`username` is ignored when `use_local_config = true`.", "") |
| 195 | + } |
| 196 | + |
| 197 | + opts.Context = getDefaultString(p.Context, "ARGOCD_CONTEXT") |
| 198 | + |
| 199 | + cp := getDefaultString(p.ConfigPath, "ARGOCD_CONFIG_PATH") |
| 200 | + |
| 201 | + if cp != "" { |
| 202 | + opts.ConfigPath = p.ConfigPath.ValueString() |
| 203 | + break |
| 204 | + } |
| 205 | + |
| 206 | + cp, err := localconfig.DefaultLocalConfigPath() |
| 207 | + if err == nil { |
| 208 | + opts.ConfigPath = cp |
| 209 | + break |
| 210 | + } |
| 211 | + |
| 212 | + diags.Append(diagnostics.Error("failed to find default ArgoCD config path", err)...) |
| 213 | + case false: |
| 214 | + // Log warnings if explicit configuration has been provided for local config when `use_local_config` is not enabled. |
| 215 | + if !p.ConfigPath.IsNull() { |
| 216 | + diags.AddWarning("`config_path` is ignored by provider unless `use_local_config = true`.", "") |
| 217 | + } |
| 218 | + |
| 219 | + if !p.Context.IsNull() { |
| 220 | + diags.AddWarning("`context` is ignored by provider unless `use_local_config = true`.", "") |
| 221 | + } |
| 222 | + } |
| 223 | + |
| 224 | + return useLocalConfig, diags |
| 225 | +} |
| 226 | + |
| 227 | +func (p ArgoCDProviderConfig) setPortForwardingOpts(ctx context.Context, opts *apiclient.ClientOptions) (bool, diag.Diagnostics) { |
| 228 | + var diags diag.Diagnostics |
| 229 | + |
| 230 | + portForwardingEnabled := opts.PortForward || opts.PortForwardNamespace != "" |
| 231 | + switch portForwardingEnabled { |
| 232 | + case true: |
| 233 | + if opts.ServerAddr != "" { |
| 234 | + diags.AddWarning("`server_addr` is ignored by the provider and overwritten when port forwarding is enabled.", "") |
| 235 | + } |
| 236 | + |
| 237 | + opts.ServerAddr = "localhost" // will be overwritten by ArgoCD module when we initialize the API client but needs to be set here to ensure we |
| 238 | + opts.ServerName = "argocd-server" |
| 239 | + |
| 240 | + if opts.PortForwardNamespace == "" { |
| 241 | + opts.PortForwardNamespace = "argocd" |
| 242 | + } |
| 243 | + |
| 244 | + if p.Kubernetes == nil { |
| 245 | + break |
| 246 | + } |
| 247 | + |
| 248 | + k := p.Kubernetes[0] |
| 249 | + opts.KubeOverrides = &clientcmd.ConfigOverrides{ |
| 250 | + AuthInfo: api.AuthInfo{ |
| 251 | + ClientCertificateData: bytes.NewBufferString(getDefaultString(k.ClientCertificate, "KUBE_CLIENT_CERT_DATA")).Bytes(), |
| 252 | + Username: getDefaultString(k.Username, "KUBE_USER"), |
| 253 | + Password: getDefaultString(k.Password, "KUBE_PASSWORD"), |
| 254 | + ClientKeyData: bytes.NewBufferString(getDefaultString(k.ClientKey, "KUBE_CLIENT_KEY_DATA")).Bytes(), |
| 255 | + Token: getDefaultString(k.Token, "KUBE_TOKEN"), |
| 256 | + }, |
| 257 | + ClusterInfo: api.Cluster{ |
| 258 | + InsecureSkipTLSVerify: getDefaultBool(ctx, k.Insecure, "KUBE_INSECURE"), |
| 259 | + CertificateAuthorityData: bytes.NewBufferString(getDefaultString(k.ClusterCACertificate, "KUBE_CLUSTER_CA_CERT_DATA")).Bytes(), |
| 260 | + }, |
| 261 | + CurrentContext: getDefaultString(k.ConfigContext, "KUBE_CTX"), |
| 262 | + Context: api.Context{ |
| 263 | + AuthInfo: getDefaultString(k.ConfigContextAuthInfo, "KUBE_CTX_AUTH_INFO"), |
| 264 | + Cluster: getDefaultString(k.ConfigContextCluster, "KUBE_CTX_CLUSTER"), |
| 265 | + }, |
| 266 | + } |
| 267 | + |
| 268 | + h := getDefaultString(k.Host, "KUBE_HOST") |
| 269 | + if h != "" { |
| 270 | + // Server has to be the complete address of the Kubernetes cluster (scheme://hostname:port), not just the hostname, |
| 271 | + // because `overrides` are processed too late to be taken into account by `defaultServerUrlFor()`. |
| 272 | + // This basically replicates what defaultServerUrlFor() does with config but for overrides, |
| 273 | + // see https://github.com/Kubernetes/client-go/blob/v12.0.0/rest/url_utils.go#L85-L87 |
| 274 | + hasCA := len(opts.KubeOverrides.ClusterInfo.CertificateAuthorityData) != 0 |
| 275 | + hasCert := len(opts.KubeOverrides.AuthInfo.ClientCertificateData) != 0 |
| 276 | + defaultTLS := hasCA || hasCert || opts.KubeOverrides.ClusterInfo.InsecureSkipTLSVerify |
| 277 | + |
| 278 | + var host *url.URL |
| 279 | + |
| 280 | + host, _, err := rest.DefaultServerURL(h, "", apimachineryschema.GroupVersion{}, defaultTLS) |
| 281 | + if err == nil { |
| 282 | + opts.KubeOverrides.ClusterInfo.Server = host.String() |
| 283 | + } else { |
| 284 | + diags.Append(diagnostics.Error(fmt.Sprintf("failed to extract default server URL for host %s", h), err)...) |
| 285 | + } |
| 286 | + } |
| 287 | + |
| 288 | + if k.Exec == nil { |
| 289 | + break |
| 290 | + } |
| 291 | + |
| 292 | + e := k.Exec[0] |
| 293 | + exec := &api.ExecConfig{ |
| 294 | + InteractiveMode: api.IfAvailableExecInteractiveMode, |
| 295 | + APIVersion: e.APIVersion.ValueString(), |
| 296 | + Command: e.Command.ValueString(), |
| 297 | + } |
| 298 | + |
| 299 | + var a []string |
| 300 | + |
| 301 | + diags.Append(e.Args.ElementsAs(ctx, &a, false)...) |
| 302 | + exec.Args = a |
| 303 | + |
| 304 | + var env map[string]string |
| 305 | + |
| 306 | + diags.Append(e.Env.ElementsAs(ctx, &env, false)...) |
| 307 | + |
| 308 | + for k, v := range env { |
| 309 | + exec.Env = append(exec.Env, api.ExecEnvVar{Name: k, Value: v}) |
| 310 | + } |
| 311 | + |
| 312 | + opts.KubeOverrides.AuthInfo.Exec = exec |
| 313 | + case false: |
| 314 | + if p.Kubernetes != nil { |
| 315 | + diags.AddWarning("`Kubernetes` configuration block is ignored by provider unless `port_forward` or `port_forward_with_namespace` are configured.", "") |
| 316 | + } |
| 317 | + } |
| 318 | + |
| 319 | + return portForwardingEnabled, diags |
| 320 | +} |
| 321 | + |
| 322 | +type Kubernetes struct { |
| 323 | + Host types.String `tfsdk:"host"` |
| 324 | + Username types.String `tfsdk:"username"` |
| 325 | + Password types.String `tfsdk:"password"` |
| 326 | + Insecure types.Bool `tfsdk:"insecure"` |
| 327 | + ClientCertificate types.String `tfsdk:"client_certificate"` |
| 328 | + ClientKey types.String `tfsdk:"client_key"` |
| 329 | + ClusterCACertificate types.String `tfsdk:"cluster_ca_certificate"` |
| 330 | + ConfigContext types.String `tfsdk:"config_context"` |
| 331 | + ConfigContextAuthInfo types.String `tfsdk:"config_context_auth_info"` |
| 332 | + ConfigContextCluster types.String `tfsdk:"config_context_cluster"` |
| 333 | + Token types.String `tfsdk:"token"` |
| 334 | + Exec []KubernetesExec `tfsdk:"exec"` |
| 335 | +} |
| 336 | + |
| 337 | +type KubernetesExec struct { |
| 338 | + APIVersion types.String `tfsdk:"api_version"` |
| 339 | + Command types.String `tfsdk:"command"` |
| 340 | + Env types.Map `tfsdk:"env"` |
| 341 | + Args types.List `tfsdk:"args"` |
| 342 | +} |
0 commit comments