Skip to content

Commit

Permalink
Enhancements (#41)
Browse files Browse the repository at this point in the history
- [TOOLS-1614] - Fetch credentials and certificates via file, environment variables and in base64 encoded form
- Added new XDR (per DC) metric nodes
  • Loading branch information
spkesan authored Jan 27, 2021
1 parent 5acd454 commit 34487bb
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 78 deletions.
40 changes: 32 additions & 8 deletions ape.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,40 +21,64 @@ log_file = ""
log_level = ""

# Basic HTTP authentication for '/metrics'.
# Supports below formats,
# 1. Credential directly - "<credential>"
# 2. Credential via file - "file:<file-that-contains-credential>"
# 3. Credential via environment variable - "env:<environment-variable-that-contains-credential>"
# 4. Credential via environment variable containing base64 encoded credential - "env-b64:<environment-variable-that-contains-base64-encoded-credential>"
# 5. Credential in base64 encoded form - "b64:<base64-encoded-credential>"
basic_auth_username=""
basic_auth_password=""

[Aerospike]
db_host="localhost"
db_port=3000

# TLS certificates.
# Supports below formats,
# 2. Certificate file path - "file:<file-path>"
# 3. Environment variable containing base64 encoded certificate - "env-b64:<environment-variable-that-contains-base64-encoded-certificate>"
# 4. Base64 encoded certificate - "b64:<base64-encoded-certificate>"
# Applicable to 'root_ca', 'cert_file' and 'key_file' configurations.

# root certificate file
root_ca=""

# certificate file
cert_file=""

# key file
key_file=""

# Passphrase for encrypted key_file. Supports below formats,
# 1. Passphrase directly - "<passphrase>"
# 2. Passphrase via file - "file:<file-that-contains-passphrase>"
# 3. Passphrase via environment variable - "env:<environment-variable-that-holds-passphrase>"
# 1. Passphrase directly - "<passphrase>"
# 2. Passphrase via file - "file:<file-that-contains-passphrase>"
# 3. Passphrase via environment variable - "env:<environment-variable-that-holds-passphrase>"
# 4. Passphrase via environment variable containing base64 encoded passphrase - "env-b64:<environment-variable-that-contains-base64-encoded-passphrase>"
# 5. Passphrase in base64 encoded form - "b64:<base64-encoded-passphrase>"
key_file_passphrase=""

# node TLS name for authentication
node_tls_name=""

# root certificate file
root_ca=""

# authentication mode: internal (for server), external (LDAP, etc.)
auth_mode=""
# Aerospike cluster security credentials.
# Supports below formats,
# 1. Credential directly - "<credential>"
# 2. Credential via file - "file:<file-that-contains-credential>"
# 3. Credential via environment variable - "env:<environment-variable-that-contains-credential>"
# 4. Credential via environment variable containing base64 encoded credential - "env-b64:<environment-variable-that-contains-base64-encoded-credential>"
# 5. Credential in base64 encoded form - "b64:<base64-encoded-credential>"
# Applicable to 'user' and 'password' configurations.

# database user
user=""

# database password
password=""

# authentication mode: internal (for server), external (LDAP, etc.)
auth_mode=""

# timeout for sending commands to the server node in seconds
timeout=5

Expand Down
40 changes: 32 additions & 8 deletions ape.toml.template
Original file line number Diff line number Diff line change
Expand Up @@ -21,40 +21,64 @@ log_file = "${AGENT_LOG_FILE}"
log_level = "${AGENT_LOG_LEVEL}"

# Basic HTTP authentication for '/metrics'.
# Supports below formats,
# 1. Credential directly - "<credential>"
# 2. Credential via file - "file:<file-that-contains-credential>"
# 3. Credential via environment variable - "env:<environment-variable-that-contains-credential>"
# 4. Credential via environment variable containing base64 encoded credential - "env-b64:<environment-variable-that-contains-base64-encoded-credential>"
# 5. Credential in base64 encoded form - "b64:<base64-encoded-credential>"
basic_auth_username="${BASIC_AUTH_USERNAME}"
basic_auth_password="${BASIC_AUTH_PASSWORD}"

[Aerospike]
db_host="${AS_HOST}"
db_port=${AS_PORT}

# TLS certificates.
# Supports below formats,
# 2. Certificate file path - "file:<file-path>"
# 3. Environment variable containing base64 encoded certificate - "env-b64:<environment-variable-that-contains-base64-encoded-certificate>"
# 4. Base64 encoded certificate - "b64:<base64-encoded-certificate>"
# Applicable to 'root_ca', 'cert_file' and 'key_file' configurations.

# root certificate file
root_ca="${AS_ROOT_CA}"

# certificate file
cert_file="${AS_CERT_FILE}"

# key file
key_file="${AS_KEY_FILE}"

# Passphrase for encrypted key_file. Supports below formats,
# 1. Passphrase directly - "<passphrase>"
# 2. Passphrase via file - "file:<file-that-contains-passphrase>"
# 3. Passphrase via environment variable - "env:<environment-variable-that-holds-passphrase>"
# 1. Passphrase directly - "<passphrase>"
# 2. Passphrase via file - "file:<file-that-contains-passphrase>"
# 3. Passphrase via environment variable - "env:<environment-variable-that-holds-passphrase>"
# 4. Passphrase via environment variable containing base64 encoded passphrase - "env-b64:<environment-variable-that-contains-base64-encoded-passphrase>"
# 5. Passphrase in base64 encoded form - "b64:<base64-encoded-passphrase>"
key_file_passphrase="${AS_KEY_FILE_PASSPHRASE}"

# node TLS name for authentication
node_tls_name="${AS_NODE_TLS_NAME}"

# root certificate file
root_ca="${AS_ROOT_CA}"

# authentication mode: internal (for server), external (LDAP, etc.)
auth_mode="${AS_AUTH_MODE}"
# Aerospike cluster security credentials.
# Supports below formats,
# 1. Credential directly - "<credential>"
# 2. Credential via file - "file:<file-that-contains-credential>"
# 3. Credential via environment variable - "env:<environment-variable-that-contains-credential>"
# 4. Credential via environment variable containing base64 encoded credential - "env-b64:<environment-variable-that-contains-base64-encoded-credential>"
# 5. Credential in base64 encoded form - "b64:<base64-encoded-credential>"
# Applicable to 'user' and 'password' configurations.

# database user
user="${AS_AUTH_USER}"

# database password
password="${AS_AUTH_PASSWORD}"

# authentication mode: internal (for server), external (LDAP, etc.)
auth_mode="${AS_AUTH_MODE}"

# timeout for sending commands to the server node in seconds
timeout=${TICKER_TIMEOUT}

Expand Down
116 changes: 84 additions & 32 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package main
import (
"bytes"
"crypto/subtle"
"errors"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
Expand All @@ -16,8 +16,6 @@ import (
"github.com/gobwas/glob"
"github.com/jameskeane/bcrypt"
"github.com/prometheus/client_golang/prometheus"

log "github.com/sirupsen/logrus"
)

func makeMetric(namespace, name string, t metricType, constLabels map[string]string, labels ...string) promMetric {
Expand Down Expand Up @@ -103,7 +101,7 @@ func hashPassword(password string) ([]byte, error) {

// Check HTTP Basic Authentication.
// Validate username, password from the http request against the configured values.
func validateBasicAuth(_ http.ResponseWriter, r *http.Request, username string, password string) bool {
func validateBasicAuth(r *http.Request, username string, password string) bool {
user, pass, ok := r.BasicAuth()

if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 {
Expand Down Expand Up @@ -173,48 +171,102 @@ func filterBlockedMetrics(filteredMetrics map[string]metricType, blocklist []str
}
}

// Get key file passphrase from environment variable or from a file or directly from the config variable
// keyFilePassphraseConfig can be one of the following,
// 1. "<passphrase>"
// 2. "file:<file-that-contains-passphrase>"
// 3. "env:<environment-variable-that-contains-passphrase>"
func getKeyFilePassphrase(keyFilePassphraseConfig string) ([]byte, error) {
keyFilePassphraseSource := strings.SplitN(keyFilePassphraseConfig, ":", 2)

if len(keyFilePassphraseSource) == 2 {
if keyFilePassphraseSource[0] == "file" {
dataBytes, err := ioutil.ReadFile(keyFilePassphraseSource[1])
if err != nil {
return nil, err
// Get secret
// secretConfig can be one of the following,
// 1. "<secret>" (secret directly)
// 2. "file:<file-that-contains-secret>" (file containing secret)
// 3. "env:<environment-variable-that-contains-secret>" (environment variable containing secret)
// 4. "env-b64:<environment-variable-that-contains-base64-encoded-secret>" (environment variable containing base64 encoded secret)
// 5. "b64:<base64-encoded-secret>" (base64 encoded secret)
func getSecret(secretConfig string) ([]byte, error) {
secretSource := strings.SplitN(secretConfig, ":", 2)

if len(secretSource) == 2 {
switch secretSource[0] {
case "file":
return readFromFile(secretSource[1])

case "env":
secret, ok := os.LookupEnv(secretSource[1])
if !ok {
return nil, fmt.Errorf("Environment variable %s not set", secretSource[1])
}

keyPassphrase := bytes.TrimSuffix(dataBytes, []byte("\n"))
return []byte(secret), nil

case "env-b64":
return getValueFromBase64EnvVar(secretSource[1])

return keyPassphrase, nil
case "b64":
return getValueFromBase64(secretSource[1])

default:
return nil, fmt.Errorf("Invalid source: %s", secretSource[0])
}
}

if keyFilePassphraseSource[0] == "env" {
keyPassphrase, ok := os.LookupEnv(keyFilePassphraseSource[1])
if !ok {
return nil, errors.New("Environment variable " + keyFilePassphraseSource[1] + " not set")
}
return []byte(secretConfig), nil
}

// Get certificate
// certConfig can be one of the following,
// 1. "<file-path>" (certificate file path directly)
// 2. "file:<file-path>" (certificate file path)
// 3. "env-b64:<environment-variable-that-contains-base64-encoded-certificate>" (environment variable containing base64 encoded certificate)
// 4. "b64:<base64-encoded-certificate>" (base64 encoded certificate)
func getCertificate(certConfig string) ([]byte, error) {
certificateSource := strings.SplitN(certConfig, ":", 2)

if len(certificateSource) == 2 {
switch certificateSource[0] {
case "file":
return readFromFile(certificateSource[1])

case "env-b64":
return getValueFromBase64EnvVar(certificateSource[1])

return []byte(keyPassphrase), nil
case "b64":
return getValueFromBase64(certificateSource[1])

default:
return nil, fmt.Errorf("Invalid source %s", certificateSource[0])
}
}

return []byte(keyFilePassphraseConfig), nil
// Assume certConfig is a file path (backward compatible)
return readFromFile(certConfig)
}

// Read content from file
func readFromFile(filePath string) ([]byte, error) {
dataBytes, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("Failed to read from file `%s`: `%v`", filePath, err)
}

data := bytes.TrimSuffix(dataBytes, []byte("\n"))

return data, nil
}

// Get decoded base64 value from environment variable
func getValueFromBase64EnvVar(envVar string) ([]byte, error) {
b64Value, ok := os.LookupEnv(envVar)
if !ok {
return nil, fmt.Errorf("Environment variable %s not set", envVar)
}

return getValueFromBase64(b64Value)
}

// Read certificate file and abort if any errors
// Returns file content as byte array
func readCertFile(filename string) []byte {
dataBytes, err := ioutil.ReadFile(filename)
// Get decoded base64 value
func getValueFromBase64(b64Value string) ([]byte, error) {
value, err := base64.StdEncoding.DecodeString(b64Value)
if err != nil {
log.Fatalf("Failed to read certificate or key file `%s` : `%s`", filename, err)
return nil, fmt.Errorf("Failed to decode base64 value: %v", err)
}

return dataBytes
return value, nil
}

func sanitizeUTF8(lv string) string {
Expand Down
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ go 1.14

require (
github.com/BurntSushi/toml v0.3.1
github.com/aerospike/aerospike-client-go v3.1.1+incompatible
github.com/aerospike/aerospike-client-go v4.1.0+incompatible
github.com/gobwas/glob v0.2.3
github.com/hashicorp/go-version v1.2.1
github.com/jameskeane/bcrypt v0.0.0-20120420032655-c3cd44c1e20f
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/onsi/ginkgo v1.14.0 // indirect
github.com/prometheus/client_golang v1.8.0
github.com/prometheus/common v0.15.0 // indirect
github.com/prometheus/client_golang v1.9.0
github.com/prometheus/procfs v0.3.0 // indirect
github.com/sirupsen/logrus v1.7.0
github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da // indirect
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a // indirect
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
google.golang.org/protobuf v1.25.0 // indirect
)
21 changes: 11 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/aerospike/aerospike-client-go v3.1.1+incompatible h1:+zAuvKMI9rq/hdpwX8srmFvDKfprMPX1SQGMLkBpvuc=
github.com/aerospike/aerospike-client-go v3.1.1+incompatible/go.mod h1:zj8LBEnWBDOVEIJt8LvaRvDG5ARAoa5dBeHaB472NRc=
github.com/aerospike/aerospike-client-go v4.1.0+incompatible h1:s8l82c619XS2aps02MU5I3o3GofeIOeWfeKCoVq+1Zs=
github.com/aerospike/aerospike-client-go v4.1.0+incompatible/go.mod h1:zj8LBEnWBDOVEIJt8LvaRvDG5ARAoa5dBeHaB472NRc=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
Expand Down Expand Up @@ -241,8 +241,8 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.8.0 h1:zvJNkoCFAnYFNC24FV8nW4JdRJ3GIFcLbg65lL/JDcw=
github.com/prometheus/client_golang v1.8.0/go.mod h1:O9VU6huf47PktckDQfMTX0Y8tY0/7TSWwj+ITvv0TnM=
github.com/prometheus/client_golang v1.9.0 h1:Rrch9mh17XcxvEu9D9DEpb4isxjGBtcevQjKvxPRQIU=
github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
Expand All @@ -257,7 +257,6 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.14.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM=
github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
Expand All @@ -268,6 +267,8 @@ github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFB
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4=
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.3.0 h1:Uehi/mxLK0eiUc0H0++5tpMGTexB8wZ598MIgU8VpDM=
github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
Expand Down Expand Up @@ -361,8 +362,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down Expand Up @@ -391,9 +392,9 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
Expand Down
Loading

0 comments on commit 34487bb

Please sign in to comment.