Skip to content
This repository was archived by the owner on Jul 10, 2024. It is now read-only.

Commit 304afdb

Browse files
versilisMark Gritterliujed
authored
Simplify Kubernetes Injection (#205)
This adds two new commands, `akita kube inject` and `akita kube secret`, for simplifying the process of installing Akita as a sidecar in Kubernetes Deployments. Changes include: - #202 - #207 - #206 --------- Signed-off-by: versilis <versilis@akitasoftware.com> Co-authored-by: Mark Gritter <mgritter@akitasoftware.com> Co-authored-by: Jed Liu <liujed@users.noreply.github.com>
1 parent 9767df1 commit 304afdb

File tree

19 files changed

+1317
-68
lines changed

19 files changed

+1317
-68
lines changed

cmd/internal/cmderr/checks.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package cmderr
2+
3+
import (
4+
"errors"
5+
"github.com/akitasoftware/akita-cli/cfg"
6+
"github.com/akitasoftware/akita-cli/env"
7+
"github.com/akitasoftware/akita-cli/printer"
8+
)
9+
10+
// Checks that a user has configured their API key and secret and returned them.
11+
// If the user has not configured their API key, a user-friendly error message is printed and an error is returned.
12+
func RequireAPICredentials(explanation string) (string, string, error) {
13+
key, secret := cfg.GetAPIKeyAndSecret()
14+
if key == "" || secret == "" {
15+
printer.Errorf("No Akita API key configured. %s\n", explanation)
16+
if env.InDocker() {
17+
printer.Infof("Please set the AKITA_API_KEY_ID and AKITA_API_KEY_SECRET environment variables on the Docker command line.\n")
18+
} else {
19+
printer.Infof("Use the AKITA_API_KEY_ID and AKITA_API_KEY_SECRET environment variables, or run 'akita login'.\n")
20+
}
21+
22+
return "", "", AkitaErr{Err: errors.New("could not find an Akita API key to use")}
23+
}
24+
25+
return key, secret, nil
26+
}

cmd/internal/ecs/ecs.go

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ import (
44
"fmt"
55
"strings"
66

7-
"github.com/akitasoftware/akita-cli/cfg"
87
"github.com/akitasoftware/akita-cli/cmd/internal/cmderr"
9-
"github.com/akitasoftware/akita-cli/env"
10-
"github.com/akitasoftware/akita-cli/printer"
118
"github.com/akitasoftware/akita-cli/rest"
129
"github.com/akitasoftware/akita-cli/telemetry"
1310
"github.com/akitasoftware/akita-cli/util"
@@ -71,8 +68,18 @@ func init() {
7168
Cmd.PersistentFlags().StringVar(&awsRegionFlag, "region", "", "The AWS region in which your ECS cluster resides.")
7269
Cmd.PersistentFlags().StringVar(&ecsClusterFlag, "cluster", "", "The name or ARN of your ECS cluster.")
7370
Cmd.PersistentFlags().StringVar(&ecsServiceFlag, "service", "", "The name or ARN of your ECS service.")
74-
Cmd.PersistentFlags().StringVar(&ecsTaskDefinitionFlag, "task", "", "The name of your ECS task definition to modify.")
75-
Cmd.PersistentFlags().BoolVar(&dryRunFlag, "dry-run", false, "Perform a dry run: show what will be done, but do not modify ECS.")
71+
Cmd.PersistentFlags().StringVar(
72+
&ecsTaskDefinitionFlag,
73+
"task",
74+
"",
75+
"The name of your ECS task definition to modify.",
76+
)
77+
Cmd.PersistentFlags().BoolVar(
78+
&dryRunFlag,
79+
"dry-run",
80+
false,
81+
"Perform a dry run: show what will be done, but do not modify ECS.",
82+
)
7683

7784
// Support for credentials in a nonstandard location
7885
Cmd.PersistentFlags().StringVar(&awsCredentialsFlag, "aws-credentials", "", "Location of AWS credentials file.")
@@ -84,29 +91,34 @@ func init() {
8491

8592
func addAgentToECS(cmd *cobra.Command, args []string) error {
8693
// Check for API key
87-
key, secret := cfg.GetAPIKeyAndSecret()
88-
if key == "" || secret == "" {
89-
printer.Errorf("No Akita API key configured. The Akita agent must have an API key in order to capture traces.\n")
90-
if env.InDocker() {
91-
printer.Infof("Please set the AKITA_API_KEY_ID and AKITA_API_KEY_SECRET environment variables on the Docker command line.\n")
92-
} else {
93-
printer.Infof("Use the AKITA_API_KEY_ID and AKITA_API_KEY_SECRET environment variables, or run 'akita login'.\n")
94-
}
95-
return cmderr.AkitaErr{Err: errors.New("Could not find an Akita API key to use.")}
94+
_, _, err := cmderr.RequireAPICredentials("The Akita agent must have an API key in order to capture traces.")
95+
if err != nil {
96+
return err
9697
}
9798

9899
// Check project's existence
99100
if projectFlag == "" {
100101
return errors.New("Must specify the name of your Akita project with the --project flag.")
101102
}
102103
frontClient := rest.NewFrontClient(rest.Domain, telemetry.GetClientID())
103-
_, err := util.GetServiceIDByName(frontClient, projectFlag)
104+
_, err = util.GetServiceIDByName(frontClient, projectFlag)
104105
if err != nil {
105106
// TODO: we _could_ offer to create it, instead.
106107
if strings.Contains(err.Error(), "cannot determine project ID") {
107-
return cmderr.AkitaErr{Err: fmt.Errorf("Could not find the project %q in the Akita cloud. Please create it from the Akita web console before proceeding.", projectFlag)}
108+
return cmderr.AkitaErr{
109+
Err: fmt.Errorf(
110+
"Could not find the project %q in the Akita cloud. Please create it from the Akita web console before proceeding.",
111+
projectFlag,
112+
),
113+
}
108114
} else {
109-
return cmderr.AkitaErr{Err: errors.Wrapf(err, "Could not look up the project %q in the Akita cloud", projectFlag)}
115+
return cmderr.AkitaErr{
116+
Err: errors.Wrapf(
117+
err,
118+
"Could not look up the project %q in the Akita cloud",
119+
projectFlag,
120+
),
121+
}
110122
}
111123
}
112124

cmd/internal/kube/inject.go

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
package kube
2+
3+
import (
4+
"bytes"
5+
6+
"github.com/akitasoftware/akita-cli/cmd/internal/cmderr"
7+
"github.com/akitasoftware/akita-cli/cmd/internal/kube/injector"
8+
"github.com/akitasoftware/akita-cli/printer"
9+
"github.com/akitasoftware/akita-cli/telemetry"
10+
"github.com/akitasoftware/go-utils/optionals"
11+
"github.com/pkg/errors"
12+
"github.com/spf13/cobra"
13+
v1 "k8s.io/api/core/v1"
14+
)
15+
16+
var (
17+
// The target Yaml faile to be injected
18+
// This is required for execution of injectCmd
19+
injectFileNameFlag string
20+
// The output file to write the injected Yaml to
21+
// If not set, injectCmd will default to printing the output to stdout
22+
injectOutputFlag string
23+
// The name of the project that the injected deployments should be associated with
24+
// This will be used by the agent to determine which Akita service to report traffic to
25+
projectNameFlag string
26+
// Represents the options for generating a secret
27+
// When set to "false" or left empty, injectCmd will not generate a secret
28+
// When set to "true", injectCmd will prepend a secret to each injectable namespace found in the file to inject (injectFileNameFlag)
29+
// Otherwise, injectCmd will treat secretInjectFlag as the file path all secrets should be generated to
30+
secretInjectFlag string
31+
)
32+
33+
var injectCmd = &cobra.Command{
34+
Use: "inject",
35+
Short: "Inject Akita into a Kubernetes deployment",
36+
Long: "Inject Akita into a Kubernetes deployment or set of deployments, and output the result to stdout or a file",
37+
RunE: func(_ *cobra.Command, args []string) error {
38+
secretOpts := resolveSecretGenerationOptions(secretInjectFlag)
39+
40+
// To avoid users unintentionally attempting to apply injected Deployments via pipeline without
41+
// their dependent Secrets, require that the user explicitly specify an output file.
42+
if secretOpts.ShouldInject && secretOpts.Filepath.IsSome() && injectOutputFlag == "" {
43+
printer.Errorln("Cannot specify a Secret file path without an output file (using --output or -o)")
44+
printer.Infoln("To generate a Secret file on its own, use `akita kube secret`")
45+
return cmderr.AkitaErr{
46+
Err: errors.New("invalid flag usage"),
47+
}
48+
}
49+
50+
// Create the injector which reads from the Kubernetes YAML file specified by the user
51+
injectr, err := injector.FromYAML(injectFileNameFlag)
52+
if err != nil {
53+
return cmderr.AkitaErr{
54+
Err: errors.Wrapf(
55+
err,
56+
"Failed to read injection file %s",
57+
injectFileNameFlag,
58+
),
59+
}
60+
}
61+
62+
// Generate a secret for each namespace in the deployment if the user specified secret generation
63+
secretBuf := new(bytes.Buffer)
64+
if secretOpts.ShouldInject {
65+
key, secret, err := cmderr.RequireAPICredentials("API credentials are required to generate secret.")
66+
if err != nil {
67+
return err
68+
}
69+
70+
namespaces, err := injectr.InjectableNamespaces()
71+
if err != nil {
72+
return err
73+
}
74+
75+
for _, namespace := range namespaces {
76+
r, err := handleSecretGeneration(namespace, key, secret)
77+
if err != nil {
78+
return err
79+
}
80+
81+
secretBuf.WriteString("---\n")
82+
secretBuf.Write(r)
83+
}
84+
}
85+
86+
// Create the output buffer
87+
out := new(bytes.Buffer)
88+
89+
// Either write the secret to a file or prepend it to the output
90+
if secretFilePath, exists := secretOpts.Filepath.Get(); exists {
91+
err = writeFile(secretBuf.Bytes(), secretFilePath)
92+
if err != nil {
93+
return err
94+
}
95+
96+
printer.Infof("Kubernetes Secret generated to %s\n", secretFilePath)
97+
} else {
98+
// Assign the secret to the output buffer
99+
// We do this so that the secret is written before any injected Deployment resources that depend on it
100+
out = secretBuf
101+
}
102+
103+
// Inject the sidecar into the input file
104+
rawInjected, err := injector.ToRawYAML(injectr, createSidecar(projectNameFlag))
105+
if err != nil {
106+
return cmderr.AkitaErr{Err: errors.Wrap(err, "Failed to inject sidecars")}
107+
}
108+
// Append the injected YAML to the output
109+
out.Write(rawInjected)
110+
111+
// If the user did not specify an output file, print the output to stdout
112+
if injectOutputFlag == "" {
113+
printer.Stdout.RawOutput(out.String())
114+
return nil
115+
}
116+
117+
// Write the output to the specified file
118+
if err := writeFile(out.Bytes(), injectOutputFlag); err != nil {
119+
return err
120+
}
121+
printer.Infof("Injected YAML written to %s\n", injectOutputFlag)
122+
123+
return nil
124+
},
125+
PersistentPreRun: func(cmd *cobra.Command, args []string) {
126+
// Initialize the telemetry client, but do not allow any logs to be printed
127+
telemetry.Init(false)
128+
},
129+
}
130+
131+
// A parsed representation of the `--secret` option.
132+
type secretGenerationOptions struct {
133+
// Whether to inject a secret
134+
ShouldInject bool
135+
// The path to the secret file
136+
Filepath optionals.Optional[string]
137+
}
138+
139+
func createSidecar(projectName string) v1.Container {
140+
sidecar := v1.Container{
141+
Name: "akita",
142+
Image: "akitasoftware/cli:latest",
143+
Env: []v1.EnvVar{
144+
{
145+
Name: "AKITA_API_KEY_ID",
146+
ValueFrom: &v1.EnvVarSource{
147+
SecretKeyRef: &v1.SecretKeySelector{
148+
LocalObjectReference: v1.LocalObjectReference{
149+
Name: "akita-secrets",
150+
},
151+
Key: "akita-api-key",
152+
},
153+
},
154+
},
155+
{
156+
Name: "AKITA_API_KEY_SECRET",
157+
ValueFrom: &v1.EnvVarSource{
158+
SecretKeyRef: &v1.SecretKeySelector{
159+
LocalObjectReference: v1.LocalObjectReference{
160+
Name: "akita-secrets",
161+
},
162+
Key: "akita-api-secret",
163+
},
164+
},
165+
},
166+
},
167+
Lifecycle: &v1.Lifecycle{
168+
PreStop: &v1.LifecycleHandler{
169+
Exec: &v1.ExecAction{
170+
Command: []string{
171+
"/bin/sh",
172+
"-c",
173+
"AKITA_PID=$(pgrep akita) && kill -2 $AKITA_PID && tail -f /proc/$AKITA_PID/fd/1",
174+
},
175+
},
176+
},
177+
},
178+
Args: []string{"apidump", "--project", projectName},
179+
SecurityContext: &v1.SecurityContext{
180+
Capabilities: &v1.Capabilities{Add: []v1.Capability{"NET_RAW"}},
181+
},
182+
}
183+
184+
return sidecar
185+
}
186+
187+
// Parses the given value for the `--secret` option.
188+
func resolveSecretGenerationOptions(flagValue string) secretGenerationOptions {
189+
if flagValue == "" || flagValue == "false" {
190+
return secretGenerationOptions{
191+
ShouldInject: false,
192+
Filepath: optionals.None[string](),
193+
}
194+
}
195+
196+
if flagValue == "true" {
197+
return secretGenerationOptions{
198+
ShouldInject: true,
199+
Filepath: optionals.None[string](),
200+
}
201+
}
202+
203+
return secretGenerationOptions{
204+
ShouldInject: true,
205+
Filepath: optionals.Some(flagValue),
206+
}
207+
}
208+
209+
func init() {
210+
injectCmd.Flags().StringVarP(
211+
&injectFileNameFlag,
212+
"file",
213+
"f",
214+
"",
215+
"Path to the Kubernetes YAML file to be injected. This should contain a Deployment object.",
216+
)
217+
_ = injectCmd.MarkFlagRequired("file")
218+
219+
injectCmd.Flags().StringVarP(
220+
&injectOutputFlag,
221+
"output",
222+
"o",
223+
"",
224+
"Path to the output file. If not specified, the output will be printed to stdout.",
225+
)
226+
227+
injectCmd.Flags().StringVarP(
228+
&projectNameFlag,
229+
"project",
230+
"p",
231+
"",
232+
"Name of the Akita project to which the traffic will be uploaded.",
233+
)
234+
_ = injectCmd.MarkFlagRequired("project")
235+
236+
injectCmd.Flags().StringVarP(
237+
&secretInjectFlag,
238+
"secret",
239+
"s",
240+
"false",
241+
`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.`,
242+
)
243+
// Default value is "true" when the flag is given without an argument.
244+
injectCmd.Flags().Lookup("secret").NoOptDefVal = "true"
245+
246+
Cmd.AddCommand(injectCmd)
247+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package injector
2+
3+
import (
4+
"bytes"
5+
v1 "k8s.io/api/core/v1"
6+
kyaml "sigs.k8s.io/yaml"
7+
)
8+
9+
// Calls the given injector's Inject method and returns the result as a YAML bytes.
10+
func ToRawYAML(injector Injector, sidecar v1.Container) ([]byte, error) {
11+
injectedObjects, err := injector.Inject(sidecar)
12+
if err != nil {
13+
return nil, err
14+
}
15+
16+
out := new(bytes.Buffer)
17+
for _, obj := range injectedObjects {
18+
raw, err := kyaml.Marshal(obj)
19+
if err != nil {
20+
return nil, err
21+
}
22+
23+
out.WriteString("---\n")
24+
out.Write(raw)
25+
}
26+
27+
return out.Bytes(), nil
28+
}

0 commit comments

Comments
 (0)