Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce env_var_prefix provider configuration attribute #728

Merged
merged 5 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apstra/constants/env_vars.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package constants

const (
EnvApiTimeout = "APSTRA_API_TIMEOUT"
EnvBlueprintMutexEnabled = "APSTRA_BLUEPRINT_MUTEX_ENABLED"
EnvBlueprintMutexMessage = "APSTRA_BLUEPRINT_MUTEX_MESSAGE"
EnvExperimental = "APSTRA_EXPERIMENTAL"
EnvLogfile = "APSTRA_LOG"
EnvPassword = "APSTRA_PASS"
EnvTlsNoVerify = "APSTRA_TLS_VALIDATION_DISABLED"
EnvUrl = "APSTRA_URL"
EnvUsername = "APSTRA_USER"
)
3 changes: 0 additions & 3 deletions apstra/constants/constants.go → apstra/constants/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,4 @@ const (
ErrProviderBug = "Provider Bug. Please report this issue to the provider maintainers."
ErrInvalidConfig = "invalid configuration"
ErrStringParse = "failed to parse string value"

L3MtuMin = 1280
L3MtuMax = 9216
)
6 changes: 6 additions & 0 deletions apstra/constants/numeric.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package constants

const (
L3MtuMax = 9216
L3MtuMin = 1280
)
46 changes: 26 additions & 20 deletions apstra/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/Juniper/apstra-go-sdk/apstra"
"github.com/Juniper/terraform-provider-apstra/apstra/compatibility"
"github.com/Juniper/terraform-provider-apstra/apstra/constants"
"github.com/Juniper/terraform-provider-apstra/apstra/utils"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
Expand All @@ -31,12 +32,6 @@ const (
defaultTag = "v0.0.0"
defaultCommit = "devel"

envApiTimeout = "APSTRA_API_TIMEOUT"
envBlueprintMutexEnabled = "APSTRA_BLUEPRINT_MUTEX_ENABLED"
envBlueprintMutexMessage = "APSTRA_BLUEPRINT_MUTEX_MESSAGE"
envExperimental = "APSTRA_EXPERIMENTAL"
envTlsNoVerify = "APSTRA_TLS_VALIDATION_DISABLED"

blueprintMutexMessage = "locked by terraform at $DATE"

osxCertErrStringMatch = "certificate is not trusted"
Expand Down Expand Up @@ -132,8 +127,8 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro
"[standard syntax](https://datatracker.ietf.org/doc/html/rfc1738#section-3.1). Care should be " +
"taken to ensure that these credentials aren't accidentally committed to version control, etc... " +
"The preferred approach is to pass the credentials as environment variables `" +
utils.EnvApstraUsername + "` and `" + utils.EnvApstraPassword + "`.\n If `url` is omitted, " +
"environment variable `" + utils.EnvApstraUrl + "` can be used to in its place.\n When the " +
constants.EnvUsername + "` and `" + constants.EnvPassword + "`.\n If `url` is omitted, " +
"environment variable `" + constants.EnvUrl + "` can be used to in its place.\n When the " +
"username or password are embedded in the URL string, any special characters must be " +
"URL-encoded. For example, `pass^word` would become `pass%5eword`.",
Optional: true,
Expand Down Expand Up @@ -174,6 +169,16 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro
Optional: true,
Validators: []validator.Int64{int64validator.AtLeast(0)},
},
"env_var_prefix": schema.StringAttribute{
MarkdownDescription: fmt.Sprintf("This attribute defines a prefix which redefines all of the " +
"`APSTRA_*` environment variables. For example, setting `env_var_prefix = \"FOO_\"` will cause " +
"the provider to learn the Apstra service URL from the `FOO_APSTRA_URL` environment variable " +
"rather than the `APSTRA_URL` environment variable. This capability is intended to be used " +
"when configuring multiple instances of the Apstra provider (which talk to multiple Apstra " +
"servers) in a single Terraform project."),
Optional: true,
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
},
},
}
}
Expand All @@ -186,48 +191,49 @@ type providerConfig struct {
MutexMessage types.String `tfsdk:"blueprint_mutex_message"`
Experimental types.Bool `tfsdk:"experimental"`
ApiTimeout types.Int64 `tfsdk:"api_timeout"`
EnvVarPrefix types.String `tfsdk:"env_var_prefix"`
}

func (o *providerConfig) fromEnv(_ context.Context, diags *diag.Diagnostics) {
if s, ok := os.LookupEnv(envTlsNoVerify); ok && o.TlsNoVerify.IsNull() {
if s, ok := os.LookupEnv(o.EnvVarPrefix.String() + constants.EnvTlsNoVerify); ok && o.TlsNoVerify.IsNull() {
v, err := strconv.ParseBool(s)
if err != nil {
diags.AddError(fmt.Sprintf("error parsing environment variable %q", envTlsNoVerify), err.Error())
diags.AddError(fmt.Sprintf("error parsing environment variable %q", o.EnvVarPrefix.String()+constants.EnvTlsNoVerify), err.Error())
}
o.TlsNoVerify = types.BoolValue(v)
}

if s, ok := os.LookupEnv(envBlueprintMutexEnabled); ok && o.MutexEnable.IsNull() {
if s, ok := os.LookupEnv(o.EnvVarPrefix.String() + constants.EnvBlueprintMutexEnabled); ok && o.MutexEnable.IsNull() {
v, err := strconv.ParseBool(s)
if err != nil {
diags.AddError(fmt.Sprintf("error parsing environment variable %q", envBlueprintMutexEnabled), err.Error())
diags.AddError(fmt.Sprintf("error parsing environment variable %q", o.EnvVarPrefix.String()+constants.EnvBlueprintMutexEnabled), err.Error())
}
o.MutexEnable = types.BoolValue(v)
}

if s, ok := os.LookupEnv(envBlueprintMutexMessage); ok && o.MutexMessage.IsNull() {
if s, ok := os.LookupEnv(o.EnvVarPrefix.String() + constants.EnvBlueprintMutexMessage); ok && o.MutexMessage.IsNull() {
if len(s) < 1 {
diags.AddError(fmt.Sprintf("error parsing environment variable %q", envBlueprintMutexMessage),
diags.AddError(fmt.Sprintf("error parsing environment variable %q", o.EnvVarPrefix.String()+constants.EnvBlueprintMutexMessage),
fmt.Sprintf("minimum string length 1; got %q", s))
}
o.MutexMessage = types.StringValue(s)
}

if s, ok := os.LookupEnv(envExperimental); ok && o.Experimental.IsNull() {
if s, ok := os.LookupEnv(o.EnvVarPrefix.String() + constants.EnvExperimental); ok && o.Experimental.IsNull() {
v, err := strconv.ParseBool(s)
if err != nil {
diags.AddError(fmt.Sprintf("error parsing environment variable %q", envExperimental), err.Error())
diags.AddError(fmt.Sprintf("error parsing environment variable %q", o.EnvVarPrefix.String()+constants.EnvExperimental), err.Error())
}
o.Experimental = types.BoolValue(v)
}

if s, ok := os.LookupEnv(envApiTimeout); ok && o.ApiTimeout.IsNull() {
if s, ok := os.LookupEnv(o.EnvVarPrefix.String() + o.EnvVarPrefix.String() + constants.EnvApiTimeout); ok && o.ApiTimeout.IsNull() {
v, err := strconv.ParseInt(s, 0, 64)
if err != nil {
diags.AddError(fmt.Sprintf("error parsing environment variable %q", envApiTimeout), err.Error())
diags.AddError(fmt.Sprintf("error parsing environment variable %q", o.EnvVarPrefix.String()+constants.EnvApiTimeout), err.Error())
}
if v < 0 {
diags.AddError(fmt.Sprintf("invalid value in environment variable %q", envApiTimeout),
diags.AddError(fmt.Sprintf("invalid value in environment variable %q", o.EnvVarPrefix.String()+constants.EnvApiTimeout),
fmt.Sprintf("minimum permitted value is 0, got %d", v))
}
o.ApiTimeout = types.Int64Value(v)
Expand Down Expand Up @@ -274,7 +280,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest,
}

// Create the Apstra client configuration from the URL and the environment.
clientCfg, err := utils.NewClientConfig(config.Url.ValueString())
clientCfg, err := utils.NewClientConfig(config.Url.ValueString(), config.EnvVarPrefix.ValueString())
if err != nil {
resp.Diagnostics.AddError("Error creating Apstra client configuration", err.Error())
return
Expand Down
7 changes: 4 additions & 3 deletions apstra/resource_template_rack_based_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"context"
"fmt"
apiversions "github.com/Juniper/terraform-provider-apstra/apstra/api_versions"
"github.com/Juniper/terraform-provider-apstra/apstra/constants"
"github.com/Juniper/terraform-provider-apstra/apstra/utils"
"github.com/hashicorp/go-version"
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
Expand Down Expand Up @@ -38,12 +39,12 @@ resource "apstra_template_rack_based" "test" {
func TestResourceTemplateRackBased(t *testing.T) {
ctx := context.Background()

apstraUrl, ok := os.LookupEnv(utils.EnvApstraUrl)
apstraUrl, ok := os.LookupEnv(constants.EnvUrl)
if !ok || apstraUrl == "" {
t.Fatalf("apstra url environment variable (%s) must be set and non-empty", utils.EnvApstraUrl)
t.Fatalf("apstra url environment variable (%s) must be set and non-empty", constants.EnvUrl)
}

clientCfg, err := utils.NewClientConfig(apstraUrl)
clientCfg, err := utils.NewClientConfig(apstraUrl, "")
if err != nil {
t.Fatal(err)
}
Expand Down
15 changes: 8 additions & 7 deletions apstra/test_utils/test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"github.com/Juniper/apstra-go-sdk/apstra"
"github.com/Juniper/terraform-provider-apstra/apstra/constants"
"github.com/Juniper/terraform-provider-apstra/apstra/utils"
"github.com/hashicorp/hcl/v2/hclsimple"
"net/http"
Expand Down Expand Up @@ -41,7 +42,7 @@ func GetTestClient(t testing.TB, ctx context.Context) *apstra.Client {
t.Fatal(err)
}

clientCfg, err := utils.NewClientConfig("")
clientCfg, err := utils.NewClientConfig("", "")
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -76,23 +77,23 @@ func TestCfgFileToEnv() error {
}

if testCfg.Url != "" {
err = os.Setenv(utils.EnvApstraUrl, testCfg.Url)
err = os.Setenv(constants.EnvUrl, testCfg.Url)
if err != nil {
return fmt.Errorf("failed setting environment variable %q - %w", utils.EnvApstraUrl, err)
return fmt.Errorf("failed setting environment variable %q - %w", constants.EnvUrl, err)
}
}

if testCfg.Username != "" {
err = os.Setenv(utils.EnvApstraUsername, testCfg.Username)
err = os.Setenv(constants.EnvUsername, testCfg.Username)
if err != nil {
return fmt.Errorf("failed setting environment variable %q - %w", utils.EnvApstraUsername, err)
return fmt.Errorf("failed setting environment variable %q - %w", constants.EnvUsername, err)
}
}

if testCfg.Password != "" {
err = os.Setenv(utils.EnvApstraPassword, testCfg.Password)
err = os.Setenv(constants.EnvPassword, testCfg.Password)
if err != nil {
return fmt.Errorf("failed setting environment variable %q - %w", utils.EnvApstraPassword, err)
return fmt.Errorf("failed setting environment variable %q - %w", constants.EnvPassword, err)
}
}

Expand Down
28 changes: 12 additions & 16 deletions apstra/utils/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,20 @@ import (
"crypto/tls"
"errors"
"fmt"
"github.com/Juniper/apstra-go-sdk/apstra"
"io"
"log"
"net/http"
"net/url"
"os"
"strings"

"github.com/Juniper/apstra-go-sdk/apstra"
"github.com/Juniper/terraform-provider-apstra/apstra/constants"
)

const (
EnvApstraUrl = "APSTRA_URL"
EnvApstraUsername = "APSTRA_USER"
EnvApstraPassword = "APSTRA_PASS"
EnvApstraLogfile = "APSTRA_LOG"
EnvApstraExperimental = "APSTRA_EXPERIMENTAL"
EnvTlsKeyLogFile = "SSLKEYLOGFILE"

urlEncodeMsg = `
envTlsKeyLogFile = "SSLKEYLOGFILE"
urlEncodeMsg = `
Note that when the Username or Password fields contain special characters and are
embedded in the URL, they must be URL-encoded by substituting '%%<hex-value>' in
place of each special character. The following table demonstrates some common
Expand All @@ -30,10 +26,10 @@ substitutions:
%s`
)

func NewClientConfig(apstraUrl string) (*apstra.ClientCfg, error) {
func NewClientConfig(apstraUrl, envVarPrefix string) (*apstra.ClientCfg, error) {
// Populate raw URL string from config or environment.
if apstraUrl == "" {
apstraUrl = os.Getenv(EnvApstraUrl)
apstraUrl = os.Getenv(envVarPrefix + constants.EnvUrl)
}

if apstraUrl == "" {
Expand Down Expand Up @@ -62,7 +58,7 @@ func NewClientConfig(apstraUrl string) (*apstra.ClientCfg, error) {
// Determine the Apstra username.
user := parsedUrl.User.Username()
if user == "" {
if val, ok := os.LookupEnv(EnvApstraUsername); ok {
if val, ok := os.LookupEnv(envVarPrefix + constants.EnvUsername); ok {
user = val
} else {
return nil, errors.New("unable to determine apstra username - " + fmt.Sprintf(urlEncodeMsg, UrlEscapeTable()))
Expand All @@ -72,7 +68,7 @@ func NewClientConfig(apstraUrl string) (*apstra.ClientCfg, error) {
// Determine the Apstra password.
pass, found := parsedUrl.User.Password()
if !found {
if val, ok := os.LookupEnv(EnvApstraPassword); ok {
if val, ok := os.LookupEnv(envVarPrefix + constants.EnvPassword); ok {
pass = val
} else {
return nil, errors.New("unable to determine apstra password")
Expand All @@ -84,7 +80,7 @@ func NewClientConfig(apstraUrl string) (*apstra.ClientCfg, error) {

// Set up a logger.
var logger *log.Logger
if logFileName, ok := os.LookupEnv(EnvApstraLogfile); ok {
if logFileName, ok := os.LookupEnv(envVarPrefix + constants.EnvLogfile); ok {
logFile, err := os.OpenFile(logFileName, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
if err != nil {
return nil, err
Expand All @@ -94,7 +90,7 @@ func NewClientConfig(apstraUrl string) (*apstra.ClientCfg, error) {

// Set up the TLS session key log.
var klw io.Writer
if fileName, ok := os.LookupEnv(EnvTlsKeyLogFile); ok {
if fileName, ok := os.LookupEnv(envTlsKeyLogFile); ok {
klw, err = newKeyLogWriter(fileName)
if err != nil {
return nil, err
Expand All @@ -110,7 +106,7 @@ func NewClientConfig(apstraUrl string) (*apstra.ClientCfg, error) {
},
}

_, experimental := os.LookupEnv(EnvApstraExperimental)
_, experimental := os.LookupEnv(envVarPrefix + constants.EnvExperimental)

// Create the clientCfg
return &apstra.ClientCfg{
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ may be set via environment variables: `APSTRA_API_TIMEOUT`,
- `api_timeout` (Number) Timeout in seconds for completing API transactions with the Apstra server. Omit for default value of 10 seconds. Value of 0 results in infinite timeout.
- `blueprint_mutex_enabled` (Boolean) Blueprint mutexes are indicators that changes are being made in a staging Blueprint and other automation processes (including other instances of Terraform) should wait before beginning to make changes of their own. Setting this attribute 'true' causes the provider to lock a blueprint-specific mutex before making any changes. [More info here](https://github.com/Juniper/terraform-provider-apstra/blob/main/kb/blueprint_mutex.md).
- `blueprint_mutex_message` (String) Blueprint mutexes are signals that changes are being made in a staging Blueprint and other automation processes (including other instances of Terraform) should wait before beginning to make changes of their own. The mutexes embed a human-readable field to reduce confusion in the event a mutex needs to be cleared manually. This attribute overrides the default message in that field: "locked by terraform at $DATE".
- `env_var_prefix` (String) This attribute defines a prefix which redefines all of the `APSTRA_*` environment variables. For example, setting `env_var_prefix = "FOO_"` will cause the provider to learn the Apstra service URL from the `FOO_APSTRA_URL` environment variable rather than the `APSTRA_URL` environment variable. This capability is intended to be used when configuring multiple instances of the Apstra provider (which talk to multiple Apstra servers) in a single Terraform project.
- `experimental` (Boolean) Enable *experimental* features. In this release that means:
- Set the `experimental` flag in the underlying Apstra SDK client object. Doing so permits connections to Apstra instances not supported by the SDK.
- `tls_validation_disabled` (Boolean) Set 'true' to disable TLS certificate validation.
Expand Down
Loading