diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 63f3dd3..99e74b2 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -18,7 +18,7 @@ jobs: name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.16 + go-version: 1.18 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 diff --git a/.gitignore b/.gitignore index 215dc9e..cb3210e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,6 @@ # vendor/ .todo +.idea/ bin/ \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml index d967dc6..4906d93 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -8,7 +8,6 @@ builds: ldflags: - -s -w -X github.com/foomo/gograpple/cmd/actions.version={{.Version}} goos: - - windows - darwin - linux goarch: diff --git a/Makefile b/Makefile index be11d12..5336113 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,7 @@ -pack-data: - go-bindata -o bindata/bindata.go -pkg bindata the-hook/... - -build: pack-data +build: go build -o bin/gograpple cmd/main.go -install: pack-data +install: go build -o /usr/local/bin/gograpple cmd/main.go test: diff --git a/app b/app new file mode 100755 index 0000000..2b0ceaa Binary files /dev/null and b/app differ diff --git a/bindata/bindata.go b/bindata/bindata.go deleted file mode 100644 index 8cfe3c0..0000000 --- a/bindata/bindata.go +++ /dev/null @@ -1,260 +0,0 @@ -// Code generated by go-bindata. -// sources: -// the-hook/Dockerfile -// the-hook/deployment-patch.yaml -// DO NOT EDIT! - -package bindata - -import ( - "bytes" - "compress/gzip" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" - "time" -) - -func bindataRead(data []byte, name string) ([]byte, error) { - gz, err := gzip.NewReader(bytes.NewBuffer(data)) - if err != nil { - return nil, fmt.Errorf("Read %q: %v", name, err) - } - - var buf bytes.Buffer - _, err = io.Copy(&buf, gz) - clErr := gz.Close() - - if err != nil { - return nil, fmt.Errorf("Read %q: %v", name, err) - } - if clErr != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -type asset struct { - bytes []byte - info os.FileInfo -} - -type bindataFileInfo struct { - name string - size int64 - mode os.FileMode - modTime time.Time -} - -func (fi bindataFileInfo) Name() string { - return fi.name -} -func (fi bindataFileInfo) Size() int64 { - return fi.size -} -func (fi bindataFileInfo) Mode() os.FileMode { - return fi.mode -} -func (fi bindataFileInfo) ModTime() time.Time { - return fi.modTime -} -func (fi bindataFileInfo) IsDir() bool { - return false -} -func (fi bindataFileInfo) Sys() interface{} { - return nil -} - -var _theHookDockerfile = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x24\xca\xb1\x6e\x83\x30\x10\x80\xe1\xdd\x4f\x71\xb5\x3a\xd6\x35\x52\xa5\x0e\x45\x1d\x18\x08\x62\x00\x22\x8b\x0c\x51\x94\x01\xec\xd3\x19\xc9\x70\x08\x0c\x79\xfd\x88\x64\xf9\x96\xff\xcf\x4c\x01\x65\x95\x15\xf9\x3f\x71\xe8\x26\xfa\x0b\x5d\xc4\x35\x8a\x93\x69\x2a\xf8\x7c\x15\x21\xcc\xa5\x06\x62\x20\x8c\x40\x43\xf4\x5b\xff\x6d\x79\xd4\xc4\xca\x61\xd8\x51\xbf\xb5\xa3\xd3\x2e\xec\x42\xe4\x75\x6b\xae\xe7\xa6\xac\x5b\xb8\x49\xdd\x0f\x93\x5e\xbd\xfc\x02\xa9\xec\xe1\xc3\x0f\x01\x21\x2e\x1b\xa6\xe0\x18\xd0\x7a\x06\x62\x45\x4b\x37\xcf\x01\x15\xf1\x47\x0a\x6b\x40\x9c\xe1\xe7\x37\x49\x8e\x67\x42\x79\x7f\x06\x00\x00\xff\xff\xee\xc6\x23\xf3\xa7\x00\x00\x00") - -func theHookDockerfileBytes() ([]byte, error) { - return bindataRead( - _theHookDockerfile, - "the-hook/Dockerfile", - ) -} - -func theHookDockerfile() (*asset, error) { - bytes, err := theHookDockerfileBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "the-hook/Dockerfile", size: 167, mode: os.FileMode(420), modTime: time.Unix(1620302229, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var _theHookDeploymentPatchYaml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x91\x41\x6a\xf3\x30\x10\x85\xf7\x39\xc5\x60\xbc\xfc\x9d\x03\x18\xfe\x55\xbb\x68\xa1\x81\x5c\x61\x22\x4f\x6c\x81\x46\x12\xb2\x1c\x28\x46\x77\x2f\x92\x62\x5b\x4e\x4b\x1b\xaf\xa4\x37\x33\x4f\xef\x1b\x1f\x46\x4b\xa2\x3d\x00\x78\x62\xab\xd0\x53\x3c\x03\x30\x79\xec\xd0\x63\xbe\x01\x28\xbc\x90\x1a\x97\x1b\xc0\x3c\xc3\xf1\x23\x6a\x10\x42\x0b\x95\x77\x13\x55\xa9\xb8\xd8\xc5\x4f\x18\xed\x51\x6a\x72\xeb\x60\x03\x1a\x99\xda\x34\xfe\xb2\x54\x21\x84\xd5\x57\x32\xf6\xf7\xfa\x7b\x3c\x96\x35\x61\x98\x51\x77\x5b\x08\x74\x7d\x11\x49\xc9\x1b\x69\x1a\xc7\xb3\x33\x17\xda\x64\x47\xd8\xc9\x1f\xf4\x9b\x51\x13\xd3\xc9\x4c\xda\x17\x26\x5b\x44\x8b\x5e\x0c\x8d\x30\xfa\x2a\x7b\x46\x5b\x74\x00\x70\x9c\x3a\xa3\x1f\x56\x92\xab\xec\x4f\x68\x93\x5b\x19\x39\x2d\xca\xa1\xee\x09\x6a\xf9\x0f\xea\x34\x08\xed\x7f\x38\xe6\x87\xf7\xbd\xcb\xd3\x55\x7e\x3b\x35\x37\xf3\x0c\xb5\x84\x10\xaa\x5f\x12\x64\xdf\xec\x19\xc5\x6f\x11\x48\x77\x9b\x96\xc9\x0b\xe8\xbf\x91\xc5\x42\xd8\xee\x52\x6c\x3f\xf3\x95\xac\x32\x9f\x4c\x7b\xfc\xe7\xe1\x9f\x46\x1f\xcc\x98\xb9\x77\x41\xec\xc3\x26\xde\xee\x5d\x0f\x61\xf2\x1a\xbe\x02\x00\x00\xff\xff\x97\x35\x00\xfe\xf3\x02\x00\x00") - -func theHookDeploymentPatchYamlBytes() ([]byte, error) { - return bindataRead( - _theHookDeploymentPatchYaml, - "the-hook/deployment-patch.yaml", - ) -} - -func theHookDeploymentPatchYaml() (*asset, error) { - bytes, err := theHookDeploymentPatchYamlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "the-hook/deployment-patch.yaml", size: 755, mode: os.FileMode(420), modTime: time.Unix(1620302229, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -// Asset loads and returns the asset for the given name. -// It returns an error if the asset could not be found or -// could not be loaded. -func Asset(name string) ([]byte, error) { - cannonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[cannonicalName]; ok { - a, err := f() - if err != nil { - return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) - } - return a.bytes, nil - } - return nil, fmt.Errorf("Asset %s not found", name) -} - -// MustAsset is like Asset but panics when Asset would return an error. -// It simplifies safe initialization of global variables. -func MustAsset(name string) []byte { - a, err := Asset(name) - if err != nil { - panic("asset: Asset(" + name + "): " + err.Error()) - } - - return a -} - -// AssetInfo loads and returns the asset info for the given name. -// It returns an error if the asset could not be found or -// could not be loaded. -func AssetInfo(name string) (os.FileInfo, error) { - cannonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[cannonicalName]; ok { - a, err := f() - if err != nil { - return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) - } - return a.info, nil - } - return nil, fmt.Errorf("AssetInfo %s not found", name) -} - -// AssetNames returns the names of the assets. -func AssetNames() []string { - names := make([]string, 0, len(_bindata)) - for name := range _bindata { - names = append(names, name) - } - return names -} - -// _bindata is a table, holding each asset generator, mapped to its name. -var _bindata = map[string]func() (*asset, error){ - "the-hook/Dockerfile": theHookDockerfile, - "the-hook/deployment-patch.yaml": theHookDeploymentPatchYaml, -} - -// AssetDir returns the file names below a certain -// directory embedded in the file by go-bindata. -// For example if you run go-bindata on data/... and data contains the -// following hierarchy: -// data/ -// foo.txt -// img/ -// a.png -// b.png -// then AssetDir("data") would return []string{"foo.txt", "img"} -// AssetDir("data/img") would return []string{"a.png", "b.png"} -// AssetDir("foo.txt") and AssetDir("notexist") would return an error -// AssetDir("") will return []string{"data"}. -func AssetDir(name string) ([]string, error) { - node := _bintree - if len(name) != 0 { - cannonicalName := strings.Replace(name, "\\", "/", -1) - pathList := strings.Split(cannonicalName, "/") - for _, p := range pathList { - node = node.Children[p] - if node == nil { - return nil, fmt.Errorf("Asset %s not found", name) - } - } - } - if node.Func != nil { - return nil, fmt.Errorf("Asset %s not found", name) - } - rv := make([]string, 0, len(node.Children)) - for childName := range node.Children { - rv = append(rv, childName) - } - return rv, nil -} - -type bintree struct { - Func func() (*asset, error) - Children map[string]*bintree -} -var _bintree = &bintree{nil, map[string]*bintree{ - "the-hook": &bintree{nil, map[string]*bintree{ - "Dockerfile": &bintree{theHookDockerfile, map[string]*bintree{}}, - "deployment-patch.yaml": &bintree{theHookDeploymentPatchYaml, map[string]*bintree{}}, - }}, -}} - -// RestoreAsset restores an asset under the given directory -func RestoreAsset(dir, name string) error { - data, err := Asset(name) - if err != nil { - return err - } - info, err := AssetInfo(name) - if err != nil { - return err - } - err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) - if err != nil { - return err - } - err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) - if err != nil { - return err - } - err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) - if err != nil { - return err - } - return nil -} - -// RestoreAssets restores an asset under the given directory recursively -func RestoreAssets(dir, name string) error { - children, err := AssetDir(name) - // File - if err != nil { - return RestoreAsset(dir, name) - } - // Dir - for _, child := range children { - err = RestoreAssets(dir, filepath.Join(name, child)) - if err != nil { - return err - } - } - return nil -} - -func _filePath(dir, name string) string { - cannonicalName := strings.Replace(name, "\\", "/", -1) - return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) -} - diff --git a/cmd/actions/flags.go b/cmd/actions/flags.go index 1c7f19b..4727b76 100644 --- a/cmd/actions/flags.go +++ b/cmd/actions/flags.go @@ -13,7 +13,7 @@ type HostPort struct { Port int } -func newHostPort(host string, port int) *HostPort { +func NewHostPort(host string, port int) *HostPort { addr, err := gograpple.CheckTCPConnection(host, port) if err == nil { host = addr.IP.String() @@ -56,7 +56,7 @@ type StringList struct { items []string } -func newStringList(separator string) *StringList { +func NewStringList(separator string) *StringList { return &StringList{separator: separator} } diff --git a/cmd/actions/root.go b/cmd/actions/root.go index 6091bad..75a21b8 100644 --- a/cmd/actions/root.go +++ b/cmd/actions/root.go @@ -15,11 +15,10 @@ func init() { rootCmd.PersistentFlags().StringVarP(&flagPod, "pod", "p", "", "pod name (default most recent one)") rootCmd.PersistentFlags().StringVarP(&flagContainer, "container", "c", "", "container name (default deployment name)") patchCmd.Flags().StringVarP(&flagImage, "image", "i", "", "image to be used for patching (default deployment image)") + patchCmd.Flags().StringVarP(&flagRepo, "repo", "r", "", "repository to be used for pushing patched image (default none)") patchCmd.Flags().StringArrayVarP(&flagMounts, "mount", "m", []string{}, "host path to be mounted (default none)") patchCmd.Flags().BoolVar(&flagRollback, "rollback", false, "rollback deployment to a previous state") - delveCmd.Flags().StringVar(&flagInput, "input", "", "go file input (default cwd)") - delveCmd.Flags().BoolVar(&flagCleanup, "cleanup", false, "cleanup delve debug session") - delveCmd.Flags().BoolVar(&flagContinue, "continue", false, "delve --continue option") + delveCmd.Flags().StringVar(&flagSourcePath, "source", "", ".go file source path (default cwd)") delveCmd.Flags().Var(flagArgs, "args", "go file args") delveCmd.Flags().Var(flagListen, "listen", "delve host:port to listen on") delveCmd.Flags().BoolVar(&flagVscode, "vscode", false, "launch a debug configuration in vscode") @@ -28,22 +27,22 @@ func init() { } var ( - flagTag string - flagDir string - flagVerbose bool - flagNamespace string - flagPod string - flagContainer string - flagImage string - flagMounts []string - flagInput string - flagArgs = newStringList(" ") - flagCleanup bool - flagRollback bool - flagContinue bool - flagListen = newHostPort("127.0.0.1", 0) - flagVscode bool - flagJSONLog bool + flagTag string + flagDir string + flagVerbose bool + flagNamespace string + flagPod string + flagContainer string + flagRepo string + flagImage string + flagMounts []string + flagSourcePath string + flagArgs = NewStringList(" ") + flagRollback bool + flagContinue bool + flagListen = NewHostPort("127.0.0.1", 0) + flagVscode bool + flagJSONLog bool ) var ( @@ -66,7 +65,7 @@ var ( if err != nil { return err } - return gograpple.ValidatePath(flagDir, &flagInput) + return gograpple.ValidatePath(flagDir, &flagSourcePath) }, } patchCmd = &cobra.Command{ @@ -81,7 +80,7 @@ var ( if err != nil { return err } - return grapple.Patch(flagImage, flagTag, flagContainer, mounts) + return grapple.Patch(flagRepo, flagImage, flagTag, flagContainer, mounts) }, } shellCmd = &cobra.Command{ @@ -93,23 +92,20 @@ var ( }, } delveCmd = &cobra.Command{ - Use: "delve [DEPLOYMENT] --input {INPUT} -n {NAMESPACE} -c {CONTAINER}", + Use: "delve [DEPLOYMENT] --source {SRC} -n {NAMESPACE} -c {CONTAINER}", Short: "start a headless delve debug server for .go input on a patched deployment", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - if flagCleanup { - return grapple.Cleanup(flagPod, flagContainer) - } - return grapple.Delve(flagPod, flagContainer, flagInput, flagArgs.items, flagListen.Host, flagListen.Port, flagContinue, flagVscode) + return grapple.Delve(flagPod, flagContainer, flagSourcePath, flagArgs.items, flagListen.Host, flagListen.Port, flagVscode) }, } ) func Execute() { if err := rootCmd.Execute(); err != nil { + l = logrus.NewEntry(logrus.New()) l.Fatal(err) } - } func newLogger(verbose, jsonLog bool) *logrus.Entry { diff --git a/delve.go b/delve.go new file mode 100644 index 0000000..ca164b7 --- /dev/null +++ b/delve.go @@ -0,0 +1,192 @@ +package gograpple + +import ( + "context" + "fmt" + "os" + "path" + "path/filepath" + "strings" + "time" + + "github.com/foomo/gograpple/delve" + "github.com/sirupsen/logrus" +) + +const delveBin = "dlv" + +func (g Grapple) Delve(pod, container, sourcePath string, binArgs []string, host string, + port int, vscode bool) error { + validateCtx := context.Background() + // validate k8s resources for delve session + if err := g.kubeCmd.ValidatePod(validateCtx, g.deployment, &pod); err != nil { + return err + } + if err := g.kubeCmd.ValidateContainer(g.deployment, &container); err != nil { + return err + } + if !g.isPatched() { + return fmt.Errorf("deployment not patched, stopping delve") + } + // populate bin args if empty + if len(binArgs) == 0 { + var err error + d, err := g.kubeCmd.GetDeploymentFromConfigMap(validateCtx, g.DeploymentConfigMapName(), + defaultConfigMapDeploymentKey) + if err != nil { + return err + } + c, err := g.kubeCmd.GetContainerFromDeployment(container, d) + if err != nil { + return err + } + binArgs = c.Args + } + // validate sourcePath + goModPath, err := findGoProjectRoot(sourcePath) + if err != nil { + return fmt.Errorf("couldnt find go.mod path for source %q", sourcePath) + } + + RunWithInterrupt(g.l, func(ctx context.Context) { + // run pre-start cleanup + clog := g.componentLog("cleanup") + clog.Info("running pre-start cleanup") + if err := g.cleanupPIDs(ctx, pod, container); err != nil { + clog.Error(err) + return + } + // deploy bin + dlog := g.componentLog("deploy") + dlog.Info("building and deploying bin") + if err := g.deployBin(ctx, pod, container, goModPath, sourcePath); err != nil { + dlog.Error(err) + return + } + // start delve server + dslog := g.componentLog("server") + dslog.Infof("starting delve server on %v:%v", host, port) + ds := delve.NewKubeDelveServer(dslog, g.deployment.Namespace, host, port) + ds.StartNoWait(ctx, pod, container, g.binDestination(), binArgs) + // port forward to pod with delve server + dclog := g.componentLog("client") + g.portForwardDelve(dclog, ctx, pod, host, port) + // check server state with delve client + if err := g.checkDelveConnection(dclog, ctx, 10, host, port); err != nil { + dclog.WithError(err).Error("couldnt connect to delver server") + return + } + // launch vscode + if vscode { + vlog := g.componentLog("vscode") + if err := launchVSCode(ctx, vlog, goModPath, host, port, 5); err != nil { + vlog.WithError(err).Error("couldnt launch vscode") + } + } + }) + defer g.cleanupPIDs(context.Background(), pod, container) + return nil +} +func (g Grapple) componentLog(name string) *logrus.Entry { + return g.l.WithField("component", name) +} + +func (g Grapple) binName() string { + return g.deployment.Name +} + +func (g Grapple) binDestination() string { + return "/" + g.binName() +} + +func (g Grapple) cleanupPIDs(ctx context.Context, pod, container string) error { + // get pids of delve and app were debugging + binPids, errBinPids := g.kubeCmd.GetPIDsOf(ctx, pod, container, g.binName()) + if errBinPids != nil { + return errBinPids + } + delvePids, errDelvePids := g.kubeCmd.GetPIDsOf(ctx, pod, container, delveBin) + if errDelvePids != nil { + return errDelvePids + } + // kill pids directly on pod container + maxTries := 10 + pids := append(binPids, delvePids...) + return tryCallWithContext(ctx, maxTries, time.Millisecond*200, func(i int) error { + killErrs := g.kubeCmd.KillPidsOnPod(ctx, pod, container, pids, true) + if len(killErrs) == 0 { + return nil + } + return fmt.Errorf("could not kill processes after %v attempts", maxTries) + }) +} + +func (g Grapple) deployBin(ctx context.Context, pod, container, goModPath, sourcePath string) error { + // build bin + binSource := path.Join(os.TempDir(), g.binName()) + var relInputs []string + inputInfo, errInputInfo := os.Stat(sourcePath) + if errInputInfo != nil { + return errInputInfo + } + if inputInfo.IsDir() { + if files, err := os.ReadDir(sourcePath); err != nil { + return err + } else { + for _, file := range files { + if path.Ext(file.Name()) == ".go" { + relInputs = append(relInputs, strings.TrimPrefix(path.Join(sourcePath, + file.Name()), goModPath+string(filepath.Separator))) + } + } + } + } else { + relInputs = append(relInputs, strings.TrimPrefix(sourcePath, goModPath+string(filepath.Separator))) + } + + _, errBuild := g.goCmd.Build(goModPath, binSource, relInputs, "-gcflags", "-N -l").Env("GOOS=linux", "GOARCH=amd64").Run(ctx) + if errBuild != nil { + return errBuild + } + // copy bin to pod + _, errCopyToPod := g.kubeCmd.CopyToPod(pod, container, binSource, g.binDestination()).Run(ctx) + return errCopyToPod +} + +func (g Grapple) portForwardDelve(l *logrus.Entry, ctx context.Context, pod, host string, port int) { + l.Info("port-forwarding pod for delve server") + cmd := g.kubeCmd.PortForwardPod(pod, host, port) + go func() { + _, err := cmd.Run(ctx) + if err != nil && err.Error() != "signal: killed" { + l.WithError(err).Errorf("port-forwarding %v pod failed", pod) + } + }() + <-cmd.Started() +} + +func (g Grapple) checkDelveConnection(l *logrus.Entry, ctx context.Context, tries int, host string, port int) error { + time.Sleep(1 * time.Second) // allow delve to become available + err := tryCallWithContext(ctx, tries, 1*time.Second, func(i int) error { + l.Infof("connecting to %v:%v (%d/%d)", host, port, i, tries) + dc, err := delve.NewKubeDelveClient(ctx, host, port) + if err != nil { + l.WithError(err).Warn("couldnt connect to delve server") + return err + } + defer func() { + if err := dc.Close(); err != nil { + l.WithError(err).Warn("couldnt close delve client") + } + }() + if err := dc.ValidateState(); err != nil { + l.WithError(err).Warn("couldnt get running state from delve server") + return err + } + return nil + }) + if err == nil { + l.Infof("delve server connection and state ok") + } + return err +} diff --git a/delve/client.go b/delve/client.go new file mode 100644 index 0000000..999d8b2 --- /dev/null +++ b/delve/client.go @@ -0,0 +1,53 @@ +package delve + +import ( + "context" + "fmt" + "net" + "net/rpc/jsonrpc" + + "github.com/go-delve/delve/service/rpc2" +) + +type KubeDelveClient struct { + *rpc2.RPCClient + conn net.Conn +} + +func NewKubeDelveClient(ctx context.Context, host string, port int) (*KubeDelveClient, error) { + addr := fmt.Sprintf("%v:%v", host, port) + _, err := jsonrpc.Dial("tcp", addr) + if err != nil { + return nil, err + } + d := net.Dialer{} + conn, err := d.DialContext(ctx, "tcp", addr) + if err != nil { + return nil, err + } + // beware: the use of rpc2.NewClient will land you with a log.Fatal upon error + return &KubeDelveClient{rpc2.NewClientFromConn(conn), conn}, nil +} + +func (kdc KubeDelveClient) ValidateState() error { + state, err := kdc.GetStateNonBlocking() + if err != nil { + return err + } + if state.Exited { + // Exited indicates whether the debugged process has exited. + return fmt.Errorf("delve debugged process has exited") + } else if !state.Running { + // Running is true if the process is running and no other information can be collected. + // theres a case when you are in a breakpoint on a zombie process + // dlv will not handle that gracefully + return fmt.Errorf("delve debugged process is not running (zombie/breakpoint)") + } else { + // were good + return nil + } +} + +func (kdc KubeDelveClient) Close() error { + return kdc.conn.Close() +} diff --git a/delve/server.go b/delve/server.go new file mode 100644 index 0000000..404afb4 --- /dev/null +++ b/delve/server.go @@ -0,0 +1,77 @@ +package delve + +import ( + "context" + "fmt" + "os" + + "github.com/foomo/gograpple/exec" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +type KubeDelveServer struct { + host string + port int + kubeCmd *exec.KubectlCmd + process *os.Process +} + +func (kds KubeDelveServer) Host() string { + return kds.host +} + +func (kds KubeDelveServer) Port() int { + return kds.port +} + +func NewKubeDelveServer(l *logrus.Entry, namespace, host string, port int) *KubeDelveServer { + kubectl := exec.NewKubectlCommand() + kubectl.Logger(l).Args("-n", namespace) + return &KubeDelveServer{host, port, kubectl, nil} +} + +func (kds *KubeDelveServer) StartNoWait(ctx context.Context, pod, container string, + binDest string, binArgs []string) { + cmd := kds.kubeCmd.ExecPod(pod, container, kds.buildCommand(binDest, binArgs)) + cmd.PostStart( + func(p *os.Process) error { + kds.process = p + return nil + }).NoWait().Run(ctx) + <-cmd.Started() +} + +func (kds *KubeDelveServer) Start(ctx context.Context, pod, container string, + binDest string, binArgs []string) error { + cmd := kds.kubeCmd.ExecPod(pod, container, kds.buildCommand(binDest, binArgs)) + // execute command to run dlv on container + out, err := cmd.PostStart( + func(p *os.Process) error { + kds.process = p + return nil + }).Run(ctx) + return errors.WithMessage(err, out) +} + +func (kds KubeDelveServer) buildCommand(binDest string, binArgs []string) []string { + cmd := []string{ + "dlv", "exec", binDest, "--headless", + fmt.Sprintf("--listen=:%v", kds.port), "--accept-multiclient", "--continue", + } + if len(binArgs) > 0 { + cmd = append(cmd, "--") + cmd = append(cmd, binArgs...) + } + return cmd +} + +func (kds *KubeDelveServer) Stop() error { + if kds.process == nil { + return fmt.Errorf("no process found, run Start first") + } + if err := kds.process.Release(); err != nil { + return err + } + return kds.process.Kill() +} diff --git a/delve_test.go b/delve_test.go new file mode 100644 index 0000000..5d35911 --- /dev/null +++ b/delve_test.go @@ -0,0 +1,63 @@ +package gograpple + +import ( + "net" + "testing" + + "github.com/sirupsen/logrus" +) + +const testNamespace = "test" + +func testGrapple(t *testing.T, deployment string) *Grapple { + g, err := NewGrapple(logrus.NewEntry(logrus.StandardLogger()), testNamespace, deployment) + if err != nil { + t.Fatal(err) + } + return g +} + +func setUp(t *testing.T, context string) { + // set context via kubectl +} + +func delveSetUp(t *testing.T, g *Grapple) { + // todo + // make build + // make deploy, use testNamespace + // patch +} + +func testAddr(t *testing.T) *net.TCPAddr { + addr, err := CheckTCPConnection("127.0.0.1", 0) + if err != nil { + t.Fatal(err) + } + return addr +} + +func TestGrapple_Delve(t *testing.T) { + g := testGrapple(t, "example") + delveSetUp(t, g) + addr := testAddr(t) + type args struct { + sourcePath string + host string + port int + vscode bool + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"test", args{"test/app", addr.IP.String(), addr.Port, true}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := g.Delve("", "", tt.args.sourcePath, nil, tt.args.host, tt.args.port, tt.args.vscode); (err != nil) != tt.wantErr { + t.Errorf("Grapple.Delve() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/exec/cmd.go b/exec/cmd.go index b72aa7e..b37aafd 100644 --- a/exec/cmd.go +++ b/exec/cmd.go @@ -2,17 +2,17 @@ package exec import ( "bytes" + "context" "io" "os" goexec "os/exec" - "time" "github.com/sirupsen/logrus" ) type Cmd struct { l *logrus.Entry - // cmd *exec.Cmd + // actual *exec.Cmd command []string cwd string env []string @@ -20,18 +20,21 @@ type Cmd struct { stdoutWriters []io.Writer stderrWriters []io.Writer wait bool - t time.Duration + // t time.Duration preStartFunc func() error - postStartFunc func() error + postStartFunc func(p *os.Process) error postEndFunc func() error + chanStarted chan struct{} + chanDone chan struct{} } -func NewCommand(l *logrus.Entry, name string) *Cmd { +func NewCommand(name string) *Cmd { return &Cmd{ - l: l, - command: []string{name}, - wait: true, - env: os.Environ(), + command: []string{name}, + wait: true, + env: os.Environ(), + chanStarted: make(chan struct{}), + chanDone: make(chan struct{}), } } @@ -80,11 +83,6 @@ func (c *Cmd) Stderr(w io.Writer) *Cmd { return c } -func (c *Cmd) Timeout(t time.Duration) *Cmd { - c.t = t - return c -} - func (c *Cmd) NoWait() *Cmd { c.wait = false return c @@ -95,7 +93,7 @@ func (c *Cmd) PreStart(f func() error) *Cmd { return c } -func (c *Cmd) PostStart(f func() error) *Cmd { +func (c *Cmd) PostStart(f func(p *os.Process) error) *Cmd { c.postStartFunc = f return c } @@ -105,20 +103,37 @@ func (c *Cmd) PostEnd(f func() error) *Cmd { return c } -func (c *Cmd) Run() (string, error) { - cmd := goexec.Command(c.command[0], c.command[1:]...) +func (c *Cmd) Logger(l *logrus.Entry) *Cmd { + c.l = l + return c +} + +func (c *Cmd) Run(ctx context.Context) (string, error) { + return c.run(goexec.CommandContext(ctx, c.command[0], c.command[1:]...)) +} + +func (c *Cmd) Started() <-chan struct{} { + return c.chanStarted +} + +func (c *Cmd) Done() <-chan struct{} { + return c.chanDone +} + +func (c *Cmd) run(cmd *goexec.Cmd) (string, error) { cmd.Env = append(os.Environ(), c.env...) if c.cwd != "" { cmd.Dir = c.cwd } - c.l.Tracef("executing %q", cmd.String()) combinedBuf := new(bytes.Buffer) - traceWriter := c.l.WriterLevel(logrus.TraceLevel) - warnWriter := c.l.WriterLevel(logrus.WarnLevel) - - c.stdoutWriters = append(c.stdoutWriters, combinedBuf, traceWriter) - c.stderrWriters = append(c.stderrWriters, combinedBuf, warnWriter) + c.stdoutWriters = append(c.stdoutWriters, combinedBuf) + c.stderrWriters = append(c.stderrWriters, combinedBuf) + if c.l != nil { + c.l.Tracef("executing %q", cmd.String()) + c.stdoutWriters = append(c.stdoutWriters, c.l.WriterLevel(logrus.TraceLevel)) + c.stderrWriters = append(c.stderrWriters, c.l.WriterLevel(logrus.WarnLevel)) + } cmd.Stdout = io.MultiWriter(c.stdoutWriters...) cmd.Stderr = io.MultiWriter(c.stderrWriters...) @@ -133,23 +148,18 @@ func (c *Cmd) Run() (string, error) { } if c.postStartFunc != nil { - if err := c.postStartFunc(); err != nil { + if err := c.postStartFunc(cmd.Process); err != nil { return "", err } } - if c.wait { - if c.t != 0 { - timer := time.AfterFunc(c.t, func() { - cmd.Process.Kill() - }) - defer timer.Stop() - } + go func() { + c.chanStarted <- struct{}{} + }() + if c.wait { if err := cmd.Wait(); err != nil { - if c.t == 0 { - return "", err - } + return "", err } if c.postEndFunc != nil { if err := c.postEndFunc(); err != nil { @@ -158,5 +168,9 @@ func (c *Cmd) Run() (string, error) { } } + go func() { + c.chanDone <- struct{}{} + }() + return combinedBuf.String(), nil } diff --git a/exec/docker.go b/exec/docker.go index f6e5d51..e06eed9 100644 --- a/exec/docker.go +++ b/exec/docker.go @@ -2,22 +2,20 @@ package exec import ( "fmt" - - "github.com/sirupsen/logrus" ) type DockerCmd struct { Cmd } -func NewDockerCommand(l *logrus.Entry) *DockerCmd { - return &DockerCmd{*NewCommand(l, "docker")} +func NewDockerCommand() *DockerCmd { + return &DockerCmd{*NewCommand("docker")} } func (c DockerCmd) Build(workDir string, options ...string) *Cmd { return c.Args("build", workDir).Args(options...) } -func (c DockerCmd) Push(image, tag string, options ...string) (string, error) { - return c.Args("push", fmt.Sprintf("%v:%v", image, tag)).Args(options...).Run() +func (c DockerCmd) Push(image, tag string, options ...string) *Cmd { + return c.Args("push", fmt.Sprintf("%v:%v", image, tag)).Args(options...) } diff --git a/exec/go.go b/exec/go.go index 7877ad3..13a9f2f 100644 --- a/exec/go.go +++ b/exec/go.go @@ -1,15 +1,11 @@ package exec -import ( - "github.com/sirupsen/logrus" -) - type GoCmd struct { Cmd } -func NewGoCommand(l *logrus.Entry) *GoCmd { - return &GoCmd{*NewCommand(l, "go")} +func NewGoCommand() *GoCmd { + return &GoCmd{*NewCommand("go")} } func (c GoCmd) Build(workDir, output string, inputs []string, flags ...string) *Cmd { diff --git a/exec/kubectl.go b/exec/kubectl.go index 8f616bf..d6a859b 100644 --- a/exec/kubectl.go +++ b/exec/kubectl.go @@ -1,22 +1,24 @@ package exec import ( + "context" "encoding/json" + "errors" "fmt" "os" "strconv" "strings" - "github.com/sirupsen/logrus" - v1 "k8s.io/api/apps/v1" + apps "k8s.io/api/apps/v1" + core "k8s.io/api/core/v1" ) type KubectlCmd struct { Cmd } -func NewKubectlCommand(l *logrus.Entry) *KubectlCmd { - return &KubectlCmd{*NewCommand(l, "kubectl")} +func NewKubectlCommand() *KubectlCmd { + return &KubectlCmd{*NewCommand("kubectl")} } func (c KubectlCmd) RollbackDeployment(deployment string) *Cmd { @@ -28,13 +30,14 @@ func (c KubectlCmd) WaitForRollout(deployment, timeout string) *Cmd { "-w", "--timeout", timeout) } -func (c KubectlCmd) GetMostRecentPodBySelectors(selectors map[string]string) (string, error) { +func (c KubectlCmd) GetMostRecentPodBySelectors(ctx context.Context, + selectors map[string]string) (string, error) { var selector []string for k, v := range selectors { selector = append(selector, fmt.Sprintf("%v=%v", k, v)) } out, err := c.Args("--selector", strings.Join(selector, ","), - "get", "pods", "--sort-by=.status.startTime", "-o", "name").Run() + "get", "pods", "--sort-by=.status.startTime", "-o", "name").Run(ctx) if err != nil { return "", err } @@ -82,58 +85,57 @@ func (c KubectlCmd) ExposePod(pod string, host string, port int) *Cmd { fmt.Sprintf("--port=%v", port), fmt.Sprintf("--external-ip=%v", host)) } -func (c KubectlCmd) ForwardPod(pod string, host string, port int) *Cmd { - // kubectl port-forward pods/mongo-75f59d57f4-4nd6q 28015:27017 - return c.Args("port-forward", "pods/" + pod, strconv.Itoa(port) +":"+strconv.Itoa(port)) +func (c KubectlCmd) PortForwardPod(pod string, host string, port int) *Cmd { + return c.Args("port-forward", "pods/"+pod, strconv.Itoa(port)+":"+strconv.Itoa(port)) } func (c KubectlCmd) DeleteService(service string) *Cmd { return c.Args("delete", "service", service) } -func (c KubectlCmd) GetDeployment(deployment string) (*v1.Deployment, error) { - out, err := c.Args("get", "deployment", deployment, "-o", "json").Run() +func (c KubectlCmd) GetDeployment(ctx context.Context, deployment string) (*apps.Deployment, error) { + out, err := c.Args("get", "deployment", deployment, "-o", "json").Run(ctx) if err != nil { return nil, err } - var d v1.Deployment + var d apps.Deployment if err := json.Unmarshal([]byte(out), &d); err != nil { return nil, err } return &d, nil } -func (c KubectlCmd) GetNamespaces() ([]string, error) { - out, err := c.Args("get", "namespace", "-o", "name").Run() +func (c KubectlCmd) GetNamespaces(ctx context.Context) ([]string, error) { + out, err := c.Args("get", "namespace", "-o", "name").Run(ctx) if err != nil { return nil, err } return parseResources(out, "\n", "namespace/") } -func (c KubectlCmd) GetDeployments() ([]string, error) { - out, err := c.Args("get", "deployment", "-o", "name").Run() +func (c KubectlCmd) GetDeployments(ctx context.Context) ([]string, error) { + out, err := c.Args("get", "deployment", "-o", "name").Run(ctx) if err != nil { return nil, err } return parseResources(out, "\n", "deployment.apps/") } -func (c KubectlCmd) GetPods(selectors map[string]string) ([]string, error) { +func (c KubectlCmd) GetPods(ctx context.Context, selectors map[string]string) ([]string, error) { var selector []string for k, v := range selectors { selector = append(selector, fmt.Sprintf("%v=%v", k, v)) } out, err := c.Args("--selector", strings.Join(selector, ","), "get", "pods", "--sort-by=.status.startTime", - "-o", "name").Run() + "-o", "name").Run(ctx) if err != nil { return nil, err } return parseResources(out, "\n", "pod/") } -func (c KubectlCmd) GetContainers(deployment v1.Deployment) []string { +func (c KubectlCmd) GetContainers(deployment apps.Deployment) []string { var containers []string for _, c := range deployment.Spec.Template.Spec.Containers { containers = append(containers, c.Name) @@ -141,8 +143,8 @@ func (c KubectlCmd) GetContainers(deployment v1.Deployment) []string { return containers } -func (c KubectlCmd) GetPodsByLabels(labels []string) ([]string, error) { - out, err := c.Args("get", "pods", "-l", strings.Join(labels, ","), "-o", "name", "-A").Run() +func (c KubectlCmd) GetPodsByLabels(ctx context.Context, labels []string) ([]string, error) { + out, err := c.Args("get", "pods", "-l", strings.Join(labels, ","), "-o", "name", "-A").Run(ctx) if err != nil { return nil, err } @@ -153,27 +155,27 @@ func (c KubectlCmd) RestartDeployment(deployment string) *Cmd { return c.Args("rollout", "restart", fmt.Sprintf("deployment/%v", deployment)) } -func (c KubectlCmd) CreateConfigMapFromFile(name, path string) (string, error) { - return c.Args("create", "configmap", name, "--from-file", path).Run() +func (c KubectlCmd) CreateConfigMapFromFile(name, path string) *Cmd { + return c.Args("create", "configmap", name, "--from-file", path) } -func (c KubectlCmd) CreateConfigMap(name string, keyMap map[string]string) (string, error) { +func (c KubectlCmd) CreateConfigMap(name string, keyMap map[string]string) *KubectlCmd { c.Args("create", "configmap", name) for key, value := range keyMap { c.Args(fmt.Sprintf("--from-literal=%v=%v", key, value)) } - return c.Run() + return &c } -func (c KubectlCmd) DeleteConfigMap(name string) (string, error) { - return c.Args("delete", "configmap", name).Run() +func (c KubectlCmd) DeleteConfigMap(name string) *Cmd { + return c.Args("delete", "configmap", name) } -func (c KubectlCmd) GetConfigMapKey(name, key string) (string, error) { +func (c KubectlCmd) GetConfigMapKey(ctx context.Context, name, key string) (string, error) { key = strings.ReplaceAll(key, ".", "\\.") // jsonpath map key is not very fond of dots out, err := c.Args("get", "configmap", name, "-o", - fmt.Sprintf("jsonpath={.data.%v}", key)).Run() + fmt.Sprintf("jsonpath={.data.%v}", key)).Run(ctx) if err != nil { return out, err } @@ -204,3 +206,111 @@ func parseResources(out, delimiter, prefix string) ([]string, error) { } return res, nil } + +func (c KubectlCmd) KillPidsOnPod(ctx context.Context, pod, container string, pids []string, murder bool) []error { + var errs []error + for _, pid := range pids { + cmd := []string{"kill"} + if murder { + cmd = append(cmd, "-s", "9") + } + cmd = append(cmd, pid) + _, errKill := c.ExecPod(pod, container, cmd).Run(ctx) + if errKill != nil { + errs = append(errs, errKill) + } + } + return errs +} + +func (c KubectlCmd) GetDeploymentFromConfigMap(ctx context.Context, deployment, configMapKey string) (*apps.Deployment, error) { + out, err := c.GetConfigMapKey(ctx, deployment, configMapKey) + if err != nil { + return nil, err + } + var d apps.Deployment + if err := json.Unmarshal([]byte(out), &d); err != nil { + return nil, err + } + return &d, nil + +} + +func (_ KubectlCmd) GetContainerFromDeployment(container string, d *apps.Deployment) (*core.Container, error) { + for _, c := range d.Spec.Template.Spec.Containers { + if c.Name == container { + return &c, nil + } + } + return nil, fmt.Errorf("no container %q found in deployment %q", container, d.Name) +} + +func (c KubectlCmd) GetPIDsOf(ctx context.Context, pod, container, process string) (pids []string, err error) { + rawPids, errExec := c.ExecPod(pod, container, []string{"pidof", process}).Run(ctx) + if errExec != nil { + if errExec.Error() == "exit status 1" { + return []string{}, nil + } + return nil, errors.New("could not get pid of process: " + errExec.Error()) + } + stripped := []string{} + for _, rawPid := range strings.Split(rawPids, " ") { + stripped = append(stripped, strings.Trim(rawPid, "\n")) + } + return stripped, nil +} + +func (c KubectlCmd) ValidateNamespace(ctx context.Context, namespace string) error { + available, err := c.GetNamespaces(ctx) + if err != nil { + return err + } + return validateResource("namespace", namespace, "", available) +} + +func (c KubectlCmd) ValidateDeployment(ctx context.Context, namespace, deployment string) error { + available, err := c.GetDeployments(ctx) + if err != nil { + return err + } + return validateResource("deployment", deployment, fmt.Sprintf("for namespace %q", namespace), available) +} + +func (c KubectlCmd) ValidatePod(ctx context.Context, d apps.Deployment, pod *string) error { + if *pod == "" { + var err error + *pod, err = c.GetMostRecentPodBySelectors(ctx, d.Spec.Selector.MatchLabels) + if err != nil || *pod == "" { + return err + } + return nil + } + available, err := c.GetPods(ctx, d.Spec.Selector.MatchLabels) + if err != nil { + return err + } + return validateResource("pod", *pod, fmt.Sprintf("for deployment %q", d.Name), available) +} + +func (c KubectlCmd) ValidateContainer(d apps.Deployment, container *string) error { + if *container == "" { + *container = d.Name + } + return validateResource("container", *container, fmt.Sprintf("for deployment %q", d.Name), c.GetContainers(d)) +} + +func validateResource(resourceType, resource, suffix string, available []string) error { + if !stringIsInSlice(resource, available) { + return fmt.Errorf("%v %q not found %v, available: %v", resourceType, resource, suffix, strings.Join(available, ", ")) + } + return nil +} + +func stringIsInSlice(a string, list []string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} diff --git a/go.mod b/go.mod index 5e608ba..8a99d84 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,37 @@ module github.com/foomo/gograpple -go 1.16 +go 1.18 require ( - github.com/go-delve/delve v1.8.0 + github.com/go-delve/delve v1.8.2 + github.com/pkg/errors v0.9.1 + github.com/sirupsen/logrus v1.8.1 + github.com/spf13/cobra v1.4.0 + k8s.io/api v0.23.5 +) + +require ( + github.com/cilium/ebpf v0.7.0 // indirect + github.com/go-logr/logr v1.2.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/magefile/mage v1.11.0 // indirect - github.com/sirupsen/logrus v1.7.1 - github.com/spf13/cobra v1.1.3 - gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c // indirect - k8s.io/api v0.20.2 - k8s.io/klog/v2 v2.5.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.0.3 // indirect + github.com/google/go-cmp v0.5.5 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mattn/go-isatty v0.0.3 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4 // indirect + golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect + golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect + golang.org/x/text v0.3.7 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/apimachinery v0.23.5 // indirect + k8s.io/klog/v2 v2.30.0 // indirect + k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect + sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect ) diff --git a/go.sum b/go.sum index 08008b5..12b56b6 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,7 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cosiner/argv v0.1.0/go.mod h1:EusR6TucWKX+zFgtdUsKT2Cvg45K5rtpCcWz4hK06d8= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -50,85 +51,80 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/derekparker/trie v0.0.0-20200317170641-1fdf38b7b0e9/go.mod h1:D6ICZm05D9VN1n/8iOtBxLpXtoGp6HDFUJ1RNVieOSE= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-delve/delve v1.8.0 h1:uvwPyfr6n2g1S6/bt270lM4fBK76P/3FDCufOsK0CjQ= -github.com/go-delve/delve v1.8.0/go.mod h1:yXxoIaOOdTmNapkt5yfol6NIeNSqMxWRTfqy9qQMWZY= +github.com/go-delve/delve v1.8.2 h1:gsRTPR3Yi61RpeuCFvJb6vIxB3xABx6pnNKGISxdsSU= +github.com/go-delve/delve v1.8.2/go.mod h1:XB6XKpI5DqMCNai0MkNPVbrd3OtBovJ/vfcVofkWy/k= +github.com/go-delve/liner v1.2.2-1/go.mod h1:biJCRbqp51wS+I92HMqn5H8/A0PAhxn2vyOT+JqhiGI= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= -github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-dap v0.6.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= @@ -160,13 +156,12 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -177,12 +172,9 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= -github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls= -github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -190,6 +182,7 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -200,26 +193,32 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4 github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/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= @@ -234,36 +233,39 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.7.1 h1:rsizeFmZP+GYwyb4V6t6qpG7ZNWzA2bvgW/yC2xHCcg= -github.com/sirupsen/logrus v1.7.1/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= @@ -271,6 +273,7 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -286,7 +289,6 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -309,6 +311,7 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -324,15 +327,15 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY= +golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -358,25 +361,30 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI= +golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -384,7 +392,6 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -395,7 +402,6 @@ golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -403,9 +409,11 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -430,6 +438,7 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -443,12 +452,16 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= @@ -461,31 +474,37 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo= -gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -k8s.io/api v0.20.2 h1:y/HR22XDZY3pniu9hIFDLpUCPq2w5eQ6aV/VFQ7uJMw= -k8s.io/api v0.20.2/go.mod h1:d7n6Ehyzx+S+cE3VhTGfVNNqtGc/oL9DCdYYahlurV8= -k8s.io/apimachinery v0.20.2 h1:hFx6Sbt1oG0n6DZ+g4bFt5f6BoMkOjKWsQFu077M3Vg= -k8s.io/apimachinery v0.20.2/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/api v0.23.5 h1:zno3LUiMubxD/V1Zw3ijyKO3wxrhbUF1Ck+VjBvfaoA= +k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= +k8s.io/apimachinery v0.23.5 h1:Va7dwhp8wgkUPWsEXk6XglXWU4IKYLKNlv8VkX7SDM0= +k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= +k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.5.0 h1:8mOnjf1RmUPW6KRqQCfYSZq/K20Unmp3IhuZUhxl8KI= -k8s.io/klog/v2 v2.5.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= -k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= +k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20211116205334-6203023598ed h1:ck1fRPWPJWsMd8ZRFsWc6mh/zHp5fZ/shhbrgPUxDAE= +k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s= +sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.0.3 h1:4oyYo8NREp49LBBhKxEqCulFjg26rawYKrnCmg+Sr6c= -sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/gograpple.go b/gograpple.go index f499914..5bd0e0a 100644 --- a/gograpple.go +++ b/gograpple.go @@ -1,19 +1,22 @@ package gograpple import ( + "context" + "github.com/foomo/gograpple/exec" "github.com/sirupsen/logrus" v1 "k8s.io/api/apps/v1" ) const ( - devDeploymentPatchFile = "deployment-patch.yaml" - defaultWaitTimeout = "30s" - conditionContainersReady = "condition=ContainersReady" - defaultPatchedLabel = "dev-mode-patched" - defaultPatchImage = "gograpple-patch:latest" - defaultConfigMapMount = "/etc/config/mounted" - defaultConfigMapDeploymentKey = "deployment.json" + devDeploymentPatchFile = "deployment-patch.yaml" + defaultWaitTimeout = "30s" + conditionContainersReady = "condition=ContainersReady" + defaultPatchedLabel = "dev-mode-patched" + defaultPatchImageSuffix = "-patch" + defaultConfigMapMount = "/etc/config/mounted" + defaultConfigMapDeploymentKey = "deployment.json" + defaultConfigMapDeploymentSuffix = "-patch" ) type Grapple struct { @@ -26,19 +29,23 @@ type Grapple struct { func NewGrapple(l *logrus.Entry, namespace, deployment string) (*Grapple, error) { g := &Grapple{l: l} - g.kubeCmd = exec.NewKubectlCommand(l) - g.dockerCmd = exec.NewDockerCommand(l) - g.goCmd = exec.NewGoCommand(l) + g.kubeCmd = exec.NewKubectlCommand() + g.dockerCmd = exec.NewDockerCommand() + g.goCmd = exec.NewGoCommand() + g.kubeCmd.Logger(l) + g.dockerCmd.Logger(l) + g.goCmd.Logger(l) g.kubeCmd.Args("-n", namespace) - if err := g.validateNamespace(namespace); err != nil { + validateCtx := context.Background() + if err := g.kubeCmd.ValidateNamespace(validateCtx, namespace); err != nil { return nil, err } - if err := g.validateDeployment(namespace, deployment); err != nil { + if err := g.kubeCmd.ValidateDeployment(validateCtx, namespace, deployment); err != nil { return nil, err } - d, err := g.kubeCmd.GetDeployment(deployment) + d, err := g.kubeCmd.GetDeployment(validateCtx, deployment) if err != nil { return nil, err } diff --git a/grappledlv.go b/grappledlv.go deleted file mode 100644 index 4b7889d..0000000 --- a/grappledlv.go +++ /dev/null @@ -1,430 +0,0 @@ -package gograpple - -import ( - "errors" - "fmt" - "log" - "net" - "os" - "os/signal" - "path" - "path/filepath" - "strings" - "sync" - "time" - - "github.com/go-delve/delve/service/api" - "github.com/go-delve/delve/service/rpc2" - "github.com/sirupsen/logrus" -) - -func (g Grapple) Delve(pod, container, input string, args []string, host string, port int, delveContinue, vscode bool) error { - // input validation - if err := g.validatePod(&pod); err != nil { - return err - } - if err := g.validateContainer(&container); err != nil { - return err - } - goModDir, err := findGoProjectRoot(input) - if err != nil { - return fmt.Errorf("couldnt find go.mod dir for input %q", input) - } - - // are patched? - if !g.isPatched() { - return fmt.Errorf("deployment not patched, stopping delve") - } - - binDest := "/" + g.deployment.Name - chanExitRun, lockingCleanup := g.dlvGetLockingCleanup(pod, container) - cmd, errCmd := g.dlvGetCommand(container, binDest, port, delveContinue, args) - if errCmd != nil { - return errCmd - } - - chanExit, chanReload, chanErr, errListenForInterrupts := listenForInterrupts(g.l.WithField("task", "interrupt-listener")) - if errListenForInterrupts != nil { - lockingCleanup("failed to listen interrupt signals: " + errListenForInterrupts.Error()) - return errListenForInterrupts - } - - chanRunErr := make(chan error) - iteration := 0 - run := func(chanExitRun chan struct{}) { - iteration++ - g.l.Infof("executing delve command on pod %v", pod) - _, errRun := g.kubeCmd.ExecPod(pod, container, cmd).PostStart( - g.dlvWatch(host, pod, port, iteration, lockingCleanup, vscode, goModDir, chanExitRun), - ).Run() - if errRun != nil && errRun.Error() != "signal: interrupt" { - chanRunErr <- errRun - } - } - g.l.Info("running initial cleanup, just in case ...") - g.dlvCleanup(g.l, pod, container) - go func() { - chanReload <- "initial load" - }() - for { - select { - case errRun := <-chanRunErr: - lockingCleanup(errRun.Error()) - return errRun - case <-chanExitRun: - - case <-chanExit: - lockingCleanup("termination") - return nil - case <-chanReload: - var binTemp string - var errRebuild error - wgReload := sync.WaitGroup{} - wgReload.Add(2) - go func() { - lockingCleanup("reload it baby") - wgReload.Done() - }() - go func() { - binTemp, errRebuild = g.rebuildAndUpload(goModDir, pod, container, input, binDest) - wgReload.Done() - }() - wgReload.Wait() - if errRebuild != nil { - return errRebuild - } - errMove := g.dlvMoveBinary(pod, container, binTemp, binDest) - if errMove != nil { - return errMove - } - go func() { - errExpose := g.dlvExpose(pod, host, port) - if errExpose != nil { - log.Println(errExpose) - } - }() - - go run(chanExitRun) - case err := <-chanErr: - lockingCleanup("an error occurred while listening for keyboard commands:" + err.Error()) - return err - } - } -} - -func (g Grapple) dlvGetCommand(container, binDest string, port int, delveContinue bool, args []string) (cmd []string, err error) { - cmd = []string{ - "dlv", "exec", binDest, - "--api-version=2", "--headless", - fmt.Sprintf("--listen=:%v", port), - "--accept-multiclient", - } - if delveContinue { - cmd = append(cmd, "--continue") - } - if len(args) == 0 { - args, err = g.getArgsFromConfigMap(g.deployment.Name, container) - if err != nil { - return nil, err - } - } - if len(args) > 0 { - cmd = append(cmd, "--") - cmd = append(cmd, args...) - } - return cmd, nil -} - -func (g Grapple) dlvMoveBinary(pod, container, binTemp, binDest string) error { - cmd := []string{"mv", binTemp, binDest} - _, errRun := g.kubeCmd.ExecPod(pod, container, cmd).Run() - return errRun - -} - -func (g Grapple) dlvWatch(host string, pod string, port, iteration int, cleanup func(reason string) error, vscode bool, goModDir string, chanExitRun chanCommand) func() error { - return func() error { - client, errTryDelveServer := dlvTryServer(g.l, host, port, 50, 200*time.Millisecond) - if errTryDelveServer != nil { - return errors.New("could not get dlv client: " + errTryDelveServer.Error()) - } - go func() { - i := 0 - for { - select { - case <-chanExitRun: - return - case <-time.After(1000 * time.Millisecond): - _, state, errState := dlvCheckServer(g.l.WithField("task", "watch-dlv"), host, port, 3*time.Second, client) - if errState != nil { - g.l.WithError(errState).Error("dlv seems to be down on", host, ":", port) - cleanup("dlv is down") - os.Exit(1) - } - if i%20 == 0 { - g.l.WithField("pid", client.ProcessPid()).Info("dlv is up") - } - if state.Exited { - cleanup("dlv state.Exited == true") - os.Exit(1) - } else if !state.Running { - // there still is the case, when you are in a breakpoint on a zombie process - // dlv will not handle that gracefully - g.l.WithField("pid", client.ProcessPid()).Info("dlv is up - process is not running - is it a zombie, or has it been halted by a breakpoint ?") - } - i++ - } - } - }() - if vscode { - if err := launchVscode(g.l, goModDir, pod, host, port, 5, iteration, 1*time.Second); err != nil { - return err - } - } - return nil - } -} - -func (g Grapple) dlvGetLockingCleanup(pod, container string) (chanExitRun chanCommand, cleanup func(reason string) error) { - cleanupLock := sync.Mutex{} - cleanupStarted := false - chanExitRun = make(chanCommand) - return chanExitRun, func(reason string) error { - cleanupLock.Lock() - defer cleanupLock.Unlock() - cl := g.l.WithField("reason", reason) - if cleanupStarted { - cl.Warning("aborting cleanup already started") - return nil - } - cleanupStarted = true - defer func() { - cleanupStarted = false - }() - cl.Info("cleaning up") - go func() { chanExitRun <- struct{}{} }() - cl.Info("informed running tasks") - errDelveCleanup := g.dlvCleanup(cl, pod, container) - if errDelveCleanup != nil { - cl.WithError(errDelveCleanup).Error("could not clean up") - return errDelveCleanup - } - return nil - } -} - -func (g Grapple) dlvExpose(pod, host string, port int) error { - g.l.Infof("exposing deployment %v for delve", g.deployment.Name) - _, err := g.kubeCmd.ForwardPod(pod, host, port).Run() - if err != nil { - return err - } - return nil -} - -func (g Grapple) _dlvExpose(pod, host string, port int) error { - g.l.Infof("exposing deployment %v for delve", g.deployment.Name) - _, err := g.kubeCmd.ExposePod(pod, host, port).Run() - if err != nil { - return err - } - return nil -} - -func (g Grapple) dlvCleanup(l *logrus.Entry, pod, container string) error { - l.Infof("removing delve service") - outDeleteService, errDeleteService := g.kubeCmd.DeleteService(pod).Run() - if errDeleteService != nil { - l.WithError(errDeleteService).Warn("could not delete exposing service: " + outDeleteService) - } - l.Info("cleaning up debug processes") - const nameDLV = "dlv" - nameProgram := g.deployment.Name - pidsOfProgram, errGetPIDsOfProgram := g.getPIDsOf(pod, container, nameProgram) - if errGetPIDsOfProgram != nil { - return errGetPIDsOfProgram - } - pidsOfDLV, errGetPIDsOfDelve := g.getPIDsOf(pod, container, nameDLV) - if errGetPIDsOfDelve != nil { - return errGetPIDsOfDelve - } - kill := func(name string, pids []string, murder bool) (leftToKill []string) { - leftToKill = []string{} - for _, pid := range pids { - remainingPIDs, errGetPIDs := g.getPIDsOf(pod, container, name) - killed := true - if errGetPIDs == nil { - for _, remainingPID := range remainingPIDs { - if remainingPID == pid { - killed = false - break - } - } - } - if killed { - continue - } - cmd := []string{"kill"} - if murder { - cmd = append(cmd, "-s", "9") - } - cmd = append(cmd, pid) - outKill, errKill := g.kubeCmd.ExecPod(pod, container, cmd).Run() - if errKill != nil { - l.WithError(errKill).Warn("could not kill process", outKill) - } - leftToKill = append(leftToKill, pid) - - } - return leftToKill - } - const maxAttempts = 10 - for i := 0; i < maxAttempts; i++ { - pidsOfProgram = kill(nameProgram, pidsOfProgram, i > 0) - pidsOfDLV = kill(nameDLV, pidsOfDLV, i > 0) - if len(pidsOfDLV) == 0 && len(pidsOfProgram) == 0 { - return nil - } - time.Sleep(time.Millisecond * 200) - } - return fmt.Errorf("could not kill processes after max attempts %v", maxAttempts) -} - -func dlvTryServer(l *logrus.Entry, host string, port, tries int, sleep time.Duration) (client *rpc2.RPCClient, err error) { - errTryCall := tryCall(l, tries, sleep, func(i int) error { - l.Infof("checking delve connection on %v:%v (%v/%v)", host, port, i, tries) - newClient, _, errCheck := dlvCheckServer(l, host, port, 200*time.Millisecond, client) - client = newClient - return errCheck - }) - if errTryCall != nil { - return nil, errTryCall - } - l.Infof("delve server listening on %v:%v", host, port) - return client, nil -} - -func dlvCheckServer( - l *logrus.Entry, host string, port int, timeout time.Duration, - client *rpc2.RPCClient, -) ( - *rpc2.RPCClient, *api.DebuggerState, error, -) { - if client == nil { - // connection timeouts suck with k8s, because the port is open and you can connect, but ... - // early on the connection is a dead and the timeout does not kick in, despite the fact - // that the connection is not "really" establised - conn, err := net.DialTimeout("tcp", fmt.Sprintf("%v:%v", host, port), time.Hour) - if err != nil { - return nil, nil, err - } - chanClient := make(chan struct{}) - go func() { - // the next level of suck is the rpc client - // it does not return an error for its constructor, - // even though it actually does make a call and that fails - // and the cient will not do any other calls, - // because it has shutdown internally, without telling us - // at least that is what I understand here - client = rpc2.NewClientFromConn(conn) - chanClient <- struct{}{} - }() - select { - case <-time.After(timeout): - // this is the actual timeout, because the connetion timeout does not work, - // because k8s ... - conn.Close() - // l.Warn("dlv server check timeout", timeout) - return nil, nil, errors.New("stale connection to dlv, aborting after timeout") - case <-chanClient: - // we are good to go - // l.Info("hey, we got a client") - } - } - st, errState := client.GetState() - if errState != nil { - // l.Info("could not get state from server using client", errState) - return nil, nil, errState - } - return client, st, nil -} - -type chanCommand chan struct{} - -func listenForInterrupts(l *logrus.Entry) (chanExit chanCommand, chanReload chan string, chanErr chan error, err error) { - sigchan := make(chan os.Signal, 1) - signal.Notify(sigchan) - chanExit = make(chanCommand) - chanReload = make(chan string) - chanErr = make(chan error) - i := 0 - exiting := false - readyToReset := false - durReset := 2 * time.Second - go func() { - for { - select { - case <-time.After(durReset): - if readyToReset { - l.Info("resetting termination timer") - readyToReset = false - } - i = 0 - case sig := <-sigchan: - switch sig { - case os.Interrupt: - l.Info("received interrupt signal, tigger one more interrupt within ", durReset, " to terminate") - readyToReset = true - if exiting { - l.Warn("already exiting - ignoring interupt") - continue - } - if i == 0 { - l.Info("triggering reload") - chanReload <- "interrupt => reload" - } else { - l.Info("triggering exit") - exiting = true - chanExit <- struct{}{} - } - i++ - } - } - } - }() - return chanExit, chanReload, chanErr, nil -} - -func (g Grapple) rebuildAndUpload(goModDir, pod, container, input, binDest string) (tempDest string, err error) { - binPath := path.Join(os.TempDir(), g.deployment.Name) - g.l.Infof("building %q for debug", input) - - var relInputs []string - inputInfo, errInputInfo := os.Stat(input) - if errInputInfo != nil { - return "", err - } - if inputInfo.IsDir() { - if files, err := os.ReadDir(input); err != nil { - return "", err - } else { - for _, file := range files { - if path.Ext(file.Name()) == ".go" { - relInputs = append(relInputs, strings.TrimPrefix(path.Join(input, file.Name()), goModDir+string(filepath.Separator))) - } - } - } - } else { - relInputs = append(relInputs, strings.TrimPrefix(input, goModDir+string(filepath.Separator))) - } - - _, errBuild := g.goCmd.Build(goModDir, binPath, relInputs, `-gcflags="all=-N -l"`).Env("GOOS=linux").Run() - if errBuild != nil { - return "", errBuild - } - - g.l.Infof("copying binary to pod %v", pod) - tempDest = binDest + "-build" - _, errCopyToPod := g.kubeCmd.CopyToPod(pod, container, binPath, tempDest).Run() - return tempDest, errCopyToPod -} diff --git a/grapplepatch.go b/grapplepatch.go deleted file mode 100644 index 299e7d1..0000000 --- a/grapplepatch.go +++ /dev/null @@ -1,141 +0,0 @@ -package gograpple - -import ( - "encoding/json" - "fmt" - "os" - "path" - - "github.com/foomo/gograpple/bindata" -) - -type Mount struct { - HostPath string - MountPath string -} - -type patchValues struct { - Label string - Deployment string - Container string - ConfigMapMount string - Mounts []Mount - Image string -} - -func newPatchValues(deployment, container string, mounts []Mount) *patchValues { - return &patchValues{ - Label: defaultPatchedLabel, - Deployment: deployment, - Container: container, - ConfigMapMount: defaultConfigMapMount, - Mounts: mounts, - Image: defaultPatchImage, - } -} - -func (g Grapple) Patch(image, tag, container string, mounts []Mount) error { - if g.isPatched() { - g.l.Warn("deployment already patched, rolling back first") - if err := g.rollbackUntilUnpatched(); err != nil { - return err - } - } - if err := g.validateContainer(&container); err != nil { - return err - } - if err := g.validateImage(container, &image, &tag); err != nil { - return err - } - - g.l.Infof("creating a ConfigMap with deployment data") - bs, err := json.Marshal(g.deployment) - if err != nil { - return err - } - data := map[string]string{defaultConfigMapDeploymentKey: string(bs)} - _, err = g.kubeCmd.CreateConfigMap(g.deployment.Name, data) - if err != nil { - return err - } - - g.l.Infof("waiting for deployment to get ready") - _, err = g.kubeCmd.WaitForRollout(g.deployment.Name, defaultWaitTimeout).Run() - if err != nil { - return err - } - - g.l.Infof("extracting patch files") - const patchFolder = "the-hook" - if err := bindata.RestoreAssets(os.TempDir(), patchFolder); err != nil { - return err - } - theHookPath := path.Join(os.TempDir(), patchFolder) - - g.l.Infof("building patch image with %v:%v", image, tag) - _, err = g.dockerCmd.Build(theHookPath, "--build-arg", - fmt.Sprintf("IMAGE=%v:%v", image, tag), "-t", defaultPatchImage).Run() - if err != nil { - return err - } - - g.l.Infof("rendering deployment patch template") - patch, err := renderTemplate( - path.Join(theHookPath, devDeploymentPatchFile), - newPatchValues(g.deployment.Name, container, mounts), - ) - if err != nil { - return err - } - - g.l.Infof("patching deployment for development %s with patch %s", g.deployment.Name, patch) - _, err = g.kubeCmd.PatchDeployment(patch, g.deployment.Name).Run() - return err -} - -func (g *Grapple) Rollback() error { - if !g.isPatched() { - return fmt.Errorf("deployment not patched, stopping rollback") - } - return g.rollbackUntilUnpatched() -} - -func (g Grapple) isPatched() bool { - _, ok := g.deployment.Spec.Template.ObjectMeta.Labels[defaultPatchedLabel] - return ok -} - -func (g *Grapple) rollbackUntilUnpatched() error { - if !g.isPatched() { - return nil - } - if err := g.rollback(); err != nil { - return err - } - if err := g.updateDeployment(); err != nil { - return err - } - return g.rollbackUntilUnpatched() -} - -func (g Grapple) rollback() error { - g.l.Infof("removing ConfigMap %v", g.deployment.Name) - _, err := g.kubeCmd.DeleteConfigMap(g.deployment.Name) - if err != nil { - // may not exist - g.l.Warn(err) - } - - g.l.Infof("waiting for deployment to get ready") - _, err = g.kubeCmd.WaitForRollout(g.deployment.Name, defaultWaitTimeout).Run() - if err != nil { - return err - } - - g.l.Infof("rolling back deployment %v", g.deployment.Name) - _, err = g.kubeCmd.RollbackDeployment(g.deployment.Name).Run() - if err != nil { - return err - } - return nil -} diff --git a/grappleutils.go b/grappleutils.go deleted file mode 100644 index 176f14a..0000000 --- a/grappleutils.go +++ /dev/null @@ -1,64 +0,0 @@ -package gograpple - -import ( - "encoding/json" - "errors" - "fmt" - "strings" - - v1 "k8s.io/api/apps/v1" -) - -func (g Grapple) Cleanup(pod, container string) error { - if !g.isPatched() { - return fmt.Errorf("deployment not patched, stopping delve") - } - if err := g.validatePod(&pod); err != nil { - return err - } - if err := g.validateContainer(&container); err != nil { - return err - } - return g.dlvCleanup(g.l, pod, container) -} - -func (g Grapple) getPIDsOf(pod, container, name string) (pids []string, err error) { - rawPids, errExec := g.kubeCmd.ExecPod(pod, container, []string{"pidof", name}).Run() - if errExec != nil { - if errExec.Error() == "exit status 1" { - return []string{}, nil - } - return nil, errors.New("could not get pid of process: " + errExec.Error()) - } - stripped := []string{} - for _, rawPid := range strings.Split(rawPids, " ") { - stripped = append(stripped, strings.Trim(rawPid, "\n")) - } - return stripped, nil -} - -func (g *Grapple) updateDeployment() error { - d, err := g.kubeCmd.GetDeployment(g.deployment.Name) - if err != nil { - return err - } - g.deployment = *d - return nil -} - -func (g Grapple) getArgsFromConfigMap(configMap, container string) ([]string, error) { - out, err := g.kubeCmd.GetConfigMapKey(configMap, defaultConfigMapDeploymentKey) - if err != nil { - return nil, err - } - var d v1.Deployment - if err := json.Unmarshal([]byte(out), &d); err != nil { - return nil, err - } - for _, c := range d.Spec.Template.Spec.Containers { - if c.Name == container { - return c.Args, nil - } - } - return nil, fmt.Errorf("no args found for container %q", container) -} diff --git a/grapplevalidation.go b/grapplevalidation.go deleted file mode 100644 index f3d60c6..0000000 --- a/grapplevalidation.go +++ /dev/null @@ -1,63 +0,0 @@ -package gograpple - -import ( - "fmt" - "strings" -) - -func (g Grapple) validateNamespace(namespace string) error { - available, err := g.kubeCmd.GetNamespaces() - if err != nil { - return err - } - return validateResource("namespace", namespace, "", available) -} - -func (g Grapple) validateDeployment(namespace, deployment string) error { - available, err := g.kubeCmd.GetDeployments() - if err != nil { - return err - } - return validateResource("deployment", deployment, fmt.Sprintf("for namespace %q", namespace), available) -} - -func (g Grapple) validatePod(pod *string) error { - if *pod == "" { - var err error - *pod, err = g.kubeCmd.GetMostRecentPodBySelectors(g.deployment.Spec.Selector.MatchLabels) - if err != nil || *pod == "" { - return err - } - return nil - } - available, err := g.kubeCmd.GetPods(g.deployment.Spec.Selector.MatchLabels) - if err != nil { - return err - } - return validateResource("pod", *pod, fmt.Sprintf("for deployment %q", g.deployment.Name), available) -} - -func (g Grapple) validateContainer(container *string) error { - if *container == "" { - *container = g.deployment.Name - } - available := g.kubeCmd.GetContainers(g.deployment) - return validateResource("container", *container, fmt.Sprintf("for deployment %q", g.deployment.Name), available) -} - -func (g Grapple) validateImage(container string, image, tag *string) error { - if *image == "" { - for _, c := range g.deployment.Spec.Template.Spec.Containers { - if container == c.Name { - pieces := strings.Split(c.Image, ":") - if len(pieces) != 2 { - return fmt.Errorf("deployment image %q has invalid format", c.Image) - } - *image = pieces[0] - *tag = pieces[1] - return nil - } - } - } - return nil -} diff --git a/interrupt.go b/interrupt.go new file mode 100644 index 0000000..6e77e27 --- /dev/null +++ b/interrupt.go @@ -0,0 +1,38 @@ +package gograpple + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/sirupsen/logrus" +) + +func RunWithInterrupt(l *logrus.Entry, callback func(ctx context.Context)) { + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, os.Interrupt) + durReload := 3 * time.Second + for { + ctx, cancelCtx := context.WithCancel(context.Background()) + // do stuff + go callback(ctx) + select { + case <-signalChan: // first signal + l.Info("-") + l.Infof("interrupt received, trigger one more within %v to terminate", durReload) + cancelCtx() + select { + case <-time.After(durReload): // reloads durReload after first signal + l.Info("-") + l.Info("reloading") + case <-signalChan: // second signal, hard exit + l.Info("-") + l.Info("terminating") + signal.Stop(signalChan) + // exit loop + return + } + } + } +} diff --git a/patch.go b/patch.go new file mode 100644 index 0000000..ea0ed13 --- /dev/null +++ b/patch.go @@ -0,0 +1,196 @@ +package gograpple + +import ( + "context" + "embed" + "encoding/json" + "fmt" + "os" + "path" + "path/filepath" +) + +var ( + //go:embed the-hook + bindata embed.FS +) + +type Mount struct { + HostPath string + MountPath string +} + +type patchValues struct { + Label string + Deployment string + Container string + ConfigMapMount string + Mounts []Mount + Image string +} + +func (g Grapple) newPatchValues(deployment, container, image string, mounts []Mount) *patchValues { + return &patchValues{ + Label: defaultPatchedLabel, + Deployment: deployment, + Container: container, + ConfigMapMount: defaultConfigMapMount, + Mounts: mounts, + Image: image, + } +} + +func (g Grapple) Patch(repo, image, tag, container string, mounts []Mount) error { + ctx := context.Background() + if g.isPatched() { + g.l.Warn("deployment already patched, rolling back first") + if err := g.rollbackUntilUnpatched(ctx); err != nil { + return err + } + } + if err := g.kubeCmd.ValidateContainer(g.deployment, &container); err != nil { + return err + } + if err := ValidateImage(g.deployment, container, &image, &tag); err != nil { + return err + } + + g.l.Infof("creating a ConfigMap with deployment data") + bs, err := json.Marshal(g.deployment) + if err != nil { + return err + } + _, _ = g.kubeCmd.DeleteConfigMap(g.DeploymentConfigMapName()).Run(ctx) + data := map[string]string{defaultConfigMapDeploymentKey: string(bs)} + _, err = g.kubeCmd.CreateConfigMap(g.DeploymentConfigMapName(), data).Run(ctx) + if err != nil { + return err + } + + g.l.Infof("waiting for deployment to get ready") + _, err = g.kubeCmd.WaitForRollout(g.deployment.Name, defaultWaitTimeout).Run(ctx) + if err != nil { + return err + } + + g.l.Infof("extracting patch files") + + const ( + patchFolder = "the-hook" + dockerfileName = "Dockerfile" + patchFileName = "deployment-patch.yaml" + perm = 0700 + ) + + dockerFile, err := bindata.ReadFile(filepath.Join(patchFolder, dockerfileName)) + if err != nil { + return err + } + deploymentPatch, err := bindata.ReadFile(filepath.Join(patchFolder, patchFileName)) + if err != nil { + return err + } + + theHookPath := path.Join(os.TempDir(), patchFolder) + _ = os.Mkdir(theHookPath, perm) + err = os.WriteFile(filepath.Join(theHookPath, dockerfileName), dockerFile, perm) + if err != nil { + return err + } + err = os.WriteFile(filepath.Join(theHookPath, patchFileName), deploymentPatch, perm) + if err != nil { + return err + } + + pathedImageName := g.patchedImageName(repo) + g.l.Infof("building patch image with %v:%v", pathedImageName, tag) + _, err = g.dockerCmd.Build(theHookPath, "--build-arg", + fmt.Sprintf("IMAGE=%v:%v", image, tag), "-t", pathedImageName, + "--platform", "linux/amd64").Run(ctx) + if err != nil { + return err + } + + if repo != "" { + //contains a repo, push the built image + g.l.Infof("pushing patch image with %v:%v", pathedImageName, tag) + _, err = g.dockerCmd.Push(pathedImageName, tag).Run(ctx) + if err != nil { + return err + } + } + + g.l.Infof("rendering deployment patch template") + patch, err := renderTemplate( + path.Join(theHookPath, devDeploymentPatchFile), + g.newPatchValues(g.deployment.Name, container, fmt.Sprintf("%v:%v", pathedImageName, tag), mounts), + ) + if err != nil { + return err + } + + g.l.Infof("patching deployment for development %s with patch %s", g.deployment.Name, patch) + _, err = g.kubeCmd.PatchDeployment(patch, g.deployment.Name).Run(ctx) + return err +} + +func (g *Grapple) Rollback() error { + if !g.isPatched() { + return fmt.Errorf("deployment not patched, stopping rollback") + } + return g.rollbackUntilUnpatched(context.Background()) +} + +func (g Grapple) isPatched() bool { + _, ok := g.deployment.Spec.Template.ObjectMeta.Labels[defaultPatchedLabel] + return ok +} + +func (g *Grapple) rollbackUntilUnpatched(ctx context.Context) error { + if !g.isPatched() { + return nil + } + if err := g.rollback(ctx); err != nil { + return err + } + if err := g.updateDeployment(); err != nil { + return err + } + return g.rollbackUntilUnpatched(ctx) +} + +func (g Grapple) rollback(ctx context.Context) error { + g.l.Infof("removing ConfigMap %v", g.DeploymentConfigMapName()) + _, err := g.kubeCmd.DeleteConfigMap(g.DeploymentConfigMapName()).Run(ctx) + if err != nil { + // may not exist + g.l.Warn(err) + } + + g.l.Infof("rolling back deployment %v", g.deployment.Name) + _, err = g.kubeCmd.RollbackDeployment(g.deployment.Name).Run(ctx) + if err != nil { + return err + } + return nil +} + +func (g Grapple) DeploymentConfigMapName() string { + return g.deployment.Name + defaultConfigMapDeploymentSuffix +} + +func (g Grapple) patchedImageName(repo string) string { + if repo != "" { + return path.Join(repo, g.deployment.Name) + defaultPatchImageSuffix + } + return g.deployment.Name + defaultPatchImageSuffix +} + +func (g *Grapple) updateDeployment() error { + d, err := g.kubeCmd.GetDeployment(context.Background(), g.deployment.Name) + if err != nil { + return err + } + g.deployment = *d + return nil +} diff --git a/patch_test.go b/patch_test.go new file mode 100644 index 0000000..2e1fe24 --- /dev/null +++ b/patch_test.go @@ -0,0 +1,30 @@ +package gograpple + +import ( + "testing" +) + +func TestGrapple_Patch(t *testing.T) { + type args struct { + repo string + image string + tag string + container string + mounts []Mount + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"test", args{"k3d-local-registry:50442", "golang", "latest", "", nil}, false}, + } + g := testGrapple(t, "example") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := g.Patch(tt.args.repo, tt.args.image, tt.args.tag, tt.args.container, tt.args.mounts); (err != nil) != tt.wantErr { + t.Errorf("Grapple.Patch() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/grappleshell.go b/shell.go similarity index 72% rename from grappleshell.go rename to shell.go index f85490b..6ee47c6 100644 --- a/grappleshell.go +++ b/shell.go @@ -1,21 +1,25 @@ package gograpple -import "fmt" +import ( + "context" + "fmt" +) func (g Grapple) Shell(pod string) error { + ctx := context.Background() if !g.isPatched() { return fmt.Errorf("deployment not patched, stopping shell") } - if err := g.validatePod(&pod); err != nil { + if err := g.kubeCmd.ValidatePod(context.Background(), g.deployment, &pod); err != nil { return err } g.l.Infof("waiting for pod %v with %q", pod, conditionContainersReady) - _, err := g.kubeCmd.WaitForPodState(pod, conditionContainersReady, defaultWaitTimeout).Run() + _, err := g.kubeCmd.WaitForPodState(pod, conditionContainersReady, defaultWaitTimeout).Run(ctx) if err != nil { return err } g.l.Infof("running interactive shell for patched deployment %v", g.deployment.Name) - _, err = g.kubeCmd.ExecShell(fmt.Sprintf("pod/%v", pod), "/").Run() + _, err = g.kubeCmd.ExecShell(fmt.Sprintf("pod/%v", pod), "/").Run(ctx) return err } diff --git a/test/app/Dockerfile b/test/app/Dockerfile new file mode 100644 index 0000000..303d79b --- /dev/null +++ b/test/app/Dockerfile @@ -0,0 +1,8 @@ +FROM golang:alpine AS builder +WORKDIR /build +COPY . . +RUN go build -o /helloworld . + +FROM alpine:latest +COPY --from=builder /helloworld /helloworld +ENTRYPOINT ["/helloworld"] \ No newline at end of file diff --git a/test/app/Makefile b/test/app/Makefile new file mode 100644 index 0000000..a60522e --- /dev/null +++ b/test/app/Makefile @@ -0,0 +1,8 @@ +repo = k3d-local-registry:50442 +namespace = test + +build: + docker build -t hello:latest . + +deploy: + helm upgrade --install example chart --set image.repository=hello --set image.tag=latest \ No newline at end of file diff --git a/test/app/README.md b/test/app/README.md new file mode 100644 index 0000000..0daa5ac --- /dev/null +++ b/test/app/README.md @@ -0,0 +1,52 @@ +# README + +Instructions to debug the example app. + +## STEPS + +Install dependencies: + + $ brew install helm kubectx k9s + +Start docker, check which context you are connected to: + + $ kubectx + docker-desktop + +Setup local cluster, deploy test chart: + + $ cd test/app + $ make build && make deploy + +Use k9s to check if the example app pod is running: + + $ k9s + +then patch: + + $ gograpple patch example --image golang + +and debug (using vscode): + + $ gograpple delve example --source . --vscode + +set a breakpoint in the HTTP handler. + +> Note: before the startup of the service debugging does not work as delve is not started yet! + +Use port forwarding via k9s to expose the pod (shift-F): + +- App Port: 80 +- Local Port: 8080 + +> Note: This will be reverted once you exit k9s! + +Now access the web service by visiting: http://localhost:8080 + +You should be dropped into the debugger in VSCode at the breakpoint! + +Once done, rollback: + + $ gograpple patch example --rollback + +Happy debugging! \ No newline at end of file diff --git a/test/app/chart/.helmignore b/test/app/chart/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/test/app/chart/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/test/app/chart/Chart.yaml b/test/app/chart/Chart.yaml new file mode 100644 index 0000000..b4d972f --- /dev/null +++ b/test/app/chart/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: example +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/test/app/chart/templates/NOTES.txt b/test/app/chart/templates/NOTES.txt new file mode 100644 index 0000000..863493f --- /dev/null +++ b/test/app/chart/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "example.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "example.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "example.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "example.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/test/app/chart/templates/_helpers.tpl b/test/app/chart/templates/_helpers.tpl new file mode 100644 index 0000000..16daf58 --- /dev/null +++ b/test/app/chart/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "example.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "example.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "example.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "example.labels" -}} +helm.sh/chart: {{ include "example.chart" . }} +{{ include "example.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "example.selectorLabels" -}} +app.kubernetes.io/name: {{ include "example.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "example.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "example.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/test/app/chart/templates/deployment.yaml b/test/app/chart/templates/deployment.yaml new file mode 100644 index 0000000..56d8cd0 --- /dev/null +++ b/test/app/chart/templates/deployment.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "example.fullname" . }} + labels: + {{- include "example.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "example.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "example.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "example.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/test/app/chart/templates/hpa.yaml b/test/app/chart/templates/hpa.yaml new file mode 100644 index 0000000..e8b775d --- /dev/null +++ b/test/app/chart/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "example.fullname" . }} + labels: + {{- include "example.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "example.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/test/app/chart/templates/ingress.yaml b/test/app/chart/templates/ingress.yaml new file mode 100644 index 0000000..e9d7a37 --- /dev/null +++ b/test/app/chart/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "example.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "example.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/test/app/chart/templates/service.yaml b/test/app/chart/templates/service.yaml new file mode 100644 index 0000000..6ef4319 --- /dev/null +++ b/test/app/chart/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "example.fullname" . }} + labels: + {{- include "example.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "example.selectorLabels" . | nindent 4 }} diff --git a/test/app/chart/templates/serviceaccount.yaml b/test/app/chart/templates/serviceaccount.yaml new file mode 100644 index 0000000..94fb47c --- /dev/null +++ b/test/app/chart/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "example.serviceAccountName" . }} + labels: + {{- include "example.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/test/app/chart/templates/tests/test-connection.yaml b/test/app/chart/templates/tests/test-connection.yaml new file mode 100644 index 0000000..35f374a --- /dev/null +++ b/test/app/chart/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "example.fullname" . }}-test-connection" + labels: + {{- include "example.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "example.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/test/app/chart/values.yaml b/test/app/chart/values.yaml new file mode 100644 index 0000000..2e50f09 --- /dev/null +++ b/test/app/chart/values.yaml @@ -0,0 +1,82 @@ +# Default values for example. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/test/app/go.mod b/test/app/go.mod new file mode 100644 index 0000000..afdf576 --- /dev/null +++ b/test/app/go.mod @@ -0,0 +1,3 @@ +module hello + +go 1.17 diff --git a/test/app/main.go b/test/app/main.go new file mode 100644 index 0000000..89cfae0 --- /dev/null +++ b/test/app/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "flag" + "log" + "net/http" +) + +func main() { + var flagAddress string + var flagGreeting string + flag.StringVar(&flagAddress, "address", ":80", "address to listen to ") + flag.StringVar(&flagGreeting, "greeting", "HELLO", "sets the greeting message") + flag.Parse() + + greeting := []byte(flagGreeting) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write(greeting) + }) + log.Printf("listening on %v", flagAddress) + log.Fatal(http.ListenAndServe(flagAddress, handler)) +} diff --git a/the-hook/Dockerfile b/the-hook/Dockerfile index 641aebf..b6fd9c7 100644 --- a/the-hook/Dockerfile +++ b/the-hook/Dockerfile @@ -1,6 +1,6 @@ ARG IMAGE=golang:latest FROM $IMAGE -RUN go get github.com/go-delve/delve/cmd/dlv +RUN go install github.com/go-delve/delve/cmd/dlv@latest ENTRYPOINT ["/bin/sh", "-c", "while true; do echo go-grapple-go!; sleep 3600; done"] \ No newline at end of file diff --git a/the-hook/deployment-patch.yaml b/the-hook/deployment-patch.yaml index 932b472..af0f95b 100644 --- a/the-hook/deployment-patch.yaml +++ b/the-hook/deployment-patch.yaml @@ -22,7 +22,7 @@ spec: volumes: - name: patch-configmap configMap: - name: {{ .Deployment }} + name: {{ .Deployment }}-patch {{ range $i, $mount := .Mounts }} - name: "patch-mount-{{ $i }}" hostPath: diff --git a/utils.go b/utils.go index 5ad2f9b..bf924de 100644 --- a/utils.go +++ b/utils.go @@ -2,6 +2,7 @@ package gograpple import ( "bytes" + "context" "fmt" "net" "os" @@ -35,22 +36,22 @@ func CheckTCPConnection(host string, port int) (*net.TCPAddr, error) { return l.Addr().(*net.TCPAddr), nil } -func runOpen(l *logrus.Entry, path string) (string, error) { +func Open(l *logrus.Entry, ctx context.Context, path string) (string, error) { var cmd *exec.Cmd switch runtime.GOOS { case "linux": - cmd = exec.NewCommand(l, "xdg-open").Args(path) + cmd = exec.NewCommand("xdg-open").Logger(l).Args(path) case "windows": - cmd = exec.NewCommand(l, "rundll32").Args("url.dll,FileProtocolHandler", path) + cmd = exec.NewCommand("rundll32").Logger(l).Args("url.dll,FileProtocolHandler", path) case "darwin": - cmd = exec.NewCommand(l, "open").Args(path) + cmd = exec.NewCommand("open").Logger(l).Args(path) default: return "", fmt.Errorf("unsupported platform") } - return cmd.Run() + return cmd.Run(ctx) } -func tryCall(l *logrus.Entry, tries int, waitBetweenAttempts time.Duration, f func(i int) error) error { +func TryCall(tries int, waitBetweenAttempts time.Duration, f func(i int) error) error { var err error for i := 1; i < tries+1; i++ { err = f(i) @@ -62,6 +63,23 @@ func tryCall(l *logrus.Entry, tries int, waitBetweenAttempts time.Duration, f fu return err } +func tryCallWithContext(ctx context.Context, tries int, waitBetweenAttempts time.Duration, f func(i int) error) error { + var err error + for i := 1; i < tries+1; i++ { + select { + case <-ctx.Done(): + return context.Canceled + default: + err = f(i) + if err == nil { + return nil + } + time.Sleep(waitBetweenAttempts) + } + } + return err +} + func findGoProjectRoot(path string) (string, error) { abs, errAbs := filepath.Abs(path) if errAbs != nil { diff --git a/validation.go b/validation.go index 63efeab..a38294c 100644 --- a/validation.go +++ b/validation.go @@ -6,8 +6,34 @@ import ( "path" "path/filepath" "strings" + + v1 "k8s.io/api/apps/v1" ) +func ValidateImage(d v1.Deployment, container string, image, tag *string) error { + if *image == "" { + for _, c := range d.Spec.Template.Spec.Containers { + if container == c.Name { + colonPieces := strings.Split(c.Image, ":") + if len(colonPieces) < 2 { + // invalid + return fmt.Errorf("deployment image %q has invalid format", c.Image) + } else if len(colonPieces) > 2 { + // there might be a repo with a port in there + *image = strings.Join(colonPieces[:len(colonPieces)-1], ":") + *tag = colonPieces[len(colonPieces)-1] + } else { + // image:tag + *image = colonPieces[0] + *tag = colonPieces[1] + } + return nil + } + } + } + return nil +} + func ValidateMounts(wd string, ms []string) ([]Mount, error) { var mounts []Mount for _, m := range ms { @@ -29,13 +55,6 @@ func ValidateMounts(wd string, ms []string) ([]Mount, error) { } -func validateResource(resourceType, resource, suffix string, available []string) error { - if !stringIsInSlice(resource, available) { - return fmt.Errorf("%v %q not found %v, available: %v", resourceType, resource, suffix, strings.Join(available, ", ")) - } - return nil -} - func ValidatePath(wd string, p *string) error { if !filepath.IsAbs(*p) { *p = path.Join(wd, *p) diff --git a/vscode.go b/vscode.go index afac210..db11b5c 100644 --- a/vscode.go +++ b/vscode.go @@ -1,6 +1,7 @@ package gograpple import ( + "context" "encoding/json" "fmt" "io/ioutil" @@ -26,10 +27,10 @@ type launchArgs struct { ShowLog bool `json:"showLog,omitempty"` } -func newLaunchArgs(pod, host string, port, iteration int, workspaceFolder string) *launchArgs { +func newLaunchArgs(host string, port int, workspaceFolder string) *launchArgs { return &launchArgs{ Host: host, - Name: fmt.Sprintf("delve-%v-run-%v", pod, iteration), + Name: fmt.Sprintf("delve-%v", time.Now().Unix()), Port: port, Request: "attach", Type: "go", @@ -50,8 +51,7 @@ func (la *launchArgs) toJson() (string, error) { return string(bytes), nil } -func launchVscode(l *logrus.Entry, goModDir, pod, host string, port, tries, iteration int, sleep time.Duration) error { - +func launchVSCode(ctx context.Context, l *logrus.Entry, goModDir, host string, port, tries int) error { openFile := goModDir workspaceFolder := "${workspaceFolder}" // is there a workspace in that dir @@ -66,20 +66,20 @@ func launchVscode(l *logrus.Entry, goModDir, pod, host string, port, tries, iter } } - exec.NewCommand(l, "code").Args(openFile).PostEnd(func() error { - return tryCall(l, tries, 200*time.Millisecond, func(i int) error { + exec.NewCommand("code").Logger(l).Args(openFile).PostEnd(func() error { + return tryCallWithContext(ctx, tries, 200*time.Millisecond, func(i int) error { l.Infof("waiting for vscode status (%v/%v)", i, tries) - _, err := exec.NewCommand(l, "code").Args("-s").Run() + _, err := exec.NewCommand("code").Logger(l).Args("-s").Run(ctx) return err }) - }).Run() + }).Run(ctx) l.Infof("opening debug configuration") - la, err := newLaunchArgs(pod, host, port, iteration, workspaceFolder).toJson() + la, err := newLaunchArgs(host, port, workspaceFolder).toJson() if err != nil { return err } - _, err = runOpen(l, `vscode://fabiospampinato.vscode-debug-launcher/launch?args=`+url.QueryEscape(la)) + _, err = Open(l, ctx, `vscode://fabiospampinato.vscode-debug-launcher/launch?args=`+url.QueryEscape(la)) if err != nil { return err }