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 }