Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add command to generate Kubernetes Secrets #202

Merged
merged 25 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
746248e
Add base kube command
versilis Mar 13, 2023
ad244de
Refactor API credential check into a function
versilis Mar 14, 2023
3789778
Add template to generate secrets
versilis Mar 15, 2023
e5b4a88
Add kube secret command
versilis Mar 15, 2023
10ad02d
Add to root command
versilis Mar 15, 2023
0caeed3
Print success message when secret config is generated
versilis Mar 16, 2023
e651574
Fix issues with output generation
versilis Mar 16, 2023
1c7ae51
Mark namespace flag as required
versilis Mar 16, 2023
0fd9974
Add comment
versilis Mar 16, 2023
a7a2c07
Update unit test
versilis Mar 16, 2023
ed0dfc8
Apply suggestions from code review
versilis Mar 20, 2023
8d66125
Remove old credential check in addAgentToECS
versilis Mar 20, 2023
6596d5e
Use the default namespace when none is provided
versilis Mar 20, 2023
69857a4
Rename flag variables to fit standard conventions
versilis Mar 20, 2023
5acfe06
Print output on successful secret generation
versilis Mar 20, 2023
df81990
Add function to initialize telemetry
versilis Mar 20, 2023
3dbe526
Return an error if directory does not exist
versilis Mar 21, 2023
935b4bb
Add aliases for kube command
versilis Mar 21, 2023
562adb3
Tweak test names
versilis Mar 21, 2023
24a8bc3
Fix test
versilis Mar 21, 2023
edb4246
Update buffer initialization to use builtin constructor
versilis Mar 21, 2023
4490c52
Only print when no file is specified
versilis Mar 21, 2023
ce8ba09
Fix doc comment
versilis Mar 21, 2023
058e72c
Use null analytics client by default
versilis Mar 21, 2023
44ba56b
Add contextual information on successful file generation
versilis Mar 21, 2023
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
26 changes: 26 additions & 0 deletions cmd/internal/cmderr/checks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package cmderr

import (
"errors"
"github.com/akitasoftware/akita-cli/cfg"
"github.com/akitasoftware/akita-cli/env"
"github.com/akitasoftware/akita-cli/printer"
)

// Checks that a user has configured their API key and secret and returned them.
// If the user has not configured their API key, a user-friendly error message is printed and an error is returned.
func RequireAPICredentials(explanation string) (string, string, error) {
key, secret := cfg.GetAPIKeyAndSecret()
if key == "" || secret == "" {
printer.Errorf("No Akita API key configured. %s\n", explanation)
if env.InDocker() {
printer.Infof("Please set the AKITA_API_KEY_ID and AKITA_API_KEY_SECRET environment variables on the Docker command line.\n")
} else {
printer.Infof("Use the AKITA_API_KEY_ID and AKITA_API_KEY_SECRET environment variables, or run 'akita login'.\n")
}

return "", "", AkitaErr{Err: errors.New("could not find an Akita API key to use")}
}

return key, secret, nil
}
46 changes: 29 additions & 17 deletions cmd/internal/ecs/ecs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import (
"fmt"
"strings"

"github.com/akitasoftware/akita-cli/cfg"
"github.com/akitasoftware/akita-cli/cmd/internal/cmderr"
"github.com/akitasoftware/akita-cli/env"
"github.com/akitasoftware/akita-cli/printer"
"github.com/akitasoftware/akita-cli/rest"
"github.com/akitasoftware/akita-cli/telemetry"
"github.com/akitasoftware/akita-cli/util"
Expand Down Expand Up @@ -71,8 +68,18 @@ func init() {
Cmd.PersistentFlags().StringVar(&awsRegionFlag, "region", "", "The AWS region in which your ECS cluster resides.")
Cmd.PersistentFlags().StringVar(&ecsClusterFlag, "cluster", "", "The name or ARN of your ECS cluster.")
Cmd.PersistentFlags().StringVar(&ecsServiceFlag, "service", "", "The name or ARN of your ECS service.")
Cmd.PersistentFlags().StringVar(&ecsTaskDefinitionFlag, "task", "", "The name of your ECS task definition to modify.")
Cmd.PersistentFlags().BoolVar(&dryRunFlag, "dry-run", false, "Perform a dry run: show what will be done, but do not modify ECS.")
Cmd.PersistentFlags().StringVar(
&ecsTaskDefinitionFlag,
"task",
"",
"The name of your ECS task definition to modify.",
)
Cmd.PersistentFlags().BoolVar(
&dryRunFlag,
"dry-run",
false,
"Perform a dry run: show what will be done, but do not modify ECS.",
)

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

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

// Check project's existence
if projectFlag == "" {
return errors.New("Must specify the name of your Akita project with the --project flag.")
}
frontClient := rest.NewFrontClient(rest.Domain, telemetry.GetClientID())
_, err := util.GetServiceIDByName(frontClient, projectFlag)
_, err = util.GetServiceIDByName(frontClient, projectFlag)
if err != nil {
// TODO: we _could_ offer to create it, instead.
if strings.Contains(err.Error(), "cannot determine project ID") {
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)}
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,
),
}
} else {
return cmderr.AkitaErr{Err: errors.Wrapf(err, "Could not look up the project %q in the Akita cloud", projectFlag)}
return cmderr.AkitaErr{
Err: errors.Wrapf(
err,
"Could not look up the project %q in the Akita cloud",
projectFlag,
),
}
}
}

Expand Down
19 changes: 19 additions & 0 deletions cmd/internal/kube/kube.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package kube

import (
"github.com/akitasoftware/akita-cli/cmd/internal/cmderr"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

var Cmd = &cobra.Command{
Use: "kube",
Short: "Install Akita in your Kubernetes cluster",
Aliases: []string{
"k8s",
"kubernetes",
},
RunE: func(_ *cobra.Command, _ []string) error {
return cmderr.AkitaErr{Err: errors.New("no subcommand specified")}
},
versilis marked this conversation as resolved.
Show resolved Hide resolved
}
179 changes: 179 additions & 0 deletions cmd/internal/kube/secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package kube

import (
"bytes"
"encoding/base64"
"os"
"path/filepath"
"text/template"

"github.com/akitasoftware/akita-cli/telemetry"

"github.com/akitasoftware/akita-cli/cmd/internal/cmderr"
"github.com/akitasoftware/akita-cli/printer"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

var (
secretFilePathFlag string
namespaceFlag string
// Store a parsed representation of /template/akita-secret.tmpl
secretTemplate *template.Template
)

var secretCmd = &cobra.Command{
Use: "secret",
Short: "Generate a Kubernetes secret containing the Akita credentials",
RunE: func(cmd *cobra.Command, args []string) error {
key, secret, err := cmderr.RequireAPICredentials("Akita API key is required for Kubernetes Secret generation")
if err != nil {
return err
}

output, err := handleSecretGeneration(namespaceFlag, key, secret)
if err != nil {
return err
}

// If the secret file path flag hasn't been set, print the generated secret to stdout
if secretFilePathFlag == "" {
printer.RawOutput(string(output))
return nil
}

// Otherwise, write the generated secret to the given file path
err = writeSecretFile(output, secretFilePathFlag)
if err != nil {
return cmderr.AkitaErr{Err: errors.Wrapf(err, "Failed to write generated secret to %s", output)}
}

printer.Infof("Successfully generated a Kubernetes Secret file for Akita at %s\n", secretFilePathFlag)
printer.Infof("To apply, run: kubectl apply -f %s\n", secretFilePathFlag)
return nil
},
// Override the parent command's PersistentPreRun to prevent any logs from being printed.
// This is necessary because the secret command is intended to be used in a pipeline
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// Initialize the telemetry client, but do not allow any logs to be printed
telemetry.Init(false)
},
}

// Represents the input used by secretTemplate
type secretTemplateInput struct {
Namespace string
APIKey string
APISecret string
}

func initSecretTemplate() error {
var err error

secretTemplate, err = template.ParseFS(templateFS, "template/akita-secret.tmpl")
if err != nil {
return cmderr.AkitaErr{Err: errors.Wrap(err, "failed to parse secret template")}
}

return nil
}

// Generates a Kubernetes secret config file for Akita
// On success, the generated output is returned as a string.
func handleSecretGeneration(namespace, key, secret string) ([]byte, error) {
err := initSecretTemplate()
if err != nil {
return nil, cmderr.AkitaErr{Err: errors.Wrap(err, "failed to initialize secret template")}
}

input := secretTemplateInput{
Namespace: namespace,
APIKey: base64.StdEncoding.EncodeToString([]byte(key)),
APISecret: base64.StdEncoding.EncodeToString([]byte(secret)),
}

buf := bytes.NewBuffer([]byte{})

err = secretTemplate.Execute(buf, input)
if err != nil {
return nil, cmderr.AkitaErr{Err: errors.Wrap(err, "failed to generate template")}
}

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,
"namespace",
"n",
"default",
"The Kubernetes namespace the secret should be applied to",
)

secretCmd.Flags().StringVarP(
&secretFilePathFlag,
"file",
"f",
"",
"File to output the generated secret. If not set, the secret will be printed to stdout.",
)

Cmd.AddCommand(secretCmd)
}
28 changes: 28 additions & 0 deletions cmd/internal/kube/secret_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package kube

import (
_ "embed"
"github.com/stretchr/testify/assert"
"testing"
)

//go:embed test_resource/akita-secret.yml
var testAkitaSecretYAML []byte

func Test_secretGeneration(t *testing.T) {
// GIVEN
const (
namespace = "default"
key = "api-key"
secret = "api-secret"
)

// WHEN
output, err := handleSecretGeneration(namespace, key, secret)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}

// THEN
assert.Equal(t, testAkitaSecretYAML, output)
}
6 changes: 6 additions & 0 deletions cmd/internal/kube/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package kube

import "embed"

//go:embed template
var templateFS embed.FS
9 changes: 9 additions & 0 deletions cmd/internal/kube/template/akita-secret.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: akita-secrets
namespace: {{.Namespace}}
versilis marked this conversation as resolved.
Show resolved Hide resolved
type: Opaque
data:
akita-api-key: {{.APIKey}}
akita-api-secret: {{.APISecret}}
9 changes: 9 additions & 0 deletions cmd/internal/kube/test_resource/akita-secret.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: akita-secrets
namespace: default
type: Opaque
data:
akita-api-key: YXBpLWtleQ==
akita-api-secret: YXBpLXNlY3JldA==
4 changes: 4 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/akitasoftware/akita-cli/cmd/internal/daemon"
"github.com/akitasoftware/akita-cli/cmd/internal/ecs"
"github.com/akitasoftware/akita-cli/cmd/internal/get"
"github.com/akitasoftware/akita-cli/cmd/internal/kube"
"github.com/akitasoftware/akita-cli/cmd/internal/learn"
"github.com/akitasoftware/akita-cli/cmd/internal/legacy"
"github.com/akitasoftware/akita-cli/cmd/internal/login"
Expand Down Expand Up @@ -73,6 +74,8 @@ var (
)

func preRun(cmd *cobra.Command, args []string) {
telemetry.Init(true)

switch logFormatFlag {
case "json":
printer.SwitchToJSON()
Expand Down Expand Up @@ -279,6 +282,7 @@ func init() {
rootCmd.AddCommand(ci_guard.GuardCommand(get.Cmd))
rootCmd.AddCommand(ecs.Cmd)
rootCmd.AddCommand(nginx.Cmd)
rootCmd.AddCommand(kube.Cmd)

// Legacy commands, included for backward compatibility but are hidden.
legacy.SessionsCmd.Hidden = true
Expand Down
Loading