istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/ztunnelconfig/ztunnelconfig.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package ztunnelconfig
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"io"
    21  	"os"
    22  	"regexp"
    23  	"sort"
    24  	"strings"
    25  
    26  	"github.com/hashicorp/go-multierror"
    27  	"github.com/spf13/cobra"
    28  	corev1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/types"
    31  	"k8s.io/kubectl/pkg/util/podutils"
    32  
    33  	"istio.io/istio/istioctl/pkg/cli"
    34  	"istio.io/istio/istioctl/pkg/completion"
    35  	ambientutil "istio.io/istio/istioctl/pkg/util/ambient"
    36  	ztunnelDump "istio.io/istio/istioctl/pkg/writer/ztunnel/configdump"
    37  	"istio.io/istio/pkg/config"
    38  	"istio.io/istio/pkg/kube"
    39  	"istio.io/istio/pkg/log"
    40  	"istio.io/istio/pkg/slices"
    41  )
    42  
    43  const (
    44  	jsonOutput    = "json"
    45  	yamlOutput    = "yaml"
    46  	summaryOutput = "short"
    47  
    48  	defaultProxyAdminPort = 15000
    49  )
    50  
    51  func ZtunnelConfig(ctx cli.Context) *cobra.Command {
    52  	configCmd := &cobra.Command{
    53  		Use:   "ztunnel-config",
    54  		Short: "Update or retrieve current Ztunnel configuration.",
    55  		Long:  "A group of commands used to update or retrieve Ztunnel configuration from a Ztunnel instance.",
    56  		Example: `  # Retrieve summary about workload configuration
    57    istioctl x ztunnel-config workload
    58  
    59    # Retrieve summary about certificates
    60    istioctl x ztunnel-config certificates`,
    61  		Aliases: []string{"zc"},
    62  	}
    63  
    64  	configCmd.AddCommand(logCmd(ctx))
    65  	configCmd.AddCommand(workloadConfigCmd(ctx))
    66  	configCmd.AddCommand(certificatesConfigCmd(ctx))
    67  	configCmd.AddCommand(servicesCmd(ctx))
    68  	configCmd.AddCommand(policiesCmd(ctx))
    69  	configCmd.AddCommand(allCmd(ctx))
    70  	configCmd.AddCommand(connectionsCmd(ctx))
    71  
    72  	return configCmd
    73  }
    74  
    75  type Command struct {
    76  	Name string
    77  }
    78  
    79  func certificatesConfigCmd(ctx cli.Context) *cobra.Command {
    80  	common := new(commonFlags)
    81  	cmd := &cobra.Command{
    82  		Use:   "certificate",
    83  		Short: "Retrieves certificate for the specified Ztunnel pod.",
    84  		Long:  `Retrieve information about certificates for the Ztunnel instance.`,
    85  		Example: `  # Retrieve summary about workload configuration for a randomly chosen ztunnel.
    86    istioctl x ztunnel-config certificates
    87  
    88    # Retrieve full certificate dump of workloads for a given Ztunnel instance.
    89    istioctl x ztunnel-config certificates <ztunnel-name[.namespace]> -o json
    90  `,
    91  		Aliases: []string{"certificates", "certs", "cert"},
    92  		Args:    common.validateArgs,
    93  		RunE: runConfigDump(ctx, common, func(cw *ztunnelDump.ConfigWriter) error {
    94  			switch common.outputFormat {
    95  			case summaryOutput:
    96  				return cw.PrintSecretSummary()
    97  			case jsonOutput, yamlOutput:
    98  				return cw.PrintSecretDump(common.outputFormat)
    99  			default:
   100  				return fmt.Errorf("output format %q not supported", common.outputFormat)
   101  			}
   102  		}),
   103  		ValidArgsFunction: completion.ValidPodsNameArgs(ctx),
   104  	}
   105  
   106  	common.attach(cmd)
   107  
   108  	return cmd
   109  }
   110  
   111  func servicesCmd(ctx cli.Context) *cobra.Command {
   112  	var serviceNamespace string
   113  	common := new(commonFlags)
   114  	cmd := &cobra.Command{
   115  		Use:   "service",
   116  		Short: "Retrieves services for the specified Ztunnel pod.",
   117  		Long:  `Retrieve information about services for the Ztunnel instance.`,
   118  		Example: `  # Retrieve summary about services configuration for a randomly chosen ztunnel.
   119    istioctl x ztunnel-config services
   120  
   121    # Retrieve full certificate dump of workloads for a given Ztunnel instance.
   122    istioctl x ztunnel-config services <ztunnel-name[.namespace]> -o json
   123  `,
   124  		Aliases: []string{"services", "s", "svc"},
   125  		Args:    common.validateArgs,
   126  		RunE: runConfigDump(ctx, common, func(cw *ztunnelDump.ConfigWriter) error {
   127  			filter := ztunnelDump.ServiceFilter{
   128  				Namespace: serviceNamespace,
   129  			}
   130  			switch common.outputFormat {
   131  			case summaryOutput:
   132  				return cw.PrintServiceSummary(filter)
   133  			case jsonOutput, yamlOutput:
   134  				return cw.PrintServiceDump(filter, common.outputFormat)
   135  			default:
   136  				return fmt.Errorf("output format %q not supported", common.outputFormat)
   137  			}
   138  		}),
   139  		ValidArgsFunction: completion.ValidPodsNameArgs(ctx),
   140  	}
   141  
   142  	common.attach(cmd)
   143  	cmd.PersistentFlags().StringVar(&serviceNamespace, "service-namespace", "",
   144  		"Filter services by namespace field")
   145  
   146  	return cmd
   147  }
   148  
   149  func policiesCmd(ctx cli.Context) *cobra.Command {
   150  	var policyNamespace string
   151  	common := new(commonFlags)
   152  	cmd := &cobra.Command{
   153  		Use:   "policy",
   154  		Short: "Retrieves policies for the specified Ztunnel pod.",
   155  		Long:  `Retrieve information about policies for the Ztunnel instance.`,
   156  		Example: `  # Retrieve summary about policy configuration for a randomly chosen ztunnel.
   157    istioctl x ztunnel-config policies
   158  
   159    # Retrieve full policy dump of workloads for a given Ztunnel instance.
   160    istioctl x ztunnel-config policies <ztunnel-name[.namespace]> -o json
   161  `,
   162  		Aliases: []string{"policies", "p", "pol"},
   163  		Args:    common.validateArgs,
   164  		RunE: runConfigDump(ctx, common, func(cw *ztunnelDump.ConfigWriter) error {
   165  			filter := ztunnelDump.PolicyFilter{
   166  				Namespace: policyNamespace,
   167  			}
   168  			switch common.outputFormat {
   169  			case summaryOutput:
   170  				return cw.PrintPolicySummary(filter)
   171  			case jsonOutput, yamlOutput:
   172  				return cw.PrintPolicyDump(filter, common.outputFormat)
   173  			default:
   174  				return fmt.Errorf("output format %q not supported", common.outputFormat)
   175  			}
   176  		}),
   177  		ValidArgsFunction: completion.ValidPodsNameArgs(ctx),
   178  	}
   179  
   180  	common.attach(cmd)
   181  	cmd.PersistentFlags().StringVar(&policyNamespace, "policy-namespace", "",
   182  		"Filter policies by namespace field")
   183  
   184  	return cmd
   185  }
   186  
   187  func allCmd(ctx cli.Context) *cobra.Command {
   188  	common := new(commonFlags)
   189  	cmd := &cobra.Command{
   190  		Use:   "all",
   191  		Short: "Retrieves all configuration for the specified Ztunnel pod.",
   192  		Long:  `Retrieve information about all configuration for the Ztunnel instance.`,
   193  		Example: `  # Retrieve summary about all configuration for a randomly chosen ztunnel.
   194    istioctl x ztunnel-config all
   195  
   196    # Retrieve full configuration dump of workloads for a given Ztunnel instance.
   197    istioctl x ztunnel-config policies <ztunnel-name[.namespace]> -o json
   198  `,
   199  		Args: common.validateArgs,
   200  		RunE: runConfigDump(ctx, common, func(cw *ztunnelDump.ConfigWriter) error {
   201  			switch common.outputFormat {
   202  			case summaryOutput:
   203  				return cw.PrintFullSummary()
   204  			case jsonOutput, yamlOutput:
   205  				return cw.PrintFullDump(common.outputFormat)
   206  			default:
   207  				return fmt.Errorf("output format %q not supported", common.outputFormat)
   208  			}
   209  		}),
   210  		ValidArgsFunction: completion.ValidPodsNameArgs(ctx),
   211  	}
   212  
   213  	common.attach(cmd)
   214  
   215  	return cmd
   216  }
   217  
   218  func workloadConfigCmd(ctx cli.Context) *cobra.Command {
   219  	var workloadsNamespace string
   220  	var workloadNode string
   221  	var verboseProxyConfig bool
   222  
   223  	var address string
   224  
   225  	common := new(commonFlags)
   226  	cmd := &cobra.Command{
   227  		Use:   "workload [<type>/]<name>[.<namespace>]",
   228  		Short: "Retrieves workload configuration for the specified Ztunnel pod.",
   229  		Long:  `Retrieve information about workload configuration for the Ztunnel instance.`,
   230  		Example: `  # Retrieve summary about workload configuration for a randomly chosen ztunnel.
   231    istioctl x ztunnel-config workload
   232  
   233    # Retrieve summary of workloads on node XXXX for a given Ztunnel instance.
   234    istioctl x ztunnel-config workload <ztunnel-name[.namespace]> --node ambient-worker
   235  
   236    # Retrieve full workload dump of workloads with address XXXX for a given Ztunnel instance.
   237    istioctl x ztunnel-config workload <ztunnel-name[.namespace]> --address 0.0.0.0 -o json
   238  
   239    # Retrieve Ztunnel config dump separately and inspect from file.
   240    kubectl exec -it $ZTUNNEL -n istio-system -- curl localhost:15000/config_dump > ztunnel-config.json
   241    istioctl x ztunnel-config workloads --file ztunnel-config.json
   242  
   243    # Retrieve workload summary for a specific namespace
   244    istioctl x ztunnel-config workloads <ztunnel-name[.namespace]> --workloads-namespace foo
   245  `,
   246  		Aliases: []string{"w", "workloads"},
   247  		Args:    common.validateArgs,
   248  		RunE: runConfigDump(ctx, common, func(cw *ztunnelDump.ConfigWriter) error {
   249  			filter := ztunnelDump.WorkloadFilter{
   250  				Namespace: workloadsNamespace,
   251  				Address:   address,
   252  				Node:      workloadNode,
   253  				Verbose:   verboseProxyConfig,
   254  			}
   255  
   256  			switch common.outputFormat {
   257  			case summaryOutput:
   258  				return cw.PrintWorkloadSummary(filter)
   259  			case jsonOutput, yamlOutput:
   260  				return cw.PrintWorkloadDump(filter, common.outputFormat)
   261  			default:
   262  				return fmt.Errorf("output format %q not supported", common.outputFormat)
   263  			}
   264  		}),
   265  		ValidArgsFunction: completion.ValidPodsNameArgs(ctx),
   266  	}
   267  
   268  	common.attach(cmd)
   269  	cmd.PersistentFlags().StringVar(&address, "address", "", "Filter workloads by address field")
   270  	cmd.PersistentFlags().BoolVar(&verboseProxyConfig, "verbose", true, "Output more information")
   271  	cmd.PersistentFlags().StringVar(&workloadsNamespace, "workload-namespace", "",
   272  		"Filter workloads by namespace field")
   273  	cmd.PersistentFlags().StringVar(&workloadNode, "workload-node", "",
   274  		"Filter workloads by node")
   275  
   276  	return cmd
   277  }
   278  
   279  func connectionsCmd(ctx cli.Context) *cobra.Command {
   280  	var workloadsNamespace string
   281  	var direction string
   282  	var raw bool
   283  
   284  	common := new(commonFlags)
   285  	cmd := &cobra.Command{
   286  		Use:    "connections [<type>/]<name>[.<namespace>]",
   287  		Hidden: true,
   288  		Short:  "Retrieves connections for the specified Ztunnel pod.",
   289  		Long:   `Retrieve information about connections for the Ztunnel instance.`,
   290  		Example: `  # Retrieve summary about connections for the ztunnel on a specific node.
   291    istioctl x ztunnel-config connections --node ambient-worker
   292  
   293    # Retrieve summary of connections for a given Ztunnel instance.
   294    istioctl x ztunnel-config workload <ztunnel-name[.namespace]>
   295  `,
   296  		Aliases: []string{"cons"},
   297  		Args:    common.validateArgs,
   298  		RunE: runConfigDump(ctx, common, func(cw *ztunnelDump.ConfigWriter) error {
   299  			filter := ztunnelDump.ConnectionsFilter{
   300  				Namespace: workloadsNamespace,
   301  				Direction: direction,
   302  				Raw:       raw,
   303  			}
   304  
   305  			switch common.outputFormat {
   306  			case summaryOutput:
   307  				return cw.PrintConnectionsSummary(filter)
   308  			case jsonOutput, yamlOutput:
   309  				return cw.PrintConnectionsDump(filter, common.outputFormat)
   310  			default:
   311  				return fmt.Errorf("output format %q not supported", common.outputFormat)
   312  			}
   313  		}),
   314  		ValidArgsFunction: completion.ValidPodsNameArgs(ctx),
   315  	}
   316  
   317  	common.attach(cmd)
   318  	cmd.PersistentFlags().StringVar(&direction, "direction", "", "Filter workloads by direction (inbound or outbound)")
   319  	cmd.PersistentFlags().BoolVar(&raw, "raw", false, "If set, show IP addresses instead of names")
   320  	cmd.PersistentFlags().StringVar(&workloadsNamespace, "workload-namespace", "",
   321  		"Filter workloads by namespace field")
   322  
   323  	return cmd
   324  }
   325  
   326  // Level is an enumeration of all supported log levels.
   327  type Level int
   328  
   329  const (
   330  	defaultLoggerName = "level"
   331  )
   332  
   333  const (
   334  	// OffLevel disables logging
   335  	OffLevel Level = iota
   336  	// CriticalLevel enables critical level logging
   337  	CriticalLevel
   338  	// ErrorLevel enables error level logging
   339  	ErrorLevel
   340  	// WarningLevel enables warning level logging
   341  	WarningLevel
   342  	// InfoLevel enables info level logging
   343  	InfoLevel
   344  	// DebugLevel enables debug level logging
   345  	DebugLevel
   346  	// TraceLevel enables trace level logging
   347  	TraceLevel
   348  )
   349  
   350  var levelToString = map[Level]string{
   351  	TraceLevel:    "trace",
   352  	DebugLevel:    "debug",
   353  	InfoLevel:     "info",
   354  	WarningLevel:  "warning",
   355  	ErrorLevel:    "error",
   356  	CriticalLevel: "critical",
   357  	OffLevel:      "off",
   358  }
   359  
   360  var stringToLevel = map[string]Level{
   361  	"trace":    TraceLevel,
   362  	"debug":    DebugLevel,
   363  	"info":     InfoLevel,
   364  	"warning":  WarningLevel,
   365  	"warn":     WarningLevel,
   366  	"error":    ErrorLevel,
   367  	"critical": CriticalLevel,
   368  	"off":      OffLevel,
   369  }
   370  
   371  var (
   372  	loggerLevelString = ""
   373  	reset             = false
   374  )
   375  
   376  func ztunnelLogLevel(level string) string {
   377  	switch level {
   378  	case "warning":
   379  		return "warn"
   380  	case "critical":
   381  		return "error"
   382  	default:
   383  		return level
   384  	}
   385  }
   386  
   387  func logCmd(ctx cli.Context) *cobra.Command {
   388  	common := new(commonFlags)
   389  
   390  	cmd := &cobra.Command{
   391  		Use:   "log [<type>/]<name>[.<namespace>]",
   392  		Short: "Retrieves logging levels of the Ztunnel instance in the specified pod.",
   393  		Long:  "Retrieve information about logging levels of the Ztunnel instance in the specified pod, and update optionally.",
   394  		Example: `  # Retrieve information about logging levels from all Ztunnel pods
   395   istioctl x ztunnel-config log
   396  
   397   # Update levels of the all loggers for a specific Ztunnel pod
   398   istioctl x ztunnel-config log <pod-name[.namespace]> --level off
   399  
   400   # Update levels of the specified loggers for all Ztunnl pods
   401   istioctl x ztunnel-config log --level access:debug,info
   402  
   403   # Reset levels of all the loggers to default value (warning)  for a specific Ztunnel pod.
   404   istioctl x ztunnel-config log <pod-name[.namespace]> -r
   405  `,
   406  		Aliases: []string{"o"},
   407  		Args: func(cmd *cobra.Command, args []string) error {
   408  			if err := common.validateArgs(cmd, args); err != nil {
   409  				return err
   410  			}
   411  			if reset && loggerLevelString != "" {
   412  				cmd.Println(cmd.UsageString())
   413  				return fmt.Errorf("--level cannot be combined with --reset")
   414  			}
   415  			if common.outputFormat != "" && common.outputFormat != summaryOutput {
   416  				return fmt.Errorf("--output is not applicable for this command")
   417  			}
   418  			return nil
   419  		},
   420  		RunE: func(c *cobra.Command, args []string) error {
   421  			kubeClient, err := ctx.CLIClient()
   422  			if err != nil {
   423  				return err
   424  			}
   425  			var podNames []string
   426  			var podNamespace string
   427  			if len(args) == 1 {
   428  				podName, ns, err := getComponentPodName(ctx, args[0])
   429  				if err != nil {
   430  					return err
   431  				}
   432  				ztunnelPod := ambientutil.IsZtunnelPod(kubeClient, podName, ns)
   433  				if !ztunnelPod {
   434  					return fmt.Errorf("workloads command is only supported by Ztunnel proxies: %v", podName)
   435  				}
   436  				podNames = []string{podName}
   437  				podNamespace = ns
   438  			} else {
   439  				var err error
   440  				podNames, podNamespace, err = ctx.InferPodsFromTypedResource("daemonset/ztunnel", ctx.IstioNamespace())
   441  				if err != nil {
   442  					return err
   443  				}
   444  			}
   445  
   446  			destLoggerLevels := map[string]Level{}
   447  			if reset {
   448  				log.Warn("log level reset; using default value \"info\" for Ztunnel")
   449  				loggerLevelString = "info"
   450  			} else if loggerLevelString != "" {
   451  				levels := strings.Split(loggerLevelString, ",")
   452  				for _, ol := range levels {
   453  					if !strings.Contains(ol, ":") && !strings.Contains(ol, "=") {
   454  						level, ok := stringToLevel[ol]
   455  						if ok {
   456  							destLoggerLevels = map[string]Level{
   457  								defaultLoggerName: level,
   458  							}
   459  						} else {
   460  							return fmt.Errorf("unrecognized logging level: %v", ol)
   461  						}
   462  					} else {
   463  						logParts := strings.Split(ol, "::") // account for any specified namespace
   464  						loggerAndLevelOnly := logParts[len(logParts)-1]
   465  						loggerLevel := regexp.MustCompile(`[:=]`).Split(loggerAndLevelOnly, 2)
   466  						// TODO validate ztunnel logger name when available: https://github.com/istio/ztunnel/issues/426
   467  						level, ok := stringToLevel[loggerLevel[1]]
   468  						if !ok {
   469  							return fmt.Errorf("unrecognized logging level: %v", loggerLevel[1])
   470  						}
   471  						destLoggerLevels[loggerLevel[0]] = level
   472  					}
   473  				}
   474  			}
   475  
   476  			var errs *multierror.Error
   477  			for _, podName := range podNames {
   478  				q := "level=" + ztunnelLogLevel(loggerLevelString)
   479  				if reset {
   480  					q += "&reset"
   481  				}
   482  				resp, err := setupZtunnelLogs(kubeClient, q, podName, podNamespace, common.proxyAdminPort)
   483  				if err == nil {
   484  					_, _ = fmt.Fprintf(c.OutOrStdout(), "%v.%v:\n%v\n", podName, podNamespace, resp)
   485  				} else {
   486  					errs = multierror.Append(fmt.Errorf("%v.%v: %v", podName, podNamespace, err))
   487  				}
   488  			}
   489  			if err := multierror.Flatten(errs.ErrorOrNil()); err != nil {
   490  				return err
   491  			}
   492  			return nil
   493  		},
   494  		ValidArgsFunction: completion.ValidPodsNameArgs(ctx),
   495  	}
   496  
   497  	levelListString := fmt.Sprintf("[%s, %s, %s, %s, %s, %s, %s]",
   498  		levelToString[TraceLevel],
   499  		levelToString[DebugLevel],
   500  		levelToString[InfoLevel],
   501  		levelToString[WarningLevel],
   502  		levelToString[ErrorLevel],
   503  		levelToString[CriticalLevel],
   504  		levelToString[OffLevel])
   505  	common.attach(cmd)
   506  	cmd.PersistentFlags().BoolVarP(&reset, "reset", "r", reset, "Reset levels to default value (warning).")
   507  	cmd.PersistentFlags().StringVar(&loggerLevelString, "level", loggerLevelString,
   508  		fmt.Sprintf("Comma-separated minimum per-logger level of messages to output, in the form of"+
   509  			" [<logger>:]<level>,[<logger>:]<level>,... or <level> to change all active loggers, "+
   510  			"where logger components can be listed by running \"istioctl x ztunnel-config log <pod-name[.namespace]>\""+
   511  			", and level can be one of %s", levelListString))
   512  	return cmd
   513  }
   514  
   515  func setupZtunnelLogs(kubeClient kube.CLIClient, param, podName, podNamespace string, port int) (string, error) {
   516  	path := "logging"
   517  	if param != "" {
   518  		path = path + "?" + param
   519  	}
   520  	// "Envoy" applies despite this being ztunnel
   521  	result, err := kubeClient.EnvoyDoWithPort(context.TODO(), podName, podNamespace, "POST", path, port)
   522  	if err != nil {
   523  		return "", fmt.Errorf("failed to execute command on Ztunnel: %v", err)
   524  	}
   525  	return string(result), nil
   526  }
   527  
   528  // getComponentPodName returns the pod name and namespace of the Istio component
   529  func getComponentPodName(ctx cli.Context, podflag string) (string, string, error) {
   530  	return getPodNameWithNamespace(ctx, podflag, ctx.IstioNamespace())
   531  }
   532  
   533  func getPodNameWithNamespace(ctx cli.Context, podflag, ns string) (string, string, error) {
   534  	var podName, podNamespace string
   535  	podName, podNamespace, err := ctx.InferPodInfoFromTypedResource(podflag, ns)
   536  	if err != nil {
   537  		return "", "", err
   538  	}
   539  	return podName, podNamespace, nil
   540  }
   541  
   542  func setupZtunnelConfigDumpWriter(kubeClient kube.CLIClient, podName, podNamespace string, out io.Writer) (*ztunnelDump.ConfigWriter, error) {
   543  	debug, err := extractZtunnelConfigDump(kubeClient, podName, podNamespace)
   544  	if err != nil {
   545  		return nil, err
   546  	}
   547  	return setupConfigdumpZtunnelConfigWriter(debug, out)
   548  }
   549  
   550  func readFile(filename string) ([]byte, error) {
   551  	file := os.Stdin
   552  	if filename != "-" {
   553  		var err error
   554  		file, err = os.Open(filename)
   555  		if err != nil {
   556  			return nil, err
   557  		}
   558  	}
   559  	defer func() {
   560  		if err := file.Close(); err != nil {
   561  			log.Errorf("failed to close %s: %s", filename, err)
   562  		}
   563  	}()
   564  	return io.ReadAll(file)
   565  }
   566  
   567  func extractZtunnelConfigDump(kubeClient kube.CLIClient, podName, podNamespace string) ([]byte, error) {
   568  	path := "config_dump"
   569  	debug, err := kubeClient.EnvoyDoWithPort(context.TODO(), podName, podNamespace, "GET", path, 15000)
   570  	if err != nil {
   571  		return nil, fmt.Errorf("failed to execute command on %s.%s Ztunnel: %v", podName, podNamespace, err)
   572  	}
   573  	return debug, err
   574  }
   575  
   576  func setupConfigdumpZtunnelConfigWriter(debug []byte, out io.Writer) (*ztunnelDump.ConfigWriter, error) {
   577  	cw := &ztunnelDump.ConfigWriter{Stdout: out, FullDump: debug}
   578  	err := cw.Prime(debug)
   579  	if err != nil {
   580  		return nil, err
   581  	}
   582  	return cw, nil
   583  }
   584  
   585  func setupFileZtunnelConfigdumpWriter(filename string, out io.Writer) (*ztunnelDump.ConfigWriter, error) {
   586  	data, err := readFile(filename)
   587  	if err != nil {
   588  		return nil, err
   589  	}
   590  	return setupConfigdumpZtunnelConfigWriter(data, out)
   591  }
   592  
   593  func runConfigDump(ctx cli.Context, common *commonFlags, f func(cw *ztunnelDump.ConfigWriter) error) func(c *cobra.Command, args []string) error {
   594  	return func(c *cobra.Command, args []string) error {
   595  		var podName, podNamespace string
   596  		kubeClient, err := ctx.CLIClient()
   597  		if err != nil {
   598  			return err
   599  		}
   600  		var configWriter *ztunnelDump.ConfigWriter
   601  		if common.configDumpFile != "" {
   602  			configWriter, err = setupFileZtunnelConfigdumpWriter(common.configDumpFile, c.OutOrStdout())
   603  		} else {
   604  			lookup := "daemonset/ztunnel"
   605  			if len(args) > 0 {
   606  				lookup = args[0]
   607  			}
   608  			if common.node != "" {
   609  				nsn, err := PodOnNodeFromDaemonset(common.node, "ztunnel", ctx.IstioNamespace(), kubeClient)
   610  				if err != nil {
   611  					return err
   612  				}
   613  				podName, podNamespace = nsn.Name, nsn.Namespace
   614  			} else {
   615  				if podName, podNamespace, err = getComponentPodName(ctx, lookup); err != nil {
   616  					return err
   617  				}
   618  			}
   619  			ztunnelPod := ambientutil.IsZtunnelPod(kubeClient, podName, podNamespace)
   620  			if !ztunnelPod {
   621  				return fmt.Errorf("workloads command is only supported by Ztunnel proxies: %v", podName)
   622  			}
   623  			configWriter, err = setupZtunnelConfigDumpWriter(kubeClient, podName, podNamespace, c.OutOrStdout())
   624  		}
   625  		if err != nil {
   626  			return err
   627  		}
   628  		return f(configWriter)
   629  	}
   630  }
   631  
   632  func PodOnNodeFromDaemonset(node string, name, namespace string, client kube.Client) (types.NamespacedName, error) {
   633  	ds, err := client.Kube().AppsV1().DaemonSets(namespace).Get(context.Background(), name, metav1.GetOptions{})
   634  	if err != nil {
   635  		return types.NamespacedName{}, err
   636  	}
   637  	selector := ds.Spec.Selector
   638  	if selector == nil {
   639  		return types.NamespacedName{}, fmt.Errorf("selector is required")
   640  	}
   641  
   642  	sel := selector.MatchLabels
   643  	kv := []string{}
   644  	for k, v := range sel {
   645  		kv = append(kv, k+"="+v)
   646  	}
   647  	podsr, err := client.Kube().CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{
   648  		TypeMeta:      metav1.TypeMeta{},
   649  		LabelSelector: strings.Join(kv, ","),
   650  		FieldSelector: "spec.nodeName=" + node,
   651  	})
   652  	if err != nil {
   653  		return types.NamespacedName{}, err
   654  	}
   655  	pods := slices.Reference(podsr.Items)
   656  	if len(pods) > 0 {
   657  		// We need to pass in a sorter, and the one used by `kubectl logs` is good enough.
   658  		sortBy := func(pods []*corev1.Pod) sort.Interface { return podutils.ByLogging(pods) }
   659  		sort.Sort(sortBy(pods))
   660  		return config.NamespacedName(pods[0]), nil
   661  	}
   662  	return types.NamespacedName{}, fmt.Errorf("no pods found")
   663  }
   664  
   665  type commonFlags struct {
   666  	// output format (json, yaml or short)
   667  	outputFormat string
   668  
   669  	proxyAdminPort int
   670  
   671  	configDumpFile string
   672  
   673  	node string
   674  }
   675  
   676  func (c *commonFlags) attach(cmd *cobra.Command) {
   677  	cmd.PersistentFlags().IntVar(&c.proxyAdminPort, "proxy-admin-port", defaultProxyAdminPort, "Ztunnel proxy admin port")
   678  	cmd.PersistentFlags().StringVarP(&c.outputFormat, "output", "o", summaryOutput, "Output format: one of json|yaml|short")
   679  	cmd.PersistentFlags().StringVar(&c.node, "node", "", "Filter workloads by node field")
   680  	cmd.PersistentFlags().StringVarP(&c.configDumpFile, "file", "f", "",
   681  		"Ztunnel config dump JSON file")
   682  }
   683  
   684  func (c *commonFlags) validateArgs(cmd *cobra.Command, args []string) error {
   685  	set := 0
   686  	if c.configDumpFile != "" {
   687  		set++
   688  	}
   689  	if len(args) == 1 {
   690  		set++
   691  	}
   692  	if c.node != "" {
   693  		set++
   694  	}
   695  	if set > 1 {
   696  		cmd.Println(cmd.UsageString())
   697  		return fmt.Errorf("at most one of --file, --node, or pod name must be passed")
   698  	}
   699  	return nil
   700  }