This repository was archived by the owner on Jul 10, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 38
Add kube inject command #206
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
4ff1655
Update gomod
versilis a0242fb
Add injector
versilis d2ae573
Update resources yaml for testing
versilis 642c84d
Add helper for parsing to YAML
versilis 1b37a2c
Add kube inject command
versilis 8184be8
Fix formatting of info logs
versilis 105e84e
Merge remote-tracking branch 'origin/versilis/kube' into versilis/kub…
versilis 6f13071
Apply documentation suggestions raised during code review
versilis ac9a76a
Update cmd/internal/kube/inject.go
versilis 7a50cd1
Replace write call with assignment
versilis d81c238
Fix help description for secret flag
versilis b6ad9d1
Add better context on flag validation check
versilis 47d540c
Add better flag documentation
versilis 036536b
Fix typo
versilis 12b58e4
Update cmd/internal/kube/inject.go
versilis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,247 @@ | ||
| package kube | ||
|
|
||
| import ( | ||
| "bytes" | ||
|
|
||
| "github.com/akitasoftware/akita-cli/cmd/internal/cmderr" | ||
| "github.com/akitasoftware/akita-cli/cmd/internal/kube/injector" | ||
| "github.com/akitasoftware/akita-cli/printer" | ||
| "github.com/akitasoftware/akita-cli/telemetry" | ||
| "github.com/akitasoftware/go-utils/optionals" | ||
| "github.com/pkg/errors" | ||
| "github.com/spf13/cobra" | ||
| v1 "k8s.io/api/core/v1" | ||
| ) | ||
|
|
||
| var ( | ||
| // The target Yaml faile to be injected | ||
| // This is required for execution of injectCmd | ||
| injectFileNameFlag string | ||
| // The output file to write the injected Yaml to | ||
| // If not set, injectCmd will default to printing the output to stdout | ||
| injectOutputFlag string | ||
| // The name of the project that the injected deployments should be associated with | ||
| // This will be used by the agent to determine which Akita service to report traffic to | ||
| projectNameFlag string | ||
| // Represents the options for generating a secret | ||
| // When set to "false" or left empty, injectCmd will not generate a secret | ||
| // When set to "true", injectCmd will prepend a secret to each injectable namespace found in the file to inject (injectFileNameFlag) | ||
| // Otherwise, injectCmd will treat secretInjectFlag as the file path all secrets should be generated to | ||
| secretInjectFlag string | ||
| ) | ||
|
|
||
| var injectCmd = &cobra.Command{ | ||
| Use: "inject", | ||
| Short: "Inject Akita into a Kubernetes deployment", | ||
| Long: "Inject Akita into a Kubernetes deployment or set of deployments, and output the result to stdout or a file", | ||
| RunE: func(_ *cobra.Command, args []string) error { | ||
| secretOpts := resolveSecretGenerationOptions(secretInjectFlag) | ||
|
|
||
| // To avoid users unintentionally attempting to apply injected Deployments via pipeline without | ||
| // their dependent Secrets, require that the user explicitly specify an output file. | ||
| if secretOpts.ShouldInject && secretOpts.Filepath.IsSome() && injectOutputFlag == "" { | ||
| printer.Errorln("Cannot specify a Secret file path without an output file (using --output or -o)") | ||
| printer.Infoln("To generate a Secret file on its own, use `akita kube secret`") | ||
| return cmderr.AkitaErr{ | ||
| Err: errors.New("invalid flag usage"), | ||
| } | ||
| } | ||
|
|
||
| // Create the injector which reads from the Kubernetes YAML file specified by the user | ||
| injectr, err := injector.FromYAML(injectFileNameFlag) | ||
| if err != nil { | ||
| return cmderr.AkitaErr{ | ||
| Err: errors.Wrapf( | ||
| err, | ||
| "Failed to read injection file %s", | ||
| injectFileNameFlag, | ||
| ), | ||
| } | ||
| } | ||
|
|
||
| // Generate a secret for each namespace in the deployment if the user specified secret generation | ||
| secretBuf := new(bytes.Buffer) | ||
| if secretOpts.ShouldInject { | ||
| key, secret, err := cmderr.RequireAPICredentials("API credentials are required to generate secret.") | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| namespaces, err := injectr.InjectableNamespaces() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| for _, namespace := range namespaces { | ||
| r, err := handleSecretGeneration(namespace, key, secret) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| secretBuf.WriteString("---\n") | ||
| secretBuf.Write(r) | ||
| } | ||
| } | ||
|
|
||
| // Create the output buffer | ||
| out := new(bytes.Buffer) | ||
|
|
||
| // Either write the secret to a file or prepend it to the output | ||
| if secretFilePath, exists := secretOpts.Filepath.Get(); exists { | ||
| err = writeFile(secretBuf.Bytes(), secretFilePath) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| printer.Infof("Kubernetes Secret generated to %s\n", secretFilePath) | ||
| } else { | ||
| // Assign the secret to the output buffer | ||
| // We do this so that the secret is written before any injected Deployment resources that depend on it | ||
| out = secretBuf | ||
| } | ||
|
|
||
| // Inject the sidecar into the input file | ||
| rawInjected, err := injector.ToRawYAML(injectr, createSidecar(projectNameFlag)) | ||
| if err != nil { | ||
| return cmderr.AkitaErr{Err: errors.Wrap(err, "Failed to inject sidecars")} | ||
| } | ||
| // Append the injected YAML to the output | ||
| out.Write(rawInjected) | ||
versilis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // If the user did not specify an output file, print the output to stdout | ||
| if injectOutputFlag == "" { | ||
| printer.Stdout.RawOutput(out.String()) | ||
| return nil | ||
| } | ||
|
|
||
| // Write the output to the specified file | ||
| if err := writeFile(out.Bytes(), injectOutputFlag); err != nil { | ||
| return err | ||
| } | ||
| printer.Infof("Injected YAML written to %s\n", injectOutputFlag) | ||
|
|
||
| return nil | ||
| }, | ||
| PersistentPreRun: func(cmd *cobra.Command, args []string) { | ||
| // Initialize the telemetry client, but do not allow any logs to be printed | ||
| telemetry.Init(false) | ||
| }, | ||
| } | ||
|
|
||
| // A parsed representation of the `--secret` option. | ||
| type secretGenerationOptions struct { | ||
versilis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Whether to inject a secret | ||
| ShouldInject bool | ||
| // The path to the secret file | ||
| Filepath optionals.Optional[string] | ||
| } | ||
|
|
||
| func createSidecar(projectName string) v1.Container { | ||
| sidecar := v1.Container{ | ||
| Name: "akita", | ||
| Image: "akitasoftware/cli:latest", | ||
| Env: []v1.EnvVar{ | ||
| { | ||
| Name: "AKITA_API_KEY_ID", | ||
| ValueFrom: &v1.EnvVarSource{ | ||
| SecretKeyRef: &v1.SecretKeySelector{ | ||
| LocalObjectReference: v1.LocalObjectReference{ | ||
| Name: "akita-secrets", | ||
| }, | ||
| Key: "akita-api-key", | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| Name: "AKITA_API_KEY_SECRET", | ||
| ValueFrom: &v1.EnvVarSource{ | ||
| SecretKeyRef: &v1.SecretKeySelector{ | ||
| LocalObjectReference: v1.LocalObjectReference{ | ||
| Name: "akita-secrets", | ||
| }, | ||
| Key: "akita-api-secret", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| Lifecycle: &v1.Lifecycle{ | ||
| PreStop: &v1.LifecycleHandler{ | ||
| Exec: &v1.ExecAction{ | ||
| Command: []string{ | ||
| "/bin/sh", | ||
| "-c", | ||
| "AKITA_PID=$(pgrep akita) && kill -2 $AKITA_PID && tail -f /proc/$AKITA_PID/fd/1", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| Args: []string{"apidump", "--project", projectName}, | ||
| SecurityContext: &v1.SecurityContext{ | ||
| Capabilities: &v1.Capabilities{Add: []v1.Capability{"NET_RAW"}}, | ||
| }, | ||
| } | ||
|
|
||
| return sidecar | ||
| } | ||
|
|
||
| // Parses the given value for the `--secret` option. | ||
| func resolveSecretGenerationOptions(flagValue string) secretGenerationOptions { | ||
versilis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if flagValue == "" || flagValue == "false" { | ||
| return secretGenerationOptions{ | ||
| ShouldInject: false, | ||
| Filepath: optionals.None[string](), | ||
| } | ||
| } | ||
|
|
||
| if flagValue == "true" { | ||
| return secretGenerationOptions{ | ||
| ShouldInject: true, | ||
| Filepath: optionals.None[string](), | ||
| } | ||
versilis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| return secretGenerationOptions{ | ||
| ShouldInject: true, | ||
| Filepath: optionals.Some(flagValue), | ||
| } | ||
| } | ||
|
|
||
| func init() { | ||
| injectCmd.Flags().StringVarP( | ||
| &injectFileNameFlag, | ||
| "file", | ||
| "f", | ||
| "", | ||
| "Path to the Kubernetes YAML file to be injected. This should contain a Deployment object.", | ||
| ) | ||
| _ = injectCmd.MarkFlagRequired("file") | ||
|
|
||
| injectCmd.Flags().StringVarP( | ||
| &injectOutputFlag, | ||
| "output", | ||
| "o", | ||
| "", | ||
| "Path to the output file. If not specified, the output will be printed to stdout.", | ||
| ) | ||
liujed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| injectCmd.Flags().StringVarP( | ||
| &projectNameFlag, | ||
| "project", | ||
| "p", | ||
| "", | ||
| "Name of the Akita project to which the traffic will be uploaded.", | ||
| ) | ||
| _ = injectCmd.MarkFlagRequired("project") | ||
|
|
||
| injectCmd.Flags().StringVarP( | ||
| &secretInjectFlag, | ||
| "secret", | ||
| "s", | ||
| "false", | ||
| `Whether to generate a Kubernetes Secret. If set to "true", the secret will be added to the modified Kubernetes YAML file. Specify a path to write the secret to a separate file; if this is done, an output file must also be specified with --output.`, | ||
| ) | ||
| // Default value is "true" when the flag is given without an argument. | ||
| injectCmd.Flags().Lookup("secret").NoOptDefVal = "true" | ||
versilis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| Cmd.AddCommand(injectCmd) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| package kube | ||
|
|
||
| import ( | ||
| "github.com/akitasoftware/akita-cli/cmd/internal/cmderr" | ||
| "github.com/pkg/errors" | ||
| "os" | ||
| "path/filepath" | ||
| ) | ||
|
|
||
| // Writes the generated secret to the given file path | ||
| func writeFile(data []byte, filePath string) error { | ||
| f, err := createFile(filePath) | ||
| if err != nil { | ||
| return cmderr.AkitaErr{ | ||
| Err: cmderr.AkitaErr{ | ||
| Err: errors.Wrapf( | ||
| err, | ||
| "failed to create file %s", | ||
| filePath, | ||
| ), | ||
| }, | ||
| } | ||
| } | ||
| defer f.Close() | ||
|
|
||
| _, err = f.Write(data) | ||
| if err != nil { | ||
| return errors.Errorf("failed to write to file %s", filePath) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // Creates a file at the given path to be used for storing of a Kubernetes configuration object | ||
| // If the directory provided does not exist, an error will be returned and the file will not be created | ||
| func createFile(path string) (*os.File, error) { | ||
| // Split the output flag value into directory and filename | ||
| outputDir, outputName := filepath.Split(path) | ||
|
|
||
| // Get the absolute path of the output directory | ||
| absOutputDir, err := filepath.Abs(outputDir) | ||
| if err != nil { | ||
| return nil, errors.Wrapf(err, "failed to resolve the absolute path of the output directory") | ||
| } | ||
|
|
||
| // Check that the output directory exists | ||
| if _, statErr := os.Stat(absOutputDir); os.IsNotExist(statErr) { | ||
| return nil, errors.Errorf("output directory %s does not exist", absOutputDir) | ||
| } | ||
|
|
||
| // Check if the output file already exists | ||
| outputFilePath := filepath.Join(absOutputDir, outputName) | ||
| if _, statErr := os.Stat(outputFilePath); statErr == nil { | ||
| return nil, errors.Errorf("output file %s already exists", outputFilePath) | ||
| } | ||
|
|
||
| // Create the output file in the output directory | ||
| outputFile, err := os.Create(outputFilePath) | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "failed to create the output file") | ||
| } | ||
|
|
||
| return outputFile, nil | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.