diff --git a/.gitignore b/.gitignore index bd9c315..d6ede9a 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,5 @@ attache-control !attache-control/ !attache-check/ +# Mac +*.DS_Store diff --git a/README.md b/README.md index a642663..496332d 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,15 @@ Nomad and Consul. - Create a new cluster when no cluster is present - Add new primary node and perform a shard slot rebalance - Add new replica node to the primary node with the least replicas +- Full support for Redis mTLS and ACL Auth +- Full support for Consul mTLS and ACL Tokens #### To Do -- Drain and failover and FORGET an existing primary node -- Remove and FORGET an existing replica node -- Redis ACL -- Redis Password -- Redis mTLS -- Redis Static ClusterHub Port selection (from Nomad Job Specification) +- [x] Redis ACL +- [x] Redis Password +- [x] Redis mTLS +- [ ] Drain, failover, and FORGET an existing primary node +- [ ] Remove and FORGET an existing replica node ### `attache-check` A sidecar that servers an HTTP API that allows Consul to track the health of @@ -23,13 +24,24 @@ once they've joined a cluster. #### Usage ```shell +$ attache-check -help Usage of ./attache-check: -check-serv-addr string - address this utility should listen on + address this utility should listen on (e.g. 127.0.0.1:8080) + -redis-auth-password-file string + redis-server password file path, (required) + -redis-auth-username string + redis-server username, (required) -redis-node-addr string - redis-server listening address + redis-server listening address, (required) + -redis-tls-ca-cert string + Redis client CA certificate file, (required) + -redis-tls-cert-file string + Redis client certificate file, (required) + -redis-tls-key-file string + Redis client key file, (required) -shutdown-grace duration - duration to wait before shutting down (e.g. '1s') (default 5s) + duration to wait before shutting down (e.g. '1s') (default 5s) ``` ### `attache-control` @@ -48,7 +60,7 @@ Usage of ./attache-control: -attempt-limit int Number of times to attempt joining or creating a cluster before exiting (default 20) -await-service-name string - Consul Service for any newly created Redis Cluster Nodes + Consul Service for newly created Redis Cluster Nodes, (required) -consul-acl-token string Consul client ACL token -consul-addr string @@ -60,27 +72,42 @@ Usage of ./attache-control: -consul-tls-cert string Consul client certificate file -consul-tls-enable - Enable TLS for the Consul client + Enable mTLS for the Consul client -consul-tls-key string Consul client key file -dest-service-name string - Consul Service for any existing Redis Cluster Nodes + Consul Service for healthy Redis Cluster Nodes, (required) -lock-kv-path string - KV path used by Consul to aquire a distributed lock for operations (default "service/attache/leader") + Consul KV path used as a distributed lock for operations (default "service/attache/leader") -log-level string Set the log level (default "info") + -redis-auth-password-file string + Redis password file path, (required) + -redis-auth-username string + Redis username, (required) -redis-node-addr string - redis-server listening address + redis-server listening address, (required) -redis-primary-count int - Total number of expected Redis shard primary nodes + Total number of expected Redis shard primary nodes, (required) -redis-replica-count int Total number of expected Redis shard replica nodes + -redis-tls-ca-cert string + Redis client CA certificate file, (required) + -redis-tls-cert-file string + Redis client certificate file, (required) + -redis-tls-key-file string + Redis client key file, (required) ``` ### Running the Example Nomad Job -Note: these steps assume that you have the Nomad and Consul binaries installed +Note: these steps assume that you have the `nomad` and `consul` binaries installed on your machine and that they exist in your `PATH`. +Build the attache-control and attache-check binaries: +```shell +$ go build -o attache-check ./cmd/attache-check/main.go && go build -o attache-control ./cmd/attache-control/main.go ./cmd/attache-control/config.go +``` + Start the Consul server in `dev` mode: ```shell $ consul agent -dev -datacenter dev-general -log-level ERROR @@ -93,7 +120,7 @@ $ sudo nomad agent -dev -bind 0.0.0.0 -log-level ERROR -dc dev-general Start a Nomad job deployment: ```shell -nomad job run -verbose ./example/redis-cluster.hcl +$ nomad job run -verbose -var-file=./example/vars-file.hcl ./example/job-specification.hcl ``` Open the Nomad UI: http://localhost:4646/ui diff --git a/cmd/attache-check/main.go b/cmd/attache-check/main.go index 887d99b..f693c4e 100644 --- a/cmd/attache-check/main.go +++ b/cmd/attache-check/main.go @@ -11,10 +11,12 @@ import ( "github.com/gorilla/mux" redis "github.com/letsencrypt/attache/src/redis/client" + "github.com/letsencrypt/attache/src/redis/config" logger "github.com/sirupsen/logrus" ) -// CheckHandler is a wrapper around an inner redis.Client. +// CheckHandler is a wraps an inner redis client with some methods for handling +// health check requests. type CheckHandler struct { redis.Client } @@ -36,21 +38,32 @@ func (h *CheckHandler) StateOk(w http.ResponseWriter, r *http.Request) { func main() { checkServAddr := flag.String("check-serv-addr", "", "address this utility should listen on (e.g. 127.0.0.1:8080)") shutdownGrace := flag.Duration("shutdown-grace", time.Second*5, "duration to wait before shutting down (e.g. '1s')") - redisNodeAddr := flag.String("redis-node-addr", "", "redis-server listening address") - logger.Infof("starting %s", os.Args[0]) + var redisOpts config.RedisOpts + flag.StringVar(&redisOpts.NodeAddr, "redis-node-addr", "", "redis-server listening address, (required)") + flag.StringVar(&redisOpts.Username, "redis-auth-username", "", "redis-server username, (required)") + flag.StringVar(&redisOpts.PasswordFile, "redis-auth-password-file", "", "redis-server password file path, (required)") + flag.StringVar(&redisOpts.CACertFile, "redis-tls-ca-cert", "", "Redis client CA certificate file, (required)") + flag.StringVar(&redisOpts.CertFile, "redis-tls-cert-file", "", "Redis client certificate file, (required)") + flag.StringVar(&redisOpts.KeyFile, "redis-tls-key-file", "", "Redis client key file, (required)") flag.Parse() if *checkServAddr == "" { logger.Fatal("Missing required opt 'check-serv-addr'") } - if *redisNodeAddr == "" { - logger.Fatal("Missing required opt 'redis-node-addr'") + err := redisOpts.Validate() + if err != nil { + logger.Fatal(err) } + logger.Infof("starting %s", os.Args[0]) + router := mux.NewRouter() - redisClient := redis.New(*redisNodeAddr, "") + redisClient, err := redis.New(redisOpts) + if err != nil { + logger.Fatalf("redis: %s", err) + } handler := CheckHandler{*redisClient} router.HandleFunc("/clusterinfo/state/ok", handler.StateOk) diff --git a/cmd/attache-control/config.go b/cmd/attache-control/config.go index e6956ab..4d4677d 100644 --- a/cmd/attache-control/config.go +++ b/cmd/attache-control/config.go @@ -1,141 +1,83 @@ package main import ( + "errors" "flag" - "fmt" - "net/http" "time" - consul "github.com/hashicorp/consul/api" - logger "github.com/sirupsen/logrus" + c "github.com/letsencrypt/attache/src/consul/config" + r "github.com/letsencrypt/attache/src/redis/config" ) +// CLIOpts is exported for use with flag.Parse(). type CLIOpts struct { - // Exported for use with flag.Parse() - RedisNodeAddr string - // Exported for use with flag.Parse() + RedisOpts r.RedisOpts RedisPrimaryCount int - // Exported for use with flag.Parse() RedisReplicaCount int - // Exported for use with flag.Parse() - LockPath string - // Exported for use with flag.Parse() - AttemptInterval time.Duration - // Exported for use with flag.Parse() - AttemptLimit int - // Exported for use with flag.Parse() - AwaitServiceName string - // Exported for use with flag.Parse() - DestServiceName string - // Exported for use with flag.Parse() - LogLevel string - // Exported for use with flag.Parse() - ConsulOpts ConsulOpts + LockPath string + AttemptInterval time.Duration + AttemptLimit int + AwaitServiceName string + DestServiceName string + LogLevel string + ConsulOpts c.ConsulOpts } -type ConsulOpts struct { - // Exported for use with flag.Parse() - DC string - // Exported for use with flag.Parse() - Address string - // Exported for use with flag.Parse() - ACLToken string - // Exported for use with flag.Parse() - TLSEnable bool - // Exported for use with flag.Parse() - TLSCACert string - // Exported for use with flag.Parse() - TLSCert string - // Exported for use with flag.Parse() - TLSKey string -} +func (c CLIOpts) Validate() error { + if c.RedisPrimaryCount == 0 { + return errors.New("missing required opt: 'redis-primary-count'") + } + + if c.DestServiceName == "" { + return errors.New("missing required opt: 'dest-service-name'") + } -func (c *ConsulOpts) MakeConsulClient() (*consul.Client, error) { - config := consul.DefaultConfig() - config.Datacenter = c.DC - config.Address = c.Address - config.Token = c.ACLToken - if c.TLSEnable { - config.Scheme = "https" - tlsConfig := consul.TLSConfig{ - Address: c.Address, - CAFile: c.TLSCACert, - CertFile: c.TLSCert, - KeyFile: c.TLSKey, - } - tlsClientConf, err := consul.SetupTLSConfig(&tlsConfig) - if err != nil { - return nil, fmt.Errorf("error creating TLS client config for consul: %w", err) - } - config.HttpClient.Transport = &http.Transport{ - TLSClientConfig: tlsClientConf, - } + if c.AwaitServiceName == "" { + return errors.New("missing required opt: 'await-service-name'") } - client, err := consul.NewClient(config) + err := c.ConsulOpts.Validate() if err != nil { - return nil, err + return err } - return client, nil + err = c.RedisOpts.Validate() + if err != nil { + return err + } + return nil } func ParseFlags() CLIOpts { var conf CLIOpts // CLI - flag.StringVar(&conf.RedisNodeAddr, "redis-node-addr", "", "redis-server listening address") - flag.IntVar(&conf.RedisPrimaryCount, "redis-primary-count", 0, "Total number of expected Redis shard primary nodes") + flag.IntVar(&conf.RedisPrimaryCount, "redis-primary-count", 0, "Total number of expected Redis shard primary nodes, (required)") flag.IntVar(&conf.RedisReplicaCount, "redis-replica-count", 0, "Total number of expected Redis shard replica nodes") flag.StringVar(&conf.LockPath, "lock-kv-path", "service/attache/leader", "Consul KV path used as a distributed lock for operations") flag.DurationVar(&conf.AttemptInterval, "attempt-interval", 3*time.Second, "Duration to wait between attempts to join or create a cluster") flag.IntVar(&conf.AttemptLimit, "attempt-limit", 20, "Number of times to attempt joining or creating a cluster before exiting") - flag.StringVar(&conf.AwaitServiceName, "await-service-name", "", "Consul Service for any newly created Redis Cluster Nodes") + flag.StringVar(&conf.AwaitServiceName, "await-service-name", "", "Consul Service for newly created Redis Cluster Nodes, (required)") + flag.StringVar(&conf.DestServiceName, "dest-service-name", "", "Consul Service for healthy Redis Cluster Nodes, (required)") flag.StringVar(&conf.LogLevel, "log-level", "info", "Set the log level") + // Redis + flag.StringVar(&conf.RedisOpts.NodeAddr, "redis-node-addr", "", "redis-server listening address, (required)") + flag.StringVar(&conf.RedisOpts.Username, "redis-auth-username", "", "Redis username, (required)") + flag.StringVar(&conf.RedisOpts.PasswordFile, "redis-auth-password-file", "", "Redis password file path, (required)") + flag.StringVar(&conf.RedisOpts.CACertFile, "redis-tls-ca-cert", "", "Redis client CA certificate file, (required)") + flag.StringVar(&conf.RedisOpts.CertFile, "redis-tls-cert-file", "", "Redis client certificate file, (required)") + flag.StringVar(&conf.RedisOpts.KeyFile, "redis-tls-key-file", "", "Redis client key file, (required)") + // Consul - flag.StringVar(&conf.DestServiceName, "dest-service-name", "", "Consul Service for any existing Redis Cluster Nodes") flag.StringVar(&conf.ConsulOpts.DC, "consul-dc", "dev-general", "Consul client datacenter") flag.StringVar(&conf.ConsulOpts.Address, "consul-addr", "127.0.0.1:8500", "Consul client address") flag.StringVar(&conf.ConsulOpts.ACLToken, "consul-acl-token", "", "Consul client ACL token") - flag.BoolVar(&conf.ConsulOpts.TLSEnable, "consul-tls-enable", false, "Enable TLS for the Consul client") + flag.BoolVar(&conf.ConsulOpts.EnableTLS, "consul-tls-enable", false, "Enable mTLS for the Consul client") flag.StringVar(&conf.ConsulOpts.TLSCACert, "consul-tls-ca-cert", "", "Consul client CA certificate file") flag.StringVar(&conf.ConsulOpts.TLSCert, "consul-tls-cert", "", "Consul client certificate file") flag.StringVar(&conf.ConsulOpts.TLSKey, "consul-tls-key", "", "Consul client key file") flag.Parse() - return conf } - -func ValidateConfig(conf CLIOpts) { - if conf.ConsulOpts.TLSEnable { - if conf.ConsulOpts.TLSCACert == "" { - logger.Fatal("Missing required opt: 'consul-tls-ca-cert") - } - - if conf.ConsulOpts.TLSCert == "" { - logger.Fatal("Missing required opt: 'consul-tls-cert") - } - - if conf.ConsulOpts.TLSKey == "" { - logger.Fatal("Missing required opt: 'consul-tls-key") - } - } - - if conf.RedisNodeAddr == "" { - logger.Fatal("Missing required opt: 'redis-node-addr'") - } - - if conf.RedisPrimaryCount == 0 { - logger.Fatal("Missing required opt: 'redis-primary-count'") - } - - if conf.DestServiceName == "" { - logger.Fatal("Missing required opt: 'dest-service-name'") - } - - if conf.AwaitServiceName == "" { - logger.Fatal("Missing required opt: 'await-service-name'") - } -} diff --git a/cmd/attache-control/main.go b/cmd/attache-control/main.go index 0189368..a6255b4 100644 --- a/cmd/attache-control/main.go +++ b/cmd/attache-control/main.go @@ -5,10 +5,11 @@ import ( "os/signal" "time" - consul "github.com/letsencrypt/attache/src/consul/client" - lock "github.com/letsencrypt/attache/src/consul/lock" - rediscli "github.com/letsencrypt/attache/src/redis/cli" - redisclient "github.com/letsencrypt/attache/src/redis/client" + consulClient "github.com/letsencrypt/attache/src/consul/client" + lockClient "github.com/letsencrypt/attache/src/consul/lock" + redisCLI "github.com/letsencrypt/attache/src/redis/cli" + redisClient "github.com/letsencrypt/attache/src/redis/client" + "github.com/letsencrypt/attache/src/redis/config" logger "github.com/sirupsen/logrus" ) @@ -23,18 +24,20 @@ func setLogLevel(level string) { func main() { start := time.Now() conf := ParseFlags() - ValidateConfig(conf) + err := conf.Validate() + if err != nil { + logger.Fatal(err) + } + setLogLevel(conf.LogLevel) logger.Infof("starting %s", os.Args[0]) - logger.Info("consul: setting up a consul client") - consulClient, err := conf.ConsulOpts.MakeConsulClient() + logger.Info("consul: setting up a redis client") + newNodeClient, err := redisClient.New(conf.RedisOpts) if err != nil { - logger.Fatalf("consul: %s", err) + logger.Fatalf("redis: %s", err) } - redisClient := redisclient.New(conf.RedisNodeAddr, "") - var nodesInDest []string var nodesInAwait []string @@ -43,7 +46,7 @@ func main() { for range ticks { attemptCount++ - nodeIsNew, err := redisClient.StateNewCheck() + nodeIsNew, err := newNodeClient.StateNewCheck() if err != nil { logger.Fatalf("redis: %s", err) } @@ -53,8 +56,7 @@ func main() { break } - lock := lock.New(consulClient, conf.LockPath, "10s") - err = lock.CreateSession() + lock, err := lockClient.New(conf.ConsulOpts, conf.LockPath, "10s") if err != nil { logger.Fatalf("consul: %s", err) } @@ -87,13 +89,17 @@ func main() { // Stop renewing the lock session. close(doneChan) lock.Cleanup() - } // Check the Consul service catalog for an existing Redis Cluster // that we can join. We're limiting the scope of our search to nodes // in the destService Consul service that Consul considers healthy. - destService := consul.New(consulClient, conf.DestServiceName, "", true) + destService, err := consulClient.New(conf.ConsulOpts, conf.DestServiceName, "", true) + if err != nil { + cleanup() + logger.Fatal(err) + } + nodesInDest, err = destService.GetNodeAddresses() if err != nil { cleanup() @@ -108,7 +114,12 @@ func main() { // waiting to form a cluster. We're limiting the scope of our // search to nodes in the awaitService Consul service that // Consul considers healthy. - awaitService := consul.New(consulClient, conf.AwaitServiceName, "", true) + awaitService, err := consulClient.New(conf.ConsulOpts, conf.AwaitServiceName, "", true) + if err != nil { + cleanup() + logger.Fatal(err) + } + nodesInAwait, err = awaitService.GetNodeAddresses() if err != nil { cleanup() @@ -127,7 +138,7 @@ func main() { if replicasPerPrimary == 0 { // This handles a special case for clusters that are // started with less than enough replicas to give at - // lease one to each primary. Once the first primary + // least one to each primary. Once the first primary // only cluster is started and the lock is released our // remaining replica nodes will be able to add // themselves to the newly created cluster. @@ -137,12 +148,12 @@ func main() { } logger.Infof("attempting to create a new cluster with nodes %q", nodesToCluster) - err := rediscli.CreateCluster(nodesToCluster, replicasPerPrimary) + err := redisCLI.CreateCluster(conf.RedisOpts, nodesToCluster, replicasPerPrimary) if err != nil { cleanup() logger.Fatalf("redis: %s", err) } - logger.Info("redis: suceeded") + logger.Info("redis: succeeded") cleanup() break } else { @@ -153,12 +164,25 @@ func main() { } logger.Infof("redis: gathering info from the cluster that %q belongs to", nodesInDest[0]) - clusterClient := redisclient.New(nodesInDest[0], "") + clusterClient, err := redisClient.New( + config.RedisOpts{ + NodeAddr: nodesInDest[0], + Username: conf.RedisOpts.Username, + PasswordConfig: conf.RedisOpts.PasswordConfig, + TLSConfig: conf.RedisOpts.TLSConfig, + }, + ) + if err != nil { + cleanup() + logger.Fatalf("redis: %s", err) + } + primaryNodesInCluster, err := clusterClient.GetPrimaryNodes() if err != nil { cleanup() logger.Fatalf("redis: %s", err) } + replicaNodesInCluster, err := clusterClient.GetReplicaNodes() if err != nil { cleanup() @@ -169,15 +193,15 @@ func main() { // The current cluster has less than `shardPrimaryCount` shard // primary nodes. This node should be added as a new primary and // the existing cluster shardslots should be rebalanced. - logger.Infof("redis: node %q should be added as a new shard primary", conf.RedisNodeAddr) - logger.Infof("redis: attempting to join %q to the cluster that %q belongs to", conf.RedisNodeAddr, nodesInDest[0]) + logger.Infof("redis: node %q should be added as a new shard primary", conf.RedisOpts.NodeAddr) + logger.Infof("redis: attempting to join %q to the cluster that %q belongs to", conf.RedisOpts.NodeAddr, nodesInDest[0]) - err := rediscli.AddNewShardPrimary(conf.RedisNodeAddr, nodesInDest[0]) + err := redisCLI.AddNewShardPrimary(conf.RedisOpts, nodesInDest[0]) if err != nil { cleanup() logger.Fatalf("redis: %s", err) } - logger.Info("redis: suceeded") + logger.Info("redis: succeeded") cleanup() break @@ -185,20 +209,20 @@ func main() { // All `shardPrimaryCount` shard primary nodes exist in the // current cluster. This node should be added as a replica to // the primary node with the least number of replicas. - logger.Infof("redis: node %q should be added as a new shard replica", conf.RedisNodeAddr) - logger.Infof("redis: attempting to join %q to the cluster that %q belongs to", conf.RedisNodeAddr, nodesInDest[0]) + logger.Infof("redis: node %q should be added as a new shard replica", conf.RedisOpts.NodeAddr) + logger.Infof("redis: attempting to join %q to the cluster that %q belongs to", conf.RedisOpts.NodeAddr, nodesInDest[0]) - err := rediscli.AddNewShardReplica(conf.RedisNodeAddr, nodesInDest[0]) + err := redisCLI.AddNewShardReplica(conf.RedisOpts, nodesInDest[0]) if err != nil { cleanup() logger.Fatalf("redis: %s", err) } - logger.Info("redis: suceeded") + logger.Info("redis: succeeded") cleanup() break } } else { - if attemptCount == conf.AttemptLimit { + if attemptCount >= conf.AttemptLimit { logger.Fatal("failed to join or initialize a cluster during the time permitted") } logger.Info("another node currently has the lock") diff --git a/example/job-specification.hcl b/example/job-specification.hcl new file mode 100644 index 0000000..d61c58b --- /dev/null +++ b/example/job-specification.hcl @@ -0,0 +1,224 @@ +// await-service-name is the name of the Consul Service that Attache should +// check for Redis Nodes that are waiting to join a Redis Cluster or waiting to +// form a new Redis Cluster. +variable "await-service-name" { + type = string +} + +// dest-service-name is the name of the Consul Service that Attache should check +// for Redis Nodes that are part of a Redis Cluster that new Redis Nodes should +// join. +variable "dest-service-name" { + type = string +} + +// primary-count is the count of Redis Shard Primary Nodes that should exist in +// the resulting Redis Cluster. +variable "primary-count" { + type = number +} + +// replica-count is the count of Redis Shard Replica Nodes that should exist in +// the resulting Redis Cluster. +variable "replica-count" { + type = number +} + +// redis-username is the username that will be set as `masteruser` for each +// Redis Cluster Node and used each time Attaché connects to a Redis Cluster +// Node. +variable "redis-username" { + type = string +} + +// redis-password is the password that will be set as `masterauth` for each +// Redis Cluster Node and used each time Attaché connects to a Redis Cluster +// Node. +variable "redis-password" { + type = string +} + +// redis-tls-cacert is the contents of the CA cert file, in PEM format, used for +// mutal TLS authentication between Redis Server and Attaché. +variable "redis-tls-cacert" { + type = string +} + +// redis-tls-cert is the contents of the cert file, in PEM format, used for +// mutal TLS authentication between Redis Server and Attaché. +variable "redis-tls-cert" { + type = string +} + +// redis-tls-key is the contents of the key file, in PEM format, used for mutal +// TLS authentication between Redis Server and Attaché. +variable "redis-tls-key" { + type = string +} + +// redis-config-template is template used to create the configuration file used +// loaded by each Redis Cluster Node +variable "redis-config-template" { + type = string +} + +// attache-redis-tls-cert is the contents of the cert file, in PEM format, used +// for mutal TLS authentication between Attaché and the Redis Server. +variable "attache-redis-tls-cert" { + type = string +} + +// attache-redis-tls-key is the contents of the key file, in PEM format, used +// for mutal TLS authentication between Attaché and the Redis Server. +variable "attache-redis-tls-key" { + type = string +} + +job "redis-cluster" { + datacenters = ["dev-general"] + type = "service" + update { + max_parallel = 1 + min_healthy_time = "5s" + healthy_deadline = "5m" + progress_deadline = "10m" + } + group "nodes" { + count = var.primary-count + var.replica-count + network { + // Redis + port "db" {} + // Attaché Sidecar + port "attache" {} + } + ephemeral_disk { + sticky = true + migrate = true + } + task "server" { + service { + name = var.dest-service-name + port = "db" + check { + name = "attache:tcp-alive" + type = "tcp" + port = "attache" + interval = "3s" + timeout = "2s" + } + check { + name = "attache-check:clusterinfo/state/ok" + type = "http" + port = "attache" + path = "/clusterinfo/state/ok" + interval = "3s" + timeout = "2s" + } + } + driver = "raw_exec" + config { + command = "redis-server" + args = ["${NOMAD_ALLOC_DIR}/data/redis.conf"] + } + env { + redis-password = "${var.redis-password}" + } + template { + data = var.redis-config-template + destination = "${NOMAD_ALLOC_DIR}/data/redis.conf" + change_mode = "restart" + } + template { + data = var.redis-password + destination = "${NOMAD_ALLOC_DIR}/data/password.txt" + change_mode = "restart" + } + template { + data = var.redis-tls-cacert + destination = "${NOMAD_ALLOC_DIR}/data/redis-tls/ca-cert.pem" + change_mode = "restart" + } + template { + data = var.redis-tls-cert + destination = "${NOMAD_ALLOC_DIR}/data/redis-tls/cert.pem" + change_mode = "restart" + } + template { + data = var.redis-tls-key + destination = "${NOMAD_ALLOC_DIR}/data/redis-tls/key.pem" + change_mode = "restart" + } + template { + data = var.attache-redis-tls-cert + destination = "${NOMAD_ALLOC_DIR}/data/attache-tls/cert.pem" + change_mode = "restart" + } + template { + data = var.attache-redis-tls-key + destination = "${NOMAD_ALLOC_DIR}/data/attache-tls/key.pem" + change_mode = "restart" + } + } + task "attache-control" { + lifecycle { + hook = "poststart" + sidecar = false + } + service { + name = var.await-service-name + port = "db" + check { + name = "db:tcp-alive" + type = "tcp" + port = "db" + interval = "3s" + timeout = "2s" + } + check { + name = "attache:tcp-alive" + type = "tcp" + port = "attache" + interval = "3s" + timeout = "2s" + } + } + driver = "raw_exec" + config { + // command is the path to the built attache-control binary. + command = "$${HOME}/repos/attache/attache-control" + args = [ + "-redis-node-addr", "${NOMAD_ADDR_db}", + "-redis-primary-count", "${var.primary-count}", + "-redis-replica-count", "${var.replica-count}", + "-dest-service-name", "${var.dest-service-name}", + "-await-service-name", "${var.await-service-name}", + "-redis-auth-username", "${var.redis-username}", + "-redis-auth-password-file", "${NOMAD_ALLOC_DIR}/data/password.txt", + "-redis-tls-ca-cert", "${NOMAD_ALLOC_DIR}/data/redis-tls/ca-cert.pem", + "-redis-tls-cert-file", "${NOMAD_ALLOC_DIR}/data/attache-tls/cert.pem", + "-redis-tls-key-file", "${NOMAD_ALLOC_DIR}/data/attache-tls/key.pem" + ] + } + } + task "attache-check" { + lifecycle { + hook = "poststart" + sidecar = true + } + driver = "raw_exec" + config { + // command is the path to the built attache-check binary. + command = "$${HOME}/repos/attache/attache-check" + args = [ + "-redis-node-addr", "${NOMAD_ADDR_db}", + "-check-serv-addr", "${NOMAD_ADDR_attache}", + "-redis-auth-username", "${var.redis-username}", + "-redis-auth-password-file", "${NOMAD_ALLOC_DIR}/data/password.txt", + "-redis-tls-ca-cert", "${NOMAD_ALLOC_DIR}/data/redis-tls/ca-cert.pem", + "-redis-tls-cert-file", "${NOMAD_ALLOC_DIR}/data/attache-tls/cert.pem", + "-redis-tls-key-file", "${NOMAD_ALLOC_DIR}/data/attache-tls/key.pem" + ] + } + } + } +} diff --git a/example/redis-cluster.hcl b/example/redis-cluster.hcl deleted file mode 100644 index 404bde5..0000000 --- a/example/redis-cluster.hcl +++ /dev/null @@ -1,148 +0,0 @@ -locals { - // await-service-name is the name of the Consul Service that Attache should - // check for Redis Nodes that are waiting to join a Redis Cluster or waiting - // to form a new Redis Cluster. - await-service-name = "redis-cluster-await" - - // dest-service-name is the name of the Consul Service that Attache should - // check for Redis Nodes that are part of a Redis Cluster that new Redis Nodes - // should join. - dest-service-name = "redis-cluster" - - // primary-count is the count of Redis Shard Primary Nodes that should exist - // in the resulting Redis Cluster. - primary-count = 3 - - // replica-count is the count of Redis Shard Replica Nodes that should exist - // in the resulting Redis Cluster. - replica-count = 3 - - // redis-config-template is the Consul Template used to produce the config - // file for each Redis Node. - redis-config-template = <<-EOF - bind {{ env "NOMAD_IP_db" }} - port {{ env "NOMAD_PORT_db" }} - daemonize no - cluster-enabled yes - cluster-node-timeout 15000 - cluster-config-file {{ env "NOMAD_ALLOC_DIR" }}/data/nodes.conf - EOF -} - -job "redis-cluster" { - datacenters = ["dev-general"] - type = "service" - update { - max_parallel = 1 - min_healthy_time = "5s" - healthy_deadline = "5m" - progress_deadline = "10m" - } - group "nodes" { - count = local.primary-count + local.replica-count - network { - // Redis - port "db" {} - // Attaché Sidecar - port "attache" {} - } - ephemeral_disk { - sticky = true - migrate = true - size = 600 - } - task "server" { - service { - name = local.dest-service-name - port = "db" - check { - name = "db:tcp-alive" - type = "tcp" - port = "db" - interval = "3s" - timeout = "2s" - } - check { - name = "attache:tcp-alive" - type = "tcp" - port = "attache" - interval = "3s" - timeout = "2s" - } - check { - name = "attache-check:clusterinfo/state/ok" - type = "http" - port = "attache" - path = "/clusterinfo/state/ok" - interval = "3s" - timeout = "2s" - } - } - resources { - cpu = 500 - memory = 512 - } - driver = "raw_exec" - config { - command = "redis-server" - args = ["${NOMAD_ALLOC_DIR}/data/redis.conf"] - } - template { - data = local.redis-config-template - destination = "${NOMAD_ALLOC_DIR}/data/redis.conf" - change_mode = "restart" - } - } - task "attache-control" { - lifecycle { - hook = "poststart" - sidecar = false - } - service { - name = local.await-service-name - port = "db" - check { - name = "db:tcp-alive" - type = "tcp" - port = "db" - interval = "3s" - timeout = "2s" - } - check { - name = "attache:tcp-alive" - type = "tcp" - port = "attache" - interval = "3s" - timeout = "2s" - } - } - driver = "raw_exec" - config { - // command is the path to the built attache-control binary. - command = "$${HOME}/repos/attache/attache-control" - args = [ - "-redis-node-addr", "${NOMAD_ADDR_db}", - "-redis-primary-count", "${local.primary-count}", - "-redis-replica-count", "${local.replica-count}", - "-dest-service-name", "${local.dest-service-name}", - "-await-service-name", "${local.await-service-name}", - ] - } - } - task "attache-check" { - lifecycle { - hook = "poststart" - sidecar = true - } - driver = "raw_exec" - config { - // command is the path to the built attache-check binary. - command = "$${HOME}/repos/attache/attache-check" - args = [ - "-redis-node-addr", "${NOMAD_ADDR_db}", - "-check-serv-addr", "${NOMAD_ADDR_attache}" - ] - } - } - } -} diff --git a/example/tls/attache/cert.pem b/example/tls/attache/cert.pem new file mode 100644 index 0000000..95ae1a3 --- /dev/null +++ b/example/tls/attache/cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDJzCCAg+gAwIBAgIIH6kXr5uW/6gwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVbWluaWNhIHJvb3QgY2EgMDIwZGJhMB4XDTIxMTIwNjIxNTA0NloXDTI0MDEw +NTIxNTA0NlowFDESMBAGA1UEAxMJMTI3LjAuMC4xMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA+KjVq9tqg6Q8RAAM+FtfYEp+ge31wnwBieLv/CGnEaZS +QAd9zZqSjJPhrgCAT3qanBXFgT23vjV9ycjj8gkUpWuVRaeeF4/RlT3A9G5FElBn +2kfX2UefBg2N4LB6vBUFdk0Eosk6eOGmO+BFoQ6Z8SljIFfDjdoRNrSsWhxWgNw/ +SsIot23gk7mvVOyEZ9Pnsua36xE/rClL4ywXs3LeykdQTwWVE86LOi+IBWTh9V+Q +AR7j59AYpFl3DIkZGnGSYb5ZluKMlPL8SG+UqqH7qdRJIiwEg1Z0cNcBX17+j6QC +CA9C51PGhg6/+yHbHgackEvdgGW25vRyvU0wxGNkSwIDAQABo3EwbzAOBgNVHQ8B +Af8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB +/wQCMAAwHwYDVR0jBBgwFoAUD+hW9Gu1xM0hlT2WORo47lijEK0wDwYDVR0RBAgw +BocEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAh5ZS+cogsguME6tfbJyhBy7bPfUP +5zXr22Xuw41FWBIytGDwpcslnyxhYAX+ngCyNu5pzxgC3TKW5Jri5wPoMJlaFeya +keKZShbzJEQIjjGLWbxl9ojsrrKuesgz5XqKt06VVC/RoiVX5ybD1IGv7nSQdWVn +Q8ZYxmEcHrPn2juXHF4yguucEwuXyZnlWVGKrFchAMqMVHwSuvMMNLvTfsOWTuqU +KkDkLTeOacmvTsb7J/yyXxzJzzpwmpPIrgO6Igz4zSADp4ErMk+6MduZQKTA3rhX +r53Jtmv+CLORODWe5+Cw38dJYlsRdyf7ShPuJQDEDOEFSKVFHJEOU9F1MQ== +-----END CERTIFICATE----- diff --git a/example/tls/attache/key.pem b/example/tls/attache/key.pem new file mode 100644 index 0000000..141521d --- /dev/null +++ b/example/tls/attache/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA+KjVq9tqg6Q8RAAM+FtfYEp+ge31wnwBieLv/CGnEaZSQAd9 +zZqSjJPhrgCAT3qanBXFgT23vjV9ycjj8gkUpWuVRaeeF4/RlT3A9G5FElBn2kfX +2UefBg2N4LB6vBUFdk0Eosk6eOGmO+BFoQ6Z8SljIFfDjdoRNrSsWhxWgNw/SsIo +t23gk7mvVOyEZ9Pnsua36xE/rClL4ywXs3LeykdQTwWVE86LOi+IBWTh9V+QAR7j +59AYpFl3DIkZGnGSYb5ZluKMlPL8SG+UqqH7qdRJIiwEg1Z0cNcBX17+j6QCCA9C +51PGhg6/+yHbHgackEvdgGW25vRyvU0wxGNkSwIDAQABAoIBAB/cGgx7/4jAaUxZ +KVBE/NJsmQryv1Nc6iGNpywJ78sOIWm8y/yk+nPymq7dt5L3ZYnsLDMkAj/nwKcz +Cym+yhtrzmNvV40zSyoxEGEBI+51yOip3dkkGRcAc5Y/Zmpk0x9WPOrSl6BXYSI4 +2RMKuOSyZdYGCLNLJnt46MBe8yJtVTK+VQnZqgTxtVjlvVH0MoLfd0j8VFwW/pak +3Cv50N5fduSBZIbEH+KTBfjqfcT9bAcoLSd1HA11zv9bjlHKPpA8F748FeNvwgwe +lyp6YAcmVaGY+FcKBQdStGiBox9h3XqjHUOgdDWVwhezAF8mNr48sqTE2AEoGf1r +fP8JJKkCgYEA+mVPEYMXU29nj6NnWXOMA0pN8dvWvLJLTer+XAkEnVOeRRR4qb01 +whX4WyjE+4HykZBFOqspe9V+hGwKqjOysRIlNRfu2nKo5SzQT+1V5+okzEJKWe3o +2J/L0kJwjdCTt64G7VJhCFLQYI4AMjXtFvTEGDDSkypgJtzBfuDROJcCgYEA/jmT +5sFCKNIizIllg/xf5uA6kFPWE5/bxrzl6T+57exUjlrEQJsU2SgHeg6ULBlkPlrF +eBpYRpK9Lazin6O/kg92wPnhjWGPlgeC/4/qePr4yRBWbwrK3ddVCCyxHJn2E2YO +joJURrCL6UZhKYbrZfK9jbf6Z/9IIpy6U9IBlG0CgYEAkWNjnrJ0R9Dm2+MwLiNG +R97MFUPlkpkf2nU5De16jXMw8cFqMnyXi0NAeoXYooSYeObBG8iohKu5E2C8bIkq +F2CG1CY6XQK4iKEVr2MKP2eXyDYxf7gBPE7EhShovB9AtiVJBmGPz8puDbJF8OGY +8XxbpAQtMKApRkdl3qrhMK8CgYBND/MPbeG6Mgiua6/EFIqVl77o5SDtjfW3BqfC +zrhzsMHo7Qa0ds4ZDZNGooiz3XaPmEBnqcS8j9qcr916es6lXd6nnJeMndhCqEBD +a8KtrZYgjL1Gp8Ta/l0ePz3o55q6QqOC+2rEitu+eMEXL3jHzI89GFnlkHKzW0L4 +CZ7E+QKBgQCFuXf6evp56eS0440jZbQRtTw9fRodbpTkEJcncwIEHb5CyBQR7KG3 +icajrMNqHmdqSTZE2x/t/G2IyQ7qdWc4oHV7fHdavTRIED1qnPttihc7LX+56q6h +ptuRGx51m1nbP0LU+0H5vzZy712aCPtS92/n1BqKHOdTR6zYViR33w== +-----END RSA PRIVATE KEY----- diff --git a/example/tls/ca-cert.pem b/example/tls/ca-cert.pem new file mode 100644 index 0000000..4a4a7b0 --- /dev/null +++ b/example/tls/ca-cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDSzCCAjOgAwIBAgIIAg26dvKrbYkwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVbWluaWNhIHJvb3QgY2EgMDIwZGJhMCAXDTIxMTAyMzAyMTUxOVoYDzIxMjEx +MDIzMDMxNTE5WjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSAwMjBkYmEwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDMlMwTusfIaEz7eB8RZua6kW81 +1G5DzvO8X9GAi8mdlqoOuSmSvHNz59Vn0lUZ7H1NwyAXqPm7FgYrGzAPBKJ+Kkw9 +drJJLoeSWPtFT9lISbl9qRpYg99aWzWfuEdKYJDa5woZLoQEaARW88TcCB44wLtK +yJpakMJZKV5gXqfpfSARyQPLsP/jirVhD+bNEs3sBBxw2WMtDdVS12V1soD2iKA5 +wTiKNjjpbyea6Q5zWcjFq2K8upx65hL75tdkHaYCqLZeeq/ciglGvXDKenZMMSY9 +Oz4qUDhxWTcb15zyolT8fQ9QdZqvBoOD6WBuWTNXbnT3zISiZGAAPQgdv9d7AgMB +AAGjgYYwgYMwDgYDVR0PAQH/BAQDAgKEMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggr +BgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQP6Fb0a7XEzSGV +PZY5GjjuWKMQrTAfBgNVHSMEGDAWgBQP6Fb0a7XEzSGVPZY5GjjuWKMQrTANBgkq +hkiG9w0BAQsFAAOCAQEASfe9zRlpIXHy4+mp1PIpjGjJjk0NhPOcoN8B2vCqYWsJ +nnfl9zfORkWPL6PgiXWqS6nNC+iqRFBWphaRqtSle0j+4NLFnmmOMXI/NlCjAvTH +6TNJ/H0nHlJ9p3Ui9a5MvZ8I/dOJLrFDX4/d9Lg76txKhFJBzXvxd9PSVKPJvnfx +x3aare5fkXy+JlZwP8FhbzIwVTmHGPxKEUCbImhmailXTfLTmm+bS1CW2OrOnlSn +ZPlEA8N1Y8ogNZQf2v65QCT7k64a1IuEA7XcH+W4+JhRAPPp1NujMTbeo855gMMm +D6LXhbMEV2jO6Yfqgr2H+fmiWq3nILj/XBSTEYNBqQ== +-----END CERTIFICATE----- diff --git a/example/tls/ca-key.pem b/example/tls/ca-key.pem new file mode 100644 index 0000000..023f224 --- /dev/null +++ b/example/tls/ca-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAzJTME7rHyGhM+3gfEWbmupFvNdRuQ87zvF/RgIvJnZaqDrkp +krxzc+fVZ9JVGex9TcMgF6j5uxYGKxswDwSifipMPXaySS6Hklj7RU/ZSEm5faka +WIPfWls1n7hHSmCQ2ucKGS6EBGgEVvPE3AgeOMC7SsiaWpDCWSleYF6n6X0gEckD +y7D/44q1YQ/mzRLN7AQccNljLQ3VUtdldbKA9oigOcE4ijY46W8nmukOc1nIxati +vLqceuYS++bXZB2mAqi2Xnqv3IoJRr1wynp2TDEmPTs+KlA4cVk3G9ec8qJU/H0P +UHWarwaDg+lgblkzV25098yEomRgAD0IHb/XewIDAQABAoIBAEJgxRZhtBDCRrgQ +8YOj75j5NywwENbPfyXPsRoUQQZwrBy611JU8uDYh9V32UTgBogEl5UVrnGVY8r4 +t08oIdDtyG7o5E/6WOKTHHQQxF9ADH9JLtMpdn7KuUtpbzgivN1JuW0SOqNzXHUa +AvWhbKzdW+eXzv0zmttzILwD+lc3PXEwk5mBe81wExOTSJYRN5jg2Ww+2RZBsc8S +APWddtoK08sEwK+l1GRyWE8GERlz1+f0EvUEMzBIGTXvAopLz/fDp/qkcZWhYEXn +cFtZmadlJyiuBh66BD42kcS7PtWs/HM0R/Q30lDG4ke/udZOAafzZDrGQGNKti/y +8NfZJ4ECgYEA3tdfOscgPYA5cviBMOIJIlcfaXswsYKE8PTSmwIVopNeuDxAc2qS +ay/UH+jeqXNA7qoIX4bMf53o5aCgxT0UcGP+uggZHfmUwG0FSksmWbyol6Da2EF6 +iAR3+AF3MZQ9teIs2xGt0Mo0NvVQh9PJaX1+VFiBlVCHs0KKg9sH2wUCgYEA6wXZ +pK7JIoV2PMdAvkf1R64GktN27qrKSa3poVSec8ZYjDrZC2EN5FTH83QI2h9vhHCi +HFXR4/wO+iRGvUj1PtKhIMOkpi8CgTPGhZ798o5kXFyOEJ2ELC9NT1cW/GewfZVk +fPSfQi58iy1L+B8vw87lxYg+eO639jIgTc9ocH8CgYAgoq4hr5P7LdI8EkTpYdEw +pE3HZvFErfbGSzSk2vNMMgUHOlu+C3eSFxkb60Dg1C5IRcKgKt+8OOYo6xNgj4d0 +xlBB8nmrOCge3liN/t+I+OY//qDOVxiY3v6q5ZwNOMao4ozrMHWiRFrNSbQXkF7J +AkYEGEoyEe8tw6sBkIxf+QKBgBUyjuHKnfuOHA75Tb6b0OSpLpCZoBWAtAQXOoZB +kpUQo7XqLN9Y3p7kgrBTm+TIhw9j9Usm9mpgtp0bHoI+DVigOMYyvyv5+3jZyaMN +pwv0idrGwk1/V4eAsLFiQoF7fLCnA8w9aAvZE4SeDkcP0QgRJio90pyns1HyTXWX +Km1TAoGAa7UyLJnaQIg7P+XnDpm/RdNyJPkXfUz4sW3tZNTSjBcmeIsf1REvnt0R ++xR58ANZIAxNjhWzDkMq0bMFlWE3aMTVVwpWD3+fqi0b8uOIPH55hKdUgYW7fN9d +lzXHeP37rzOFAdZCC1co5gCeqbodoJ4U802eKP75OAdEfj8BbQQ= +-----END RSA PRIVATE KEY----- diff --git a/example/tls/redis/cert.pem b/example/tls/redis/cert.pem new file mode 100644 index 0000000..0563d98 --- /dev/null +++ b/example/tls/redis/cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDJzCCAg+gAwIBAgIIEguoVcAkRXwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVbWluaWNhIHJvb3QgY2EgMDIwZGJhMB4XDTIxMTIwNjIxNTIwOFoXDTI0MDEw +NTIxNTIwOFowFDESMBAGA1UEAxMJMTI3LjAuMC4xMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAsBrTn0RmwyiZlOxB779Vam0M96SJbyf0w+EDVZXqVjuG +dJxXsuSuEqF8fDZIycsRji+1WQ9IG5er4A/0TFUAxE7gFzag27hk0Y7vRnzrZi3P +FivekkP1r4SZuIgF2UnCfZcsMOMkBGJ8t36DygaDEOJ+eu+rPR6nlznbOtYlJOtY +NUZh3OZ927dBPlyAi8rAdLvHNAjYZWYL8mlNU9WoI+JKE9iDoQnFlJSaNtld7RsN +TzIOC/CaVjBjmCjgf70SirFLXk23xC44gOywRuO+Oo9dMRjEY2SjX1/9FKgSsyRQ +nQRTBzMqq4Q3L0qTgrnNKRT9Fsi+aiMtza5CAXoYbQIDAQABo3EwbzAOBgNVHQ8B +Af8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB +/wQCMAAwHwYDVR0jBBgwFoAUD+hW9Gu1xM0hlT2WORo47lijEK0wDwYDVR0RBAgw +BocEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAhXQiQrK5nGOJRKBwePXl84PzWO0y +z2MEOJmmZcmom1uRW/LlInTRXbX1s8wkFlCkSsGtW+IE4uF8WmKrKR+/shXzVRm2 +TfYWU07GuJhg768BONCVqC2K8kWn2TwSAz6im1rPsQaoQcjlD1F3IrYDYQvxP4cz +5mPdgHDpSPzsfkAghxr6wWr8VHnUSEiIF+WzIjne0lsW8Mscs2iQSU+vy9/5sxxd +zwieFZvgQAysNs4MVW4XUvQbxe1bgrUFTgFJlrYn1axZJxoxhPdyUzkkzDi5l7GR +tdmiwilYpqjGVUUOmchowYSHN4PjLCE7ZINlhsnUhcrTqLjpQRFsW8oyWg== +-----END CERTIFICATE----- diff --git a/example/tls/redis/key.pem b/example/tls/redis/key.pem new file mode 100644 index 0000000..2ef1f05 --- /dev/null +++ b/example/tls/redis/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAsBrTn0RmwyiZlOxB779Vam0M96SJbyf0w+EDVZXqVjuGdJxX +suSuEqF8fDZIycsRji+1WQ9IG5er4A/0TFUAxE7gFzag27hk0Y7vRnzrZi3PFive +kkP1r4SZuIgF2UnCfZcsMOMkBGJ8t36DygaDEOJ+eu+rPR6nlznbOtYlJOtYNUZh +3OZ927dBPlyAi8rAdLvHNAjYZWYL8mlNU9WoI+JKE9iDoQnFlJSaNtld7RsNTzIO +C/CaVjBjmCjgf70SirFLXk23xC44gOywRuO+Oo9dMRjEY2SjX1/9FKgSsyRQnQRT +BzMqq4Q3L0qTgrnNKRT9Fsi+aiMtza5CAXoYbQIDAQABAoIBAGWkdjRcxHsrucks +u7nm0yQEIRHmE7TmeO19t/D0ADcZUDeJ7UxBlP8H2dPPeR+PZ2iLvL3Uhif22KsQ +Sk6sWS70335Gd32Z5gbV2uDyROPK2NXRKDt/ohRWEmthhw6s9eaLFGR7FVS6i4VV +Ljeynn9mWt4V6t3yDYTJTfGdm/68KV7UJvD6TYnfs3MNeYsq0MsfXWWfsbVVj7CP +en7XIkh+9R3qNmZgtIb1jJXMVCzEuK/r0DezW5FRUFbS19Cp/DhIvjTICZXURt7p +uY3zYlWHpziTBnAgFz+6YQ1doV9GV1mKqqa2Hx+L9eml3vruc3p0E2uh8xNXcXKH +cWnsrrkCgYEAw/EuXHTM4wroFb17PGVIwNd1BrT0QzkJ11G/I13o9WUITAyj6Y4k +SXxl3J8faGRPPHXXgSswc9C3cGnkP3Py4zrZXT/Drf3Cly9I70ht8pQHckN56P8y +uNid/Se7IWUy2NObQxazMu7G7Szc3qFqaU+/t7Rjby4pIQeVmFCcdhMCgYEA5hUW +c9maBxKce8PLREgL98Akpu8z09mYM8YmjKyQVjfdkX5Igrv9b85tHbNHYUHc+5On +us7HMagasJyFhzykLylCpFpn4p4H6/XE4tJhtOeHfjVNwgt6iggpSPqsVPYgM8xE +LSBluSMWuQ8IHf4eK+1QkzPunEi5TbDoMSj7B38CgYBnT/Rs5VzeXXLPe6/NwW2h +2DipB6I/C4UH1d9dC3f4Y4QDbSrDy6GQaZnfwLqztSgeLdgqEBalCiieigbB+iXX +78CKLUPEqqb+Rf1DxUHLhIeElNVjp6Mb2YM75sYBLrWno7MapY5ozYNvrJbsf9l2 +m4jvmJpRFdqzwqb6v44vpwKBgQChv+d18F9pY3shQydOTHwlYy4hMX61C38FvuLw ++IvMISAiHa5qQjDMfkmVnKisxfnN3yMGoEHHNg/1Y0Q4K7ic8xvHoUrxNPoKt0// +ybkozbAiWOTeauVtzoj/pkKqxBEleQ/gzarVucZKuTeSpkidxwtjQRoZQsMKzDif +/thjjwKBgAN33iDDck6UGI0sWJE9aDT/X5u7p7G0AKvB7nlUKTFaMGwXLwZ00Ug6 +B6uoa2NUtqh4wWV1D43vzFzz4pKrsXlYyapTzI3Qw7lQ8SWOKcheAYDItFfrIdw1 +seI2E/pQAa8Hk7LWdm7auECu17avSc3RQ3uINnNSZQVFAc7qESjV +-----END RSA PRIVATE KEY----- diff --git a/example/vars-file.hcl b/example/vars-file.hcl new file mode 100644 index 0000000..515a117 --- /dev/null +++ b/example/vars-file.hcl @@ -0,0 +1,176 @@ +await-service-name = "redis-cluster-await" +dest-service-name = "redis-cluster" +primary-count = 3 +replica-count = 3 +redis-username = "replication-user" +redis-password = "435e9c4225f08813ef3af7c725f0d30d263b9cd3" +redis-tls-cacert = <<-EOF + -----BEGIN CERTIFICATE----- + MIIDSzCCAjOgAwIBAgIIAg26dvKrbYkwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE + AxMVbWluaWNhIHJvb3QgY2EgMDIwZGJhMCAXDTIxMTAyMzAyMTUxOVoYDzIxMjEx + MDIzMDMxNTE5WjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSAwMjBkYmEwggEi + MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDMlMwTusfIaEz7eB8RZua6kW81 + 1G5DzvO8X9GAi8mdlqoOuSmSvHNz59Vn0lUZ7H1NwyAXqPm7FgYrGzAPBKJ+Kkw9 + drJJLoeSWPtFT9lISbl9qRpYg99aWzWfuEdKYJDa5woZLoQEaARW88TcCB44wLtK + yJpakMJZKV5gXqfpfSARyQPLsP/jirVhD+bNEs3sBBxw2WMtDdVS12V1soD2iKA5 + wTiKNjjpbyea6Q5zWcjFq2K8upx65hL75tdkHaYCqLZeeq/ciglGvXDKenZMMSY9 + Oz4qUDhxWTcb15zyolT8fQ9QdZqvBoOD6WBuWTNXbnT3zISiZGAAPQgdv9d7AgMB + AAGjgYYwgYMwDgYDVR0PAQH/BAQDAgKEMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggr + BgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQP6Fb0a7XEzSGV + PZY5GjjuWKMQrTAfBgNVHSMEGDAWgBQP6Fb0a7XEzSGVPZY5GjjuWKMQrTANBgkq + hkiG9w0BAQsFAAOCAQEASfe9zRlpIXHy4+mp1PIpjGjJjk0NhPOcoN8B2vCqYWsJ + nnfl9zfORkWPL6PgiXWqS6nNC+iqRFBWphaRqtSle0j+4NLFnmmOMXI/NlCjAvTH + 6TNJ/H0nHlJ9p3Ui9a5MvZ8I/dOJLrFDX4/d9Lg76txKhFJBzXvxd9PSVKPJvnfx + x3aare5fkXy+JlZwP8FhbzIwVTmHGPxKEUCbImhmailXTfLTmm+bS1CW2OrOnlSn + ZPlEA8N1Y8ogNZQf2v65QCT7k64a1IuEA7XcH+W4+JhRAPPp1NujMTbeo855gMMm + D6LXhbMEV2jO6Yfqgr2H+fmiWq3nILj/XBSTEYNBqQ== + -----END CERTIFICATE----- + +EOF +redis-tls-cert = <<-EOF + -----BEGIN CERTIFICATE----- + MIIDJzCCAg+gAwIBAgIIEguoVcAkRXwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE + AxMVbWluaWNhIHJvb3QgY2EgMDIwZGJhMB4XDTIxMTIwNjIxNTIwOFoXDTI0MDEw + NTIxNTIwOFowFDESMBAGA1UEAxMJMTI3LjAuMC4xMIIBIjANBgkqhkiG9w0BAQEF + AAOCAQ8AMIIBCgKCAQEAsBrTn0RmwyiZlOxB779Vam0M96SJbyf0w+EDVZXqVjuG + dJxXsuSuEqF8fDZIycsRji+1WQ9IG5er4A/0TFUAxE7gFzag27hk0Y7vRnzrZi3P + FivekkP1r4SZuIgF2UnCfZcsMOMkBGJ8t36DygaDEOJ+eu+rPR6nlznbOtYlJOtY + NUZh3OZ927dBPlyAi8rAdLvHNAjYZWYL8mlNU9WoI+JKE9iDoQnFlJSaNtld7RsN + TzIOC/CaVjBjmCjgf70SirFLXk23xC44gOywRuO+Oo9dMRjEY2SjX1/9FKgSsyRQ + nQRTBzMqq4Q3L0qTgrnNKRT9Fsi+aiMtza5CAXoYbQIDAQABo3EwbzAOBgNVHQ8B + Af8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB + /wQCMAAwHwYDVR0jBBgwFoAUD+hW9Gu1xM0hlT2WORo47lijEK0wDwYDVR0RBAgw + BocEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAhXQiQrK5nGOJRKBwePXl84PzWO0y + z2MEOJmmZcmom1uRW/LlInTRXbX1s8wkFlCkSsGtW+IE4uF8WmKrKR+/shXzVRm2 + TfYWU07GuJhg768BONCVqC2K8kWn2TwSAz6im1rPsQaoQcjlD1F3IrYDYQvxP4cz + 5mPdgHDpSPzsfkAghxr6wWr8VHnUSEiIF+WzIjne0lsW8Mscs2iQSU+vy9/5sxxd + zwieFZvgQAysNs4MVW4XUvQbxe1bgrUFTgFJlrYn1axZJxoxhPdyUzkkzDi5l7GR + tdmiwilYpqjGVUUOmchowYSHN4PjLCE7ZINlhsnUhcrTqLjpQRFsW8oyWg== + -----END CERTIFICATE----- + +EOF +redis-tls-key = <<-EOF + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEAsBrTn0RmwyiZlOxB779Vam0M96SJbyf0w+EDVZXqVjuGdJxX + suSuEqF8fDZIycsRji+1WQ9IG5er4A/0TFUAxE7gFzag27hk0Y7vRnzrZi3PFive + kkP1r4SZuIgF2UnCfZcsMOMkBGJ8t36DygaDEOJ+eu+rPR6nlznbOtYlJOtYNUZh + 3OZ927dBPlyAi8rAdLvHNAjYZWYL8mlNU9WoI+JKE9iDoQnFlJSaNtld7RsNTzIO + C/CaVjBjmCjgf70SirFLXk23xC44gOywRuO+Oo9dMRjEY2SjX1/9FKgSsyRQnQRT + BzMqq4Q3L0qTgrnNKRT9Fsi+aiMtza5CAXoYbQIDAQABAoIBAGWkdjRcxHsrucks + u7nm0yQEIRHmE7TmeO19t/D0ADcZUDeJ7UxBlP8H2dPPeR+PZ2iLvL3Uhif22KsQ + Sk6sWS70335Gd32Z5gbV2uDyROPK2NXRKDt/ohRWEmthhw6s9eaLFGR7FVS6i4VV + Ljeynn9mWt4V6t3yDYTJTfGdm/68KV7UJvD6TYnfs3MNeYsq0MsfXWWfsbVVj7CP + en7XIkh+9R3qNmZgtIb1jJXMVCzEuK/r0DezW5FRUFbS19Cp/DhIvjTICZXURt7p + uY3zYlWHpziTBnAgFz+6YQ1doV9GV1mKqqa2Hx+L9eml3vruc3p0E2uh8xNXcXKH + cWnsrrkCgYEAw/EuXHTM4wroFb17PGVIwNd1BrT0QzkJ11G/I13o9WUITAyj6Y4k + SXxl3J8faGRPPHXXgSswc9C3cGnkP3Py4zrZXT/Drf3Cly9I70ht8pQHckN56P8y + uNid/Se7IWUy2NObQxazMu7G7Szc3qFqaU+/t7Rjby4pIQeVmFCcdhMCgYEA5hUW + c9maBxKce8PLREgL98Akpu8z09mYM8YmjKyQVjfdkX5Igrv9b85tHbNHYUHc+5On + us7HMagasJyFhzykLylCpFpn4p4H6/XE4tJhtOeHfjVNwgt6iggpSPqsVPYgM8xE + LSBluSMWuQ8IHf4eK+1QkzPunEi5TbDoMSj7B38CgYBnT/Rs5VzeXXLPe6/NwW2h + 2DipB6I/C4UH1d9dC3f4Y4QDbSrDy6GQaZnfwLqztSgeLdgqEBalCiieigbB+iXX + 78CKLUPEqqb+Rf1DxUHLhIeElNVjp6Mb2YM75sYBLrWno7MapY5ozYNvrJbsf9l2 + m4jvmJpRFdqzwqb6v44vpwKBgQChv+d18F9pY3shQydOTHwlYy4hMX61C38FvuLw + +IvMISAiHa5qQjDMfkmVnKisxfnN3yMGoEHHNg/1Y0Q4K7ic8xvHoUrxNPoKt0// + ybkozbAiWOTeauVtzoj/pkKqxBEleQ/gzarVucZKuTeSpkidxwtjQRoZQsMKzDif + /thjjwKBgAN33iDDck6UGI0sWJE9aDT/X5u7p7G0AKvB7nlUKTFaMGwXLwZ00Ug6 + B6uoa2NUtqh4wWV1D43vzFzz4pKrsXlYyapTzI3Qw7lQ8SWOKcheAYDItFfrIdw1 + seI2E/pQAa8Hk7LWdm7auECu17avSc3RQ3uINnNSZQVFAc7qESjV + -----END RSA PRIVATE KEY----- + +EOF +redis-config-template = <<-EOF + user default off + masteruser replication-user + masterauth {{ env "redis-password" }} + user replication-user on +@all ~* >{{ env "redis-password" }} + # Working Directory + dir {{ env "NOMAD_ALLOC_DIR" }}/data/ + daemonize no + # TCP Port (0 to disable) + port 0 + bind {{ env "NOMAD_IP_db" }} + tls-port {{ env "NOMAD_PORT_db" }} + tls-ca-cert-file {{ env "NOMAD_ALLOC_DIR" }}/data/redis-tls/ca-cert.pem + tls-cert-file {{ env "NOMAD_ALLOC_DIR" }}/data/redis-tls/cert.pem + tls-key-file {{ env "NOMAD_ALLOC_DIR" }}/data/redis-tls/key.pem + tls-cluster yes + tls-replication yes + cluster-enabled yes + cluster-node-timeout 5000 + cluster-config-file {{ env "NOMAD_ALLOC_DIR" }}/data/nodes.conf + cluster-require-full-coverage no + # Enable snapshotting and save a snapshot every 60 seconds if at least one key has changed. + save 60 1 + maxmemory-policy noeviction + loglevel warning + # List of renamed commands comes from: + # https://www.digitalocean.com/community/tutorials/how-to-secure-your-redis-installation-on-ubuntu-18-04 + rename-command BGREWRITEAOF "" + rename-command BGSAVE "" + rename-command CONFIG "" + rename-command DEBUG "" + rename-command DEL "" + rename-command FLUSHALL "" + rename-command FLUSHDB "" + rename-command KEYS "" + rename-command PEXPIRE "" + rename-command RENAME "" + rename-command SAVE "" + rename-command SHUTDOWN "" + rename-command SPOP "" + rename-command SREM "" + +EOF +attache-redis-tls-cert = <<-EOF + -----BEGIN CERTIFICATE----- + MIIDJzCCAg+gAwIBAgIIH6kXr5uW/6gwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE + AxMVbWluaWNhIHJvb3QgY2EgMDIwZGJhMB4XDTIxMTIwNjIxNTA0NloXDTI0MDEw + NTIxNTA0NlowFDESMBAGA1UEAxMJMTI3LjAuMC4xMIIBIjANBgkqhkiG9w0BAQEF + AAOCAQ8AMIIBCgKCAQEA+KjVq9tqg6Q8RAAM+FtfYEp+ge31wnwBieLv/CGnEaZS + QAd9zZqSjJPhrgCAT3qanBXFgT23vjV9ycjj8gkUpWuVRaeeF4/RlT3A9G5FElBn + 2kfX2UefBg2N4LB6vBUFdk0Eosk6eOGmO+BFoQ6Z8SljIFfDjdoRNrSsWhxWgNw/ + SsIot23gk7mvVOyEZ9Pnsua36xE/rClL4ywXs3LeykdQTwWVE86LOi+IBWTh9V+Q + AR7j59AYpFl3DIkZGnGSYb5ZluKMlPL8SG+UqqH7qdRJIiwEg1Z0cNcBX17+j6QC + CA9C51PGhg6/+yHbHgackEvdgGW25vRyvU0wxGNkSwIDAQABo3EwbzAOBgNVHQ8B + Af8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB + /wQCMAAwHwYDVR0jBBgwFoAUD+hW9Gu1xM0hlT2WORo47lijEK0wDwYDVR0RBAgw + BocEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAh5ZS+cogsguME6tfbJyhBy7bPfUP + 5zXr22Xuw41FWBIytGDwpcslnyxhYAX+ngCyNu5pzxgC3TKW5Jri5wPoMJlaFeya + keKZShbzJEQIjjGLWbxl9ojsrrKuesgz5XqKt06VVC/RoiVX5ybD1IGv7nSQdWVn + Q8ZYxmEcHrPn2juXHF4yguucEwuXyZnlWVGKrFchAMqMVHwSuvMMNLvTfsOWTuqU + KkDkLTeOacmvTsb7J/yyXxzJzzpwmpPIrgO6Igz4zSADp4ErMk+6MduZQKTA3rhX + r53Jtmv+CLORODWe5+Cw38dJYlsRdyf7ShPuJQDEDOEFSKVFHJEOU9F1MQ== + -----END CERTIFICATE----- + +EOF +attache-redis-tls-key = <<-EOF + -----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEA+KjVq9tqg6Q8RAAM+FtfYEp+ge31wnwBieLv/CGnEaZSQAd9 + zZqSjJPhrgCAT3qanBXFgT23vjV9ycjj8gkUpWuVRaeeF4/RlT3A9G5FElBn2kfX + 2UefBg2N4LB6vBUFdk0Eosk6eOGmO+BFoQ6Z8SljIFfDjdoRNrSsWhxWgNw/SsIo + t23gk7mvVOyEZ9Pnsua36xE/rClL4ywXs3LeykdQTwWVE86LOi+IBWTh9V+QAR7j + 59AYpFl3DIkZGnGSYb5ZluKMlPL8SG+UqqH7qdRJIiwEg1Z0cNcBX17+j6QCCA9C + 51PGhg6/+yHbHgackEvdgGW25vRyvU0wxGNkSwIDAQABAoIBAB/cGgx7/4jAaUxZ + KVBE/NJsmQryv1Nc6iGNpywJ78sOIWm8y/yk+nPymq7dt5L3ZYnsLDMkAj/nwKcz + Cym+yhtrzmNvV40zSyoxEGEBI+51yOip3dkkGRcAc5Y/Zmpk0x9WPOrSl6BXYSI4 + 2RMKuOSyZdYGCLNLJnt46MBe8yJtVTK+VQnZqgTxtVjlvVH0MoLfd0j8VFwW/pak + 3Cv50N5fduSBZIbEH+KTBfjqfcT9bAcoLSd1HA11zv9bjlHKPpA8F748FeNvwgwe + lyp6YAcmVaGY+FcKBQdStGiBox9h3XqjHUOgdDWVwhezAF8mNr48sqTE2AEoGf1r + fP8JJKkCgYEA+mVPEYMXU29nj6NnWXOMA0pN8dvWvLJLTer+XAkEnVOeRRR4qb01 + whX4WyjE+4HykZBFOqspe9V+hGwKqjOysRIlNRfu2nKo5SzQT+1V5+okzEJKWe3o + 2J/L0kJwjdCTt64G7VJhCFLQYI4AMjXtFvTEGDDSkypgJtzBfuDROJcCgYEA/jmT + 5sFCKNIizIllg/xf5uA6kFPWE5/bxrzl6T+57exUjlrEQJsU2SgHeg6ULBlkPlrF + eBpYRpK9Lazin6O/kg92wPnhjWGPlgeC/4/qePr4yRBWbwrK3ddVCCyxHJn2E2YO + joJURrCL6UZhKYbrZfK9jbf6Z/9IIpy6U9IBlG0CgYEAkWNjnrJ0R9Dm2+MwLiNG + R97MFUPlkpkf2nU5De16jXMw8cFqMnyXi0NAeoXYooSYeObBG8iohKu5E2C8bIkq + F2CG1CY6XQK4iKEVr2MKP2eXyDYxf7gBPE7EhShovB9AtiVJBmGPz8puDbJF8OGY + 8XxbpAQtMKApRkdl3qrhMK8CgYBND/MPbeG6Mgiua6/EFIqVl77o5SDtjfW3BqfC + zrhzsMHo7Qa0ds4ZDZNGooiz3XaPmEBnqcS8j9qcr916es6lXd6nnJeMndhCqEBD + a8KtrZYgjL1Gp8Ta/l0ePz3o55q6QqOC+2rEitu+eMEXL3jHzI89GFnlkHKzW0L4 + CZ7E+QKBgQCFuXf6evp56eS0440jZbQRtTw9fRodbpTkEJcncwIEHb5CyBQR7KG3 + icajrMNqHmdqSTZE2x/t/G2IyQ7qdWc4oHV7fHdavTRIED1qnPttihc7LX+56q6h + ptuRGx51m1nbP0LU+0H5vzZy712aCPtS92/n1BqKHOdTR6zYViR33w== + -----END RSA PRIVATE KEY----- + +EOF diff --git a/src/consul/client/client.go b/src/consul/client/client.go index d95b56a..2067a40 100644 --- a/src/consul/client/client.go +++ b/src/consul/client/client.go @@ -4,6 +4,7 @@ import ( "fmt" consul "github.com/hashicorp/consul/api" + "github.com/letsencrypt/attache/src/consul/config" ) type ServiceInfo struct { @@ -13,8 +14,17 @@ type ServiceInfo struct { onlyHealthy bool } -func New(client *consul.Client, serviceName, tagName string, onlyHealthy bool) *ServiceInfo { - return &ServiceInfo{client, serviceName, tagName, onlyHealthy} +func New(conf config.ConsulOpts, serviceName, tagName string, onlyHealthy bool) (*ServiceInfo, error) { + consulConfig, err := conf.MakeConsulConfig() + if err != nil { + return nil, err + } + + client, err := consul.NewClient(consulConfig) + if err != nil { + return nil, err + } + return &ServiceInfo{client, serviceName, tagName, onlyHealthy}, nil } func (s *ServiceInfo) GetNodeAddresses() ([]string, error) { diff --git a/src/consul/config/config.go b/src/consul/config/config.go new file mode 100644 index 0000000..5e70359 --- /dev/null +++ b/src/consul/config/config.go @@ -0,0 +1,61 @@ +package config + +import ( + "errors" + "fmt" + "net/http" + + consul "github.com/hashicorp/consul/api" +) + +// ConsulOpts is exported for use with flag.Parse(). +type ConsulOpts struct { + DC string + Address string + ACLToken string + EnableTLS bool + TLSCACert string + TLSCert string + TLSKey string +} + +func (c *ConsulOpts) Validate() error { + if c.EnableTLS { + if c.TLSCACert == "" { + return errors.New("missing required opt: 'consul-tls-ca-cert") + } + + if c.TLSCert == "" { + return errors.New("missing required opt: 'consul-tls-cert") + } + + if c.TLSKey == "" { + return errors.New("missing required opt: 'consul-tls-key") + } + } + return nil +} + +func (c *ConsulOpts) MakeConsulConfig() (*consul.Config, error) { + config := consul.DefaultConfig() + config.Datacenter = c.DC + config.Address = c.Address + config.Token = c.ACLToken + if c.EnableTLS { + config.Scheme = "https" + tlsConfig := consul.TLSConfig{ + Address: c.Address, + CAFile: c.TLSCACert, + CertFile: c.TLSCert, + KeyFile: c.TLSKey, + } + tlsClientConf, err := consul.SetupTLSConfig(&tlsConfig) + if err != nil { + return nil, fmt.Errorf("error creating TLS client config for consul: %w", err) + } + config.HttpClient.Transport = &http.Transport{ + TLSClientConfig: tlsClientConf, + } + } + return config, nil +} diff --git a/src/consul/lock/lock.go b/src/consul/lock/lock.go index b5726cf..0aab57b 100644 --- a/src/consul/lock/lock.go +++ b/src/consul/lock/lock.go @@ -2,6 +2,7 @@ package client import ( consul "github.com/hashicorp/consul/api" + "github.com/letsencrypt/attache/src/consul/config" logger "github.com/sirupsen/logrus" ) @@ -12,17 +13,32 @@ type Lock struct { sessionTimeout string } -func New(client *consul.Client, key string, sessionTimeout string) *Lock { - return &Lock{ +func New(conf config.ConsulOpts, key string, sessionTimeout string) (*Lock, error) { + consulConfig, err := conf.MakeConsulConfig() + if err != nil { + return nil, err + } + + client, err := consul.NewClient(consulConfig) + if err != nil { + return nil, err + } + lock := &Lock{ client: client, key: key, sessionTimeout: sessionTimeout, } + + err = lock.createSession() + if err != nil { + return nil, err + } + return lock, err } -// CreateSession creates a new ephemeral session using the Consul client. Any +// createSession creates a new ephemeral session using the Consul client. Any // data stored during this session will be deleted once the session expires. -func (l *Lock) CreateSession() error { +func (l *Lock) createSession() error { sessionConf := &consul.SessionEntry{ TTL: l.sessionTimeout, Behavior: "delete", diff --git a/src/redis/cli/cli.go b/src/redis/cli/cli.go index 30cd335..7026a30 100644 --- a/src/redis/cli/cli.go +++ b/src/redis/cli/cli.go @@ -7,10 +7,58 @@ import ( "time" "github.com/letsencrypt/attache/src/redis/client" + "github.com/letsencrypt/attache/src/redis/config" ) -func execute(command []string) error { - redisCli, _ := exec.LookPath("redis-cli") +func makeAuthArgs(conf config.RedisOpts) ([]string, error) { + password, err := conf.LoadPassword() + if err != nil { + return nil, err + } + + return []string{ + "--user", + conf.Username, + "--pass", + password, + }, nil +} + +func makeTLSArgs(conf config.RedisOpts) ([]string, error) { + _, err := conf.LoadTLS() + if err != nil { + return nil, err + } + + return []string{ + "--tls", + "--cert", + conf.TLSConfig.CertFile, + "--key", + conf.TLSConfig.KeyFile, + "--cacert", + conf.TLSConfig.CACertFile, + }, nil +} + +func execute(conf config.RedisOpts, command []string) error { + redisCli, err := exec.LookPath("redis-cli") + if err != nil { + return err + } + + tlsArgs, err := makeTLSArgs(conf) + if err != nil { + return err + } + command = append(command, tlsArgs...) + + authArgs, err := makeAuthArgs(conf) + if err != nil { + return err + } + command = append(command, authArgs...) + cmd := &exec.Cmd{ Path: redisCli, Args: append([]string{redisCli}, command...), @@ -18,7 +66,7 @@ func execute(command []string) error { Stderr: os.Stderr, } - err := cmd.Run() + err = cmd.Run() if err != nil { return fmt.Errorf( "problem encountered while running command[%q %q]: %w", @@ -30,16 +78,16 @@ func execute(command []string) error { return nil } -func CreateCluster(nodes []string, replicasPerShard int) error { +func CreateCluster(conf config.RedisOpts, nodes []string, replicasPerShard int) error { var opts []string opts = append(opts, "--cluster", "create") opts = append(opts, nodes...) opts = append(opts, "--cluster-yes", "--cluster-replicas", fmt.Sprint(replicasPerShard)) - return execute(opts) + return execute(conf, opts) } -func AddNewShardPrimary(newNodeAddr, destNodeAddr string) error { - err := execute([]string{"--cluster", "add-node", newNodeAddr, destNodeAddr}) +func AddNewShardPrimary(conf config.RedisOpts, destNodeAddr string) error { + err := execute(conf, []string{"--cluster", "add-node", conf.NodeAddr, destNodeAddr}) if err != nil { return err } @@ -51,9 +99,9 @@ func AddNewShardPrimary(newNodeAddr, destNodeAddr string) error { var ticks = time.Tick(5 * time.Second) for range ticks { attempts++ - err = execute([]string{"--cluster", "rebalance", newNodeAddr, "--cluster-use-empty-masters"}) + err = execute(conf, []string{"--cluster", "rebalance", conf.NodeAddr, "--cluster-use-empty-masters"}) if err != nil { - if attempts == 5 { + if attempts >= 5 { return err } continue @@ -63,18 +111,30 @@ func AddNewShardPrimary(newNodeAddr, destNodeAddr string) error { return nil } -func AddNewShardReplica(newNodeAddr, destNodeAddr string) error { - redisClient := client.New(destNodeAddr, "") - primaryAddr, primaryID, err := redisClient.GetPrimaryWithLeastReplicas() +func AddNewShardReplica(conf config.RedisOpts, destNodeAddr string) error { + clusterClient, err := client.New( + config.RedisOpts{ + NodeAddr: destNodeAddr, + Username: conf.Username, + PasswordConfig: conf.PasswordConfig, + TLSConfig: conf.TLSConfig, + }, + ) + if err != nil { + return err + } + + primaryAddr, primaryID, err := clusterClient.GetPrimaryWithLeastReplicas() if err != nil { return err } return execute( + conf, []string{ "--cluster", "add-node", - newNodeAddr, + conf.NodeAddr, primaryAddr, "--cluster-slave", "--cluster-master-id", diff --git a/src/redis/client/client.go b/src/redis/client/client.go index a1665e6..17f37c3 100644 --- a/src/redis/client/client.go +++ b/src/redis/client/client.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/go-redis/redis/v8" + "github.com/letsencrypt/attache/src/redis/config" "gopkg.in/yaml.v3" ) @@ -182,14 +183,20 @@ func (h *Client) getClusterNodes(connectedOnly, primaryOnly, replicaOnly bool) ( return parseClusterNodesResult(connectedOnly, primaryOnly, replicaOnly, result) } -func New(redisNodeAddr, redisNodePass string) *Client { - return &Client{ - NodeAddr: redisNodeAddr, - Client: redis.NewClient( - &redis.Options{ - Addr: redisNodeAddr, - Password: redisNodePass, - }, - ), +func New(conf config.RedisOpts) (*Client, error) { + options := &redis.Options{Addr: conf.NodeAddr} + + password, err := conf.LoadPassword() + if err != nil { + return nil, err + } + options.Username = conf.Username + options.Password = password + + tlsConfig, err := conf.LoadTLS() + if err != nil { + return nil, err } + options.TLSConfig = tlsConfig + return &Client{conf.NodeAddr, redis.NewClient(options)}, nil } diff --git a/src/redis/config/config.go b/src/redis/config/config.go new file mode 100644 index 0000000..dd1c9b8 --- /dev/null +++ b/src/redis/config/config.go @@ -0,0 +1,96 @@ +package config + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io/ioutil" + "strings" +) + +// RedisOpts is exported for use with flag.Parse(). +type RedisOpts struct { + NodeAddr string + Username string + PasswordConfig + TLSConfig +} + +func (c RedisOpts) Validate() error { + if c.NodeAddr == "" { + return errors.New("missing required opt: 'redis-node-addr'") + } + + if c.Username == "" { + return errors.New("missing required opt: 'redis-auth-username'") + } + + if c.PasswordFile == "" { + return errors.New("missing required opt: 'redis-auth-password-file'") + } + + if c.CACertFile == "" { + return errors.New("missing required opt: 'redis-tls-ca-cert'") + } + + if c.CertFile == "" { + return errors.New("missing required opt: 'redis-tls-cert-file'") + } + + if c.KeyFile == "" { + return errors.New("missing required opt: 'redis-tls-key-file'") + } + return nil +} + +// PasswordConfig contains a path to a file containing a password. +type PasswordConfig struct { + PasswordFile string +} + +// LoadPassword returns the password loaded from the inner `File`. +func (c PasswordConfig) LoadPassword() (string, error) { + contents, err := ioutil.ReadFile(c.PasswordFile) + if err != nil { + return "", fmt.Errorf("cannot load password: %w", err) + } + return strings.TrimRight(string(contents), "\n"), nil +} + +// TLSConfig contains certificates and a key used for redis-go client +// connections or passed as paths the the redis-cli. +type TLSConfig struct { + CertFile string + KeyFile string + CACertFile string +} + +// LoadTLS reads and parses the certificates and key provided by the TLSConfig and +// returns a *tls.Config suitable for redis-go client use. +func (c TLSConfig) LoadTLS() (*tls.Config, error) { + caCertBytes, err := ioutil.ReadFile(c.CACertFile) + if err != nil { + return nil, fmt.Errorf("reading CA cert from %q: %s", c.CACertFile, err) + } + + rootCAs := x509.NewCertPool() + ok := rootCAs.AppendCertsFromPEM(caCertBytes) + if !ok { + return nil, fmt.Errorf("parsing CA cert from %q failed", c.CACertFile) + } + + cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile) + if err != nil { + return nil, fmt.Errorf( + "loading key pair from %q and %q: %s", + c.CertFile, + c.KeyFile, + err, + ) + } + return &tls.Config{ + RootCAs: rootCAs, + Certificates: []tls.Certificate{cert}, + }, nil +}