Skip to content

Commit

Permalink
Merge pull request hashicorp#15 from hashicorp/f-envoy
Browse files Browse the repository at this point in the history
Connect Injector Webhook with Envoy
  • Loading branch information
mitchellh committed Oct 10, 2018
2 parents ee548ca + 8915160 commit 095af4b
Show file tree
Hide file tree
Showing 9 changed files with 587 additions and 199 deletions.
37 changes: 37 additions & 0 deletions connect-inject/container_env.go
@@ -0,0 +1,37 @@
package connectinject

import (
"fmt"
"strconv"
"strings"

corev1 "k8s.io/api/core/v1"
)

func (h *Handler) containerEnvVars(pod *corev1.Pod) []corev1.EnvVar {
raw, ok := pod.Annotations[annotationUpstreams]
if !ok || raw == "" {
return []corev1.EnvVar{}
}

var result []corev1.EnvVar
for _, raw := range strings.Split(raw, ",") {
parts := strings.SplitN(raw, ":", 2)
port, _ := portValue(pod, strings.TrimSpace(parts[1]))
if port > 0 {
name := strings.TrimSpace(parts[0])
name = strings.ToUpper(strings.Replace(name, "-", "_", -1))
portStr := strconv.Itoa(int(port))

result = append(result, corev1.EnvVar{
Name: fmt.Sprintf("%s_CONNECT_SERVICE_HOST", name),
Value: "127.0.0.1",
}, corev1.EnvVar{
Name: fmt.Sprintf("%s_CONNECT_SERVICE_PORT", name),
Value: portStr,
})
}
}

return result
}
150 changes: 150 additions & 0 deletions connect-inject/container_init.go
@@ -0,0 +1,150 @@
package connectinject

import (
"bytes"
"strings"
"text/template"

corev1 "k8s.io/api/core/v1"
)

type initContainerCommandData struct {
PodName string
ServiceName string
ServicePort int32
Upstreams []initContainerCommandUpstreamData
}

type initContainerCommandUpstreamData struct {
Name string
LocalPort int32
}

// containerInit returns the init container spec for registering the Consul
// service, setting up the Envoy bootstrap, etc.
func (h *Handler) containerInit(pod *corev1.Pod) (corev1.Container, error) {
data := initContainerCommandData{
PodName: pod.Name,
ServiceName: pod.Annotations[annotationService],
}
if data.ServiceName == "" {
// Assertion, since we call defaultAnnotations above and do
// not mutate pods without a service specified.
panic("No service found. This should be impossible since we default it.")
}

// If a port is specified, then we determine the value of that port
// and register that port for the host service.
if raw, ok := pod.Annotations[annotationPort]; ok && raw != "" {
if port, _ := portValue(pod, raw); port > 0 {
data.ServicePort = port
}
}

// If upstreams are specified, configure those
if raw, ok := pod.Annotations[annotationUpstreams]; ok && raw != "" {
for _, raw := range strings.Split(raw, ",") {
parts := strings.SplitN(raw, ":", 2)
port, _ := portValue(pod, strings.TrimSpace(parts[1]))
if port > 0 {
data.Upstreams = append(data.Upstreams, initContainerCommandUpstreamData{
Name: strings.TrimSpace(parts[0]),
LocalPort: port,
})
}
}
}

// Render the command
var buf bytes.Buffer
tpl := template.Must(template.New("root").Parse(strings.TrimSpace(
initContainerCommandTpl)))
err := tpl.Execute(&buf, &data)
if err != nil {
return corev1.Container{}, err
}

return corev1.Container{
Name: "consul-connect-inject-init",
Image: h.ImageConsul,
Env: []corev1.EnvVar{
{
Name: "HOST_IP",
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{FieldPath: "status.hostIP"},
},
},
{
Name: "POD_IP",
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{FieldPath: "status.podIP"},
},
},
},
VolumeMounts: []corev1.VolumeMount{
corev1.VolumeMount{
Name: volumeName,
MountPath: "/consul/connect-inject",
},
},
Command: []string{"/bin/sh", "-ec", buf.String()},
}, nil
}

// initContainerCommandTpl is the template for the command executed by
// the init container.
const initContainerCommandTpl = `
export CONSUL_HTTP_ADDR="${HOST_IP}:8500"
export CONSUL_GRPC_ADDR="${HOST_IP}:8502"
# Register the service. The HCL is stored in the volume so that
# the preStop hook can access it to deregister the service.
cat <<EOF >/consul/connect-inject/service.hcl
services {
id = "{{ .PodName }}-{{ .ServiceName }}-proxy"
name = "{{ .ServiceName }}-proxy"
kind = "connect-proxy"
address = "${POD_IP}"
port = 20000
proxy {
destination_service_name = "{{ .ServiceName }}"
destination_service_id = "{{ .ServiceName}}"
{{ if (gt .ServicePort 0) -}}
local_service_address = "127.0.0.1"
local_service_port = {{ .ServicePort }}
{{ end -}}
{{ range .Upstreams -}}
upstreams {
destination_name = "{{ .Name }}"
local_bind_port = {{ .LocalPort }}
}
{{ end }}
}
checks {
name = "Proxy Public Listener"
tcp = "${POD_IP}:20000"
interval = "10s"
deregister_critical_service_after = "10m"
}
checks {
name = "Destination Alias"
alias_service = "{{ .ServiceName }}"
}
}
EOF
/bin/consul services register /consul/connect-inject/service.hcl
# Generate the envoy bootstrap code
/bin/consul connect envoy \
-proxy-id="{{ .PodName }}-{{ .ServiceName }}-proxy" \
-bootstrap > /consul/connect-inject/envoy-bootstrap.yaml
# Copy the Consul binary
cp /bin/consul /consul/connect-inject/consul
`
88 changes: 88 additions & 0 deletions connect-inject/container_init_test.go
@@ -0,0 +1,88 @@
package connectinject

import (
"strings"
"testing"

"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestHandlerContainerInit(t *testing.T) {
minimal := func() *corev1.Pod {
return &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
annotationService: "foo",
},
},

Spec: corev1.PodSpec{
Containers: []corev1.Container{
corev1.Container{
Name: "web",
},

corev1.Container{
Name: "web-side",
},
},
},
}
}

cases := []struct {
Name string
Pod func(*corev1.Pod) *corev1.Pod
Cmd string // Strings.Contains test
CmdNot string // Not contains
}{
{
"Only service",
func(pod *corev1.Pod) *corev1.Pod {
pod.Annotations[annotationService] = "web"
return pod
},
`service = "web"`,
`upstreams`,
},

{
"Service port specified",
func(pod *corev1.Pod) *corev1.Pod {
pod.Annotations[annotationService] = "web"
pod.Annotations[annotationPort] = "1234"
return pod
},
"port = 1234",
"",
},

{
"Upstream",
func(pod *corev1.Pod) *corev1.Pod {
pod.Annotations[annotationService] = "web"
pod.Annotations[annotationUpstreams] = "db:1234"
return pod
},
`destination_name = "db"`,
"",
},
}

for _, tt := range cases {
t.Run(tt.Name, func(t *testing.T) {
require := require.New(t)

var h Handler
container, err := h.containerInit(tt.Pod(minimal()))
require.NoError(err)
actual := strings.Join(container.Command, " ")
require.Contains(actual, tt.Cmd)
if tt.CmdNot != "" {
require.NotContains(actual, tt.CmdNot)
}
})
}
}
49 changes: 49 additions & 0 deletions connect-inject/container_sidecar.go
@@ -0,0 +1,49 @@
package connectinject

import (
"strings"

corev1 "k8s.io/api/core/v1"
)

func (h *Handler) containerSidecar(pod *corev1.Pod) corev1.Container {
return corev1.Container{
Name: "consul-connect-envoy-sidecar",
Image: h.ImageEnvoy,
Env: []corev1.EnvVar{
{
Name: "HOST_IP",
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{FieldPath: "status.hostIP"},
},
},
},
VolumeMounts: []corev1.VolumeMount{
corev1.VolumeMount{
Name: volumeName,
MountPath: "/consul/connect-inject",
},
},
Lifecycle: &corev1.Lifecycle{
PreStop: &corev1.Handler{
Exec: &corev1.ExecAction{
Command: []string{
"/bin/sh",
"-ec",
strings.TrimSpace(sidecarPreStopCommand),
},
},
},
},
Command: []string{
"envoy",
"--config-path", "/consul/connect-inject/envoy-bootstrap.yaml",
},
}
}

const sidecarPreStopCommand = `
export CONSUL_HTTP_ADDR="${HOST_IP}:8500"
/consul/connect-inject/consul services deregister \
/consul/connect-inject/service.hcl
`
20 changes: 20 additions & 0 deletions connect-inject/container_volume.go
@@ -0,0 +1,20 @@
package connectinject

import (
corev1 "k8s.io/api/core/v1"
)

// volumeName is the name of the volume that is created to store the
// Consul Connect injection data.
const volumeName = "consul-connect-inject-data"

// containerVolume returns the volume data to add to the pod. This volume
// is used for shared data between containers.
func (h *Handler) containerVolume() corev1.Volume {
return corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
}
}

0 comments on commit 095af4b

Please sign in to comment.