From 8b6d10914f0df8c7d0eb97ba941a264c076bb655 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Oct 2018 21:12:48 -0700 Subject: [PATCH 1/9] Move connect-inject into top-level package --- {helper/connect-inject => connect-inject}/handler.go | 0 {helper/connect-inject => connect-inject}/handler_test.go | 0 subcommand/inject-connect/command.go | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename {helper/connect-inject => connect-inject}/handler.go (100%) rename {helper/connect-inject => connect-inject}/handler_test.go (100%) diff --git a/helper/connect-inject/handler.go b/connect-inject/handler.go similarity index 100% rename from helper/connect-inject/handler.go rename to connect-inject/handler.go diff --git a/helper/connect-inject/handler_test.go b/connect-inject/handler_test.go similarity index 100% rename from helper/connect-inject/handler_test.go rename to connect-inject/handler_test.go diff --git a/subcommand/inject-connect/command.go b/subcommand/inject-connect/command.go index c1411e5eaf5b..2efb2c48c084 100644 --- a/subcommand/inject-connect/command.go +++ b/subcommand/inject-connect/command.go @@ -14,8 +14,8 @@ import ( "time" "github.com/gorilla/handlers" + "github.com/hashicorp/consul-k8s/connect-inject" "github.com/hashicorp/consul-k8s/helper/cert" - "github.com/hashicorp/consul-k8s/helper/connect-inject" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" "k8s.io/apimachinery/pkg/types" From 5fd9b66b0a8e0273335594272335b7486039e4eb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Oct 2018 21:57:03 -0700 Subject: [PATCH 2/9] connect-inject: initial work to inject Envoy --- connect-inject/container_init.go | 139 +++++++++++++++++++++++++ connect-inject/container_init_test.go | 88 ++++++++++++++++ connect-inject/container_sidecar.go | 49 +++++++++ connect-inject/container_volume.go | 20 ++++ connect-inject/handler.go | 141 +++++--------------------- connect-inject/handler_test.go | 96 ++++-------------- connect-inject/patch.go | 86 ++++++++++++++++ subcommand/inject-connect/command.go | 4 +- 8 files changed, 426 insertions(+), 197 deletions(-) create mode 100644 connect-inject/container_init.go create mode 100644 connect-inject/container_init_test.go create mode 100644 connect-inject/container_sidecar.go create mode 100644 connect-inject/container_volume.go create mode 100644 connect-inject/patch.go diff --git a/connect-inject/container_init.go b/connect-inject/container_init.go new file mode 100644 index 000000000000..27a40ea8ae22 --- /dev/null +++ b/connect-inject/container_init.go @@ -0,0 +1,139 @@ +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: "us.gcr.io/mitchellh-k8s/consul-dev:latest", + 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 </consul/connect-inject/service.hcl +services { + id = "{{ .PodName }}-{{ .ServiceName }}" + name = "{{ .ServiceName }}" + {{ if (gt .ServicePort 0) -}} + port = {{ .ServicePort }} + {{ end -}} + + connect { + sidecar_service { + checks { + name = "Connect Sidecar Alias" + alias_service = "{{ .ServiceName }}" + } + + proxy { + {{ range .Upstreams -}} + upstreams { + destination_name = "{{ .Name }}" + local_bind_port = {{ .LocalPort }} + } + {{ end }} + } + } + } +} +EOF + +/bin/consul services register /consul/connect-inject/service.hcl + +# Generate the envoy bootstrap code +/bin/consul connect envoy \ + -sidecar-for={{ .ServiceName }} \ + -bootstrap > /consul/connect-inject/envoy-bootstrap.yaml + +# Copy the Consul binary +cp /bin/consul /consul/connect-inject/consul +` diff --git a/connect-inject/container_init_test.go b/connect-inject/container_init_test.go new file mode 100644 index 000000000000..960ca0a58f77 --- /dev/null +++ b/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) + } + }) + } +} diff --git a/connect-inject/container_sidecar.go b/connect-inject/container_sidecar.go new file mode 100644 index 000000000000..63261285c2a0 --- /dev/null +++ b/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: "envoyproxy/envoy-alpine:v1.8.0", + 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 +` diff --git a/connect-inject/container_volume.go b/connect-inject/container_volume.go new file mode 100644 index 000000000000..e3a70676f129 --- /dev/null +++ b/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{}, + }, + } +} diff --git a/connect-inject/handler.go b/connect-inject/handler.go index 56bb0d03bd3b..2916f7067298 100644 --- a/connect-inject/handler.go +++ b/connect-inject/handler.go @@ -7,7 +7,6 @@ import ( "log" "net/http" "strconv" - "strings" "github.com/mattbaird/jsonpatch" "k8s.io/api/admission/v1beta1" @@ -152,11 +151,35 @@ func (h *Handler) Mutate(req *v1beta1.AdmissionRequest) *v1beta1.AdmissionRespon // Accumulate any patches here var patches []jsonpatch.JsonPatchOperation - // Add a container to it + // Add our volume that will be shared by the init container and + // the sidecar for passing data in the pod. + patches = append(patches, addVolume( + pod.Spec.Volumes, + []corev1.Volume{h.containerVolume()}, + "/spec/volumes")...) + + // Add the init container that registers the service and sets up + // the Envoy configuration. + container, err := h.containerInit(&pod) + if err != nil { + return &v1beta1.AdmissionResponse{ + Result: &metav1.Status{ + Message: fmt.Sprintf("Error configuring injection init container: %s", err), + }, + } + } + patches = append(patches, addContainer( + pod.Spec.InitContainers, + []corev1.Container{container}, + "/spec/initContainers")...) + + // Add the Envoy sidecar patches = append(patches, addContainer( pod.Spec.Containers, []corev1.Container{h.containerSidecar(&pod)}, "/spec/containers")...) + + // Add annotations so that we know we're injected patches = append(patches, updateAnnotation( pod.Annotations, map[string]string{annotationStatus: "injected"})...) @@ -237,66 +260,6 @@ func (h *Handler) defaultAnnotations(pod *corev1.Pod) error { return nil } -func (h *Handler) containerSidecar(pod *corev1.Pod) corev1.Container { - cmd := []string{ - "exec /bin/consul connect proxy", - "-http-addr=${HOST_IP}:8500", - } - - svc := pod.Annotations[annotationService] - if svc == "" { - // 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.") - } - cmd = append(cmd, "-service="+svc) - - // If a port is specified, then we determine the value of that port - // and register this proxy as a listener. This enables the proxy to - // act as an inbound connection receiver. - if raw, ok := pod.Annotations[annotationPort]; ok && raw != "" { - if port, _ := portValue(pod, raw); port > 0 { - cmd = append(cmd, - fmt.Sprintf("-service-addr=127.0.0.1:%d", port), - "-listen=${POD_IP}:12500", - "-register", - ) - } - } - - // 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 { - cmd = append(cmd, fmt.Sprintf( - "-upstream=%s:%d", strings.TrimSpace(parts[0]), port)) - } - } - } - - return corev1.Container{ - Name: "consul-connect-proxy", - Image: "consul:1.2.2", - Env: []corev1.EnvVar{ - { - Name: "POD_IP", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{FieldPath: "status.podIP"}, - }, - }, - { - Name: "HOST_IP", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{FieldPath: "status.hostIP"}, - }, - }, - }, - Command: []string{"/bin/sh", "-ec", strings.Join(cmd, " ")}, - } -} - func portValue(pod *corev1.Pod, value string) (int32, error) { // First search for the named port for _, c := range pod.Spec.Containers { @@ -319,57 +282,3 @@ func admissionError(err error) *v1beta1.AdmissionResponse { }, } } - -func addContainer(target, add []corev1.Container, base string) []jsonpatch.JsonPatchOperation { - var result []jsonpatch.JsonPatchOperation - first := len(target) == 0 - var value interface{} - for _, container := range add { - value = container - path := base - if first { - first = false - value = []corev1.Container{container} - } else { - path = path + "/-" - } - - result = append(result, jsonpatch.JsonPatchOperation{ - Operation: "add", - Path: path, - Value: value, - }) - } - - return result -} - -func updateAnnotation(target, add map[string]string) []jsonpatch.JsonPatchOperation { - var result []jsonpatch.JsonPatchOperation - if len(target) == 0 { - result = append(result, jsonpatch.JsonPatchOperation{ - Operation: "add", - Path: "/metadata/annotations", - Value: add, - }) - - return result - } - - for key, value := range add { - result = append(result, jsonpatch.JsonPatchOperation{ - Operation: "add", - Path: "/metadata/annotations/" + escapeJSONPointer(key), - Value: value, - }) - } - - return result -} - -// https://tools.ietf.org/html/rfc6901 -func escapeJSONPointer(s string) string { - s = strings.Replace(s, "~", "~0", -1) - s = strings.Replace(s, "/", "~1", -1) - return s -} diff --git a/connect-inject/handler_test.go b/connect-inject/handler_test.go index 6c8c84bba3bf..974973889af4 100644 --- a/connect-inject/handler_test.go +++ b/connect-inject/handler_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "net/http" "net/http/httptest" - "strings" "testing" "github.com/mattbaird/jsonpatch" @@ -64,7 +63,7 @@ func TestHandlerHandle(t *testing.T) { }, { - "empty pod", + "empty pod basic", Handler{}, v1beta1.AdmissionRequest{ Object: encodeRaw(t, &corev1.Pod{ @@ -73,6 +72,14 @@ func TestHandlerHandle(t *testing.T) { }, "", []jsonpatch.JsonPatchOperation{ + { + Operation: "add", + Path: "/spec/volumes", + }, + { + Operation: "add", + Path: "/spec/initContainers", + }, { Operation: "add", Path: "/spec/containers/-", @@ -118,6 +125,14 @@ func TestHandlerHandle(t *testing.T) { }, "", []jsonpatch.JsonPatchOperation{ + { + Operation: "add", + Path: "/spec/volumes", + }, + { + Operation: "add", + Path: "/spec/initContainers", + }, { Operation: "add", Path: "/spec/containers/-", @@ -294,83 +309,6 @@ func TestHandlerDefaultAnnotations(t *testing.T) { } } -func TestHandlerContainerSidecar(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", - "-register", - }, - - { - "Service port specified", - func(pod *corev1.Pod) *corev1.Pod { - pod.Annotations[annotationService] = "web" - pod.Annotations[annotationPort] = "1234" - return pod - }, - "-service-addr=127.0.0.1:1234", - "", - }, - - { - "Upstream", - func(pod *corev1.Pod) *corev1.Pod { - pod.Annotations[annotationService] = "web" - pod.Annotations[annotationUpstreams] = "db:1234" - return pod - }, - "-upstream=db:1234", - "", - }, - } - - for _, tt := range cases { - t.Run(tt.Name, func(t *testing.T) { - require := require.New(t) - - var h Handler - container := h.containerSidecar(tt.Pod(minimal())) - actual := strings.Join(container.Command, " ") - require.Contains(actual, tt.Cmd) - if tt.CmdNot != "" { - require.NotContains(actual, tt.CmdNot) - } - }) - } -} - // encodeRaw is a helper to encode some data into a RawExtension. func encodeRaw(t *testing.T, input interface{}) runtime.RawExtension { data, err := json.Marshal(input) diff --git a/connect-inject/patch.go b/connect-inject/patch.go new file mode 100644 index 000000000000..416c4c42eb8a --- /dev/null +++ b/connect-inject/patch.go @@ -0,0 +1,86 @@ +package connectinject + +import ( + "strings" + + "github.com/mattbaird/jsonpatch" + corev1 "k8s.io/api/core/v1" +) + +func addVolume(target, add []corev1.Volume, base string) []jsonpatch.JsonPatchOperation { + var result []jsonpatch.JsonPatchOperation + first := len(target) == 0 + var value interface{} + for _, v := range add { + value = v + path := base + if first { + first = false + value = []corev1.Volume{v} + } else { + path = path + "/-" + } + + result = append(result, jsonpatch.JsonPatchOperation{ + Operation: "add", + Path: path, + Value: value, + }) + } + + return result +} + +func addContainer(target, add []corev1.Container, base string) []jsonpatch.JsonPatchOperation { + var result []jsonpatch.JsonPatchOperation + first := len(target) == 0 + var value interface{} + for _, container := range add { + value = container + path := base + if first { + first = false + value = []corev1.Container{container} + } else { + path = path + "/-" + } + + result = append(result, jsonpatch.JsonPatchOperation{ + Operation: "add", + Path: path, + Value: value, + }) + } + + return result +} + +func updateAnnotation(target, add map[string]string) []jsonpatch.JsonPatchOperation { + var result []jsonpatch.JsonPatchOperation + if len(target) == 0 { + result = append(result, jsonpatch.JsonPatchOperation{ + Operation: "add", + Path: "/metadata/annotations", + Value: add, + }) + + return result + } + + for key, value := range add { + result = append(result, jsonpatch.JsonPatchOperation{ + Operation: "add", + Path: "/metadata/annotations/" + escapeJSONPointer(key), + Value: value, + }) + } + + return result +} + +// https://tools.ietf.org/html/rfc6901 +func escapeJSONPointer(s string) string { + s = strings.Replace(s, "~", "~0", -1) + s = strings.Replace(s, "/", "~1", -1) + return s +} diff --git a/subcommand/inject-connect/command.go b/subcommand/inject-connect/command.go index 2efb2c48c084..2385ed3ad475 100644 --- a/subcommand/inject-connect/command.go +++ b/subcommand/inject-connect/command.go @@ -191,9 +191,9 @@ func (c *Command) Help() string { const synopsis = "Inject Connect proxy sidecar." const help = ` -Usage: consul-k8s inject [options] +Usage: consul-k8s inject-connect [options] Run the admission webhook server for injecting the Consul Connect - proxy sidecar. + proxy sidecar. The sidecar uses Envoy by default. ` From f73b0ee083e0f81cfbcd2d9399dfe182de9e8544 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Oct 2018 22:04:50 -0700 Subject: [PATCH 3/9] connect-inject: configurable docker images --- connect-inject/container_init.go | 2 +- connect-inject/container_sidecar.go | 2 +- connect-inject/handler.go | 12 ++++++++++++ subcommand/inject-connect/command.go | 12 +++++++++++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/connect-inject/container_init.go b/connect-inject/container_init.go index 27a40ea8ae22..e935b499d9d4 100644 --- a/connect-inject/container_init.go +++ b/connect-inject/container_init.go @@ -66,7 +66,7 @@ func (h *Handler) containerInit(pod *corev1.Pod) (corev1.Container, error) { return corev1.Container{ Name: "consul-connect-inject-init", - Image: "us.gcr.io/mitchellh-k8s/consul-dev:latest", + Image: h.ImageConsul, Env: []corev1.EnvVar{ { Name: "HOST_IP", diff --git a/connect-inject/container_sidecar.go b/connect-inject/container_sidecar.go index 63261285c2a0..1dae08e88bf1 100644 --- a/connect-inject/container_sidecar.go +++ b/connect-inject/container_sidecar.go @@ -9,7 +9,7 @@ import ( func (h *Handler) containerSidecar(pod *corev1.Pod) corev1.Container { return corev1.Container{ Name: "consul-connect-envoy-sidecar", - Image: "envoyproxy/envoy-alpine:v1.8.0", + Image: h.ImageEnvoy, Env: []corev1.EnvVar{ { Name: "HOST_IP", diff --git a/connect-inject/handler.go b/connect-inject/handler.go index 2916f7067298..abbd3721bcb9 100644 --- a/connect-inject/handler.go +++ b/connect-inject/handler.go @@ -16,6 +16,11 @@ import ( "k8s.io/apimachinery/pkg/runtime/serializer" ) +const ( + DefaultConsulImage = "consul:1.3.0" + DefaultEnvoyImage = "envoyproxy/envoy-alpine:v1.8.0" +) + const ( // annotationStatus is the key of the annotation that is added to // a pod after an injection is done. @@ -56,6 +61,13 @@ var ( // Handler is the HTTP handler for admission webhooks. type Handler struct { + // ImageConsul is the container image for Consul to use. + // ImageEnvoy is the container imae for Envoy to use. + // + // Both of these MUST be set. + ImageConsul string + ImageEnvoy string + // RequireAnnotation means that the annotation must be given to inject. // If this is false, injection is default. RequireAnnotation bool diff --git a/subcommand/inject-connect/command.go b/subcommand/inject-connect/command.go index 2385ed3ad475..b96b5acfcfa7 100644 --- a/subcommand/inject-connect/command.go +++ b/subcommand/inject-connect/command.go @@ -32,6 +32,8 @@ type Command struct { flagCertFile string // TLS cert for listening (PEM) flagKeyFile string // TLS cert private key (PEM) flagDefaultInject bool // True to inject by default + flagConsulImage string // Docker image for Consul + flagEnvoyImage string // Docker image for Envoy flagSet *flag.FlagSet once sync.Once @@ -51,6 +53,10 @@ func (c *Command) init() { "PEM-encoded TLS certificate to serve. If blank, will generate random cert.") c.flagSet.StringVar(&c.flagKeyFile, "tls-key-file", "", "PEM-encoded TLS private key to serve. If blank, will generate random cert.") + c.flagSet.StringVar(&c.flagConsulImage, "consul-image", connectinject.DefaultConsulImage, + "Docker image for Consul. Defaults to an early version of Consul.") + c.flagSet.StringVar(&c.flagEnvoyImage, "envoy-image", connectinject.DefaultEnvoyImage, + "Docker image for Envoy. Defaults to Envoy 1.8.0.") c.help = flags.Usage(help, c.flagSet) } @@ -95,7 +101,11 @@ func (c *Command) Run(args []string) int { go c.certWatcher(ctx, certCh, clientset) // Build the HTTP handler and server - injector := connectinject.Handler{RequireAnnotation: !c.flagDefaultInject} + injector := connectinject.Handler{ + ImageConsul: c.flagConsulImage, + ImageEnvoy: c.flagEnvoyImage, + RequireAnnotation: !c.flagDefaultInject, + } mux := http.NewServeMux() mux.HandleFunc("/mutate", injector.Handle) mux.HandleFunc("/health/ready", c.handleReady) From 96e7a6c2f2ccf6ade1c7ef2b5c9535a60ad778b9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Oct 2018 22:38:13 -0700 Subject: [PATCH 4/9] connect-inject: -sidecar-for should point to an ID --- connect-inject/container_init.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connect-inject/container_init.go b/connect-inject/container_init.go index e935b499d9d4..11c61e045f47 100644 --- a/connect-inject/container_init.go +++ b/connect-inject/container_init.go @@ -131,7 +131,7 @@ EOF # Generate the envoy bootstrap code /bin/consul connect envoy \ - -sidecar-for={{ .ServiceName }} \ + -sidecar-for="{{ .PodName }}-{{ .ServiceName }}" \ -bootstrap > /consul/connect-inject/envoy-bootstrap.yaml # Copy the Consul binary From aab01c05260e4f697ac80f25dc934c49c1ad6aad Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Oct 2018 23:00:49 -0700 Subject: [PATCH 5/9] connectInject: need to set address to be routable --- connect-inject/container_init.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/connect-inject/container_init.go b/connect-inject/container_init.go index 11c61e045f47..1b0f903b6acb 100644 --- a/connect-inject/container_init.go +++ b/connect-inject/container_init.go @@ -109,6 +109,8 @@ services { connect { sidecar_service { + address = "${POD_IP}" + checks { name = "Connect Sidecar Alias" alias_service = "{{ .ServiceName }}" From 9bd23c9642680f08e2ca923c3e9fbf622b30debf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Oct 2018 21:45:52 -0700 Subject: [PATCH 6/9] connect-inject: register proxy only, add health checks --- connect-inject/container_init.go | 56 ++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/connect-inject/container_init.go b/connect-inject/container_init.go index 1b0f903b6acb..fb21d92e40af 100644 --- a/connect-inject/container_init.go +++ b/connect-inject/container_init.go @@ -101,30 +101,38 @@ export CONSUL_GRPC_ADDR="${HOST_IP}:8502" # the preStop hook can access it to deregister the service. cat </consul/connect-inject/service.hcl services { - id = "{{ .PodName }}-{{ .ServiceName }}" - name = "{{ .ServiceName }}" - {{ if (gt .ServicePort 0) -}} - port = {{ .ServicePort }} - {{ end -}} - - connect { - sidecar_service { - address = "${POD_IP}" - - checks { - name = "Connect Sidecar Alias" - alias_service = "{{ .ServiceName }}" - } - - proxy { - {{ range .Upstreams -}} - upstreams { - destination_name = "{{ .Name }}" - local_bind_port = {{ .LocalPort }} - } - {{ end }} - } + id = "{{ .PodName }}-{{ .ServiceName }}-proxy" + name = "{{ .ServiceName }}-proxy" + kind = "connect-proxy" + address = "${POD_IP}" + port = 20000 + + proxy { + destination_service_name = "{{ .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 @@ -133,7 +141,7 @@ EOF # Generate the envoy bootstrap code /bin/consul connect envoy \ - -sidecar-for="{{ .PodName }}-{{ .ServiceName }}" \ + -proxy-id="{{ .PodName }}-{{ .ServiceName }}-proxy" \ -bootstrap > /consul/connect-inject/envoy-bootstrap.yaml # Copy the Consul binary From 2bcddfea5f0f3fa453bf0242fea6a53266e20638 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Oct 2018 09:47:23 -0700 Subject: [PATCH 7/9] connect-inject: set env vars for upstreams --- connect-inject/container_env.go | 37 +++++++++++++++++++++++ connect-inject/container_init.go | 1 + connect-inject/handler.go | 15 ++++++++++ connect-inject/handler_test.go | 51 ++++++++++++++++++++++++++++++++ connect-inject/patch.go | 25 +++++++++++++++- 5 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 connect-inject/container_env.go diff --git a/connect-inject/container_env.go b/connect-inject/container_env.go new file mode 100644 index 000000000000..940f055bb6da --- /dev/null +++ b/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 +} diff --git a/connect-inject/container_init.go b/connect-inject/container_init.go index fb21d92e40af..8b2e8cfb9be6 100644 --- a/connect-inject/container_init.go +++ b/connect-inject/container_init.go @@ -109,6 +109,7 @@ services { proxy { destination_service_name = "{{ .ServiceName }}" + destination_service_id = "{{ .ServiceName}}" {{ if (gt .ServicePort 0) -}} local_service_address = "127.0.0.1" local_service_port = {{ .ServicePort }} diff --git a/connect-inject/handler.go b/connect-inject/handler.go index abbd3721bcb9..f30ffef138b4 100644 --- a/connect-inject/handler.go +++ b/connect-inject/handler.go @@ -170,6 +170,21 @@ func (h *Handler) Mutate(req *v1beta1.AdmissionRequest) *v1beta1.AdmissionRespon []corev1.Volume{h.containerVolume()}, "/spec/volumes")...) + // Add the upstream services as environment variables for easy + // service discovery. + for i, container := range pod.Spec.InitContainers { + patches = append(patches, addEnvVar( + container.Env, + h.containerEnvVars(&pod), + fmt.Sprintf("/spec/initContainers/%d/env", i))...) + } + for i, container := range pod.Spec.Containers { + patches = append(patches, addEnvVar( + container.Env, + h.containerEnvVars(&pod), + fmt.Sprintf("/spec/containers/%d/env", i))...) + } + // Add the init container that registers the service and sets up // the Envoy configuration. container, err := h.containerInit(&pod) diff --git a/connect-inject/handler_test.go b/connect-inject/handler_test.go index 974973889af4..8bf1c6b1c4ac 100644 --- a/connect-inject/handler_test.go +++ b/connect-inject/handler_test.go @@ -91,6 +91,57 @@ func TestHandlerHandle(t *testing.T) { }, }, + { + "pod with upstreams specified", + Handler{}, + v1beta1.AdmissionRequest{ + Object: encodeRaw(t, &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotationUpstreams: "echo:1234,db:1234", + }, + }, + + Spec: basicSpec, + }), + }, + "", + []jsonpatch.JsonPatchOperation{ + { + Operation: "add", + Path: "/spec/volumes", + }, + { + Operation: "add", + Path: "/spec/containers/0/env", + }, + { + Operation: "add", + Path: "/spec/containers/0/env/-", + }, + { + Operation: "add", + Path: "/spec/containers/0/env/-", + }, + { + Operation: "add", + Path: "/spec/containers/0/env/-", + }, + { + Operation: "add", + Path: "/spec/initContainers", + }, + { + Operation: "add", + Path: "/spec/containers/-", + }, + { + Operation: "add", + Path: "/metadata/annotations/" + escapeJSONPointer(annotationStatus), + }, + }, + }, + { "empty pod with injection disabled", Handler{}, diff --git a/connect-inject/patch.go b/connect-inject/patch.go index 416c4c42eb8a..665c18bccf1c 100644 --- a/connect-inject/patch.go +++ b/connect-inject/patch.go @@ -27,7 +27,6 @@ func addVolume(target, add []corev1.Volume, base string) []jsonpatch.JsonPatchOp Value: value, }) } - return result } @@ -55,6 +54,30 @@ func addContainer(target, add []corev1.Container, base string) []jsonpatch.JsonP return result } +func addEnvVar(target, add []corev1.EnvVar, base string) []jsonpatch.JsonPatchOperation { + var result []jsonpatch.JsonPatchOperation + first := len(target) == 0 + var value interface{} + for _, v := range add { + value = v + path := base + if first { + first = false + value = []corev1.EnvVar{v} + } else { + path = path + "/-" + } + + result = append(result, jsonpatch.JsonPatchOperation{ + Operation: "add", + Path: path, + Value: value, + }) + } + + return result +} + func updateAnnotation(target, add map[string]string) []jsonpatch.JsonPatchOperation { var result []jsonpatch.JsonPatchOperation if len(target) == 0 { From 5ad432b78ea74dbda3743bdda5613dd2a15750f7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 10 Oct 2018 08:37:16 -0700 Subject: [PATCH 8/9] subcommand/inject-connect: clarify Consul version --- subcommand/inject-connect/command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subcommand/inject-connect/command.go b/subcommand/inject-connect/command.go index b96b5acfcfa7..e97407808858 100644 --- a/subcommand/inject-connect/command.go +++ b/subcommand/inject-connect/command.go @@ -54,7 +54,7 @@ func (c *Command) init() { c.flagSet.StringVar(&c.flagKeyFile, "tls-key-file", "", "PEM-encoded TLS private key to serve. If blank, will generate random cert.") c.flagSet.StringVar(&c.flagConsulImage, "consul-image", connectinject.DefaultConsulImage, - "Docker image for Consul. Defaults to an early version of Consul.") + "Docker image for Consul. Defaults to an Consul 1.3.0.") c.flagSet.StringVar(&c.flagEnvoyImage, "envoy-image", connectinject.DefaultEnvoyImage, "Docker image for Envoy. Defaults to Envoy 1.8.0.") c.help = flags.Usage(help, c.flagSet) From 891516048e8646c8acce8e53baca023f8bed1163 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 10 Oct 2018 15:33:27 -0700 Subject: [PATCH 9/9] connect-inject: address Pr feedback --- connect-inject/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connect-inject/handler.go b/connect-inject/handler.go index f30ffef138b4..03b2d72a78f6 100644 --- a/connect-inject/handler.go +++ b/connect-inject/handler.go @@ -62,7 +62,7 @@ var ( // Handler is the HTTP handler for admission webhooks. type Handler struct { // ImageConsul is the container image for Consul to use. - // ImageEnvoy is the container imae for Envoy to use. + // ImageEnvoy is the container image for Envoy to use. // // Both of these MUST be set. ImageConsul string