// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package sprawl import ( "bytes" "context" "fmt" "io" "github.com/hashicorp/consul/testing/deployer/sprawl/internal/secrets" "github.com/hashicorp/consul/testing/deployer/topology" ) const ( consulUID = "100" consulGID = "1000" consulUserArg = consulUID + ":" + consulGID ) func tlsPrefixFromNode(node *topology.Node) string { switch node.Kind { case topology.NodeKindServer: return node.Partition + "." + node.Name + ".server" case topology.NodeKindClient: return node.Partition + "." + node.Name + ".client" default: return "" } } func tlsCertCreateCommand(node *topology.Node) string { if node.IsServer() { return fmt.Sprintf(`consul tls cert create -server -dc=%s -node=%s`, node.Datacenter, node.PodName()) } else { return fmt.Sprintf(`consul tls cert create -client -dc=%s`, node.Datacenter) } } func (s *Sprawl) initTLS(ctx context.Context) error { for _, cluster := range s.topology.Clusters { var buf bytes.Buffer // Create the CA if not already done, and proceed to do all of the // consul CLI calls inside of a throwaway temp directory. buf.WriteString(` if [[ ! -f consul-agent-ca-key.pem || ! -f consul-agent-ca.pem ]]; then consul tls ca create fi rm -rf tmp mkdir -p tmp cp -a consul-agent-ca-key.pem consul-agent-ca.pem tmp cd tmp `) for _, node := range cluster.Nodes { if !node.IsAgent() || node.Disabled { continue } node.TLSCertPrefix = tlsPrefixFromNode(node) if node.TLSCertPrefix == "" { continue } expectPrefix := cluster.Datacenter + "-" + string(node.Kind) + "-consul-0" // Conditionally generate these in isolation and rename them to // not rely upon the numerical indexing. buf.WriteString(fmt.Sprintf(` if [[ ! -f %[1]s || ! -f %[2]s ]]; then rm -f %[3]s %[4]s %[5]s mv -f %[3]s %[1]s mv -f %[4]s %[2]s fi `, "../"+node.TLSCertPrefix+"-key.pem", "../"+node.TLSCertPrefix+".pem", expectPrefix+"-key.pem", expectPrefix+".pem", tlsCertCreateCommand(node), )) } err := s.runner.DockerExec(ctx, []string{ "run", "--rm", "-i", "--net=none", "-v", cluster.TLSVolumeName + ":/data", // TODO: latest busted? // https://hashicorp.slack.com/archives/C03EUN3QF1C/p1691784078972959 "busybox:1.34", "sh", "-c", // Need this so the permissions stick; docker seems to treat unused volumes differently. `touch /data/VOLUME_PLACEHOLDER && chown -R ` + consulUserArg + ` /data`, }, io.Discard, nil) if err != nil { return fmt.Errorf("could not initialize docker volume for cert data %q: %w", cluster.TLSVolumeName, err) } err = s.runner.DockerExec(ctx, []string{"run", "--rm", "-i", "--net=none", "-u", consulUserArg, "-v", cluster.TLSVolumeName + ":/data", "-w", "/data", "--entrypoint", "", cluster.Images.Consul, "/bin/sh", "-ec", buf.String(), }, io.Discard, nil) if err != nil { return fmt.Errorf("could not create all necessary TLS certificates in docker volume: %v", err) } var capture bytes.Buffer err = s.runner.DockerExec(ctx, []string{"run", "--rm", "-i", "--net=none", "-u", consulUserArg, "-v", cluster.TLSVolumeName + ":/data", "-w", "/data", "busybox:1.34", "cat", "/data/consul-agent-ca.pem", }, &capture, nil) if err != nil { return fmt.Errorf("could not read CA PEM from docker volume: %v", err) } caPEM := capture.String() if caPEM == "" { return fmt.Errorf("found empty CA PEM") } s.secrets.SaveGeneric(cluster.Name, secrets.CAPEM, caPEM) } return nil }