diff --git a/go.mod b/go.mod index 50d8d60..531dac8 100644 --- a/go.mod +++ b/go.mod @@ -10,11 +10,11 @@ require ( github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.29.0 - k8s.io/apimachinery v0.29.0 - k8s.io/cli-runtime v0.29.0 - k8s.io/client-go v0.29.0 - k8s.io/kubectl v0.0.0-20240318124913-31199ade16bc + k8s.io/api v0.30.2 + k8s.io/apimachinery v0.30.2 + k8s.io/cli-runtime v0.30.2 + k8s.io/client-go v0.30.2 + k8s.io/kubectl v0.30.2 ) require ( @@ -22,13 +22,10 @@ require ( github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/distribution/reference v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect - github.com/fatih/camelcase v1.0.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/fvbommel/sortorder v1.1.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect @@ -38,7 +35,6 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.4.0 // indirect @@ -61,7 +57,6 @@ require ( github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -78,7 +73,7 @@ require ( google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/component-base v0.29.0 // indirect + k8s.io/component-base v0.30.2 // indirect k8s.io/klog/v2 v2.120.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect diff --git a/go.sum b/go.sum index 186bf89..f1250e7 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= -github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= @@ -65,16 +63,12 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= -github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= -github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= -github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= @@ -283,8 +277,6 @@ github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE= github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI= @@ -535,22 +527,22 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/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-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= -k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= -k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= -k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= -k8s.io/cli-runtime v0.29.0 h1:q2kC3cex4rOBLfPOnMSzV2BIrrQlx97gxHJs21KxKS4= -k8s.io/cli-runtime v0.29.0/go.mod h1:VKudXp3X7wR45L+nER85YUzOQIru28HQpXr0mTdeCrk= -k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8= -k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38= -k8s.io/component-base v0.29.0 h1:T7rjd5wvLnPBV1vC4zWd/iWRbV8Mdxs+nGaoaFzGw3s= -k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M= +k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= +k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI= +k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= +k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/cli-runtime v0.30.2 h1:ooM40eEJusbgHNEqnHziN9ZpLN5U4WcQGsdLKVxpkKE= +k8s.io/cli-runtime v0.30.2/go.mod h1:Y4g/2XezFyTATQUbvV5WaChoUGhojv/jZAtdp5Zkm0A= +k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50= +k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs= +k8s.io/component-base v0.30.2 h1:pqGBczYoW1sno8q9ObExUqrYSKhtE5rW3y6gX88GZII= +k8s.io/component-base v0.30.2/go.mod h1:yQLkQDrkK8J6NtP+MGJOws+/PPeEXNpwFixsUI7h/OE= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= -k8s.io/kubectl v0.0.0-20240318124913-31199ade16bc h1:m3FnrjEl+K82AmXlyZXhQ1HkArgWMxHw28XaQxTrBjM= -k8s.io/kubectl v0.0.0-20240318124913-31199ade16bc/go.mod h1:QEWsjPyW/2IZ9zeasXccFIE7iLohysciSu02jYStnmo= +k8s.io/kubectl v0.30.2 h1:cgKNIvsOiufgcs4yjvgkK0+aPCfa8pUwzXdJtkbhsH8= +k8s.io/kubectl v0.30.2/go.mod h1:rz7GHXaxwnigrqob0lJsiA07Df8RE3n1TSaC2CTeuB4= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/pkg/command/config.go b/pkg/command/config.go index 314af06..d89b9a3 100644 --- a/pkg/command/config.go +++ b/pkg/command/config.go @@ -5,4 +5,5 @@ package command var ( flagProfileName string flagImage string + flagDebug bool ) diff --git a/pkg/command/debug.go b/pkg/command/debug.go index 18510b3..612a361 100644 --- a/pkg/command/debug.go +++ b/pkg/command/debug.go @@ -4,69 +4,29 @@ package command import ( "context" - "encoding/json" "fmt" "os" + "os/exec" "slices" - "time" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - utilrand "k8s.io/apimachinery/pkg/util/rand" - "k8s.io/apimachinery/pkg/util/strategicpatch" - "k8s.io/apimachinery/pkg/watch" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" - "k8s.io/cli-runtime/pkg/printers" - "k8s.io/cli-runtime/pkg/resource" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" - "k8s.io/client-go/tools/cache" - watchtools "k8s.io/client-go/tools/watch" - "k8s.io/kubectl/pkg/cmd/attach" - kubectldebug "k8s.io/kubectl/pkg/cmd/debug" - "k8s.io/kubectl/pkg/cmd/exec" cmdutil "k8s.io/kubectl/pkg/cmd/util" - "k8s.io/kubectl/pkg/scheme" - "k8s.io/kubectl/pkg/util/interrupt" - "k8s.io/kubectl/pkg/util/term" "github.com/bavarianbidi/kubectl-dpm/pkg/config" - "github.com/bavarianbidi/kubectl-dpm/pkg/internal/util" "github.com/bavarianbidi/kubectl-dpm/pkg/profile" ) -type CustomDebugOptions struct { - *kubectldebug.DebugOptions - podClient corev1client.CoreV1Interface - kubectlPath string -} - -func NewCustomDebugOptions(streams genericiooptions.IOStreams) *CustomDebugOptions { - debugOptions := kubectldebug.NewDebugOptions(streams) - - return &CustomDebugOptions{ - DebugOptions: debugOptions, - } -} - var ( MatchVersionKubeConfigFlags *cmdutil.MatchVersionFlags debugProfile profile.Profile ) -func NewCmdDebugProfile(streams genericiooptions.IOStreams) *cobra.Command { - o := NewCustomDebugOptions(streams) - - // add kubeconfig flags - kubeConfigFlags := genericclioptions.NewConfigFlags(true) - MatchVersionKubeConfigFlags = cmdutil.NewMatchVersionFlags(kubeConfigFlags) - +func NewCmdDebugProfile(_ genericiooptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "run", // DisableFlagsInUseLine: true, @@ -84,7 +44,7 @@ func NewCmdDebugProfile(streams genericiooptions.IOStreams) *cobra.Command { return err } - if err := o.run(streams, args); err != nil { + if err := run(args); err != nil { return err } @@ -96,23 +56,28 @@ func NewCmdDebugProfile(streams genericiooptions.IOStreams) *cobra.Command { flags := cmd.PersistentFlags() // add kubeconfig flags + kubeConfigFlags := genericclioptions.NewConfigFlags(true) + MatchVersionKubeConfigFlags = cmdutil.NewMatchVersionFlags(kubeConfigFlags) kubeConfigFlags.AddFlags(flags) // add custom flag cmd.Flags().StringVar(&flagProfileName, "profile", "", "profile name") cmd.Flags().StringVar(&flagImage, "image", "", "image to use for the debug container") + cmd.Flags().BoolVar(&flagDebug, "debug", false, "print debug information") return cmd } -func (o *CustomDebugOptions) run(streams genericiooptions.IOStreams, args []string) error { +func run(args []string) error { // validate kubectl path if err := profile.ValidateKubectlPath(); err != nil { return err } - // set kubectl path - o.kubectlPath = profile.Config.KubectlPath + // check kubectl version + if err := profile.CheckKubectlVersion(); err != nil { + return err + } // validate and complete profile if err := profile.ValidateAndCompleteProfile(flagProfileName); err != nil { @@ -126,429 +91,99 @@ func (o *CustomDebugOptions) run(streams genericiooptions.IOStreams, args []stri debugProfile = profile.Config.Profiles[idx] - fmt.Printf("Profile: %+v\n", debugProfile) - - // this is not needed atm as we want support kubectl versions < 1.30 - // - // to just wrap the kubectl-debug command, we have to change a few things in here - // nothing special, but it has to be done :-) - // - // enable custom debug profile via env var KUBECTL_DEBUG_CUSTOM_PROFILE - // os.Setenv(string(cmdutil.DebugCustomProfile), "true") - - if err := o.prepareEphemeralContainer(streams, args); err != nil { - return err - } - - return nil -} - -func (o *CustomDebugOptions) prepareEphemeralContainer(streams genericiooptions.IOStreams, args []string) error { - o.DebugOptions.CustomProfileFile = os.ExpandEnv(debugProfile.CustomProfileFile) - o.DebugOptions.Profile = kubectldebug.ProfileLegacy - - // use image from profile - o.DebugOptions.Image = debugProfile.Image - - // the flag has the highest priority - if flagImage != "" { - o.DebugOptions.Image = flagImage - } - - if o.DebugOptions.Image == "" { - return fmt.Errorf("image is required") - } - - // i'm not using the upstream complete func as it requires flags of the debug cmd which i do not have atm - // - // do the custom complete func - if err := o.complete(MatchVersionKubeConfigFlags); err != nil { - return err - } - - // get the namespace from the profile or the current kube context - // if no namespace is given, use the default namespace - // the namespace flag has the biggest priority - // - // TODO: move this to the complete func - //nolint:godox - kubectlNamespace, _, _ := MatchVersionKubeConfigFlags.ToRawKubeConfigLoader().Namespace() + var targetContainer string + namespace := getTargetNamespace() switch { - case kubectlNamespace != corev1.NamespaceDefault: - o.Namespace = kubectlNamespace - case debugProfile.Namespace != "": - o.Namespace = debugProfile.Namespace - default: - o.Namespace = corev1.NamespaceDefault - } - - // get the pod name from the profile label selector if no args are given - if len(args) == 0 && len(debugProfile.MatchLabels) > 0 { - matchingPods, err := o.podClient.Pods(o.Namespace).List(context.TODO(), metav1.ListOptions{ - LabelSelector: metav1.FormatLabelSelector( - &metav1.LabelSelector{ - MatchLabels: debugProfile.MatchLabels, - }), - }) + case len(args) > 1: + return fmt.Errorf("too many arguments") + case len(args) == 1: + targetContainer = args[0] + case len(debugProfile.MatchLabels) > 0: + var err error + targetContainer, err = getTargetPod(namespace) if err != nil { return err } - - if len(matchingPods.Items) == 0 { - return fmt.Errorf("no pods in namespace %s found with label selector %v", o.Namespace, debugProfile.MatchLabels) - } - - for _, pod := range matchingPods.Items { - args = append(args, pod.Name) - } - } - - // set target names - o.DebugOptions.TargetNames = args - - // Validate is the upstream package Validate func - if err := o.DebugOptions.Validate(); err != nil { - return err - } - - if err := o.Run(MatchVersionKubeConfigFlags, streams); err != nil { - return err - } - - return nil -} - -// Complete completes all the required options to make the debug command work -// it's primary a copy of the upstream complete func -func (o *CustomDebugOptions) complete(restClientGetter genericclioptions.RESTClientGetter) error { - o.PullPolicy = corev1.PullIfNotPresent - - o.TTY = true - o.Interactive = true - - // - applier, err := kubectldebug.NewProfileApplier(o.Profile) - if err != nil { - return err - } - o.Applier = applier - - o.Attach = true - - o.WarningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)}) - - if o.CustomProfileFile != "" { - customProfileBytes, err := os.ReadFile(o.CustomProfileFile) - if err != nil { - return fmt.Errorf("must pass a container spec json file for custom profile: %w", err) - } - - err = json.Unmarshal(customProfileBytes, &o.CustomProfile) - if err != nil { - return fmt.Errorf("%s does not contain a valid container spec: %w", o.CustomProfileFile, err) - } - } - - config, err := restClientGetter.ToRESTConfig() - if err != nil { - return err + default: + return fmt.Errorf("no target container specified") } - client, err := corev1client.NewForConfig(config) - if err != nil { - return err + if flagDebug { + fmt.Printf("Using profile: %+v\n", debugProfile) + fmt.Printf("kubectl path: %s\n", os.ExpandEnv(profile.Config.KubectlPath)) + fmt.Printf("profile path: %s\n", debugProfile.CustomProfileFile) + fmt.Printf("profile path resolved: %s\n", os.ExpandEnv(debugProfile.CustomProfileFile)) } - o.podClient = client - - o.Builder = resource.NewBuilder(restClientGetter) - - return nil -} - -func (o *CustomDebugOptions) Run(restClientGetter genericclioptions.RESTClientGetter, streams genericiooptions.IOStreams) error { - ctx := context.Background() - - r := o.Builder. - WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). - FilenameParam(true, &o.FilenameOptions). - NamespaceParam(o.Namespace).DefaultNamespace().ResourceNames("pods", o.TargetNames...). - Do() - if err := r.Err(); err != nil { - return err - } - - err := r.Visit(func(info *resource.Info, err error) error { - if err != nil { - return err - } - - var ( - debugPod *corev1.Pod - containerName string - visitErr error - ) - switch obj := info.Object.(type) { - case *corev1.Pod: - debugPod, containerName, visitErr = o.debugByEphemeralContainer(ctx, obj) - default: - visitErr = fmt.Errorf("%q not supported by debug", info.Mapping.GroupVersionKind) - } - if visitErr != nil { - return visitErr - } - - o.AttachFunc = func(ctx context.Context, restClientGetter genericclioptions.RESTClientGetter, cmdPath, ns, podName, containerName string) error { - return o.attachPod(ctx, streams, restClientGetter, cmdPath, debugPod.Namespace, debugPod.Name, containerName) - } - - if o.Attach && len(containerName) > 0 && o.AttachFunc != nil { - if err := o.AttachFunc(ctx, restClientGetter, "cmdPath", debugPod.Namespace, debugPod.Name, containerName); err != nil { - return err - } - } - - return nil - }) + // nolint:gosec + debugCommand := exec.Command( + os.ExpandEnv(profile.Config.KubectlPath), + "debug", + "--namespace", namespace, + "--custom", os.ExpandEnv(debugProfile.CustomProfileFile), + "--image", debugProfile.Image, targetContainer, + "-it", + ) + debugCommand.Env = os.Environ() + debugCommand.Env = append(debugCommand.Env, string(cmdutil.DebugCustomProfile)+"=true") - return err -} + debugCommand.Stdout = os.Stdout + debugCommand.Stderr = os.Stderr + debugCommand.Stdin = os.Stdin -// func attachPod(ctx context.Context, streams genericiooptions.IOStreams, restClientGetter genericclioptions.RESTClientGetter, cmdPath string, ns, podName, containerName string) kubectldebug.DebugAttachFunc { -func (o *CustomDebugOptions) attachPod(ctx context.Context, streams genericiooptions.IOStreams, restClientGetter genericclioptions.RESTClientGetter, cmdPath string, ns, podName, containerName string) error { - opts := &attach.AttachOptions{ - StreamOptions: exec.StreamOptions{ - IOStreams: streams, - Stdin: true, - TTY: o.TTY, - Quiet: o.Quiet, - }, - CommandName: cmdPath + " attach", - - Attach: &attach.DefaultRemoteAttach{}, - } - config, err := restClientGetter.ToRESTConfig() - if err != nil { - return err + if flagDebug { + fmt.Printf("Running command: %s\n", debugCommand.String()) } - opts.Config = config - // discover pod again - // and get namespace, podname and newly created debug container name - pod, err := o.waitForContainer(ctx, ns, podName, containerName) - if err != nil { + if err := debugCommand.Run(); err != nil { return err } - opts.Namespace = ns - opts.Pod = pod - opts.PodName = podName - opts.ContainerName = containerName - if opts.AttachFunc == nil { - opts.AttachFunc = attach.DefaultAttachFunc - } - - status := util.GetContainerStatusByName(pod, containerName) - if status == nil { - // impossible path - return fmt.Errorf("error getting container status of container name %q: %+v", containerName, err) - } - if status.State.Terminated != nil { - return fmt.Errorf("ephemeral container %q terminated", containerName) - } - - if err := opts.Run(); err != nil { - fmt.Fprintf(opts.ErrOut, "couldn't attach to pod/%s\n", podName) - } return nil } -// debugByEphemeralContainer runs an EphemeralContainer in the target Pod for use as a debug container -// -// taken from: https://github.com/kubernetes/kubernetes/blob/9791f0d1f39f3f1e0796add7833c1059325d5098/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go#L456-L494 -// and modified to make it usable in here -func (o *CustomDebugOptions) debugByEphemeralContainer(ctx context.Context, pod *corev1.Pod) (*corev1.Pod, string, error) { - podJS, err := json.Marshal(pod) +func getTargetPod(namespace string) (string, error) { + restClient, err := MatchVersionKubeConfigFlags.ToRESTConfig() if err != nil { - return nil, "", fmt.Errorf("error creating JSON for pod: %v", err) + return "", err } - debugPod, debugContainer, err := o.generateDebugContainer(pod) - if err != nil { - return nil, "", err - } + podClient := corev1client.NewForConfigOrDie(restClient) - debugJS, err := json.Marshal(debugPod) + matchingPods, err := podClient.Pods(namespace).List(context.TODO(), metav1.ListOptions{ + LabelSelector: metav1.FormatLabelSelector( + &metav1.LabelSelector{ + MatchLabels: debugProfile.MatchLabels, + }), + }) if err != nil { - return nil, "", fmt.Errorf("error creating JSON for debug container: %v", err) + return "", err } - patch, err := strategicpatch.CreateTwoWayMergePatch(podJS, debugJS, pod) - if err != nil { - return nil, "", fmt.Errorf("error creating patch to add debug container: %v", err) + if len(matchingPods.Items) == 0 { + return "", fmt.Errorf("no pods in namespace %s found with label selector %v", namespace, debugProfile.MatchLabels) } - pods := o.podClient.Pods(pod.Namespace) - result, err := pods.Patch(ctx, pod.Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{}, "ephemeralcontainers") - if err != nil { - // The apiserver will return a 404 when the EphemeralContainers feature is disabled because the `/ephemeralcontainers` subresource - // is missing. Unlike the 404 returned by a missing pod, the status details will be empty. - if serr, ok := err.(*errors.StatusError); ok && serr.Status().Reason == metav1.StatusReasonNotFound && serr.ErrStatus.Details.Name == "" { - return nil, "", fmt.Errorf("ephemeral containers are disabled for this cluster (error from server: %q)", err) - } + var podName string - return nil, "", err + for _, pod := range matchingPods.Items { + podName = pod.Name } - return result, debugContainer.Name, nil + return podName, nil } -// generateDebugContainer returns a debugging pod and an EphemeralContainer suitable for use as a debug container -// in the given pod. -func (o *CustomDebugOptions) generateDebugContainer(pod *corev1.Pod) (*corev1.Pod, *corev1.EphemeralContainer, error) { - name := o.computeDebugContainerName(pod) - ec := &corev1.EphemeralContainer{ - EphemeralContainerCommon: corev1.EphemeralContainerCommon{ - Name: name, - Env: o.Env, - Image: o.Image, - ImagePullPolicy: o.PullPolicy, - Stdin: o.Interactive, - TerminationMessagePolicy: corev1.TerminationMessageReadFile, - TTY: o.TTY, - }, - TargetContainerName: o.TargetContainer, +func getTargetNamespace() string { + if debugProfile.Namespace != "" { + return debugProfile.Namespace } - if o.ArgsOnly { - ec.Args = o.Args - } else { - ec.Command = o.Args - } - - copied := pod.DeepCopy() - copied.Spec.EphemeralContainers = append(copied.Spec.EphemeralContainers, *ec) - if err := o.Applier.Apply(copied, name, copied); err != nil { - return nil, nil, err - } - - if o.CustomProfile != nil { - err := o.applyCustomProfileEphemeral(copied, ec.Name) - if err != nil { - return nil, nil, err - } - } - - ec = &copied.Spec.EphemeralContainers[len(copied.Spec.EphemeralContainers)-1] - - return copied, ec, nil -} - -func (o *CustomDebugOptions) computeDebugContainerName(pod *corev1.Pod) string { - if len(o.Container) > 0 { - return o.Container - } - - cn, containerByName := "", util.ContainerNameToRef(pod) - for len(cn) == 0 || (containerByName[cn] != nil) { - cn = fmt.Sprintf("debugger-%s", utilrand.String(5)) - } - if !o.Quiet { - fmt.Fprintf(o.Out, "Defaulting debug container name to %s.\n", cn) - } - return cn -} - -// applyCustomProfileEphemeral applies given partial container json file on to the profile -// incorporated ephemeral container of the pod. -func (o *CustomDebugOptions) applyCustomProfileEphemeral(debugPod *corev1.Pod, containerName string) error { - o.CustomProfile.Name = containerName - customJS, err := json.Marshal(o.CustomProfile) - if err != nil { - return fmt.Errorf("unable to marshall custom profile: %w", err) - } - - var index int - found := false - for i, val := range debugPod.Spec.EphemeralContainers { - if val.Name == containerName { - index = i - found = true - break - } - } - - if !found { - return fmt.Errorf("unable to find the %s ephemeral container in the pod %s", containerName, debugPod.Name) - } - - var debugContainerJS []byte - debugContainerJS, err = json.Marshal(debugPod.Spec.EphemeralContainers[index]) - if err != nil { - return fmt.Errorf("unable to marshall ephemeral container:%w", err) - } - - patchedContainer, err := strategicpatch.StrategicMergePatch(debugContainerJS, customJS, corev1.Container{}) - if err != nil { - return fmt.Errorf("error creating three way patch to add debug container: %w", err) - } - - err = json.Unmarshal(patchedContainer, &debugPod.Spec.EphemeralContainers[index]) - if err != nil { - return fmt.Errorf("unable to unmarshall patched container to ephemeral container: %w", err) - } - - return nil -} - -// waitForContainer watches the given pod until the container is running -func (o *CustomDebugOptions) waitForContainer(ctx context.Context, ns, podName, containerName string) (*corev1.Pod, error) { - ctx, cancel := watchtools.ContextWithOptionalTimeout(ctx, 0*time.Second) - defer cancel() + kubectlNamespace, _, _ := MatchVersionKubeConfigFlags.ToRawKubeConfigLoader().Namespace() - fieldSelector := fields.OneTermEqualSelector("metadata.name", podName).String() - lw := &cache.ListWatch{ - ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { - options.FieldSelector = fieldSelector - return o.podClient.Pods(ns).List(ctx, options) - }, - WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { - options.FieldSelector = fieldSelector - return o.podClient.Pods(ns).Watch(ctx, options) - }, + if kubectlNamespace != corev1.NamespaceDefault { + return kubectlNamespace } - intr := interrupt.New(nil, cancel) - var result *corev1.Pod - err := intr.Run(func() error { - ev, err := watchtools.UntilWithSync(ctx, lw, &corev1.Pod{}, nil, func(ev watch.Event) (bool, error) { - if ev.Type == watch.Deleted { - return false, errors.NewNotFound(schema.GroupResource{Resource: "pods"}, "") - } - - p, ok := ev.Object.(*corev1.Pod) - if !ok { - return false, fmt.Errorf("watch did not return a pod: %v", ev.Object) - } - - s := util.GetContainerStatusByName(p, containerName) - if s == nil { - return false, nil - } - if s.State.Running != nil || s.State.Terminated != nil { - return true, nil - } - if !o.Quiet && s.State.Waiting != nil && s.State.Waiting.Message != "" { - o.WarningPrinter.Print(fmt.Sprintf("container %s: %s", containerName, s.State.Waiting.Message)) - } - return false, nil - }) - if ev != nil { - result = ev.Object.(*corev1.Pod) - } - return err - }) - - return result, err + return corev1.NamespaceDefault } diff --git a/pkg/command/debug_test.go b/pkg/command/debug_test.go new file mode 100644 index 0000000..6981aef --- /dev/null +++ b/pkg/command/debug_test.go @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT + +package command + +import ( + "testing" + + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + + "github.com/bavarianbidi/kubectl-dpm/pkg/profile" +) + +func TestGetTargetNamespace(t *testing.T) { + tests := []struct { + name string + debugProfile profile.Profile + kubectlNs string + want string + }{ + { + name: "debug profile has namespace", + debugProfile: profile.Profile{ + Namespace: "custom-namespace", + }, + kubectlNs: "default", + want: "custom-namespace", + }, + { + name: "debug profile does not have namespace, kubectl namespace is not default", + debugProfile: profile.Profile{}, + kubectlNs: "custom-namespace", + want: "custom-namespace", + }, + { + name: "debug profile does not have namespace, kubectl namespace is default", + debugProfile: profile.Profile{}, + kubectlNs: "default", + want: "default", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + debugProfile = tt.debugProfile + + kubeConfigFlags := genericclioptions.NewConfigFlags(true) + // nolint:gosec + kubeConfigFlags.Namespace = &tt.kubectlNs + MatchVersionKubeConfigFlags = cmdutil.NewMatchVersionFlags(kubeConfigFlags) + + got := getTargetNamespace() + + if got != tt.want { + t.Errorf("getTargetNamespace() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/command/root.go b/pkg/command/root.go index 780436f..153632e 100644 --- a/pkg/command/root.go +++ b/pkg/command/root.go @@ -9,6 +9,6 @@ import ( func Root() *cobra.Command { return &cobra.Command{ Use: "kubectl-dpm", - Short: "kubectl debug profile foo", + Short: "kubectl debug profile manager", } } diff --git a/pkg/internal/util/util.go b/pkg/internal/util/util.go deleted file mode 100644 index 94e6db4..0000000 --- a/pkg/internal/util/util.go +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT - -package util - -import ( - corev1 "k8s.io/api/core/v1" -) - -func ContainerNameToRef(pod *corev1.Pod) map[string]*corev1.Container { - names := map[string]*corev1.Container{} - for i := range pod.Spec.Containers { - ref := &pod.Spec.Containers[i] - names[ref.Name] = ref - } - for i := range pod.Spec.InitContainers { - ref := &pod.Spec.InitContainers[i] - names[ref.Name] = ref - } - for i := range pod.Spec.EphemeralContainers { - ref := (*corev1.Container)(&pod.Spec.EphemeralContainers[i].EphemeralContainerCommon) - names[ref.Name] = ref - } - return names -} - -func GetContainerStatusByName(pod *corev1.Pod, containerName string) *corev1.ContainerStatus { - allContainerStatus := [][]corev1.ContainerStatus{pod.Status.InitContainerStatuses, pod.Status.ContainerStatuses, pod.Status.EphemeralContainerStatuses} - for _, statusSlice := range allContainerStatus { - for i := range statusSlice { - if statusSlice[i].Name == containerName { - return &statusSlice[i] - } - } - } - return nil -} diff --git a/pkg/profile/kubectl.go b/pkg/profile/kubectl.go new file mode 100644 index 0000000..216b1b6 --- /dev/null +++ b/pkg/profile/kubectl.go @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT + +package profile + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "strconv" + + kubectlversion "k8s.io/kubectl/pkg/cmd/version" +) + +func CheckKubectlVersion() error { + // nolint:gosec + kubeclVersionCmd := exec.Command( + os.ExpandEnv(Config.KubectlPath), + "version", + "--client", "true", + "--output", "json", + ) + + kubectlOutput, err := kubeclVersionCmd.Output() + if err != nil { + return err + } + + kubectlVersion := &kubectlversion.Version{} + + err = json.Unmarshal(kubectlOutput, kubectlVersion) + if err != nil { + return err + } + + kubectlMinorVersion, err := strconv.Atoi(kubectlVersion.ClientVersion.Minor) + if err != nil { + return err + } + + if kubectlMinorVersion < 30 { + return fmt.Errorf("your kubectl doesn't support custom debug profiles, please upgrade to kubectl 1.30 or newer or use v0.0.4 of kubectl-dpm") + } + + return nil +} diff --git a/pkg/profile/validate.go b/pkg/profile/validate.go index 17ea4a8..be20c65 100644 --- a/pkg/profile/validate.go +++ b/pkg/profile/validate.go @@ -85,10 +85,6 @@ func ValidateAllProfiles() error { } // ValidateProfile validates a single profile -// -// This function is not implemented yet -// future ideas: check if the profile file exists and -// it's a valid pod.spec func ValidateProfile(profileName string) error { idx := slices.IndexFunc(Config.Profiles, func(c Profile) bool { return c.ProfileName == profileName },