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

Commit a87238f

Browse files
versilisMark Gritter
andauthored
Add command to generate Kubernetes Secrets (#202)
This adds a new command `akita kube secret` which generates a Kubernetes secret configuration file that stores a user's base-64 encoded Akita API credentials. To simplify file generation, I've used go's built-in templating utilities; `akita-secret.tmpl` is used as the template for creating the secret. Example usage: ``` % ./bin/akita kube secret -n test -o ./tmp/deployments/configs/akita-secrets.yml [INFO] Akita Agent 0.0.0 [INFO] Generated Kubernetes secret config to ./tmp/deployments/configs/akita-secrets.yml % less ./tmp/deployments/configs/akita-secrets.yml apiVersion: v1 kind: Secret metadata: name: akita-secrets namespace: test type: Opaque data: akita-api-key: YXBrXzN1Y2h6WDyiOdyMzg2NldiM3Y0Q2Y= akita-api-secret: YmUzMzY5OTllNzNjc2DY3MmI5OWQzMTVmAnGaKmYzN2NlNjc2NWRiZDY4MzNjMWRkMzA4YjFjZDFlNWZkZg== ./tmp/deployments/configs/akita-secrets.yml lines 1-9/9 (END) --------- Signed-off-by: versilis <versilis@akitasoftware.com> Co-authored-by: Mark Gritter <mgritter@akitasoftware.com>
1 parent 9593d79 commit a87238f

File tree

11 files changed

+339
-37
lines changed

11 files changed

+339
-37
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/kube.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package kube
2+
3+
import (
4+
"github.com/akitasoftware/akita-cli/cmd/internal/cmderr"
5+
"github.com/pkg/errors"
6+
"github.com/spf13/cobra"
7+
)
8+
9+
var Cmd = &cobra.Command{
10+
Use: "kube",
11+
Short: "Install Akita in your Kubernetes cluster",
12+
Aliases: []string{
13+
"k8s",
14+
"kubernetes",
15+
},
16+
RunE: func(_ *cobra.Command, _ []string) error {
17+
return cmderr.AkitaErr{Err: errors.New("no subcommand specified")}
18+
},
19+
}

cmd/internal/kube/secret.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package kube
2+
3+
import (
4+
"bytes"
5+
"encoding/base64"
6+
"os"
7+
"path/filepath"
8+
"text/template"
9+
10+
"github.com/akitasoftware/akita-cli/telemetry"
11+
12+
"github.com/akitasoftware/akita-cli/cmd/internal/cmderr"
13+
"github.com/akitasoftware/akita-cli/printer"
14+
"github.com/pkg/errors"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
var (
19+
secretFilePathFlag string
20+
namespaceFlag string
21+
// Store a parsed representation of /template/akita-secret.tmpl
22+
secretTemplate *template.Template
23+
)
24+
25+
var secretCmd = &cobra.Command{
26+
Use: "secret",
27+
Short: "Generate a Kubernetes secret containing the Akita credentials",
28+
RunE: func(cmd *cobra.Command, args []string) error {
29+
key, secret, err := cmderr.RequireAPICredentials("Akita API key is required for Kubernetes Secret generation")
30+
if err != nil {
31+
return err
32+
}
33+
34+
output, err := handleSecretGeneration(namespaceFlag, key, secret)
35+
if err != nil {
36+
return err
37+
}
38+
39+
// If the secret file path flag hasn't been set, print the generated secret to stdout
40+
if secretFilePathFlag == "" {
41+
printer.RawOutput(string(output))
42+
return nil
43+
}
44+
45+
// Otherwise, write the generated secret to the given file path
46+
err = writeSecretFile(output, secretFilePathFlag)
47+
if err != nil {
48+
return cmderr.AkitaErr{Err: errors.Wrapf(err, "Failed to write generated secret to %s", output)}
49+
}
50+
51+
printer.Infof("Successfully generated a Kubernetes Secret file for Akita at %s\n", secretFilePathFlag)
52+
printer.Infof("To apply, run: kubectl apply -f %s\n", secretFilePathFlag)
53+
return nil
54+
},
55+
// Override the parent command's PersistentPreRun to prevent any logs from being printed.
56+
// This is necessary because the secret command is intended to be used in a pipeline
57+
PersistentPreRun: func(cmd *cobra.Command, args []string) {
58+
// Initialize the telemetry client, but do not allow any logs to be printed
59+
telemetry.Init(false)
60+
},
61+
}
62+
63+
// Represents the input used by secretTemplate
64+
type secretTemplateInput struct {
65+
Namespace string
66+
APIKey string
67+
APISecret string
68+
}
69+
70+
func initSecretTemplate() error {
71+
var err error
72+
73+
secretTemplate, err = template.ParseFS(templateFS, "template/akita-secret.tmpl")
74+
if err != nil {
75+
return cmderr.AkitaErr{Err: errors.Wrap(err, "failed to parse secret template")}
76+
}
77+
78+
return nil
79+
}
80+
81+
// Generates a Kubernetes secret config file for Akita
82+
// On success, the generated output is returned as a string.
83+
func handleSecretGeneration(namespace, key, secret string) ([]byte, error) {
84+
err := initSecretTemplate()
85+
if err != nil {
86+
return nil, cmderr.AkitaErr{Err: errors.Wrap(err, "failed to initialize secret template")}
87+
}
88+
89+
input := secretTemplateInput{
90+
Namespace: namespace,
91+
APIKey: base64.StdEncoding.EncodeToString([]byte(key)),
92+
APISecret: base64.StdEncoding.EncodeToString([]byte(secret)),
93+
}
94+
95+
buf := bytes.NewBuffer([]byte{})
96+
97+
err = secretTemplate.Execute(buf, input)
98+
if err != nil {
99+
return nil, cmderr.AkitaErr{Err: errors.Wrap(err, "failed to generate template")}
100+
}
101+
102+
return buf.Bytes(), nil
103+
}
104+
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+
161+
func init() {
162+
secretCmd.Flags().StringVarP(
163+
&namespaceFlag,
164+
"namespace",
165+
"n",
166+
"default",
167+
"The Kubernetes namespace the secret should be applied to",
168+
)
169+
170+
secretCmd.Flags().StringVarP(
171+
&secretFilePathFlag,
172+
"file",
173+
"f",
174+
"",
175+
"File to output the generated secret. If not set, the secret will be printed to stdout.",
176+
)
177+
178+
Cmd.AddCommand(secretCmd)
179+
}

cmd/internal/kube/secret_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package kube
2+
3+
import (
4+
_ "embed"
5+
"github.com/stretchr/testify/assert"
6+
"testing"
7+
)
8+
9+
//go:embed test_resource/akita-secret.yml
10+
var testAkitaSecretYAML []byte
11+
12+
func Test_secretGeneration(t *testing.T) {
13+
// GIVEN
14+
const (
15+
namespace = "default"
16+
key = "api-key"
17+
secret = "api-secret"
18+
)
19+
20+
// WHEN
21+
output, err := handleSecretGeneration(namespace, key, secret)
22+
if err != nil {
23+
t.Errorf("Unexpected error: %s", err)
24+
}
25+
26+
// THEN
27+
assert.Equal(t, testAkitaSecretYAML, output)
28+
}

cmd/internal/kube/template.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package kube
2+
3+
import "embed"
4+
5+
//go:embed template
6+
var templateFS embed.FS
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: v1
2+
kind: Secret
3+
metadata:
4+
name: akita-secrets
5+
namespace: {{.Namespace}}
6+
type: Opaque
7+
data:
8+
akita-api-key: {{.APIKey}}
9+
akita-api-secret: {{.APISecret}}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: v1
2+
kind: Secret
3+
metadata:
4+
name: akita-secrets
5+
namespace: default
6+
type: Opaque
7+
data:
8+
akita-api-key: YXBpLWtleQ==
9+
akita-api-secret: YXBpLXNlY3JldA==

cmd/root.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/akitasoftware/akita-cli/cmd/internal/daemon"
2323
"github.com/akitasoftware/akita-cli/cmd/internal/ecs"
2424
"github.com/akitasoftware/akita-cli/cmd/internal/get"
25+
"github.com/akitasoftware/akita-cli/cmd/internal/kube"
2526
"github.com/akitasoftware/akita-cli/cmd/internal/learn"
2627
"github.com/akitasoftware/akita-cli/cmd/internal/legacy"
2728
"github.com/akitasoftware/akita-cli/cmd/internal/login"
@@ -73,6 +74,8 @@ var (
7374
)
7475

7576
func preRun(cmd *cobra.Command, args []string) {
77+
telemetry.Init(true)
78+
7679
switch logFormatFlag {
7780
case "json":
7881
printer.SwitchToJSON()
@@ -279,6 +282,7 @@ func init() {
279282
rootCmd.AddCommand(ci_guard.GuardCommand(get.Cmd))
280283
rootCmd.AddCommand(ecs.Cmd)
281284
rootCmd.AddCommand(nginx.Cmd)
285+
rootCmd.AddCommand(kube.Cmd)
282286

283287
// Legacy commands, included for backward compatibility but are hidden.
284288
legacy.SessionsCmd.Hidden = true

0 commit comments

Comments
 (0)