diff --git a/docs/index.md b/docs/index.md index 82ad2f3d..8b4e17be 100644 --- a/docs/index.md +++ b/docs/index.md @@ -43,20 +43,41 @@ provider "mist" { } ``` - -## Schema +### Credentials -### Required +Users are encouraged to pass the API Token or the username and password via the +environment variables (see below). If authentication information are provided +in the provider configuration and in the environment variables, the Provider +configuration will be used. -- `host` (String) URL of the Mist Cloud, e.g. `api.mist.com`. -It is also possible to pass the Mist Cloud host with the environment variable `MIST_HOST`. +Please consider whether writing credentials to a configuration file is +acceptable in your environment. + +### Proxy Support + +HTTP, HTTPS, and SOCKS5 proxies are supported through the `MIST_PROXY` environment + variables or the `proxy` provider configuration attribute. + + +## Schema ### Optional - `api_timeout` (Number) Timeout in seconds for completing API transactions with the Mist Cloud. Omit for default value of 10 seconds. Value of 0 results in infinite timeout. - `apitoken` (String, Sensitive) For API Token authentication, the Mist API Token. -The preferred approach is to pass the API Token as environment variables `MIST_API_TOKEN`. +- `host` (String) URL of the Mist Cloud, e.g. `api.mist.com`. - `password` (String, Sensitive) For username/password authentication, the Mist Account password. -The preferred approach is to pass the API Token as environment variables `MIST_PASSWORD`. +- `proxy` (String) Requests use the configured proxy to reach the Mist Cloud. +The value may be either a complete URL or a `[username:password@]host[:port]`, in which case the `http` scheme is assumed. The schemes `http`, `https`, and `socks5` are supported. - `username` (String) For username/password authentication, the Mist Account username. -The preferred approach is to pass the API Token as environment variables `MIST_USERNAME`. \ No newline at end of file + +### Environment Variables + +| Varibale Name | Provider attribute | Type | Description | +| ---- | ---- | ---- | ---- | +| `MIST_HOST` | `host` | String | URL of the Mist Cloud, e.g. `api.mist.com`. See above for the list of supported Clouds.| +| `MIST_API_TOKEN` | `apitoken` | String | For API Token authentication, the Mist API Token. | +| `MIST_USERNAME` | `username` | String | For username/password authentication, the Mist Account password. | +| `MIST_PASSWORD` | `password` | String | For username/password authentication, the Mist Account password. | +| `MIST_PROXY` | `proxy` | String | Requests use the configured proxy to reach the Mist Cloud. The value may be either a complete URL or a `[username:password@]host[:port]`, in which case the `http` scheme is assumed. The schemes `http`, `https`, and `socks5` are supported. | +| `MIST_API_TIMEOUT` | `api_timeout` | Int | Timeout in seconds for completing API transactions with the Mist Cloud. Omit for default value of 10 seconds. Value of 0 results in infinite timeout. | diff --git a/internal/provider/constants.go b/internal/provider/constants.go index 9ac10a01..23b3e05a 100644 --- a/internal/provider/constants.go +++ b/internal/provider/constants.go @@ -6,6 +6,7 @@ const ( envUsername = "MIST_USERNAME" envPassword = "MIST_PASSWORD" envApiTimeout = "MIST_API_TIMEOUT" + envProxy = "MIST_PROXY" docCategorySeparator = " --- " docCategorySite = "Site" + docCategorySeparator docCategoryOrg = "Org" + docCategorySeparator diff --git a/internal/provider/provider.go b/internal/provider/provider.go index bb5dec49..4beba891 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -3,6 +3,8 @@ package provider import ( "context" "fmt" + "net/http" + "net/url" "strconv" "strings" @@ -14,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/schema" @@ -48,6 +51,7 @@ type mistProviderModel struct { Username types.String `tfsdk:"username"` Password types.String `tfsdk:"password"` ApiTimeout types.Float64 `tfsdk:"api_timeout"` + Proxy types.String `tfsdk:"proxy"` } func (p *mistProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { @@ -68,26 +72,22 @@ func (p *mistProvider) Schema(ctx context.Context, req provider.SchemaRequest, r "* APAC 01 (api.ac5.mist.com)", Attributes: map[string]schema.Attribute{ "host": schema.StringAttribute{ - MarkdownDescription: "URL of the Mist Cloud, e.g. `api.mist.com`.\n" + - "It is also possible to pass the Mist Cloud host with the environment variable `" + envHost + "`.", - Required: true, + MarkdownDescription: "URL of the Mist Cloud, e.g. `api.mist.com`.", + Optional: true, }, "apitoken": schema.StringAttribute{ - MarkdownDescription: "For API Token authentication, the Mist API Token.\n" + - "The preferred approach is to pass the API Token as environment variables `" + envApitoken + "`.", - Optional: true, - Sensitive: true, + MarkdownDescription: "For API Token authentication, the Mist API Token.", + Optional: true, + Sensitive: true, }, "username": schema.StringAttribute{ - MarkdownDescription: "For username/password authentication, the Mist Account username.\n" + - "The preferred approach is to pass the API Token as environment variables `" + envUsername + "`.", - Optional: true, + MarkdownDescription: "For username/password authentication, the Mist Account username.", + Optional: true, }, "password": schema.StringAttribute{ - MarkdownDescription: "For username/password authentication, the Mist Account password.\n" + - "The preferred approach is to pass the API Token as environment variables `" + envPassword + "`.", - Optional: true, - Sensitive: true, + MarkdownDescription: "For username/password authentication, the Mist Account password.", + Optional: true, + Sensitive: true, }, "api_timeout": schema.Float64Attribute{ MarkdownDescription: fmt.Sprintf("Timeout in seconds for completing API transactions "+ @@ -97,40 +97,50 @@ func (p *mistProvider) Schema(ctx context.Context, req provider.SchemaRequest, r Optional: true, Validators: []validator.Float64{float64validator.AtLeast(0)}, }, + "proxy": schema.StringAttribute{ + MarkdownDescription: "Requests use the configured proxy to reach the Mist Cloud.\n" + + "The value may be either a complete URL or a `[username:password@]host[:port]`, in which case the `http` scheme is assumed. " + + "The schemes `http`, `https`, and `socks5` are supported.", + Optional: true, + }, }, } } -func (p *mistProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { - var config mistProviderModel - diags := req.Config.Get(ctx, &config) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - host := os.Getenv(envHost) - if !config.Host.IsNull() { - host = config.Host.ValueString() +func (p *mistProviderModel) fromEnv(_ context.Context, diags *diag.Diagnostics) { + if s, ok := os.LookupEnv(envHost); ok && p.Host.IsNull() { + if !strings.HasPrefix(s, "api.") { + diags.AddError(fmt.Sprintf("error parsing environment variable %q", envHost), + fmt.Sprintf("The configured Mist Host does not match the supported Clouds; got %q", s)) + } + p.Host = types.StringValue(s) } - apitoken := os.Getenv(envApitoken) - if !config.Apitoken.IsNull() { - apitoken = config.Apitoken.ValueString() + if s, ok := os.LookupEnv(envApitoken); ok && p.Apitoken.IsNull() { + if len(s) < 1 { + diags.AddError(fmt.Sprintf("error parsing environment variable %q", envApitoken), + fmt.Sprintf("minimum string length 1; got %q", s)) + } + p.Apitoken = types.StringValue(s) } - username := os.Getenv(envUsername) - if !config.Username.IsNull() { - username = config.Username.ValueString() + if s, ok := os.LookupEnv(envUsername); ok && p.Username.IsNull() { + if len(s) < 1 { + diags.AddError(fmt.Sprintf("error parsing environment variable %q", envUsername), + fmt.Sprintf("minimum string length 1; got %q", s)) + } + p.Username = types.StringValue(s) } - password := os.Getenv(envPassword) - if !config.Password.IsNull() { - password = config.Password.ValueString() + if s, ok := os.LookupEnv(envPassword); ok && p.Password.IsNull() { + if len(s) < 1 { + diags.AddError(fmt.Sprintf("error parsing environment variable %q", envPassword), + fmt.Sprintf("minimum string length 1; got %q", s)) + } + p.Password = types.StringValue(s) } - var api_timeout float64 = 10 - if s, ok := os.LookupEnv(envApiTimeout); ok && config.ApiTimeout.IsNull() { + if s, ok := os.LookupEnv(envApiTimeout); ok && p.ApiTimeout.IsNull() { v, err := strconv.ParseFloat(s, 64) if err != nil { diags.AddError(fmt.Sprintf("error parsing environment variable %q", envApiTimeout), err.Error()) @@ -139,13 +149,21 @@ func (p *mistProvider) Configure(ctx context.Context, req provider.ConfigureRequ diags.AddError(fmt.Sprintf("invalid value in environment variable %q", envApiTimeout), fmt.Sprintf("minimum permitted value is 0, got %d", int64(v))) } - config.ApiTimeout = types.Float64Value(v) - } else if !config.ApiTimeout.IsNull() { - api_timeout = config.ApiTimeout.ValueFloat64() + p.ApiTimeout = types.Float64Value(v) } - if host == "" { - resp.Diagnostics.AddAttributeError( + if s, ok := os.LookupEnv(envProxy); ok && p.Proxy.IsNull() { + if len(s) < 1 { + diags.AddError(fmt.Sprintf("error parsing environment variable %q", envProxy), + fmt.Sprintf("minimum string length 1; got %q", s)) + } + p.Proxy = types.StringValue(s) + } +} + +func (p *mistProviderModel) validateConfig(_ context.Context, diags *diag.Diagnostics) { + if p.Host.ValueString() == "" { + diags.AddAttributeError( path.Root("host"), "Missing MIST API Host", "The provider cannot create the MIST API client because there is a missing or empty value for the MIST API host. "+ @@ -153,8 +171,9 @@ func (p *mistProvider) Configure(ctx context.Context, req provider.ConfigureRequ "If either is already set, ensure the value is not empty.", ) } - if apitoken == "" && (username == "" && password == "") { - resp.Diagnostics.AddError( + + if p.Apitoken.ValueString() == "" && (p.Username.ValueString() == "" && p.Password.ValueString() == "") { + diags.AddError( "Missing MIST API Authentication", "The provider cannot create the MIST API client because the authentication configuration is missing. "+ "Set the Authentication values in the configuration or in the environment variables: "+ @@ -162,16 +181,16 @@ func (p *mistProvider) Configure(ctx context.Context, req provider.ConfigureRequ " * username and password (environment variables`"+envUsername+"` and `"+envPassword+"`)"+ "If either is already set, ensure the value is not empty.", ) - } else if apitoken == "" && (username != "" && password == "") { - resp.Diagnostics.AddAttributeError( + } else if p.Apitoken.ValueString() == "" && (p.Username.ValueString() != "" && p.Password.ValueString() == "") { + diags.AddAttributeError( path.Root("username"), "Missing MIST API Password", "The provider cannot create the MIST API client because there is a a missing or empty value for the MIST Username whereas the MIST Password is configured. "+ "Set the host value in the configuration or use the `"+envUsername+"` environment variable. "+ "If either is already set, ensure the value is not empty.", ) - } else if apitoken == "" && (username == "" && password != "") { - resp.Diagnostics.AddAttributeError( + } else if p.Apitoken.ValueString() == "" && (p.Username.ValueString() == "" && p.Password.ValueString() != "") { + diags.AddAttributeError( path.Root("password"), "Missing MIST API Password", "The provider cannot create the MIST API client because there is a a missing or empty value for the MIST Password whereas the MIST Username is configured. "+ @@ -179,13 +198,49 @@ func (p *mistProvider) Configure(ctx context.Context, req provider.ConfigureRequ "If either is already set, ensure the value is not empty.", ) } +} + +func (p *mistProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + var config mistProviderModel + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Retrieve missing config elements from environment + config.fromEnv(ctx, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } + config.validateConfig(ctx, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + if config.ApiTimeout.IsNull() { + config.ApiTimeout = types.Float64Value(defaultApiTimeout) + } + + var proxy_url *url.URL + if !config.Proxy.IsNull() { + proxy_string := config.Proxy.ValueString() + if !strings.HasPrefix(proxy_string, "http://") && + !strings.HasPrefix(proxy_string, "https://") && + !strings.HasPrefix(proxy_string, "socks5://") { + proxy_string = "http://" + proxy_string + } + u, err := url.Parse(proxy_string) + if err != nil { + resp.Diagnostics.AddError("Unable to parse proxy configuration", err.Error()) + return + } + proxy_url = u + } + var mist_cloud mistapi.Environment - switch host { + switch config.Host.ValueString() { case "api.mist.com": mist_cloud = mistapi.MIST_GLOBAL_01 case "api.gc1.mist.com": @@ -208,46 +263,52 @@ func (p *mistProvider) Configure(ctx context.Context, req provider.ConfigureRequ resp.Diagnostics.AddAttributeError( path.Root("host"), "Wrong Mist Host", - "The configured host \""+host+"\" is not a valid Mist Host. Please refer to the documentation to get the possible values", + "The configured host \""+config.Host.ValueString()+"\" is not a valid Mist Host. Please refer to the documentation to get the possible values", ) return } - var client mistapi.ClientInterface + var DefaultTransport http.RoundTripper = http.DefaultTransport + if proxy_url != nil { + DefaultTransport = &http.Transport{ + Proxy: http.ProxyURL(proxy_url), + } + } var client_config mistapi.Configuration + var cloud_config mistapi.ConfigurationOptions = mistapi.WithEnvironment(mist_cloud) + var http_config mistapi.ConfigurationOptions = mistapi.WithHttpConfiguration( + mistapi.CreateHttpConfiguration( + mistapi.WithTimeout(config.ApiTimeout.ValueFloat64()), + mistapi.WithTransport(DefaultTransport), + ), + ) // configure the client for API Token Auth - if apitoken != "" { + if config.Apitoken.ValueString() != "" { client_config = mistapi.CreateConfiguration( - mistapi.WithHttpConfiguration( - mistapi.CreateHttpConfiguration( - mistapi.WithTimeout(api_timeout), - ), - ), - mistapi.WithEnvironment(mist_cloud), + http_config, + cloud_config, mistapi.WithApiTokenCredentials( - mistapi.NewApiTokenCredentials("Token "+apitoken), + mistapi.NewApiTokenCredentials("Token "+config.Apitoken.ValueString()), ), ) + // configure the client for Basic Auth + CSRF } else { // Initiate the login API Call tmp_client := mistapi.NewClient( mistapi.CreateConfiguration( - mistapi.WithHttpConfiguration( - mistapi.CreateHttpConfiguration( - mistapi.WithTimeout(api_timeout), - ), - ), - mistapi.WithEnvironment(mist_cloud), + http_config, + cloud_config, mistapi.WithBasicAuthCredentials( - mistapi.NewBasicAuthCredentials(username, password), + mistapi.NewBasicAuthCredentials(config.Username.ValueString(), config.Password.ValueString()), ), ), ) + body := models.Login{} - body.Email = username - body.Password = password + body.Email = config.Username.ValueString() + body.Password = config.Password.ValueString() apiResponse, err := tmp_client.AdminsLogin().Login(ctx, &body) if err != nil { resp.Diagnostics.AddError("Authentication Error", err.Error()) @@ -267,14 +328,10 @@ func (p *mistProvider) Configure(ctx context.Context, req provider.ConfigureRequ csrfToken_string := strings.Split(cVal, "=")[1] csrfToken := mistapi.NewCsrfTokenCredentials(string(csrfToken_string)) client_config = mistapi.CreateConfiguration( - mistapi.WithHttpConfiguration( - mistapi.CreateHttpConfiguration( - mistapi.WithTimeout(api_timeout), - ), - ), - mistapi.WithEnvironment(mist_cloud), + http_config, + cloud_config, mistapi.WithBasicAuthCredentials( - mistapi.NewBasicAuthCredentials(username, password), + mistapi.NewBasicAuthCredentials(config.Username.ValueString(), config.Password.ValueString()), ), mistapi.WithCsrfTokenCredentials(csrfToken), ) @@ -292,7 +349,7 @@ func (p *mistProvider) Configure(ctx context.Context, req provider.ConfigureRequ } // Use the configuration to create the client and test the credentials - client = mistapi.NewClient(client_config) + var client mistapi.ClientInterface = mistapi.NewClient(client_config) _, err := client.SelfAccount().GetSelf(ctx) if err != nil { resp.Diagnostics.AddError("Authentication Error", err.Error()) diff --git a/templates/index.md.tmpl b/templates/index.md.tmpl index f84d5203..7e3964c5 100644 --- a/templates/index.md.tmpl +++ b/templates/index.md.tmpl @@ -26,17 +26,18 @@ acceptable in your environment. ### Proxy Support -HTTP, HTTPS, and SOCKS5 proxies are supported through the `HTTP_PROXY` and -`HTTPS_PROXY` environment variables or the lowercase versions of those -variables. The value of each may be either a complete URL or a "host[:port]", -in which case the "http" scheme is assumed. +HTTP, HTTPS, and SOCKS5 proxies are supported through the `MIST_PROXY` environment + variables or the `proxy` provider configuration attribute. -Hosts which should be omitted from the proxy configuration may be listed in -the `NO_PROXY` (or `no_proxy`) environment variable ([details](https://github.com/golang/go/blob/682a1d2176b02337460aeede0ff9e49429525195/src/vendor/golang.org/x/net/http/httpproxy/proxy.go#L38C1-L50C13)). +{{ .SchemaMarkdown | trimspace }} -### Additional Environment Variables +### Environment Variables -Provider attributes which have been omitted from the configuration -may be set via environment variables: `MIST_API_TIMEOUT`. - -{{ .SchemaMarkdown | trimspace }} \ No newline at end of file +| Varibale Name | Provider attribute | Type | Description | +| ---- | ---- | ---- | ---- | +| `MIST_HOST` | `host` | String | URL of the Mist Cloud, e.g. `api.mist.com`. See above for the list of supported Clouds.| +| `MIST_API_TOKEN` | `apitoken` | String | For API Token authentication, the Mist API Token. | +| `MIST_USERNAME` | `username` | String | For username/password authentication, the Mist Account password. | +| `MIST_PASSWORD` | `password` | String | For username/password authentication, the Mist Account password. | +| `MIST_PROXY` | `proxy` | String | Requests use the configured proxy to reach the Mist Cloud. The value may be either a complete URL or a `[username:password@]host[:port]`, in which case the `http` scheme is assumed. The schemes `http`, `https`, and `socks5` are supported. | +| `MIST_API_TIMEOUT` | `api_timeout` | Int | Timeout in seconds for completing API transactions with the Mist Cloud. Omit for default value of 10 seconds. Value of 0 results in infinite timeout. |