diff --git a/CHANGELOG.md b/CHANGELOG.md index e6e85ab5..6f143275 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ ## Unreleased +Features: +* Add support for `max_connections_per_host` within Agent injector [GH-579](https://github.com/hashicorp/vault-k8s/pull/579) Changes: * Dependency updates: diff --git a/agent-inject/agent/agent.go b/agent-inject/agent/agent.go index 3ecbdb07..2c9c0527 100644 --- a/agent-inject/agent/agent.go +++ b/agent-inject/agent/agent.go @@ -7,10 +7,10 @@ import ( "encoding/json" "errors" "fmt" - "strconv" "strings" jsonpatch "github.com/evanphx/json-patch" + "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/vault/sdk/helper/strutil" corev1 "k8s.io/api/core/v1" "k8s.io/utils/pointer" @@ -340,6 +340,11 @@ type VaultAgentTemplateConfig struct { // StaticSecretRenderInterval If specified, configures how often // Vault Agent Template should render non-leased secrets such as KV v2 StaticSecretRenderInterval string + + // MaxConnectionsPerHost limits the total number of connections + // that the Vault Agent templating engine can use for a particular Vault host. This limit + // includes connections in the dialing, active, and idle states. + MaxConnectionsPerHost int64 } // New creates a new instance of Agent by parsing all the Kubernetes annotations. @@ -439,12 +444,12 @@ func New(pod *corev1.Pod) (*Agent, error) { return agent, err } - agent.RunAsUser, err = strconv.ParseInt(pod.Annotations[AnnotationAgentRunAsUser], 10, 64) + agent.RunAsUser, err = parseutil.ParseInt(pod.Annotations[AnnotationAgentRunAsUser]) if err != nil { return agent, err } - agent.RunAsGroup, err = strconv.ParseInt(pod.Annotations[AnnotationAgentRunAsGroup], 10, 64) + agent.RunAsGroup, err = parseutil.ParseInt(pod.Annotations[AnnotationAgentRunAsGroup]) if err != nil { return agent, err } @@ -503,9 +508,15 @@ func New(pod *corev1.Pod) (*Agent, error) { return nil, err } + maxConnectionsPerHost, err := agent.templateConfigMaxConnectionsPerHost() + if err != nil { + return nil, err + } + agent.VaultAgentTemplateConfig = VaultAgentTemplateConfig{ ExitOnRetryFailure: exitOnRetryFailure, StaticSecretRenderInterval: pod.Annotations[AnnotationTemplateConfigStaticSecretRenderInterval], + MaxConnectionsPerHost: maxConnectionsPerHost, } agent.EnableQuit, err = agent.getEnableQuit() @@ -537,7 +548,7 @@ func ShouldInject(pod *corev1.Pod) (bool, error) { return false, nil } - inject, err := strconv.ParseBool(raw) + inject, err := parseutil.ParseBool(raw) if err != nil { return false, err } diff --git a/agent-inject/agent/annotations.go b/agent-inject/agent/annotations.go index f6b8b9f3..905facf5 100644 --- a/agent-inject/agent/annotations.go +++ b/agent-inject/agent/annotations.go @@ -12,6 +12,7 @@ import ( "time" jsonpatch "github.com/evanphx/json-patch" + "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" ) @@ -24,7 +25,7 @@ const ( // AnnotationAgentInject is the key of the annotation that controls whether // injection is explicitly enabled or disabled for a pod. This should - // be set to a true or false value, as parseable by strconv.ParseBool + // be set to a true or false value, as parseable by parseutil.ParseBool AnnotationAgentInject = "vault.hashicorp.com/agent-inject" // AnnotationAgentInjectSecret is the key annotation that configures Vault @@ -273,6 +274,11 @@ const ( // Defaults to 5 minutes. AnnotationTemplateConfigStaticSecretRenderInterval = "vault.hashicorp.com/template-static-secret-render-interval" + // AnnotationTemplateConfigMaxConnectionsPerHost limits the total number of connections + // that the Vault Agent templating engine can use for a particular Vault host. This limit + // includes connections in the dialing, active, and idle states. + AnnotationTemplateConfigMaxConnectionsPerHost = "vault.hashicorp.com/template-max-connections-per-host" + // AnnotationAgentEnableQuit configures whether the quit endpoint is // enabled in the injected agent config AnnotationAgentEnableQuit = "vault.hashicorp.com/agent-enable-quit" @@ -335,6 +341,7 @@ type AgentConfig struct { ResourceLimitEphemeral string ExitOnRetryFailure bool StaticSecretRenderInterval string + MaxConnectionsPerHost int64 AuthMinBackoff string AuthMaxBackoff string DisableIdleConnections string @@ -519,6 +526,10 @@ func Init(pod *corev1.Pod, cfg AgentConfig) error { pod.ObjectMeta.Annotations[AnnotationTemplateConfigStaticSecretRenderInterval] = cfg.StaticSecretRenderInterval } + if _, ok := pod.ObjectMeta.Annotations[AnnotationTemplateConfigMaxConnectionsPerHost]; !ok { + pod.ObjectMeta.Annotations[AnnotationTemplateConfigMaxConnectionsPerHost] = strconv.FormatInt(cfg.MaxConnectionsPerHost, 10) + } + if minBackoffString, ok := pod.ObjectMeta.Annotations[AnnotationAgentAuthMinBackoff]; ok { if minBackoffString != "" { _, err := time.ParseDuration(minBackoffString) @@ -662,7 +673,7 @@ func (a *Agent) inject() (bool, error) { return true, nil } - return strconv.ParseBool(raw) + return parseutil.ParseBool(raw) } func (a *Agent) initFirst() (bool, error) { @@ -671,7 +682,7 @@ func (a *Agent) initFirst() (bool, error) { return false, nil } - return strconv.ParseBool(raw) + return parseutil.ParseBool(raw) } func (a *Agent) prePopulate() (bool, error) { @@ -680,7 +691,7 @@ func (a *Agent) prePopulate() (bool, error) { return true, nil } - return strconv.ParseBool(raw) + return parseutil.ParseBool(raw) } func (a *Agent) prePopulateOnly() (bool, error) { @@ -689,7 +700,7 @@ func (a *Agent) prePopulateOnly() (bool, error) { return false, nil } - return strconv.ParseBool(raw) + return parseutil.ParseBool(raw) } func (a *Agent) revokeOnShutdown() (bool, error) { @@ -698,7 +709,7 @@ func (a *Agent) revokeOnShutdown() (bool, error) { return false, nil } - return strconv.ParseBool(raw) + return parseutil.ParseBool(raw) } func (a *Agent) revokeGrace() (uint64, error) { @@ -716,7 +727,7 @@ func (a *Agent) tlsSkipVerify() (bool, error) { return false, nil } - return strconv.ParseBool(raw) + return parseutil.ParseBool(raw) } func (a *Agent) preserveSecretCase(secretName string) (bool, error) { @@ -732,7 +743,7 @@ func (a *Agent) preserveSecretCase(secretName string) (bool, error) { return false, nil } } - return strconv.ParseBool(raw) + return parseutil.ParseBool(raw) } func (a *Agent) runAsSameID(pod *corev1.Pod) (bool, error) { @@ -740,7 +751,7 @@ func (a *Agent) runAsSameID(pod *corev1.Pod) (bool, error) { if !ok { return DefaultAgentRunAsSameUser, nil } - runAsSameID, err := strconv.ParseBool(raw) + runAsSameID, err := parseutil.ParseBool(raw) if err != nil { return DefaultAgentRunAsSameUser, err } @@ -769,7 +780,7 @@ func (a *Agent) setShareProcessNamespace(pod *corev1.Pod) (bool, bool, error) { if !ok { return false, false, nil } - shareProcessNamespace, err := strconv.ParseBool(raw) + shareProcessNamespace, err := parseutil.ParseBool(raw) if err != nil { return false, true, fmt.Errorf( "invalid value %v for annotation %q, err=%w", raw, annotation, err) @@ -791,7 +802,7 @@ func (a *Agent) setSecurityContext() (bool, error) { return DefaultAgentSetSecurityContext, nil } - return strconv.ParseBool(raw) + return parseutil.ParseBool(raw) } func (a *Agent) cacheEnable() (bool, error) { @@ -800,7 +811,7 @@ func (a *Agent) cacheEnable() (bool, error) { return false, nil } - return strconv.ParseBool(raw) + return parseutil.ParseBool(raw) } func (a *Agent) templateConfigExitOnRetryFailure() (bool, error) { @@ -809,7 +820,16 @@ func (a *Agent) templateConfigExitOnRetryFailure() (bool, error) { return DefaultTemplateConfigExitOnRetryFailure, nil } - return strconv.ParseBool(raw) + return parseutil.ParseBool(raw) +} + +func (a *Agent) templateConfigMaxConnectionsPerHost() (int64, error) { + raw, ok := a.Annotations[AnnotationTemplateConfigMaxConnectionsPerHost] + if !ok { + return 0, nil + } + + return parseutil.ParseInt(raw) } func (a *Agent) getAutoAuthExitOnError() (bool, error) { @@ -817,7 +837,8 @@ func (a *Agent) getAutoAuthExitOnError() (bool, error) { if !ok { return DefaultAutoAuthEnableOnExit, nil } - return strconv.ParseBool(raw) + + return parseutil.ParseBool(raw) } func (a *Agent) getEnableQuit() (bool, error) { @@ -825,7 +846,7 @@ func (a *Agent) getEnableQuit() (bool, error) { if !ok { return DefaultEnableQuit, nil } - return strconv.ParseBool(raw) + return parseutil.ParseBool(raw) } func (a *Agent) cachePersist(cacheEnabled bool) bool { @@ -841,7 +862,7 @@ func (a *Agent) cacheExitOnErr() (bool, error) { return false, nil } - return strconv.ParseBool(raw) + return parseutil.ParseBool(raw) } func (a *Agent) injectToken() (bool, error) { @@ -849,7 +870,7 @@ func (a *Agent) injectToken() (bool, error) { if !ok { return DefaultAgentInjectToken, nil } - return strconv.ParseBool(raw) + return parseutil.ParseBool(raw) } // telemetryConfig accumulates the agent-telemetry annotations into a map which is diff --git a/agent-inject/agent/annotations_test.go b/agent-inject/agent/annotations_test.go index a3806e44..7ec26213 100644 --- a/agent-inject/agent/annotations_test.go +++ b/agent-inject/agent/annotations_test.go @@ -698,7 +698,7 @@ func TestCouldErrorAnnotations(t *testing.T) { {AnnotationAgentInject, "f", true}, {AnnotationAgentInject, "tRuE", false}, {AnnotationAgentInject, "fAlSe", false}, - {AnnotationAgentInject, "", false}, + {AnnotationAgentInject, "", true}, {AnnotationAgentPrePopulate, "true", true}, {AnnotationAgentPrePopulate, "false", true}, @@ -710,7 +710,7 @@ func TestCouldErrorAnnotations(t *testing.T) { {AnnotationAgentPrePopulate, "f", true}, {AnnotationAgentPrePopulate, "tRuE", false}, {AnnotationAgentPrePopulate, "fAlSe", false}, - {AnnotationAgentPrePopulate, "", false}, + {AnnotationAgentPrePopulate, "", true}, {AnnotationAgentPrePopulateOnly, "true", true}, {AnnotationAgentPrePopulateOnly, "false", true}, @@ -722,7 +722,7 @@ func TestCouldErrorAnnotations(t *testing.T) { {AnnotationAgentPrePopulateOnly, "f", true}, {AnnotationAgentPrePopulateOnly, "tRuE", false}, {AnnotationAgentPrePopulateOnly, "fAlSe", false}, - {AnnotationAgentPrePopulateOnly, "", false}, + {AnnotationAgentPrePopulateOnly, "", true}, {AnnotationVaultTLSSkipVerify, "true", true}, {AnnotationVaultTLSSkipVerify, "false", true}, @@ -734,7 +734,7 @@ func TestCouldErrorAnnotations(t *testing.T) { {AnnotationVaultTLSSkipVerify, "f", true}, {AnnotationVaultTLSSkipVerify, "tRuE", false}, {AnnotationVaultTLSSkipVerify, "fAlSe", false}, - {AnnotationVaultTLSSkipVerify, "", false}, + {AnnotationVaultTLSSkipVerify, "", true}, {AnnotationAgentRevokeOnShutdown, "true", true}, {AnnotationAgentRevokeOnShutdown, "false", true}, @@ -746,7 +746,7 @@ func TestCouldErrorAnnotations(t *testing.T) { {AnnotationAgentRevokeOnShutdown, "f", true}, {AnnotationAgentRevokeOnShutdown, "tRuE", false}, {AnnotationAgentRevokeOnShutdown, "fAlSe", false}, - {AnnotationAgentRevokeOnShutdown, "", false}, + {AnnotationAgentRevokeOnShutdown, "", true}, {AnnotationAgentRevokeGrace, "5", true}, {AnnotationAgentRevokeGrace, "0", true}, @@ -768,12 +768,12 @@ func TestCouldErrorAnnotations(t *testing.T) { {AnnotationAgentShareProcessNamespace, "FALSE", true}, {AnnotationAgentShareProcessNamespace, "tRuE", false}, {AnnotationAgentShareProcessNamespace, "fAlSe", false}, - {AnnotationAgentShareProcessNamespace, "", false}, + {AnnotationAgentShareProcessNamespace, "", true}, {AnnotationAgentSetSecurityContext, "true", true}, {AnnotationAgentSetSecurityContext, "false", true}, {AnnotationAgentSetSecurityContext, "secure", false}, - {AnnotationAgentSetSecurityContext, "", false}, + {AnnotationAgentSetSecurityContext, "", true}, {AnnotationAgentCacheEnable, "true", true}, {AnnotationAgentCacheEnable, "false", true}, @@ -785,7 +785,7 @@ func TestCouldErrorAnnotations(t *testing.T) { {AnnotationAgentCacheEnable, "f", true}, {AnnotationAgentCacheEnable, "tRuE", false}, {AnnotationAgentCacheEnable, "fAlSe", false}, - {AnnotationAgentCacheEnable, "", false}, + {AnnotationAgentCacheEnable, "", true}, {AnnotationAgentAuthMinBackoff, "", true}, {AnnotationAgentAuthMinBackoff, "1s", true}, diff --git a/agent-inject/agent/config.go b/agent-inject/agent/config.go index 387aa897..d512e21f 100644 --- a/agent-inject/agent/config.go +++ b/agent-inject/agent/config.go @@ -121,6 +121,7 @@ type CachePersist struct { type TemplateConfig struct { ExitOnRetryFailure bool `json:"exit_on_retry_failure"` StaticSecretRenderInterval string `json:"static_secret_render_interval,omitempty"` + MaxConnectionsPerHost int64 `json:"max_connections_per_host,omitempty"` } // Telemetry defines the configuration for agent telemetry in Vault Agent. @@ -251,6 +252,7 @@ func (a *Agent) newConfig(init bool) ([]byte, error) { TemplateConfig: &TemplateConfig{ ExitOnRetryFailure: a.VaultAgentTemplateConfig.ExitOnRetryFailure, StaticSecretRenderInterval: a.VaultAgentTemplateConfig.StaticSecretRenderInterval, + MaxConnectionsPerHost: a.VaultAgentTemplateConfig.MaxConnectionsPerHost, }, DisableIdleConnections: a.DisableIdleConnections, DisableKeepAlives: a.DisableKeepAlives, diff --git a/agent-inject/agent/config_test.go b/agent-inject/agent/config_test.go index 283a28ee..b9919b4d 100644 --- a/agent-inject/agent/config_test.go +++ b/agent-inject/agent/config_test.go @@ -565,26 +565,49 @@ func TestConfigVaultAgentTemplateConfig(t *testing.T) { map[string]string{ AnnotationTemplateConfigExitOnRetryFailure: "true", }, - &TemplateConfig{ExitOnRetryFailure: true}, + &TemplateConfig{ + ExitOnRetryFailure: true, + MaxConnectionsPerHost: 0, + }, }, { "exit_on_retry_failure false", map[string]string{ AnnotationTemplateConfigExitOnRetryFailure: "false", }, - &TemplateConfig{ExitOnRetryFailure: false}, + &TemplateConfig{ + ExitOnRetryFailure: false, + MaxConnectionsPerHost: 0, + }, }, { "static_secret_render_interval 10s", map[string]string{ AnnotationTemplateConfigStaticSecretRenderInterval: "10s", }, - &TemplateConfig{ExitOnRetryFailure: true, StaticSecretRenderInterval: "10s"}, + &TemplateConfig{ + ExitOnRetryFailure: true, + StaticSecretRenderInterval: "10s", + MaxConnectionsPerHost: 0, + }, + }, + { + "max_connections_per_host 100", + map[string]string{ + AnnotationTemplateConfigMaxConnectionsPerHost: "100", + }, + &TemplateConfig{ + ExitOnRetryFailure: true, + MaxConnectionsPerHost: 100, + }, }, { "template_config_empty", map[string]string{}, - &TemplateConfig{ExitOnRetryFailure: true}, + &TemplateConfig{ + ExitOnRetryFailure: true, + MaxConnectionsPerHost: 0, + }, }, } diff --git a/agent-inject/handler.go b/agent-inject/handler.go index d3588504..4eed7047 100644 --- a/agent-inject/handler.go +++ b/agent-inject/handler.go @@ -72,6 +72,7 @@ type Handler struct { ResourceLimitEphemeral string ExitOnRetryFailure bool StaticSecretRenderInterval string + MaxConnectionsPerHost int64 AuthMinBackoff string AuthMaxBackoff string DisableIdleConnections string @@ -210,6 +211,7 @@ func (h *Handler) Mutate(req *admissionv1.AdmissionRequest) *admissionv1.Admissi ResourceLimitEphemeral: h.ResourceLimitEphemeral, ExitOnRetryFailure: h.ExitOnRetryFailure, StaticSecretRenderInterval: h.StaticSecretRenderInterval, + MaxConnectionsPerHost: h.MaxConnectionsPerHost, AuthMinBackoff: h.AuthMinBackoff, AuthMaxBackoff: h.AuthMaxBackoff, DisableIdleConnections: h.DisableIdleConnections, diff --git a/go.mod b/go.mod index 7a66b61a..263a3fb6 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/cenkalti/backoff/v4 v4.2.1 github.com/evanphx/json-patch v5.8.1+incompatible github.com/hashicorp/go-hclog v1.6.2 + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.3 github.com/hashicorp/vault/sdk v0.10.2 github.com/kelseyhightower/envconfig v1.4.0 @@ -46,7 +47,6 @@ require ( github.com/google/uuid v1.3.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/hashicorp/go-sockaddr v1.0.5 // indirect github.com/huandu/xstrings v1.3.2 // indirect diff --git a/subcommand/injector/command.go b/subcommand/injector/command.go index 1529b618..94e27e8d 100644 --- a/subcommand/injector/command.go +++ b/subcommand/injector/command.go @@ -51,6 +51,7 @@ type Command struct { flagKeyFile string // TLS private key to serve flagExitOnRetryFailure bool // Set template_config.exit_on_retry_failure on agent flagStaticSecretRenderInterval string // Set template_config.static_secret_render_interval on agent + flagMaxConnectionsPerHost int64 // Set template_config.max_connections_per_host on agent flagAutoName string // MutatingWebhookConfiguration for updating flagAutoHosts string // SANs for the auto-generated TLS cert. flagVaultService string // Name of the Vault service @@ -217,6 +218,7 @@ func (c *Command) Run(args []string) int { ResourceLimitEphemeral: c.flagResourceLimitEphemeral, ExitOnRetryFailure: c.flagExitOnRetryFailure, StaticSecretRenderInterval: c.flagStaticSecretRenderInterval, + MaxConnectionsPerHost: c.flagMaxConnectionsPerHost, AuthMinBackoff: c.flagAuthMinBackoff, AuthMaxBackoff: c.flagAuthMaxBackoff, DisableIdleConnections: c.flagDisableIdleConnections, diff --git a/subcommand/injector/flags.go b/subcommand/injector/flags.go index 5b809f50..b141f749 100644 --- a/subcommand/injector/flags.go +++ b/subcommand/injector/flags.go @@ -12,6 +12,7 @@ import ( "time" "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/go-secure-stdlib/tlsutil" "github.com/hashicorp/vault-k8s/agent-inject/agent" "github.com/hashicorp/vault-k8s/helper/flags" @@ -45,6 +46,10 @@ type Specification struct { // AGENT_INJECT_TEMPLATE_STATIC_SECRET_RENDER_INTERVAL environment variable. TemplateConfigStaticSecretRenderInterval string `envconfig:"AGENT_INJECT_TEMPLATE_STATIC_SECRET_RENDER_INTERVAL"` + // TemplateConfigMaxConnectionsPerHost is the + // AGENT_INJECT_TEMPLATE_MAX_CONNECTIONS_PER_HOST environment variable. + TemplateConfigMaxConnectionsPerHost string `envconfig:"AGENT_INJECT_TEMPLATE_MAX_CONNECTIONS_PER_HOST"` + // TLSAuto is the AGENT_INJECT_TLS_AUTO environment variable. TLSAuto string `envconfig:"tls_auto"` @@ -271,7 +276,7 @@ func (c *Command) parseEnvs() error { } if envs.TemplateConfigExitOnRetryFailure != "" { - c.flagExitOnRetryFailure, err = strconv.ParseBool(envs.TemplateConfigExitOnRetryFailure) + c.flagExitOnRetryFailure, err = parseutil.ParseBool(envs.TemplateConfigExitOnRetryFailure) if err != nil { return err } @@ -281,6 +286,13 @@ func (c *Command) parseEnvs() error { c.flagStaticSecretRenderInterval = envs.TemplateConfigStaticSecretRenderInterval } + if envs.TemplateConfigMaxConnectionsPerHost != "" { + c.flagMaxConnectionsPerHost, err = parseutil.ParseInt(envs.TemplateConfigMaxConnectionsPerHost) + if err != nil { + return err + } + } + if envs.TLSAuto != "" { c.flagAutoName = envs.TLSAuto } @@ -325,7 +337,7 @@ func (c *Command) parseEnvs() error { } if envs.RevokeOnShutdown != "" { - c.flagRevokeOnShutdown, err = strconv.ParseBool(envs.RevokeOnShutdown) + c.flagRevokeOnShutdown, err = parseutil.ParseBool(envs.RevokeOnShutdown) if err != nil { return err } @@ -340,14 +352,14 @@ func (c *Command) parseEnvs() error { } if envs.RunAsSameUser != "" { - c.flagRunAsSameUser, err = strconv.ParseBool(envs.RunAsSameUser) + c.flagRunAsSameUser, err = parseutil.ParseBool(envs.RunAsSameUser) if err != nil { return err } } if envs.SetSecurityContext != "" { - c.flagSetSecurityContext, err = strconv.ParseBool(envs.SetSecurityContext) + c.flagSetSecurityContext, err = parseutil.ParseBool(envs.SetSecurityContext) if err != nil { return err } @@ -358,7 +370,7 @@ func (c *Command) parseEnvs() error { } if envs.UseLeaderElector != "" { - c.flagUseLeaderElector, err = strconv.ParseBool(envs.UseLeaderElector) + c.flagUseLeaderElector, err = parseutil.ParseBool(envs.UseLeaderElector) if err != nil { return err } diff --git a/subcommand/injector/flags_test.go b/subcommand/injector/flags_test.go index 854d9bb1..595593cb 100644 --- a/subcommand/injector/flags_test.go +++ b/subcommand/injector/flags_test.go @@ -198,3 +198,31 @@ func TestCommandEnvBools(t *testing.T) { }) } } + +func TestCommandEnvInts(t *testing.T) { + var cmd Command + tests := []struct { + env string + value int64 + cmdPtr *int64 + }{ + {env: "AGENT_INJECT_TEMPLATE_MAX_CONNECTIONS_PER_HOST", value: 100, cmdPtr: &cmd.flagMaxConnectionsPerHost}, + } + + for _, tt := range tests { + t.Run(tt.env, func(t *testing.T) { + if err := os.Setenv(tt.env, strconv.FormatInt(tt.value, 10)); err != nil { + t.Errorf("got error setting env, shouldn't have: %s", err) + } + defer os.Unsetenv(tt.env) + + if err := cmd.parseEnvs(); err != nil { + t.Errorf("got error parsing envs, shouldn't have: %s", err) + } + + if *tt.cmdPtr != tt.value { + t.Errorf("env wasn't parsed, should have been: got %d, expected %d", *tt.cmdPtr, tt.value) + } + }) + } +}