Repository: tetratelabs/getenvoy
Pull request #28 (2019-08-01 11:36:37)

Implement debug framework
This also enables a basic admin API collection. I will follow up this PR with tarballing and tests for admin API collection in order to keep the size of this PR down.

Resolves https://github.com/tetratelabs/getenvoy/issues/30

Signed-off-by: Liam White <liam@tetrate.io>


Patch:
diff --git a/go.mod b/go.mod
index f0b96b6..db8ffe2 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
 	github.com/dsnet/compress v0.0.1 // indirect
 	github.com/golang/protobuf v1.3.2
 	github.com/golang/snappy v0.0.1 // indirect
+	github.com/hashicorp/go-multierror v1.0.0
 	github.com/mholt/archiver v3.1.1+incompatible
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/nwaples/rardecode v1.0.0 // indirect
@@ -14,5 +15,6 @@ require (
 	github.com/spf13/cobra v0.0.5
 	github.com/stretchr/testify v1.3.0
 	github.com/tetratelabs/getenvoy-package v0.0.0-20190730071641-da31aed4333e
+	github.com/tetratelabs/log v0.0.0-20190710134534-eb04d1e84fb8
 	github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
 )
diff --git a/go.sum b/go.sum
index d74f338..e2a46fd 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,8 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@@ -13,14 +15,23 @@ github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5Jflh
 github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
@@ -33,12 +44,16 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/natefinch/lumberjack v0.0.0-20170531160350-a96e63847dc3 h1:BDvcX7oM8ZWOS08LQXaW8ucGblfoSG4srpoW6pKhvqs=
+github.com/natefinch/lumberjack v0.0.0-20170531160350-a96e63847dc3/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
 github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
 github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
 github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
+github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
@@ -48,6 +63,7 @@ github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
 github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
 github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
@@ -62,17 +78,39 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/tetratelabs/getenvoy-package v0.0.0-20190730071641-da31aed4333e h1:P03rR9jXeD2ilkYVsRlDz/EyS2rTg5Nko8T/LzPri5k=
 github.com/tetratelabs/getenvoy-package v0.0.0-20190730071641-da31aed4333e/go.mod h1:IrGJ2YZz3+qia1dEU9GFg3QieuMUUwiJf5tjG8uhogg=
+github.com/tetratelabs/log v0.0.0-20190710134534-eb04d1e84fb8 h1:a7FN/XPymdzttMaO+u4osDaV11WTJtgJGtmPMJfqeM8=
+github.com/tetratelabs/log v0.0.0-20190710134534-eb04d1e84fb8/go.mod h1:w+dEBsxcYEFg0I6whrgkMzjD8GBBQgmDq9hykB30pt8=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
 github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
+go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/grpc v1.15.0 h1:Az/KuahOM4NAidTEuJCv/RonAA7rYsTPkqXVjr+8OOw=
+google.golang.org/grpc v1.15.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/pkg/binary/getenvoy/fetch.go b/pkg/binary/getenvoy/fetch.go
index f9e0957..3dc37c8 100644
--- a/pkg/binary/getenvoy/fetch.go
+++ b/pkg/binary/getenvoy/fetch.go
@@ -45,7 +45,7 @@ func (r *Runtime) Fetch(key *manifest.Key, binaryLocation string) error {
 func (r *Runtime) binaryPath(key *manifest.Key) string {
 	platform := strings.ToLower(key.Platform)
 	platform = strings.ReplaceAll(platform, "-", "_")
-	return filepath.Join(r.local, key.Flavor, key.Version, platform)
+	return filepath.Join(r.local, "builds", key.Flavor, key.Version, platform)
 }
 
 func fetchEnvoy(dst, src string) error {
diff --git a/pkg/binary/getenvoy/procattr.go b/pkg/binary/getenvoy/procattr.go
new file mode 100644
index 0000000..586d950
--- /dev/null
+++ b/pkg/binary/getenvoy/procattr.go
@@ -0,0 +1,31 @@
+// Copyright 2019 Tetrate
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// +build !linux
+
+package getenvoy
+
+import (
+	"syscall"
+
+	"github.com/tetratelabs/log"
+)
+
+func sysProcAttr() *syscall.SysProcAttr {
+	log.Warn("Running on a non-Linux system, cannot set child process kill signal. " +
+		"Some cases where this process terminates before the Envoy child process may not be handled gracefully.")
+	return &syscall.SysProcAttr{
+		Setpgid: true, // equivalent to setpgrp() syscall
+	}
+}
diff --git a/pkg/binary/getenvoy/procattr_linux.go b/pkg/binary/getenvoy/procattr_linux.go
new file mode 100644
index 0000000..6485341
--- /dev/null
+++ b/pkg/binary/getenvoy/procattr_linux.go
@@ -0,0 +1,24 @@
+// Copyright 2019 Tetrate
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package getenvoy
+
+import "syscall"
+
+func sysProcAttr() *syscall.SysProcAttr {
+	return &syscall.SysProcAttr{
+		Setpgid:   true,            // equivalent to setpgrp() syscall
+		Pdeathsig: syscall.SIGTERM, // ensure the child Envoy process is cleaned up even if we die
+	}
+}
diff --git a/pkg/binary/getenvoy/run.go b/pkg/binary/getenvoy/run.go
index 7bf13f1..448e8a8 100644
--- a/pkg/binary/getenvoy/run.go
+++ b/pkg/binary/getenvoy/run.go
@@ -15,29 +15,88 @@
 package getenvoy
 
 import (
+	"context"
 	"fmt"
 	"os"
+	"os/exec"
+	"os/signal"
 	"path/filepath"
+	"strconv"
 	"syscall"
+	"time"
 
 	"github.com/tetratelabs/getenvoy/pkg/manifest"
+	"github.com/tetratelabs/log"
 )
 
 // Run execs the binary defined by the key with the args passed
+// It is a blocking function that can only be terminated via SIGINT
 func (r *Runtime) Run(key *manifest.Key, args []string) error {
 	path := filepath.Join(r.binaryPath(key), "envoy")
 	return r.RunPath(path, args)
 }
 
 // RunPath execs the binary at the path with the args passed
+// It is a blocking function that can only be terminated via SIGINT
 func (r *Runtime) RunPath(path string, args []string) error {
 	if _, err := os.Stat(path); err != nil {
 		return fmt.Errorf("unable to stat %q: %v", path, err)
 	}
-	_, filename := filepath.Split(path)
-	// #nosec -> passthrough by design
-	if err := syscall.Exec(path, append([]string{filename}, args...), os.Environ()); err != nil {
-		return fmt.Errorf("unable to exec %q: %v", path, err)
+
+	// Debug store should always be created prior to any prestart functions
+	if err := r.initializeDebugStore(); err != nil {
+		return fmt.Errorf("unable to create directory to store debug information: %v", err)
 	}
+
+	ctx, cancel := context.WithCancel(context.Background())
+	r.handlePreStart()
+
+	go r.runEnvoy(path, args, cancel)
+
+	r.waitForTerminationSignals(ctx)
+	r.handleTermination()
+	cancel() // this should never actually do anything but lets avoid any future context leaks
+
+	// Block until the Envoy process and termination handler are finished cleaning up
+	r.wg.Wait()
 	return nil
 }
+
+func (r *Runtime) waitForTerminationSignals(ctx context.Context) {
+	signal.Notify(r.signals, syscall.SIGINT)
+
+	// Block until we receive SIGINT or are canceled because Envoy has died
+	select {
+	case <-ctx.Done():
+		log.Infof("No Envoy processes remaining, terminating GetEnvoy process (PID=%d)", os.Getpid())
+		return
+	case <-r.signals:
+		log.Infof("GetEnvoy process (PID=%d) received SIGINT", os.Getpid())
+		return
+	}
+}
+
+func (r *Runtime) runEnvoy(path string, args []string, cancel context.CancelFunc) {
+	// #nosec -> users can run whatever binary they like!
+	r.cmd = exec.Command(path, args...)
+	r.cmd.SysProcAttr = sysProcAttr()
+	r.cmd.Stdout = os.Stdout
+	r.cmd.Stderr = os.Stderr
+
+	r.wg.Add(1)
+	defer r.wg.Done()
+	defer cancel()
+
+	if err := r.cmd.Run(); err != nil {
+		if r.cmd.ProcessState.ExitCode() == -1 {
+			log.Infof("Envoy process (PID=%d) terminated via %v", r.cmd.Process.Pid, err)
+		} else {
+			log.Infof("Envoy process (PID=%d) terminated with an error: %v", r.cmd.Process.Pid, err)
+		}
+	}
+}
+
+func (r *Runtime) initializeDebugStore() error {
+	r.debugDir = filepath.Join(r.local, "debug", strconv.FormatInt(time.Now().UnixNano(), 10))
+	return os.MkdirAll(r.debugDir, 0750)
+}
diff --git a/pkg/binary/getenvoy/runtime.go b/pkg/binary/getenvoy/runtime.go
index 4152c07..e164c96 100644
--- a/pkg/binary/getenvoy/runtime.go
+++ b/pkg/binary/getenvoy/runtime.go
@@ -15,21 +15,46 @@
 package getenvoy
 
 import (
+	"os"
+	"os/exec"
 	"path/filepath"
+	"sync"
 
 	"github.com/mitchellh/go-homedir"
-	"github.com/tetratelabs/getenvoy/pkg/binary"
 )
 
 // New creates a new GetEnvoy binary.Runtime with the local file storage set to the home directory
-func New() (binary.Runtime, error) {
+func New(options ...func(*Runtime)) (*Runtime, error) {
 	usrDir, err := homedir.Dir()
-	return &Runtime{
-		local: filepath.Join(usrDir, ".getenvoy", "builds"),
-	}, err
+	local := filepath.Join(usrDir, ".getenvoy")
+	runtime := &Runtime{
+		local:          local,
+		wg:             &sync.WaitGroup{},
+		signals:        make(chan os.Signal),
+		preStart:       make([]preStartFunc, 0),
+		preTermination: make([]preTerminationFunc, 0),
+	}
+	for _, option := range options {
+		option(runtime)
+	}
+	return runtime, err
 }
 
 // Runtime implements the GetEnvoy binary.Runtime
 type Runtime struct {
-	local string
+	local    string
+	debugDir string
+
+	cmd *exec.Cmd
+	wg  *sync.WaitGroup
+
+	// This channel doesn't need to be on the struct, it's here to enable the testing of termination behavior.
+	// Don't push any information onto this channel as it will likely have unintended consequences.
+	signals chan os.Signal
+
+	preStart       []preStartFunc
+	preTermination []preTerminationFunc
 }
+
+type preStartFunc func(r *Runtime) error
+type preTerminationFunc func(r *Runtime) error
diff --git a/pkg/binary/getenvoy/start.go b/pkg/binary/getenvoy/start.go
new file mode 100644
index 0000000..affda59
--- /dev/null
+++ b/pkg/binary/getenvoy/start.go
@@ -0,0 +1,30 @@
+// Copyright 2019 Tetrate
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package getenvoy
+
+import "github.com/tetratelabs/log"
+
+func (r *Runtime) handlePreStart() {
+	// Execute all registered preStart functions
+	for _, f := range r.preStart {
+		if err := f(r); err != nil {
+			log.Error(err.Error())
+		}
+	}
+}
+
+func (r *Runtime) registerPreStart(f ...preStartFunc) {
+	r.preStart = append(r.preStart, f...)
+}
diff --git a/pkg/binary/getenvoy/termination.go b/pkg/binary/getenvoy/termination.go
new file mode 100644
index 0000000..ef82994
--- /dev/null
+++ b/pkg/binary/getenvoy/termination.go
@@ -0,0 +1,86 @@
+// Copyright 2019 Tetrate
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package getenvoy
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"path/filepath"
+	"syscall"
+
+	"github.com/hashicorp/go-multierror"
+	"github.com/tetratelabs/log"
+)
+
+func (r *Runtime) handleTermination() {
+	if r.cmd.ProcessState != nil {
+		log.Infof("Envoy process (PID=%d) terminated prematurely", r.cmd.Process.Pid)
+		return
+	}
+
+	// Execute all registered preTermination functions
+	for _, f := range r.preTermination {
+		if err := f(r); err != nil {
+			log.Error(err.Error())
+		}
+	}
+
+	// Forward on the SIGINT to Envoy
+	log.Infof("Sending Envoy process (PID=%d) SIGINT", r.cmd.Process.Pid)
+	_ = r.cmd.Process.Signal(syscall.SIGINT)
+
+	// TODO: tar it all up! (Liam)
+}
+
+func (r *Runtime) registerPreTermination(f ...preTerminationFunc) {
+	r.preTermination = append(r.preTermination, f...)
+}
+
+var adminAPIPaths = map[string]string{
+	"certs":             "certs.json",
+	"clusters":          "clusters.txt",
+	"config_dump":       "config_dump.json",
+	"contention":        "contention.txt",
+	"listeners":         "listeners.txt",
+	"memory":            "memory.json",
+	"server_info":       "server_info.json",
+	"stats?format=json": "stats.json",
+	"runtime":           "runtime.json",
+}
+
+// EnableEnvoyAdminDataCollection registers collection of Envoy Admin API information
+var EnableEnvoyAdminDataCollection = func(r *Runtime) {
+	r.registerPreTermination(func(r *Runtime) error {
+		var multiErr *multierror.Error
+		for path, file := range adminAPIPaths {
+			resp, err := http.Get(fmt.Sprintf("http://localhost:15001/%v", path))
+			if err != nil {
+				multiErr = multierror.Append(multiErr, err)
+			}
+			f, err := os.OpenFile(filepath.Join(r.debugDir, file), os.O_CREATE|os.O_WRONLY, 0600)
+			if err != nil {
+				multiErr = multierror.Append(multiErr, err)
+			}
+			defer func() { _ = f.Close() }()
+			defer func() { _ = resp.Body.Close() }()
+			if _, err := io.Copy(f, resp.Body); err != nil {
+				multiErr = multierror.Append(multiErr, err)
+			}
+		}
+		return multiErr.ErrorOrNil()
+	})
+}
diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go
index 8b417da..d398744 100644
--- a/pkg/cmd/run.go
+++ b/pkg/cmd/run.go
@@ -48,10 +48,13 @@ getenvoy run standard:1.10.1 -- --help
 			return nil
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
-			runtime, err := getenvoy.New()
+			runtime, err := getenvoy.New(
+				getenvoy.EnableEnvoyAdminDataCollection,
+			)
 			if err != nil {
 				return err
 			}
+
 			key, manifestErr := manifest.NewKey(args[0])
 			if manifestErr != nil {
 				if _, err := os.Stat(args[0]); err != nil {


Test patch:
diff --git a/pkg/binary/getenvoy/fetch_test.go b/pkg/binary/getenvoy/fetch_test.go
index d98fead..8d62880 100644
--- a/pkg/binary/getenvoy/fetch_test.go
+++ b/pkg/binary/getenvoy/fetch_test.go
@@ -43,14 +43,14 @@ func TestRuntime_Fetch(t *testing.T) {
 			key:              defaultDarwinKey,
 			tarballStructure: "golden",
 			responseStatus:   http.StatusOK,
-			envoyLocation:    "standard/1.11.0/darwin/envoy",
+			envoyLocation:    "builds/standard/1.11.0/darwin/envoy",
 		},
 		{
 			name:             "Handles directories called Envoy",
 			key:              defaultDarwinKey,
 			tarballStructure: "envoydirectory",
 			responseStatus:   http.StatusOK,
-			envoyLocation:    "standard/1.11.0/darwin/envoy",
+			envoyLocation:    "builds/standard/1.11.0/darwin/envoy",
 		},
 		{
 			name:             "errors if it can't find an envoy binary in tarball",
diff --git a/pkg/binary/getenvoy/run_test.go b/pkg/binary/getenvoy/run_test.go
new file mode 100644
index 0000000..32a799d
--- /dev/null
+++ b/pkg/binary/getenvoy/run_test.go
@@ -0,0 +1,116 @@
+// Copyright 2019 Tetrate
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package getenvoy
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"sync"
+	"syscall"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRuntime_RunPath(t *testing.T) {
+	tests := []struct {
+		name        string
+		args        []string
+		killerFunc  func(*Runtime)
+		wantPreTerm bool
+	}{
+		{
+			name:        "GetEnvoy shot first",
+			killerFunc:  func(r *Runtime) { r.signals <- syscall.SIGINT },
+			wantPreTerm: true,
+		},
+		{
+			name:        "Envoy shot first",
+			killerFunc:  func(r *Runtime) { r.cmd.Process.Signal(syscall.SIGINT) },
+			wantPreTerm: false,
+		},
+		{
+			name:        "Envoy simulate error",
+			killerFunc:  func(r *Runtime) { time.Sleep(time.Millisecond * 100) },
+			args:        []string{"error"},
+			wantPreTerm: false,
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+		t.Run(tc.name, func(t *testing.T) {
+			// This ensures functions are called in the correct order
+			r, preStartCalled, preTerminationCalled := newRuntimeWithMockFunctions(t)
+			tmpDir, _ := ioutil.TempDir("", "getenvoy-test-")
+			defer os.RemoveAll(tmpDir)
+			r.local = tmpDir
+
+			wg := &sync.WaitGroup{}
+			wg.Add(1)
+			go func() {
+				defer wg.Done()
+				r.RunPath(filepath.Join("testdata", "sleep.sh"), tc.args)
+			}()
+
+			waitForProcessStart(r)
+			tc.killerFunc(r)
+			wg.Wait()
+
+			// Assert appropriate functions are called
+			assert.True(t, *preStartCalled, "preStart was not called")
+			assert.Equal(t, tc.wantPreTerm, *preTerminationCalled, fmt.Sprintf("expected preTermination execution to be %v", tc.wantPreTerm))
+		})
+	}
+}
+
+func waitForProcessStart(r *Runtime) {
+	for r.cmd == nil || r.cmd.Process == nil {
+		time.Sleep(time.Millisecond)
+	}
+}
+
+// This ensures functions are called in the correct order
+func newRuntimeWithMockFunctions(t *testing.T) (*Runtime, *bool, *bool) {
+	preStartCalled := false
+	preStart := func(r *Runtime) {
+		r.registerPreStart(func(r *Runtime) error {
+			if r.cmd != nil && r.cmd.Process != nil {
+				t.Error("preStart was called after process has started")
+			}
+			preStartCalled = true
+			return nil
+		})
+	}
+
+	preTerminationCalled := false
+	preTermination := func(r *Runtime) {
+		r.registerPreTermination(func(r *Runtime) error {
+			if r.cmd != nil && r.cmd.Process == nil {
+				t.Error("preTermination was called before process was started")
+			}
+			if r.cmd != nil && r.cmd.ProcessState != nil {
+				t.Error("preTermination was called after process was terminated")
+			}
+			preTerminationCalled = true
+			return nil
+		})
+	}
+	runtime, _ := New(preStart, preTermination)
+	return runtime, &preStartCalled, &preTerminationCalled
+}
diff --git a/pkg/binary/getenvoy/testdata/sleep.sh b/pkg/binary/getenvoy/testdata/sleep.sh
new file mode 100755
index 0000000..be6140d
--- /dev/null
+++ b/pkg/binary/getenvoy/testdata/sleep.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+# Copyright 2019 Tetrate
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This script is used to simulate a process that never terminates unless it receives SIGINT, SIGTERM or SIGKILL
+terminate () {
+    echo "received $(($? - 128))"
+    exit 0
+}
+
+error() {
+    exit 1
+}
+
+trap terminate SIGINT SIGTERM SIGKILL
+
+while true; do
+    sleep 0.1
+    if "$1" == "error"; then
+        error
+    fi
+done