// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package envoy

import (
	"bytes"
	"crypto/sha1"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net"
	"net/url"
	"os"
	"path"
	"strings"
	"text/template"

	"github.com/hashicorp/consul/api"
)

const (
	selfAdminName = "self_admin"
)

// BootstrapConfig is the set of keys we care about in a Connect.Proxy.Config
// map. Note that this only includes config keys that affects Envoy bootstrap
// generation. For Envoy config keys that affect runtime xDS behavior see
// agent/xds/config.go.
type BootstrapConfig struct {
	// StatsdURL allows simple configuration of the statsd metrics sink. If
	// tagging is required, use DogstatsdURL instead. The URL must be in one of
	// the following forms:
	//   - udp://<ip>:<port>
	//   - $ENV_VAR_NAME        in this case the ENV var named will have it's
	//                          value taken and is expected to contain a URL in
	//									 				one of the supported forms above.
	StatsdURL string `mapstructure:"envoy_statsd_url"`

	// DogstatsdURL allows simple configuration of the dogstatsd metrics sink
	// which allows tags and Unix domain sockets. The URL must be in one of the
	// following forms:
	//   - udp://<ip>:<port>
	//   - unix:///full/path/to/unix.sock
	//   - $ENV_VAR_NAME        in this case the ENV var named will have it's
	//                          value taken and is expected to contain a URL in
	//									 				one of the supported forms above.
	DogstatsdURL string `mapstructure:"envoy_dogstatsd_url"`

	// StatsTags is a slice of string values that will be added as tags to
	// metrics. They are used to configure
	// https://www.envoyproxy.io/docs/envoy/v1.9.0/api-v2/config/metrics/v2/stats.proto#envoy-api-msg-config-metrics-v2-statsconfig
	// and add to the basic tags Consul adds by default like the local_cluster
	// name. Only exact values are supported here. Full configuration of
	// stats_config.stats_tags can be made by overriding envoy_stats_config_json.
	StatsTags []string `mapstructure:"envoy_stats_tags"`

	// TelemetryCollectorBindSocketDir is a string that configures the directory for a
	// unix socket where Envoy will forward metrics. These metrics get pushed to
	// the telemetry collector.
	TelemetryCollectorBindSocketDir string `mapstructure:"envoy_telemetry_collector_bind_socket_dir"`

	// PrometheusBindAddr configures an <ip>:<port> on which the Envoy will listen
	// and expose a single /metrics HTTP endpoint for Prometheus to scrape. It
	// does this by proxying that URL to the internal admin server's prometheus
	// endpoint which allows exposing metrics on the network without the security
	// risk of exposing the full admin server API. Any other URL requested will be
	// a 404.
	//
	// Note that as of Envoy 1.9.0, the built in Prometheus endpoint only exports
	// counters and gauges but not timing information via histograms. This is
	// fixed in 1.10-dev currently in Envoy master. Other changes since 1.9.0 make
	// master incompatible with the current release of Consul Connect. This will
	// be fixed in a future Consul version as Envoy 1.10 reaches stable release.
	PrometheusBindAddr string `mapstructure:"envoy_prometheus_bind_addr"`

	// StatsBindAddr configures an <ip>:<port> on which the Envoy will listen
	// and expose the /stats HTTP path prefix for any agent to access. It
	// does this by proxying that path prefix to the internal admin server
	// which allows exposing metrics on the network without the security
	// risk of exposing the full admin server API. Any other URL requested will be
	// a 404.
	StatsBindAddr string `mapstructure:"envoy_stats_bind_addr"`

	// ReadyBindAddr configures an <ip>:<port> on which Envoy will listen and
	// expose a single /ready HTTP endpoint. This is useful for checking the
	// liveness of an Envoy instance when no other listeners are garaunteed to be
	// configured, as is the case with ingress gateways.
	//
	// Note that we do not allow this to be configured via the service
	// definition config map currently.
	ReadyBindAddr string `mapstructure:"-"`

	// OverrideJSONTpl allows replacing the base template used to render the
	// bootstrap. This is an "escape hatch" allowing arbitrary control over the
	// proxy's configuration but will the most effort to maintain and correctly
	// configure the aspects that Connect relies upon to work. It's recommended
	// that this only be used if necessary, and that it be based on the default
	// template in
	// https://github.com/hashicorp/consul/blob/main/command/connect/envoy/bootstrap_tpl.go
	// for the correct version of Consul and Envoy being used.
	OverrideJSONTpl string `mapstructure:"envoy_bootstrap_json_tpl"`

	// StaticClustersJSON is a JSON string containing zero or more Cluster
	// definitions. They are appended to the "static_resources.clusters" list. A
	// single cluster should be given as a plain object, if more than one is to be
	// added, they should be separated by a comma suitable for direct injection
	// into a JSON array.
	//
	// Note that cluster names should be chosen in such a way that they won't
	// collide with service names since we use plain service names as cluster
	// names in xDS to make metrics population simpler and cluster names mush be
	// unique.
	//
	// This is mostly intended for providing clusters for tracing or metrics
	// services.
	//
	// See https://www.envoyproxy.io/docs/envoy/v1.9.0/api-v2/api/v2/cds.proto.
	StaticClustersJSON string `mapstructure:"envoy_extra_static_clusters_json"`

	// StaticListenersJSON is a JSON string containing zero or more Listener
	// definitions. They are appended to the "static_resources.listeners" list. A
	// single listener should be given as a plain object, if more than one is to
	// be added, they should be separated by a comma suitable for direct injection
	// into a JSON array.
	//
	// See https://www.envoyproxy.io/docs/envoy/v1.9.0/api-v2/api/v2/lds.proto.
	StaticListenersJSON string `mapstructure:"envoy_extra_static_listeners_json"`

	// StatsSinksJSON is a JSON string containing zero or more StatsSink
	// definititions. They are appended to the `stats_sinks` array at the top
	// level of the bootstrap config. A single sink should be given as a plain
	// object, if more than one is to be added, they should be separated by a
	// comma suitable for direct injection into a JSON array.
	//
	// See
	// https://www.envoyproxy.io/docs/envoy/v1.9.0/api-v2/config/metrics/v2/stats.proto#config-metrics-v2-statssink.
	//
	// If this is non-empty then it will override anything configured in
	// StatsTags.
	StatsSinksJSON string `mapstructure:"envoy_extra_stats_sinks_json"`

	// StatsConfigJSON is a JSON string containing an object in the right format
	// to be rendered as the body of the `stats_config` field at the top level of
	// the bootstrap config. It's format may vary based on Envoy version used. See
	// https://www.envoyproxy.io/docs/envoy/v1.9.0/api-v2/config/metrics/v2/stats.proto#envoy-api-msg-config-metrics-v2-statsconfig.
	//
	// If this is non-empty then it will override anything configured in
	// StatsdURL or DogstatsdURL.
	StatsConfigJSON string `mapstructure:"envoy_stats_config_json"`

	// StatsFlushInterval is the time duration between Envoy stats flushes. It is
	// in proto3 "duration" string format for example "1.12s" See
	// https://developers.google.com/protocol-buffers/docs/proto3#json and
	// https://www.envoyproxy.io/docs/envoy/v1.9.0/api-v2/config/bootstrap/v2/bootstrap.proto#bootstrap
	StatsFlushInterval string `mapstructure:"envoy_stats_flush_interval"`

	// TracingConfigJSON is a JSON string containing an object in the right format
	// to be rendered as the body of the `tracing` field at the top level of
	// the bootstrap config. It's format may vary based on Envoy version used.
	// See https://www.envoyproxy.io/docs/envoy/v1.9.0/api-v2/config/trace/v2/trace.proto.
	TracingConfigJSON string `mapstructure:"envoy_tracing_json"`
}

// Template returns the bootstrap template to use as a base.
func (c *BootstrapConfig) Template() string {
	if c.OverrideJSONTpl != "" {
		return c.OverrideJSONTpl
	}
	return bootstrapTemplate
}

func (c *BootstrapConfig) GenerateJSON(args *BootstrapTplArgs, omitDeprecatedTags bool) ([]byte, error) {
	if err := c.ConfigureArgs(args, omitDeprecatedTags); err != nil {
		return nil, err
	}
	t, err := template.New("bootstrap").Parse(c.Template())
	if err != nil {
		return nil, err
	}
	var buf bytes.Buffer
	err = t.Execute(&buf, args)
	if err != nil {
		return nil, err
	}

	// Pretty print the JSON.
	var buf2 bytes.Buffer
	if err := json.Indent(&buf2, buf.Bytes(), "", "  "); err != nil {
		return nil, err
	}

	return buf2.Bytes(), nil
}

// ConfigureArgs takes the basic template arguments generated from the command
// arguments and environment and modifies them according to the BootstrapConfig.
func (c *BootstrapConfig) ConfigureArgs(args *BootstrapTplArgs, omitDeprecatedTags bool) error {

	// Attempt to setup sink(s) from high-level config. Note the args are passed
	// by ref and modified in place.
	if err := c.generateStatsSinks(args); err != nil {
		return err
	}

	if c.StatsConfigJSON != "" {
		// StatsConfig overridden explicitly
		args.StatsConfigJSON = c.StatsConfigJSON
	} else {
		// Attempt to setup tags from high-level config. Note the args are passed by
		// ref and modified in place.
		stats, err := generateStatsTags(args, c.StatsTags, omitDeprecatedTags)
		if err != nil {
			return err
		}
		args.StatsConfigJSON = formatStatsTags(stats)
	}

	if c.StaticClustersJSON != "" {
		args.StaticClustersJSON = c.StaticClustersJSON
	}
	if c.StaticListenersJSON != "" {
		args.StaticListenersJSON = c.StaticListenersJSON
	}
	// Setup prometheus if needed. This MUST happen after the Static*JSON is set above
	if c.PrometheusBindAddr != "" {
		if err := c.generateListenerConfig(args, c.PrometheusBindAddr, "envoy_prometheus_metrics", "path", args.PrometheusScrapePath, "/stats/prometheus", args.PrometheusBackendPort); err != nil {
			return err
		}
	}
	// Setup /stats proxy listener if needed. This MUST happen after the Static*JSON is set above
	if c.StatsBindAddr != "" {
		if err := c.generateListenerConfig(args, c.StatsBindAddr, "envoy_metrics", "prefix", "/stats", "/stats", ""); err != nil {
			return err
		}
	}
	// Setup /ready proxy listener if needed. This MUST happen after the Static*JSON is set above
	if c.ReadyBindAddr != "" {
		if err := c.generateListenerConfig(args, c.ReadyBindAddr, "envoy_ready", "path", "/ready", "/ready", ""); err != nil {
			return err
		}
	}

	if c.TracingConfigJSON != "" {
		args.TracingConfigJSON = c.TracingConfigJSON
	}

	if c.StatsFlushInterval != "" {
		args.StatsFlushInterval = c.StatsFlushInterval
	}

	// Setup telemetry collector if needed. This MUST happen after the Static*JSON is set above
	if c.TelemetryCollectorBindSocketDir != "" {
		// Override StatsFlushInterval as 60 seconds (1 minute) to reduce number of metric flushes.
		// Only perform this override if there is no custom configuration for stats sinks and flush interval.
		if c.StatsFlushInterval == "" && args.StatsSinksJSON == "" {
			args.StatsFlushInterval = "60s"
		}
		appendTelemetryCollectorConfig(args, c.TelemetryCollectorBindSocketDir)
	}

	return nil
}

func (c *BootstrapConfig) generateStatsSinks(args *BootstrapTplArgs) error {
	var stats_sinks []string

	if c.StatsdURL != "" {
		sinkJSON, err := c.generateStatsSinkJSON(
			"envoy.stat_sinks.statsd",
			"type.googleapis.com/envoy.config.metrics.v3.StatsdSink",
			c.StatsdURL,
		)
		if err != nil {
			return err
		}
		stats_sinks = append(stats_sinks, sinkJSON)
	}
	if c.DogstatsdURL != "" {
		sinkJSON, err := c.generateStatsSinkJSON(
			"envoy.stat_sinks.dog_statsd",
			"type.googleapis.com/envoy.config.metrics.v3.DogStatsdSink",
			c.DogstatsdURL,
		)
		if err != nil {
			return err
		}
		stats_sinks = append(stats_sinks, sinkJSON)
	}
	if c.StatsSinksJSON != "" {
		stats_sinks = append(stats_sinks, c.StatsSinksJSON)
	}

	if len(stats_sinks) > 0 {
		args.StatsSinksJSON = strings.Join(stats_sinks, ",\n")
	}
	return nil
}

func (c *BootstrapConfig) generateStatsSinkJSON(name string, typeName string, addr string) (string, error) {
	// Resolve address ENV var
	if len(addr) > 2 && addr[0] == '$' {
		addr = os.Getenv(addr[1:])
	} else {
		addr = os.Expand(addr, statsSinkEnvMapping)
	}

	u, err := url.Parse(addr)
	if err != nil {
		return "", fmt.Errorf("failed to parse %s sink address %q", name, addr)
	}

	var addrJSON string
	switch u.Scheme {
	case "udp":
		addrJSON = `
		"socket_address": {
			"address": "` + u.Hostname() + `",
			"port_value": ` + u.Port() + `
		}
		`
	case "unix":
		addrJSON = `
		"pipe": {
			"path": "` + u.Path + `"
		}
		`
	default:
		return "", fmt.Errorf("unsupported address protocol %q for %s sink",
			u.Scheme, name)
	}

	return `{
		"name": "` + name + `",
		"typedConfig": {
			"@type": "` + typeName + `",
			"address": {
				` + addrJSON + `
			}
		}
	}`, nil
}

func statsSinkEnvMapping(s string) string {
	allowedStatsSinkEnvVars := map[string]bool{
		"HOST_IP": true,
	}

	if !allowedStatsSinkEnvVars[s] {
		// if the specified env var isn't explicitly allowed, unexpand it
		return fmt.Sprintf("${%s}", s)
	}
	return os.Getenv(s)
}

// resourceTagSpecifiers returns patterns used to generate tags from cluster and filter metric names.
func resourceTagSpecifiers(omitDeprecatedTags bool) ([]string, error) {
	const (
		reSegment = `[^.]+`
	)

	// For all rules:
	//   - The outer capture group is removed from the final metric name.
	//   - The inner capture group is extracted into labels.
	rules := [][]string{
		// Cluster metrics are prefixed by consul.destination
		//
		// Cluster metric name format:
		// <subset>.<service>.<namespace>.<partition>.<datacenter|peering>.<internal|internal-<version>|external>.<trustdomain>.consul
		//
		// Examples:
		// (default partition)
		// - cluster.pong.default.dc2.internal.e5b08d03-bfc3-c870-1833-baddb116e648.consul.bind_errors: 0
		// - cluster.f8f8f8f8~pong.default.dc2.internal.e5b08d03-bfc3-c870-1833-baddb116e648.consul.bind_errors: 0
		// - cluster.v2.pong.default.dc2.internal.e5b08d03-bfc3-c870-1833-baddb116e648.consul.bind_errors: 0
		// - cluster.f8f8f8f8~v2.pong.default.dc2.internal.e5b08d03-bfc3-c870-1833-baddb116e648.consul.bind_errors: 0
		// - cluster.passthrough~pong.default.dc2.internal.e5b08d03-bfc3-c870-1833-baddb116e648.consul.bind_errors: 0
		// (non-default partition)
		// - cluster.pong.default.partA.dc2.internal-v1.e5b08d03-bfc3-c870-1833-baddb116e648.consul.bind_errors: 0
		// - cluster.f8f8f8f8~pong.default.partA.dc2.internal-v1.e5b08d03-bfc3-c870-1833-baddb116e648.consul.bind_errors: 0
		// - cluster.v2.pong.default.partA.dc2.internal-v1.e5b08d03-bfc3-c870-1833-baddb116e648.consul.bind_errors: 0
		// - cluster.f8f8f8f8~v2.pong.default.partA.dc2.internal-v1.e5b08d03-bfc3-c870-1833-baddb116e648.consul.bind_errors: 0
		// - cluster.passthrough~pong.default.partA.dc2.internal-v1.e5b08d03-bfc3-c870-1833-baddb116e648.consul.bind_errors: 0
		// (peered)
		// - cluster.pong.default.cloudpeer.external.e5b08d03-bfc3-c870-1833-baddb116e648.consul.bind_errors: 0
		{"consul.destination.custom_hash",
			fmt.Sprintf(`^cluster\.(?:passthrough~)?((?:(%s)~)?(?:%s\.)?%s\.%s\.(?:%s\.)?%s\.%s\.%s\.consul\.)`,
				reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},

		{"consul.destination.service_subset",
			fmt.Sprintf(`^cluster\.(?:passthrough~)?((?:%s~)?(?:(%s)\.)?%s\.%s\.(?:%s\.)?%s\.%s\.%s\.consul\.)`,
				reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},

		{"consul.destination.service",
			fmt.Sprintf(`^cluster\.(?:passthrough~)?((?:%s~)?(?:%s\.)?(%s)\.%s\.(?:%s\.)?%s\.%s\.%s\.consul\.)`,
				reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},

		{"consul.destination.namespace",
			fmt.Sprintf(`^cluster\.(?:passthrough~)?((?:%s~)?(?:%s\.)?%s\.(%s)\.(?:%s\.)?%s\.%s\.%s\.consul\.)`,
				reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},

		{"consul.destination.partition",
			fmt.Sprintf(`^cluster\.(?:passthrough~)?((?:%s~)?(?:%s\.)?%s\.%s\.(?:(%s)\.)?%s\.internal[^.]*\.%s\.consul\.)`,
				reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},

		{"consul.destination.datacenter",
			fmt.Sprintf(`^cluster\.(?:passthrough~)?((?:%s~)?(?:%s\.)?%s\.%s\.(?:%s\.)?(%s)\.internal[^.]*\.%s\.consul\.)`,
				reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},

		{"consul.destination.peer",
			fmt.Sprintf(`^cluster\.(%s\.(?:%s\.)?(%s)\.external\.%s\.consul\.)`,
				reSegment, reSegment, reSegment, reSegment)},

		{"consul.destination.routing_type",
			fmt.Sprintf(`^cluster\.(?:passthrough~)?((?:%s~)?(?:%s\.)?%s\.%s\.(?:%s\.)?%s\.(%s)\.%s\.consul\.)`,
				reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},

		{"consul.destination.trust_domain",
			fmt.Sprintf(`^cluster\.(?:passthrough~)?((?:%s~)?(?:%s\.)?%s\.%s\.(?:%s\.)?%s\.%s\.(%s)\.consul\.)`,
				reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},

		{"consul.destination.target",
			fmt.Sprintf(`^cluster\.(?:passthrough~)?(((?:%s~)?(?:%s\.)?%s\.%s\.(?:%s\.)?%s)\.%s\.%s\.consul\.)`,
				reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},

		{"consul.destination.full_target",
			fmt.Sprintf(`^cluster\.(?:passthrough~)?(((?:%s~)?(?:%s\.)?%s\.%s\.(?:%s\.)?%s\.%s\.%s)\.consul\.)`,
				reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},

		// Upstream listener metrics are prefixed by consul.upstream
		//
		// Listener metric name format:
		// <tcp|http>.upstream.<service>.<namespace>.<partition>.<datacenter>
		//
		// Examples:
		// - tcp.upstream.db.dc1.downstream_cx_total: 0
		// - http.upstream.web.frontend.west.dc1.downstream_cx_total: 0
		//
		// Peered Listener metric name format:
		// <tcp|http>.upstream_peered.<service>.<namespace>.peer
		//
		// Examples:
		// - http.upstream_peered.web.frontend.cloudpeer.downstream_cx_total: 0
		{"consul.upstream.service",
			fmt.Sprintf(`^(?:tcp|http)\.upstream(?:_peered)?\.((%s)(?:\.%s)?(?:\.%s)?\.%s\.)`,
				reSegment, reSegment, reSegment, reSegment)},

		{"consul.upstream.datacenter",
			fmt.Sprintf(`^(?:tcp|http)\.upstream\.(%s(?:\.%s)?(?:\.%s)?\.(%s)\.)`,
				reSegment, reSegment, reSegment, reSegment)},

		{"consul.upstream.peer",
			fmt.Sprintf(`^(?:tcp|http)\.upstream_peered\.(%s(?:\.%s)?\.(%s)\.)`,
				reSegment, reSegment, reSegment)},

		{"consul.upstream.namespace",
			fmt.Sprintf(`^(?:tcp|http)\.upstream(?:_peered)?\.(%s(?:\.(%s))?(?:\.%s)?\.%s\.)`,
				reSegment, reSegment, reSegment, reSegment)},

		{"consul.upstream.partition",
			fmt.Sprintf(`^(?:tcp|http)\.upstream\.(%s(?:\.%s)?(?:\.(%s))?\.%s\.)`,
				reSegment, reSegment, reSegment, reSegment)},
	}

	// These tags were deprecated in Consul 1.9.0
	// We are leaving them enabled by default for backwards compatibility
	if !omitDeprecatedTags {
		deprecatedRules := [][]string{
			{"consul.custom_hash",
				fmt.Sprintf(`^cluster\.((?:(%s)~)?(?:%s\.)?%s\.%s\.(?:%s\.)?%s\.%s\.%s\.consul\.)`,
					reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},

			{"consul.service_subset",
				fmt.Sprintf(`^cluster\.((?:%s~)?(?:(%s)\.)?%s\.%s\.(?:%s\.)?%s\.%s\.%s\.consul\.)`,
					reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},

			{"consul.service",
				fmt.Sprintf(`^cluster\.((?:%s~)?(?:%s\.)?(%s)\.%s\.(?:%s\.)?%s\.%s\.%s\.consul\.)`,
					reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},

			{"consul.namespace",
				fmt.Sprintf(`^cluster\.((?:%s~)?(?:%s\.)?%s\.(%s)\.(?:%s\.)?%s\.%s\.%s\.consul\.)`,
					reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},

			{"consul.datacenter",
				fmt.Sprintf(`^cluster\.((?:%s~)?(?:%s\.)?%s\.%s\.(?:%s\.)?(%s)\.internal[^.]*\.%s\.consul\.)`,
					reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},

			{"consul.routing_type",
				fmt.Sprintf(`^cluster\.((?:%s~)?(?:%s\.)?%s\.%s\.(?:%s\.)?%s\.(%s)\.%s\.consul\.)`,
					reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},

			{"consul.trust_domain",
				fmt.Sprintf(`^cluster\.((?:%s~)?(?:%s\.)?%s\.%s\.(?:%s\.)?%s\.%s\.(%s)\.consul\.)`,
					reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},

			{"consul.target",
				fmt.Sprintf(`^cluster\.(((?:%s~)?(?:%s\.)?%s\.%s\.(?:%s\.)?%s)\.%s\.%s\.consul\.)`,
					reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},

			{"consul.full_target",
				fmt.Sprintf(`^cluster\.(((?:%s~)?(?:%s\.)?%s\.%s\.(?:%s\.)?%s\.%s\.%s)\.consul\.)`,
					reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment, reSegment)},
		}
		rules = append(rules, deprecatedRules...)
	}

	var tags []string
	for _, rule := range rules {
		m := map[string]string{
			"tag_name": rule[0],
			"regex":    rule[1],
		}
		d, err := json.Marshal(m)
		if err != nil {
			return nil, err
		}
		tags = append(tags, string(d))
	}
	return tags, nil
}

func formatStatsTags(tags []string) string {
	var output string
	if len(tags) > 0 {
		// use_all_default_tags is true by default but we'll make it explicit!
		output = `{
			"stats_tags": [
				` + strings.Join(tags, ",\n") + `
			],
			"use_all_default_tags": true
		}`
	}
	return output
}

func generateStatsTags(args *BootstrapTplArgs, initialTags []string, omitDeprecatedTags bool) ([]string, error) {
	var (
		// Track tags we are setting explicitly to exclude them from defaults
		tagNames = make(map[string]struct{})
		tagJSONs []string
	)

	for _, tag := range initialTags {
		parts := strings.SplitN(tag, "=", 2)
		// If there is no equals, treat it as a boolean tag and just assign value of
		// 1 e.g. "canary" will out put the tag "canary: 1"
		v := "1"
		if len(parts) == 2 {
			v = parts[1]
		}
		k := strings.ToLower(parts[0])
		tagJSON := `{
			"tag_name": "` + k + `",
			"fixed_value": "` + v + `"
		}`
		tagJSONs = append(tagJSONs, tagJSON)
		tagNames[k] = struct{}{}
	}

	// Explode listener and cluster portions.
	tags, err := resourceTagSpecifiers(omitDeprecatedTags)
	if err != nil {
		return nil, fmt.Errorf("failed to generate resource-specific envoy tags: %v", err)
	}
	tagJSONs = append(tagJSONs, tags...)

	// Default the namespace and partition here since it is also done for cluster SNI
	ns := args.Namespace
	if ns == "" {
		ns = api.IntentionDefaultNamespace
	}

	ap := args.Partition
	if ap == "" {
		ap = api.IntentionDefaultNamespace
	}

	// Add some default tags if not already overridden. Note this is a slice not a
	// map since we need ordering to be deterministic.
	defaults := []struct {
		name string
		val  string
	}{
		// local_cluster is for backwards compatibility. We originally choose this
		// name as it matched a few other Envoy metrics examples given in docs but
		// it's a little confusing in context of setting up metrics dashboards.
		{
			name: "local_cluster",
			val:  args.ProxyCluster,
		},
		{
			name: "consul.source.service",
			val:  args.ProxySourceService,
		},
		{
			name: "consul.source.namespace",
			val:  ns,
		},
		{
			name: "consul.source.partition",
			val:  ap,
		},
		{
			name: "consul.source.datacenter",
			val:  args.Datacenter,
		},
	}

	for _, kv := range defaults {
		if kv.val == "" {
			// Skip stuff we just didn't have data for.
			continue
		}
		if _, ok := tagNames[kv.name]; ok {
			// Skip anything already set explicitly.
			continue
		}
		tagJSON := `{
			"tag_name": "` + kv.name + `",
			"fixed_value": "` + kv.val + `"
		}`
		tagJSONs = append(tagJSONs, tagJSON)
	}
	return tagJSONs, nil
}

func (c *BootstrapConfig) generateListenerConfig(args *BootstrapTplArgs, bindAddr, name, matchType, matchValue, prefixRewrite, prometheusBackendPort string) error {
	host, port, err := net.SplitHostPort(bindAddr)
	if err != nil {
		return fmt.Errorf("invalid %s bind address: %s", name, err)
	}

	// If prometheusBackendPort is set (not empty string), create
	// "prometheus_backend" cluster with the prometheusBackendPort that the
	// listener will point to, rather than the "self_admin" cluster. This is for
	// the merged metrics feature in consul-k8s, so the
	// envoy_prometheus_bind_addr listener will point to the merged Envoy and
	// service metrics endpoint rather than the Envoy admin endpoint for
	// metrics. This cluster will only be created once since it's only created
	// when prometheusBackendPort is set, and prometheusBackendPort is only set
	// when calling this function if c.PrometheusBindAddr is set.
	clusterAddress := args.AdminBindAddress
	clusterPort := args.AdminBindPort
	clusterName := selfAdminName
	if prometheusBackendPort != "" {
		clusterPort = prometheusBackendPort
		clusterName = "prometheus_backend"
	}

	clusterJSON := `{
		"name": "` + clusterName + `",
		"ignore_health_on_host_removal": false,
		"connect_timeout": "5s",
		"type": "STATIC",
		"typed_extension_protocol_options": {
			"envoy.extensions.upstreams.http.v3.HttpProtocolOptions": {
				"@type": "type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions",
				"explicit_http_config": {
					"http_protocol_options": {}
				}
			}
		},
		"loadAssignment": {
			"clusterName": "` + clusterName + `",
			"endpoints": [
				{
					"lbEndpoints": [
						{
							"endpoint": {
								"address": {
									"socket_address": {
										"address": "` + clusterAddress + `",
										"port_value": ` + clusterPort + `
									}
								}
							}
						}
					]
				}
			]
		}
	}`

	// Enable TLS on the prometheus listener if cert/private key are provided.
	var tlsConfig string
	if args.PrometheusCertFile != "" {
		tlsConfig = `,
				"transportSocket": {
					"name": "tls",
					"typedConfig": {
						"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext",
						"commonTlsContext": {
							"tlsCertificateSdsSecretConfigs": [
								{
									"name": "prometheus_cert"
								}
							],
							"validationContextSdsSecretConfig": {
								"name": "prometheus_validation_context"
							}
						}
					}
				}`
	}

	listenerJSON := `{
		"name": "` + name + `_listener",
		"address": {
			"socket_address": {
				"address": "` + host + `",
				"port_value": ` + port + `
			}
		},
		"filter_chains": [
			{
				"filters": [
					{
						"name": "envoy.filters.network.http_connection_manager",
						 "typedConfig": {
							"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
							"stat_prefix": "` + name + `",
							"codec_type": "HTTP1",
							"route_config": {
								"name": "self_admin_route",
								"virtual_hosts": [
									{
										"name": "self_admin",
										"domains": [
											"*"
										],
										"routes": [
											{
												"match": {
													"` + matchType + `": "` + matchValue + `"
												},
												"route": {
													"cluster": "` + clusterName + `",
													"prefix_rewrite": "` + prefixRewrite + `"
												}
											},
											{
												"match": {
													"prefix": "/"
												},
												"direct_response": {
													"status": 404
												}
											}
										]
									}
								]
							},
							"http_filters": [
								{
									"name": "envoy.filters.http.router",
									"typedConfig": {
									"@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router"
									}
								}
							]
						}
					}
				]` + tlsConfig + `
			}
		]
	}`

	secretsTemplate := `{
		"name": "prometheus_cert",
		"tlsCertificate": {
			"certificateChain": {
				"filename": "%s"
			},
			"privateKey": {
				"filename": "%s"
			}
		}	
	},
	{
		"name": "prometheus_validation_context",
		"validationContext": {
			%s
		}
	}`
	var validationContext string
	if args.PrometheusCAPath != "" {
		validationContext = fmt.Sprintf(`"watchedDirectory": {
			"path": "%s"
		}`, args.PrometheusCAPath)
	} else {
		validationContext = fmt.Sprintf(`"trustedCa": {
			"filename": "%s"
		}`, args.PrometheusCAFile)
	}
	var secretsJSON string
	if args.PrometheusCertFile != "" {
		secretsJSON = fmt.Sprintf(secretsTemplate, args.PrometheusCertFile, args.PrometheusKeyFile, validationContext)
	}

	// Make sure we do not append the same cluster multiple times, as that will
	// cause envoy startup to fail.
	selfAdminClusterExists, err := containsSelfAdminCluster(args.StaticClustersJSON)
	if err != nil {
		return err
	}

	if args.StaticClustersJSON == "" {
		args.StaticClustersJSON = clusterJSON
	} else if !selfAdminClusterExists {
		args.StaticClustersJSON += ",\n" + clusterJSON
	}

	if args.StaticListenersJSON != "" {
		listenerJSON = ",\n" + listenerJSON
	}
	args.StaticListenersJSON += listenerJSON

	if args.StaticSecretsJSON != "" {
		secretsJSON = ",\n" + secretsJSON
	}
	args.StaticSecretsJSON += secretsJSON

	return nil
}

// appendTelemetryCollectorConfig generates config to enable a socket at path: <TelemetryCollectorBindSocketDir>/<hash of compound proxy ID>.sock
// We take the hash of the compound proxy ID for a few reasons:
//
//   - The proxy ID is included because this socket path must be unique per proxy. Each Envoy proxy will ship
//     its metrics to the collector using its own loopback listener at this path.
//
//   - The hash is needed because UNIX domain socket paths must be less than 104 characters. By using a b64 encoded
//     SHA1 hash we end up with 27 chars for the name, 5 chars for the extension, and the remainder is saved for
//     the configurable socket dir. The length of the directory's path is validated on writes to avoid going over.
func appendTelemetryCollectorConfig(args *BootstrapTplArgs, telemetryCollectorBindSocketDir string) {
	// Normalize namespace to "default". This ensures we match the namespace behaviour in proxycfg package,
	// where a dynamic listener will be created at the same socket path via xDS.
	ns := args.Namespace
	if ns == "" {
		ns = "default"
	}
	id := ns + "_" + args.ProxyID

	h := sha1.New()
	h.Write([]byte(id))
	hash := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
	path := path.Join(telemetryCollectorBindSocketDir, hash+".sock")

	if args.StatsSinksJSON != "" {
		args.StatsSinksJSON += ",\n"
	}
	args.StatsSinksJSON += `{
		"name": "envoy.stat_sinks.metrics_service",
		"typed_config": {
		  "@type": "type.googleapis.com/envoy.config.metrics.v3.MetricsServiceConfig",
		  "transport_api_version": "V3",
		  "grpc_service": {
			"envoy_grpc": {
			  "cluster_name": "consul_telemetry_collector_loopback"
			}
		  },
		  "emit_tags_as_labels": true
		}
	  }`

	if args.StaticClustersJSON != "" {
		args.StaticClustersJSON += ",\n"
	}
	args.StaticClustersJSON += fmt.Sprintf(`{
		"name": "consul_telemetry_collector_loopback",
		"type": "STATIC",
		"typed_extension_protocol_options": {
		  "envoy.extensions.upstreams.http.v3.HttpProtocolOptions": {
			"@type": "type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions",
			"explicit_http_config": {
			  "http2_protocol_options": {}
			}
		  }
		},
		"loadAssignment": {
		  "clusterName": "consul_telemetry_collector_loopback",
		  "endpoints": [
			{
			  "lbEndpoints": [
				{
				  "endpoint": {
					"address": {
					  "pipe": {
						"path": "%s"
					  }
					}
				  }
				}
			  ]
			}
		  ]
		}
	  }`, path)
}

func containsSelfAdminCluster(clustersJSON string) (bool, error) {
	clusterNames := []struct {
		Name string
	}{}

	// StaticClustersJSON is defined as a comma-separated list of clusters, so we
	// need to wrap it in JSON array brackets
	err := json.Unmarshal([]byte("["+clustersJSON+"]"), &clusterNames)
	if err != nil {
		return false, fmt.Errorf("failed to parse static clusters: %s", err)
	}

	for _, cluster := range clusterNames {
		if cluster.Name == selfAdminName {
			return true, nil
		}
	}

	return false, nil
}
