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

Commit 99dadb7

Browse files
versilisliujed
andauthored
Add kube inject command (#206)
This adds a new command `akita kube inject` that can be used to manually inject Kuberentes YAML configuration files. Along with injecting deployments, it also can generate a secret to a file or stdout with the use of the `--secret` flag. This PR depends on #207 for its injection functionality. Example usages: ``` # Print injected resources to stdout akita kube inject -f in.yml # Print secret and injected resources to stdout. (combining all using `---`) akita kube inject -s -f in.yml # Output injected resource to file, and also generate and merge any required secrets akita kube inject -s -f in.yml -o out.yml # Output injected resources and generated secrets to separate files akita kube inject -s="secret.yml" -f in.yml -o out.yml # Applying via pipe akita kube inject -f in.yml | kubectl -f - # Applying via file akita kube inject -f in.yml -o out.yml && kubectl apply -f out.yml ``` Example Output (w/merged Secrets): ``` --- apiVersion: v1 kind: Secret metadata: name: akita-secrets namespace: default type: Opaque data: akita-api-key: **** akita-api-secret: *** --- apiVersion: v1 kind: Secret metadata: name: akita-secrets namespace: ns1 type: Opaque data: akita-api-key: *** akita-api-secret: *** --- apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null name: test-deploy namespace: default spec: replicas: 1 selector: matchLabels: app: test-pod strategy: {} template: metadata: creationTimestamp: null labels: app: test-pod spec: containers: - image: ghcr.io/wzshiming/echoserver/echoserver:v0.0.1 name: test-container resources: {} - args: - apidump - --project - docker-extension-testing env: - name: AKITA_API_KEY_ID valueFrom: secretKeyRef: key: akita-api-key name: akita-secrets - name: AKITA_API_KEY_SECRET valueFrom: secretKeyRef: key: akita-api-secret name: akita-secrets image: akitasoftware/cli:latest lifecycle: preStop: exec: command: - /bin/sh - -c - AKITA_PID=$(pgrep akita) && kill -2 $AKITA_PID && tail -f /proc/$AKITA_PID/fd/1 name: akita resources: {} securityContext: capabilities: add: - NET_RAW status: {} --- apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null name: patch-demo namespace: ns1 spec: replicas: 2 selector: matchLabels: app: nginx strategy: {} template: metadata: creationTimestamp: null labels: app: nginx spec: containers: - image: nginx name: patch-demo-ctr resources: {} - args: - apidump - --project - docker-extension-testing env: - name: AKITA_API_KEY_ID valueFrom: secretKeyRef: key: akita-api-key name: akita-secrets - name: AKITA_API_KEY_SECRET valueFrom: secretKeyRef: key: akita-api-secret name: akita-secrets image: akitasoftware/cli:latest lifecycle: preStop: exec: command: - /bin/sh - -c - AKITA_PID=$(pgrep akita) && kill -2 $AKITA_PID && tail -f /proc/$AKITA_PID/fd/1 name: akita resources: {} securityContext: capabilities: add: - NET_RAW tolerations: - effect: NoSchedule key: dedicated value: test-team status: {} ``` --------- Co-authored-by: Jed Liu <liujed@users.noreply.github.com>
1 parent 771d368 commit 99dadb7

File tree

3 files changed

+312
-59
lines changed

3 files changed

+312
-59
lines changed

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+
}

cmd/internal/kube/secret.go

Lines changed: 1 addition & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package kube
33
import (
44
"bytes"
55
"encoding/base64"
6-
"os"
7-
"path/filepath"
86
"text/template"
97

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

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

105-
// Writes the generated secret to the given file path
106-
func writeSecretFile(data []byte, filePath string) error {
107-
secretFile, err := createSecretFile(filePath)
108-
if err != nil {
109-
return cmderr.AkitaErr{
110-
Err: cmderr.AkitaErr{
111-
Err: errors.Wrapf(
112-
err,
113-
"failed to create secret file %s",
114-
filePath,
115-
),
116-
},
117-
}
118-
}
119-
defer secretFile.Close()
120-
121-
_, err = secretFile.Write(data)
122-
if err != nil {
123-
return cmderr.AkitaErr{Err: errors.Wrap(err, "failed to write generated secret file")}
124-
}
125-
126-
return nil
127-
}
128-
129-
// Creates a file at the given path to be used for storing of the generated Secret configuration
130-
// If the directory provided does not exist, an error will be returned and the file will not be created
131-
func createSecretFile(path string) (*os.File, error) {
132-
// Split the output flag value into directory and filename
133-
outputDir, outputName := filepath.Split(path)
134-
135-
// Get the absolute path of the output directory
136-
absOutputDir, err := filepath.Abs(outputDir)
137-
if err != nil {
138-
return nil, errors.Wrapf(err, "failed to resolve the absolute path of the output directory")
139-
}
140-
141-
// Check that the output directory exists
142-
if _, statErr := os.Stat(absOutputDir); os.IsNotExist(statErr) {
143-
return nil, errors.Errorf("output directory %s does not exist", absOutputDir)
144-
}
145-
146-
// Check if the output file already exists
147-
outputFilePath := filepath.Join(absOutputDir, outputName)
148-
if _, statErr := os.Stat(outputFilePath); statErr == nil {
149-
return nil, errors.Errorf("output file %s already exists", outputFilePath)
150-
}
151-
152-
// Create the output file in the output directory
153-
outputFile, err := os.Create(outputFilePath)
154-
if err != nil {
155-
return nil, errors.Wrap(err, "failed to create the output file")
156-
}
157-
158-
return outputFile, nil
159-
}
160-
161103
func init() {
162104
secretCmd.Flags().StringVarP(
163105
&namespaceFlag,

cmd/internal/kube/util.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package kube
2+
3+
import (
4+
"github.com/akitasoftware/akita-cli/cmd/internal/cmderr"
5+
"github.com/pkg/errors"
6+
"os"
7+
"path/filepath"
8+
)
9+
10+
// Writes the generated secret to the given file path
11+
func writeFile(data []byte, filePath string) error {
12+
f, err := createFile(filePath)
13+
if err != nil {
14+
return cmderr.AkitaErr{
15+
Err: cmderr.AkitaErr{
16+
Err: errors.Wrapf(
17+
err,
18+
"failed to create file %s",
19+
filePath,
20+
),
21+
},
22+
}
23+
}
24+
defer f.Close()
25+
26+
_, err = f.Write(data)
27+
if err != nil {
28+
return errors.Errorf("failed to write to file %s", filePath)
29+
}
30+
31+
return nil
32+
}
33+
34+
// Creates a file at the given path to be used for storing of a Kubernetes configuration object
35+
// If the directory provided does not exist, an error will be returned and the file will not be created
36+
func createFile(path string) (*os.File, error) {
37+
// Split the output flag value into directory and filename
38+
outputDir, outputName := filepath.Split(path)
39+
40+
// Get the absolute path of the output directory
41+
absOutputDir, err := filepath.Abs(outputDir)
42+
if err != nil {
43+
return nil, errors.Wrapf(err, "failed to resolve the absolute path of the output directory")
44+
}
45+
46+
// Check that the output directory exists
47+
if _, statErr := os.Stat(absOutputDir); os.IsNotExist(statErr) {
48+
return nil, errors.Errorf("output directory %s does not exist", absOutputDir)
49+
}
50+
51+
// Check if the output file already exists
52+
outputFilePath := filepath.Join(absOutputDir, outputName)
53+
if _, statErr := os.Stat(outputFilePath); statErr == nil {
54+
return nil, errors.Errorf("output file %s already exists", outputFilePath)
55+
}
56+
57+
// Create the output file in the output directory
58+
outputFile, err := os.Create(outputFilePath)
59+
if err != nil {
60+
return nil, errors.Wrap(err, "failed to create the output file")
61+
}
62+
63+
return outputFile, nil
64+
}

0 commit comments

Comments
 (0)