github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/cli/cmd/genyaml.go (about)

     1  package cmd
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"os"
     7  	"strings"
     8  
     9  	"github.com/spf13/cobra"
    10  	"github.com/spf13/pflag"
    11  	apps "k8s.io/api/apps/v1"
    12  	core "k8s.io/api/core/v1"
    13  	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	"k8s.io/apimachinery/pkg/runtime"
    15  	"k8s.io/apimachinery/pkg/runtime/schema"
    16  	"k8s.io/apimachinery/pkg/runtime/serializer"
    17  	"k8s.io/cli-runtime/pkg/genericclioptions"
    18  	"k8s.io/client-go/kubernetes"
    19  	"sigs.k8s.io/yaml"
    20  
    21  	"github.com/datawire/k8sapi/pkg/k8sapi"
    22  	"github.com/telepresenceio/telepresence/v2/pkg/agentconfig"
    23  	"github.com/telepresenceio/telepresence/v2/pkg/agentmap"
    24  	"github.com/telepresenceio/telepresence/v2/pkg/client"
    25  	"github.com/telepresenceio/telepresence/v2/pkg/client/cli/flags"
    26  	"github.com/telepresenceio/telepresence/v2/pkg/errcat"
    27  	"github.com/telepresenceio/telepresence/v2/pkg/tracing"
    28  )
    29  
    30  type genYAMLCommand struct {
    31  	outputFile   string
    32  	inputFile    string
    33  	configFile   string
    34  	workloadName string
    35  	namespace    string
    36  }
    37  
    38  func genYAML() *cobra.Command {
    39  	info := genYAMLCommand{}
    40  	cmd := &cobra.Command{
    41  		Use:  "genyaml",
    42  		Args: cobra.NoArgs,
    43  
    44  		Short: "Generate YAML for use in kubernetes manifests.",
    45  		Long: `Generate traffic-agent yaml for use in kubernetes manifests.
    46  This allows the traffic agent to be injected by hand into existing kubernetes manifests.
    47  For your modified workload to be valid, you'll have to manually inject a container and a
    48  volume into the workload, and a corresponding configmap entry into the "telelepresence-agents"
    49  configmap; you can do this by running "genyaml config", "genyaml container", and "genyaml volume".
    50  
    51  NOTE: It is recommended that you not do this unless strictly necessary. Instead, we suggest letting
    52  telepresence's webhook injector configure the traffic agents on demand.`,
    53  		RunE: func(_ *cobra.Command, _ []string) error {
    54  			return errcat.User.New("please run genyaml as \"genyaml config\", \"genyaml container\", \"genyaml initcontainer\", or \"genyaml volume\"")
    55  		},
    56  	}
    57  	flags := cmd.PersistentFlags()
    58  	flags.StringVarP(&info.outputFile, "output", "o", "-",
    59  		"Path to the file to place the output in. Defaults to '-' which means stdout.")
    60  	cmd.AddCommand(
    61  		genConfigMapSubCommand(&info),
    62  		genContainerSubCommand(&info),
    63  		genInitContainerSubCommand(&info),
    64  		genVolumeSubCommand(&info),
    65  	)
    66  	return cmd
    67  }
    68  
    69  func getInput(inputFile string) ([]byte, error) {
    70  	var f io.ReadCloser
    71  	if inputFile == "-" {
    72  		f = os.Stdin
    73  	} else {
    74  		var err error
    75  		if f, err = os.Open(inputFile); err != nil {
    76  			return nil, errcat.User.Newf("unable to open input file %q: %w", inputFile, err)
    77  		}
    78  		defer f.Close()
    79  	}
    80  	b, err := io.ReadAll(f)
    81  	if err != nil {
    82  		return nil, errcat.User.Newf("error reading from %s: %w", inputFile, err)
    83  	}
    84  	return b, nil
    85  }
    86  
    87  func (i *genYAMLCommand) getOutputWriter() (io.WriteCloser, error) {
    88  	if i.outputFile == "-" {
    89  		return os.Stdout, nil
    90  	}
    91  	f, err := os.Create(i.outputFile)
    92  	if err != nil {
    93  		return nil, errcat.User.Newf("unable to open output file %s: %w", i.outputFile, err)
    94  	}
    95  	return f, nil
    96  }
    97  
    98  func (i *genYAMLCommand) loadConfigMapEntry(ctx context.Context) (*agentconfig.Sidecar, error) {
    99  	if i.configFile != "" {
   100  		b, err := getInput(i.configFile)
   101  		if err != nil {
   102  			return nil, err
   103  		}
   104  		var cfg agentconfig.Sidecar
   105  		if err = yaml.Unmarshal(b, &cfg); err != nil {
   106  			return nil, errcat.User.Newf("unable to parse config %s: %w", i.configFile, err)
   107  		}
   108  		return &cfg, nil
   109  	}
   110  	if i.workloadName == "" {
   111  		return nil, errcat.User.New("either --config or --workload must be provided")
   112  	}
   113  
   114  	// Load configmap entry from the telepresence-agents configmap
   115  	cm, err := k8sapi.GetK8sInterface(ctx).CoreV1().ConfigMaps(i.namespace).Get(ctx, agentconfig.ConfigMap, meta.GetOptions{})
   116  	if err != nil {
   117  		return nil, errcat.User.New(err)
   118  	}
   119  	var yml string
   120  	ok := false
   121  	if cm.Data != nil {
   122  		yml, ok = cm.Data[i.workloadName]
   123  	}
   124  	if !ok {
   125  		return nil, errcat.User.Newf("Unable to load entry for %q in configmap %q: %w", i.workloadName, agentconfig.ConfigMap, err)
   126  	}
   127  	var cfg agentconfig.Sidecar
   128  	if err = yaml.Unmarshal([]byte(yml), &cfg); err != nil {
   129  		return nil, errcat.User.Newf("Unable to parse entry for %q in configmap %q: %w", i.workloadName, agentconfig.ConfigMap, err)
   130  	}
   131  	return &cfg, nil
   132  }
   133  
   134  func (i *genYAMLCommand) loadWorkload(ctx context.Context) (k8sapi.Workload, error) {
   135  	if i.inputFile == "" {
   136  		if i.workloadName == "" {
   137  			return nil, errcat.User.New("either --input or --workload must be provided")
   138  		}
   139  		return tracing.GetWorkload(ctx, i.workloadName, i.namespace, "")
   140  	}
   141  	b, err := getInput(i.inputFile)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  
   146  	scheme := runtime.NewScheme()
   147  	scheme.AddKnownTypes(schema.GroupVersion{Group: apps.GroupName, Version: "v1"}, &apps.StatefulSet{}, &apps.Deployment{}, &apps.ReplicaSet{})
   148  	codecFactory := serializer.NewCodecFactory(scheme)
   149  	deserializer := codecFactory.UniversalDeserializer()
   150  
   151  	obj, kind, err := deserializer.Decode(b, nil, nil)
   152  	if err != nil {
   153  		return nil, errcat.User.Newf("unable to parse yaml in %s: %w", i.inputFile, err)
   154  	}
   155  	wl, err := k8sapi.WrapWorkload(obj)
   156  	if err != nil {
   157  		return nil, errcat.User.Newf("unexpected object of kind %s; please pass in a Deployment, ReplicaSet, or StatefulSet", kind)
   158  	}
   159  	if wl.GetNamespace() == "" {
   160  		if d, ok := k8sapi.DeploymentImpl(wl); ok {
   161  			d.Namespace = i.namespace
   162  		} else if r, ok := k8sapi.ReplicaSetImpl(wl); ok {
   163  			r.Namespace = i.namespace
   164  		} else if s, ok := k8sapi.StatefulSetImpl(wl); ok {
   165  			s.Namespace = i.namespace
   166  		}
   167  	}
   168  	return wl, nil
   169  }
   170  
   171  func (i *genYAMLCommand) writeObjToOutput(obj any) error {
   172  	// We use sigs.ks8.io/yaml because it treats json serialization tags as if they were yaml tags.
   173  	doc, err := yaml.Marshal(obj)
   174  	if err != nil {
   175  		return errcat.User.Newf("unable to marshal agent container: %w", err)
   176  	}
   177  	w, err := i.getOutputWriter()
   178  	if err != nil {
   179  		return err
   180  	}
   181  	defer w.Close()
   182  	_, err = w.Write(doc)
   183  	if err != nil {
   184  		return errcat.User.Newf("unable to write to output %s: %w", i.outputFile, err)
   185  	}
   186  	return nil
   187  }
   188  
   189  func (i *genYAMLCommand) withK8sInterface(ctx context.Context, flagMap map[string]string) (context.Context, error) {
   190  	configFlags := genericclioptions.NewConfigFlags(false)
   191  	flags := pflag.NewFlagSet("", 0)
   192  	configFlags.AddFlags(flags)
   193  	for k, v := range flagMap {
   194  		if err := flags.Set(k, v); err != nil {
   195  			return nil, errcat.User.Newf("error processing kubectl flag --%s=%s: %w", k, v, err)
   196  		}
   197  	}
   198  
   199  	configLoader := configFlags.ToRawKubeConfigLoader()
   200  	restConfig, err := configLoader.ClientConfig()
   201  	if err != nil {
   202  		return nil, errcat.Config.New(err)
   203  	}
   204  
   205  	config, err := configLoader.RawConfig()
   206  	if err != nil {
   207  		return nil, errcat.Config.New(err)
   208  	}
   209  	if len(config.Contexts) == 0 {
   210  		return nil, errcat.Config.New("kubeconfig has no context definition")
   211  	}
   212  
   213  	ctxName := flagMap["context"]
   214  	if ctxName == "" {
   215  		ctxName = config.CurrentContext
   216  	}
   217  	c, ok := config.Contexts[ctxName]
   218  	if !ok {
   219  		return nil, errcat.Config.Newf("context %q does not exist in the kubeconfig", ctxName)
   220  	}
   221  	i.namespace = flagMap["namespace"]
   222  	if i.namespace == "" {
   223  		i.namespace = c.Namespace
   224  		if i.namespace == "" {
   225  			i.namespace = "default"
   226  		}
   227  	}
   228  	cs, err := kubernetes.NewForConfig(restConfig)
   229  	if err == nil {
   230  		ctx = k8sapi.WithK8sInterface(ctx, cs)
   231  	}
   232  	return ctx, err
   233  }
   234  
   235  type genConfigMap struct {
   236  	agentmap.BasicGeneratorConfig
   237  	*genYAMLCommand
   238  }
   239  
   240  func allKubeFlags() *pflag.FlagSet {
   241  	kubeFlags := pflag.NewFlagSet("Kubernetes flags", 0)
   242  	kubeConfig := genericclioptions.NewConfigFlags(false)
   243  	kubeConfig.AddFlags(kubeFlags)
   244  	return kubeFlags
   245  }
   246  
   247  func genConfigMapSubCommand(yamlInfo *genYAMLCommand) *cobra.Command {
   248  	kubeFlags := allKubeFlags()
   249  	info := genConfigMap{genYAMLCommand: yamlInfo}
   250  	cmd := &cobra.Command{
   251  		Use:   "config",
   252  		Args:  cobra.NoArgs,
   253  		Short: "Generate YAML for the agent's entry in the telepresence-agents configmap.",
   254  		Long:  "Generate YAML for the agent's entry in the telepresence-agents configmap. See genyaml for more info on what this means",
   255  		RunE: func(cmd *cobra.Command, args []string) error {
   256  			return info.run(cmd, flags.Map(kubeFlags))
   257  		},
   258  	}
   259  
   260  	flags := cmd.Flags()
   261  	flags.StringVarP(&info.inputFile, "input", "i", "",
   262  		"Path to the yaml containing the workload definition (i.e. Deployment, StatefulSet, etc). Pass '-' for stdin.. Mutually exclusive to --workload")
   263  	flags.StringVarP(&info.workloadName, "workload", "w", "",
   264  		"Name of the workload. If given, the workload will be retrieved from the cluster, mutually exclusive to --input")
   265  	flags.Uint16Var(&info.AgentPort, "agent-port", 9900,
   266  		"The port number you wish the agent to listen on.")
   267  	flags.StringVar(&info.QualifiedAgentImage, "agent-image", "docker.io/datawire/tel2:"+strings.TrimPrefix(client.Version(), "v"),
   268  		`The qualified name of the agent image`)
   269  	flags.Uint16Var(&info.ManagerPort, "manager-port", 8081,
   270  		`The traffic-manager API port`)
   271  	flags.StringVar(&info.ManagerNamespace, "manager-namespace", "ambassador",
   272  		`The traffic-manager namespace`)
   273  	flags.StringVar(&info.LogLevel, "loglevel", "info",
   274  		`The loglevel for the generated traffic-agent sidecar`)
   275  	flags.AddFlagSet(kubeFlags)
   276  	return cmd
   277  }
   278  
   279  func (i *genConfigMap) generateConfigMap(ctx context.Context, wl k8sapi.Workload) (*agentconfig.Sidecar, error) {
   280  	ac, err := i.BasicGeneratorConfig.Generate(ctx, wl, nil)
   281  	if err != nil {
   282  		return nil, errcat.NoDaemonLogs.New(err)
   283  	}
   284  	return ac.AgentConfig(), nil
   285  }
   286  
   287  func (g *genConfigMap) run(cmd *cobra.Command, kubeFlags map[string]string) error {
   288  	ctx, err := g.withK8sInterface(cmd.Context(), kubeFlags)
   289  	if err != nil {
   290  		return err
   291  	}
   292  
   293  	wl, err := g.loadWorkload(ctx)
   294  	if err != nil {
   295  		return err
   296  	}
   297  
   298  	cfg, err := g.generateConfigMap(ctx, wl)
   299  	if err != nil {
   300  		return err
   301  	}
   302  	cfg.Manual = true
   303  	return g.writeObjToOutput(cfg)
   304  }
   305  
   306  type genContainerInfo struct {
   307  	*genYAMLCommand
   308  }
   309  
   310  func genContainerSubCommand(yamlInfo *genYAMLCommand) *cobra.Command {
   311  	kubeFlags := allKubeFlags()
   312  	info := genContainerInfo{genYAMLCommand: yamlInfo}
   313  	cmd := &cobra.Command{
   314  		Use:   "container",
   315  		Args:  cobra.NoArgs,
   316  		Short: "Generate YAML for the traffic-agent container.",
   317  		Long:  "Generate YAML for the traffic-agent container. See genyaml for more info on what this means",
   318  		RunE: func(cmd *cobra.Command, args []string) error {
   319  			return info.run(cmd, flags.Map(kubeFlags))
   320  		},
   321  	}
   322  	flags := cmd.Flags()
   323  	flags.StringVarP(&info.inputFile, "input", "i", "",
   324  		"Optional path to the yaml containing the workload definition (i.e. Deployment, StatefulSet, etc). Pass '-' for stdin. Loaded from cluster by default")
   325  	flags.StringVarP(&info.workloadName, "workload", "w", "",
   326  		"Name of the workload. If given, the configmap entry will be retrieved telepresence-agents configmap, mutually exclusive to --config")
   327  	flags.StringVarP(&info.configFile, "config", "c", "", "Path to the yaml containing the generated configmap entry, mutually exclusive to --workload")
   328  	flags.AddFlagSet(kubeFlags)
   329  	return cmd
   330  }
   331  
   332  func (g *genContainerInfo) run(cmd *cobra.Command, kubeFlags map[string]string) error {
   333  	ctx, err := g.withK8sInterface(cmd.Context(), kubeFlags)
   334  	if err != nil {
   335  		return err
   336  	}
   337  
   338  	cm, err := g.loadConfigMapEntry(ctx)
   339  	if err != nil {
   340  		return err
   341  	}
   342  	if g.inputFile == "" {
   343  		g.workloadName = cm.WorkloadName
   344  	}
   345  
   346  	wl, err := g.loadWorkload(ctx)
   347  	if err != nil {
   348  		return err
   349  	}
   350  
   351  	// Sanity check
   352  	if wl.GetName() != cm.WorkloadName {
   353  		return errcat.User.Newf("name %q of loaded workload is different from %q loaded configmap entry", wl.GetName(), cm.WorkloadName)
   354  	}
   355  	if wl.GetKind() != cm.WorkloadKind {
   356  		return errcat.User.Newf("kind %q of loaded workload is different from %q loaded configmap entry", wl.GetKind(), cm.WorkloadKind)
   357  	}
   358  
   359  	podTpl := wl.GetPodTemplate()
   360  	agentContainer := agentconfig.AgentContainer(
   361  		ctx,
   362  		&core.Pod{
   363  			TypeMeta: meta.TypeMeta{
   364  				Kind:       "pod",
   365  				APIVersion: "v1",
   366  			},
   367  			ObjectMeta: podTpl.ObjectMeta,
   368  			Spec:       podTpl.Spec,
   369  		},
   370  		cm,
   371  	)
   372  	return g.writeObjToOutput(agentContainer)
   373  }
   374  
   375  type genInitContainerInfo struct {
   376  	*genYAMLCommand
   377  }
   378  
   379  func genInitContainerSubCommand(yamlInfo *genYAMLCommand) *cobra.Command {
   380  	kubeFlags := allKubeFlags()
   381  	info := genInitContainerInfo{genYAMLCommand: yamlInfo}
   382  	cmd := &cobra.Command{
   383  		Use:   "initcontainer",
   384  		Args:  cobra.NoArgs,
   385  		Short: "Generate YAML for the traffic-agent init container.",
   386  		Long:  "Generate YAML for the traffic-agent init container. See genyaml for more info on what this means",
   387  		RunE: func(cmd *cobra.Command, args []string) error {
   388  			return info.run(cmd, flags.Map(kubeFlags))
   389  		},
   390  	}
   391  	flags := cmd.Flags()
   392  	flags.StringVarP(&info.workloadName, "workload", "w", "",
   393  		"Name of the workload. If given, the configmap entry will be retrieved telepresence-agents configmap, mutually exclusive to --config")
   394  	flags.StringVarP(&info.configFile, "config", "c", "", "Path to the yaml containing the generated configmap entry, mutually exclusive to --workload")
   395  	flags.AddFlagSet(kubeFlags)
   396  	return cmd
   397  }
   398  
   399  func (g *genInitContainerInfo) run(cmd *cobra.Command, kubeFlags map[string]string) error {
   400  	ctx, err := g.withK8sInterface(cmd.Context(), kubeFlags)
   401  	if err != nil {
   402  		return err
   403  	}
   404  
   405  	cm, err := g.loadConfigMapEntry(ctx)
   406  	if err != nil {
   407  		return err
   408  	}
   409  
   410  	for _, cc := range cm.Containers {
   411  		for _, ic := range cc.Intercepts {
   412  			if ic.Headless || ic.TargetPortNumeric {
   413  				return g.writeObjToOutput(agentconfig.InitContainer(cm))
   414  			}
   415  		}
   416  	}
   417  	return errcat.User.New("deployment does not need an init container")
   418  }
   419  
   420  type genVolumeInfo struct {
   421  	*genYAMLCommand
   422  }
   423  
   424  func genVolumeSubCommand(yamlInfo *genYAMLCommand) *cobra.Command {
   425  	info := genVolumeInfo{genYAMLCommand: yamlInfo}
   426  	kubeFlags := allKubeFlags()
   427  	cmd := &cobra.Command{
   428  		Use:   "volume",
   429  		Args:  cobra.NoArgs,
   430  		Short: "Generate YAML for the traffic-agent volume.",
   431  		Long:  "Generate YAML for the traffic-agent volume. See genyaml for more info on what this means",
   432  		RunE: func(cmd *cobra.Command, args []string) error {
   433  			return info.run(cmd, flags.Map(kubeFlags))
   434  		},
   435  	}
   436  	flags := cmd.Flags()
   437  	flags.StringVarP(&info.inputFile, "input", "i", "",
   438  		"Optional path to the yaml containing the workload definition (i.e. Deployment, StatefulSet, etc). Pass '-' for stdin. Loaded from cluster by default")
   439  	flags.StringVarP(&info.workloadName, "workload", "w", "",
   440  		"Name of the workload. If given, the configmap entry will be retrieved telepresence-agents configmap, mutually exclusive to --config")
   441  	flags.StringVarP(&info.configFile, "config", "c", "", "Path to the yaml containing the generated configmap entry, mutually exclusive to --workload")
   442  	flags.AddFlagSet(kubeFlags)
   443  	return cmd
   444  }
   445  
   446  func (g *genVolumeInfo) run(cmd *cobra.Command, kubeFlags map[string]string) error {
   447  	ctx, err := g.withK8sInterface(cmd.Context(), kubeFlags)
   448  	if err != nil {
   449  		return err
   450  	}
   451  
   452  	cm, err := g.loadConfigMapEntry(ctx)
   453  	if err != nil {
   454  		return err
   455  	}
   456  	if g.inputFile == "" {
   457  		g.workloadName = cm.WorkloadName
   458  	}
   459  
   460  	wl, err := g.loadWorkload(ctx)
   461  	if err != nil {
   462  		return err
   463  	}
   464  
   465  	// Sanity check
   466  	if wl.GetName() != cm.WorkloadName {
   467  		return errcat.User.Newf("name %q of loaded workload is different from %q loaded configmap entry", wl.GetName(), cm.WorkloadName)
   468  	}
   469  	if wl.GetKind() != cm.WorkloadKind {
   470  		return errcat.User.Newf("kind %q of loaded workload is different from %q loaded configmap entry", wl.GetKind(), cm.WorkloadKind)
   471  	}
   472  
   473  	podTpl := wl.GetPodTemplate()
   474  
   475  	if g.workloadName == "" {
   476  		g.workloadName = wl.GetName()
   477  	}
   478  
   479  	volumes := agentconfig.AgentVolumes(g.workloadName, &core.Pod{
   480  		TypeMeta: meta.TypeMeta{
   481  			Kind:       "pod",
   482  			APIVersion: "v1",
   483  		},
   484  		ObjectMeta: podTpl.ObjectMeta,
   485  		Spec:       podTpl.Spec,
   486  	})
   487  
   488  	return g.writeObjToOutput(&volumes)
   489  }