Skip to content

Commit

Permalink
Merge pull request #728 from Juniper/feat/727-env-var-prefix
Browse files Browse the repository at this point in the history
Introduce `env_var_prefix` provider configuration attribute
  • Loading branch information
chrismarget-j authored Jul 17, 2024
2 parents aada19a + c86e57c commit b511d31
Show file tree
Hide file tree
Showing 8 changed files with 70 additions and 49 deletions.
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

0 comments on commit b511d31

Please sign in to comment.