Skip to content
This repository was archived by the owner on Jul 10, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 247 additions & 0 deletions cmd/internal/kube/inject.go
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)

// 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 {
// 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 {
if flagValue == "" || flagValue == "false" {
return secretGenerationOptions{
ShouldInject: false,
Filepath: optionals.None[string](),
}
}

if flagValue == "true" {
return secretGenerationOptions{
ShouldInject: true,
Filepath: optionals.None[string](),
}
}

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.",
)

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"

Cmd.AddCommand(injectCmd)
}
60 changes: 1 addition & 59 deletions cmd/internal/kube/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package kube
import (
"bytes"
"encoding/base64"
"os"
"path/filepath"
"text/template"

"github.com/akitasoftware/akita-cli/telemetry"
Expand Down Expand Up @@ -43,7 +41,7 @@ var secretCmd = &cobra.Command{
}

// Otherwise, write the generated secret to the given file path
err = writeSecretFile(output, secretFilePathFlag)
err = writeFile(output, secretFilePathFlag)
if err != nil {
return cmderr.AkitaErr{Err: errors.Wrapf(err, "Failed to write generated secret to %s", output)}
}
Expand Down Expand Up @@ -102,62 +100,6 @@ func handleSecretGeneration(namespace, key, secret string) ([]byte, error) {
return buf.Bytes(), nil
}

// Writes the generated secret to the given file path
func writeSecretFile(data []byte, filePath string) error {
secretFile, err := createSecretFile(filePath)
if err != nil {
return cmderr.AkitaErr{
Err: cmderr.AkitaErr{
Err: errors.Wrapf(
err,
"failed to create secret file %s",
filePath,
),
},
}
}
defer secretFile.Close()

_, err = secretFile.Write(data)
if err != nil {
return cmderr.AkitaErr{Err: errors.Wrap(err, "failed to write generated secret file")}
}

return nil
}

// Creates a file at the given path to be used for storing of the generated Secret configuration
// If the directory provided does not exist, an error will be returned and the file will not be created
func createSecretFile(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
}

func init() {
secretCmd.Flags().StringVarP(
&namespaceFlag,
Expand Down
64 changes: 64 additions & 0 deletions cmd/internal/kube/util.go
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
}