diff --git a/agent-inject/agent/agent.go b/agent-inject/agent/agent.go index 00181407..23e6388d 100644 --- a/agent-inject/agent/agent.go +++ b/agent-inject/agent/agent.go @@ -297,6 +297,9 @@ type Vault struct { // AuthMinBackoff is the maximum time to backoff if auto auth fails. AuthMaxBackoff string + + // AgentTelemetryConfig is the agent telemetry configuration. + AgentTelemetryConfig map[string]interface{} } type VaultAgentCache struct { @@ -390,6 +393,8 @@ func New(pod *corev1.Pod) (*Agent, error) { return agent, err } + agent.Vault.AgentTelemetryConfig = agent.telemetryConfig() + agent.InitFirst, err = agent.initFirst() if err != nil { return agent, err diff --git a/agent-inject/agent/annotations.go b/agent-inject/agent/annotations.go index 8c455082..c4352628 100644 --- a/agent-inject/agent/annotations.go +++ b/agent-inject/agent/annotations.go @@ -1,6 +1,7 @@ package agent import ( + "encoding/json" "fmt" "path" "strconv" @@ -301,6 +302,11 @@ const ( // AnnotationAgentAutoAuthExitOnError is used to control if a failure in the auto_auth method will cause the agent to exit or try indefinitely (the default). AnnotationAgentAutoAuthExitOnError = "vault.hashicorp.com/agent-auto-auth-exit-on-err" + + // AnnotationAgentTelemetryConfig specifies the Agent Telemetry configuration parameters. + // The name of the parameter is any unique string after "vault.hashicorp.com/agent-telemetry-", + // such as "vault.hashicorp.com/agent-telemetry-foobar". + AnnotationAgentTelemetryConfig = "vault.hashicorp.com/agent-telemetry" ) type AgentConfig struct { @@ -759,8 +765,8 @@ func (a *Agent) setShareProcessNamespace(pod *corev1.Pod) (bool, error) { if pod.Spec.ShareProcessNamespace != nil { if !*pod.Spec.ShareProcessNamespace && shareProcessNamespace { return DefaultAgentShareProcessNamespace, - errors.New("shareProcessNamespace explicitly disabled on the pod, " + - "refusing to enable it") + errors.New("shareProcessNamespace explicitly disabled on the pod, " + + "refusing to enable it") } } @@ -834,6 +840,26 @@ func (a *Agent) injectToken() (bool, error) { return strconv.ParseBool(raw) } +// telemetryConfig accumulates the agent-telemetry annotations into a map which is +// later rendered into the telemetry{} stanza of the Vault Agent config. +func (a *Agent) telemetryConfig() map[string]interface{} { + telemetryConfig := make(map[string]interface{}) + + prefix := fmt.Sprintf("%s-", AnnotationAgentTelemetryConfig) + for annotation, value := range a.Annotations { + if strings.HasPrefix(annotation, prefix) { + param := strings.TrimPrefix(annotation, prefix) + param = strings.ReplaceAll(param, "-", "_") + var v interface{} + if err := json.Unmarshal([]byte(value), &v); err != nil { + v = value + } + telemetryConfig[param] = v + } + } + return telemetryConfig +} + func (a *Agent) authConfig() map[string]interface{} { authConfig := make(map[string]interface{}) diff --git a/agent-inject/agent/annotations_test.go b/agent-inject/agent/annotations_test.go index 746fe20f..f85d7568 100644 --- a/agent-inject/agent/annotations_test.go +++ b/agent-inject/agent/annotations_test.go @@ -1243,3 +1243,46 @@ func TestDisableKeepAlives(t *testing.T) { }) } } + +func TestParseTelemetryAnnotations(t *testing.T) { + tests := map[string]struct { + annotations map[string]string + expectedValues map[string]interface{} + }{ + "prometheus": { + annotations: map[string]string{ + "vault.hashicorp.com/agent-telemetry-prometheus_retention_time": "5s", + "vault.hashicorp.com/agent-telemetry-disable_hostname": "true", + }, + expectedValues: map[string]interface{}{ + "prometheus_retention_time": "5s", + "disable_hostname": true, + }, + }, + "common with some list annotations": { + annotations: map[string]string{ + "vault.hashicorp.com/agent-telemetry-prefix_filter": "[\"+vault.token\", \"-vault.expire\", \"+vault.expire.num_leases\"]", + "vault.hashicorp.com/agent-telemetry-maximum_gauge_cardinality": "3", + "vault.hashicorp.com/agent-telemetry-lease_metrics_epsilon": "foo", + "vault.hashicorp.com/agent-telemetry-enable_hostname_label": "true", + }, + expectedValues: map[string]interface{}{ + "prefix_filter": []interface{}{"+vault.token", "-vault.expire", "+vault.expire.num_leases"}, + "maximum_gauge_cardinality": float64(3), + "lease_metrics_epsilon": "foo", + "enable_hostname_label": true, + }, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + pod := testPod(tc.annotations) + agentConfig := basicAgentConfig() + err := Init(pod, agentConfig) + require.NoError(t, err) + agent, err := New(pod) + require.NoError(t, err) + require.Equal(t, true, reflect.DeepEqual(tc.expectedValues, agent.Vault.AgentTelemetryConfig)) + }) + } +} diff --git a/agent-inject/agent/config.go b/agent-inject/agent/config.go index 71b63298..f8a123fb 100644 --- a/agent-inject/agent/config.go +++ b/agent-inject/agent/config.go @@ -29,6 +29,7 @@ type Config struct { TemplateConfig *TemplateConfig `json:"template_config,omitempty"` DisableIdleConnections []string `json:"disable_idle_connections,omitempty"` DisableKeepAlives []string `json:"disable_keep_alives,omitempty"` + Telemetry *Telemetry `json:"telemetry,omitempty"` } // Vault contains configuration for connecting to Vault servers @@ -119,6 +120,59 @@ type TemplateConfig struct { StaticSecretRenderInterval string `json:"static_secret_render_interval,omitempty"` } +// Telemetry defines the configuration for agent telemetry in Vault Agent. +type Telemetry struct { + UsageGaugePeriod string `json:"usage_gauge_period,omitempty"` + MaximumGaugeCardinality int `json:"maximum_gauge_cardinality,omitempty"` + DisableHostname bool `json:"disable_hostname,omitempty"` + EnableHostnameLabel bool `json:"enable_hostname_label,omitempty"` + LeaseMetricsEpsilon string `json:"lease_metrics_epsilon,omitempty"` + NumLeaseMetricsBuckets int `json:"num_lease_metrics_buckets,omitempty"` + AddLeaseMetricsNamespaceLabels bool `json:"add_lease_metrics_namespace_labels,omitempty"` + FilterDefault bool `json:"filter_default,omitempty"` + PrefixFilter []string `json:"prefix_filter,omitempty"` + StatsiteAddress string `json:"statsite_address,omitempty"` + StatsdAddress string `json:"statsd_address,omitempty"` + CirconusApiToken string `json:"circonus_api_token,omitempty"` + CirconusApiApp string `json:"circonus_api_app,omitempty"` + CirconusApiURL string `json:"circonus_api_url,omitempty"` + CirconusSubmissionInterval string `json:"circonus_submission_interval,omitempty"` + CirconusSubmissionURL string `json:"circonus_submission_url,omitempty"` + CirconusCheckID string `json:"circonus_check_id,omitempty"` + CirconusCheckForceMetricActivation bool `json:"circonus_check_force_metric_activation,omitempty"` + CirconusCheckInstanceID string `json:"circonus_check_instance_id,omitempty"` + CirconusCheckSearchTag string `json:"circonus_check_search_tag,omitempty"` + CirconusCheckDisplayName string `json:"circonus_check_display_name,omitempty"` + CirconusCheckTags string `json:"circonus_check_tags,omitempty"` + CirconusBrokerID string `json:"circonus_broker_id,omitempty"` + CirconusBrokerSelectTag string `json:"circonus_broker_select_tag,omitempty"` + DogstatsdAddr string `json:"dogstatsd_addr,omitempty"` + DogstatsdTags []string `json:"dogstatsd_tags,omitempty"` + PrometheusRetentionTime string `json:"prometheus_retention_time,omitempty"` + StackdriverProjectID string `json:"stackdriver_project_id,omitempty"` + StackdriverLocation string `json:"stackdriver_location,omitempty"` + StackdriverNamespace string `json:"stackdriver_namespace,omitempty"` + StackdriverDebugLogs bool `json:"stackdriver_debug_logs,omitempty"` +} + +// newTelemetryConfig creates a Telemetry object from the accumulated agent telemetry annotations. +func (a *Agent) newTelemetryConfig() *Telemetry { + var tel Telemetry + if len(a.Vault.AgentTelemetryConfig) == 0 { + return nil + } + // First get it out of the map[string]interface{} which was created when we parsed the annotations. + telemetryBytes, err := json.Marshal(a.Vault.AgentTelemetryConfig) + if err != nil { + return nil + } + // Unmarshal it into a Telemetry object. + if err = json.Unmarshal(telemetryBytes, &tel); err != nil { + return nil + } + return &tel +} + func (a *Agent) newTemplateConfigs() []*Template { var templates []*Template for _, secret := range a.Secrets { @@ -190,6 +244,7 @@ func (a *Agent) newConfig(init bool) ([]byte, error) { }, }, Templates: a.newTemplateConfigs(), + Telemetry: a.newTelemetryConfig(), TemplateConfig: &TemplateConfig{ ExitOnRetryFailure: a.VaultAgentTemplateConfig.ExitOnRetryFailure, StaticSecretRenderInterval: a.VaultAgentTemplateConfig.StaticSecretRenderInterval, diff --git a/agent-inject/agent/config_test.go b/agent-inject/agent/config_test.go index bceb8423..9dd9a31e 100644 --- a/agent-inject/agent/config_test.go +++ b/agent-inject/agent/config_test.go @@ -766,3 +766,108 @@ func TestConfigAgentQuit(t *testing.T) { }) } } + +func TestConfigTelemetry(t *testing.T) { + tests := []struct { + name string + annotations map[string]string + expectedTelemetry *Telemetry + }{ + { + "annotations that exercise all of the annotations", + map[string]string{ + "vault.hashicorp.com/agent-telemetry-usage_gauge_period": "10m", + "vault.hashicorp.com/agent-telemetry-maximum_gauge_cardinality": "500", + "vault.hashicorp.com/agent-telemetry-disable_hostname": "false", + "vault.hashicorp.com/agent-telemetry-enable_hostname_label": "false", + "vault.hashicorp.com/agent-telemetry-lease_metrics_epsilon": "1h", + "vault.hashicorp.com/agent-telemetry-num_lease_metrics_buckets": "168", + "vault.hashicorp.com/agent-telemetry-add_lease_metrics_namespace_labels": "false", + "vault.hashicorp.com/agent-telemetry-filter_default": "true", + "vault.hashicorp.com/agent-telemetry-statsite_address": "https://foo.com", + "vault.hashicorp.com/agent-telemetry-statsd_address": "https://foo.com", + "vault.hashicorp.com/agent-telemetry-circonus_api_token": "foo", + "vault.hashicorp.com/agent-telemetry-circonus_api_app": "nomad", + "vault.hashicorp.com/agent-telemetry-circonus_api_url": "https://api.circonus.com/v2", + "vault.hashicorp.com/agent-telemetry-circonus_submission_interval": "10s", + "vault.hashicorp.com/agent-telemetry-circonus_submission_url": "https://api.circonus.com/v2", + "vault.hashicorp.com/agent-telemetry-circonus_check_id": "foo", + "vault.hashicorp.com/agent-telemetry-circonus_check_force_metric_activation": "false", + "vault.hashicorp.com/agent-telemetry-circonus_check_instance_id": "foo:bar", + "vault.hashicorp.com/agent-telemetry-circonus_check_search_tag": "foo:bar", + "vault.hashicorp.com/agent-telemetry-circonus_check_display_name": "foo", + "vault.hashicorp.com/agent-telemetry-circonus_check_tags": "foo,bar", + "vault.hashicorp.com/agent-telemetry-circonus_broker_id": "foo", + "vault.hashicorp.com/agent-telemetry-circonus_broker_select_tag": "foo:bar", + "vault.hashicorp.com/agent-telemetry-dogstatsd_addr": "https://foo.com", + "vault.hashicorp.com/agent-telemetry-dogstatsd_tags": `["foo:bar", "foo:baz"]`, + "vault.hashicorp.com/agent-telemetry-prometheus_retention_time": "24h", + "vault.hashicorp.com/agent-telemetry-stackdriver_project_id": "foo", + "vault.hashicorp.com/agent-telemetry-stackdriver_location": "useast-1", + "vault.hashicorp.com/agent-telemetry-stackdriver_namespace": "foo", + "vault.hashicorp.com/agent-telemetry-stackdriver_debug_logs": "false", + "vault.hashicorp.com/agent-telemetry-prefix_filter": `["+vault.token", "-vault.expire", "+vault.expire.num_leases"]`, + }, + &Telemetry{ + UsageGaugePeriod: "10m", + MaximumGaugeCardinality: 500, + DisableHostname: false, + EnableHostnameLabel: false, + LeaseMetricsEpsilon: "1h", + NumLeaseMetricsBuckets: 168, + AddLeaseMetricsNamespaceLabels: false, + FilterDefault: true, + PrefixFilter: []string{"+vault.token", "-vault.expire", "+vault.expire.num_leases"}, + StatsiteAddress: "https://foo.com", + StatsdAddress: "https://foo.com", + CirconusApiToken: "foo", + CirconusApiApp: "nomad", + CirconusApiURL: "https://api.circonus.com/v2", + CirconusSubmissionInterval: "10s", + CirconusSubmissionURL: "https://api.circonus.com/v2", + CirconusCheckID: "foo", + CirconusCheckForceMetricActivation: false, + CirconusCheckInstanceID: "foo:bar", + CirconusCheckSearchTag: "foo:bar", + CirconusCheckDisplayName: "foo", + CirconusCheckTags: "foo,bar", + CirconusBrokerID: "foo", + CirconusBrokerSelectTag: "foo:bar", + DogstatsdAddr: "https://foo.com", + DogstatsdTags: []string{"foo:bar", "foo:baz"}, + PrometheusRetentionTime: "24h", + StackdriverProjectID: "foo", + StackdriverLocation: "useast-1", + StackdriverNamespace: "foo", + StackdriverDebugLogs: false, + }, + }, + { + "everything empty", + map[string]string{}, + nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pod := testPod(tt.annotations) + + agentConfig := basicAgentConfig() + err := Init(pod, agentConfig) + require.NoError(t, err) + + agent, err := New(pod) + require.NoError(t, err) + // create sidecar config + cfg, err := agent.newConfig(false) + require.NoError(t, err) + + config := &Config{} + err = json.Unmarshal(cfg, config) + require.NoError(t, err) + + require.Equal(t, tt.expectedTelemetry, config.Telemetry) + }) + } +}