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 21 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
}
148 changes: 148 additions & 0 deletions cmd/internal/kube/secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
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 (
outputFlag 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, outputFlag)
if err != nil {
return err
}

// Output the generated secret to the console
printer.RawOutput(output)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I think I was unclear. The two uses cases are:

  1. Apply directly
    akita kube secret | kubectl apply -f -

  2. Apply via a file (convenience, they could always pipe to a file.)
    akita kube secret -f mysecret.yaml
    kubectl apply -f mysecret.yaml

In case #2 we should not print to standard output as well, it should be one or the other. We can support this in a few different ways, I don't much care whether (a) standard out is the default if -f not specified, or (b) -f - writes to standard output..

I kind of think like in case #1 we should not write the file that was not asked for as well.

Come talk to me if what should happen in these two cases are still unclear.


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, output string) (string, error) {
if err := initSecretTemplate(); err != nil {
return "", err
}

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

secretFile, err := createSecretFile(output)
if err != nil {
return "", cmderr.AkitaErr{Err: errors.Wrap(err, "failed to create output file")}
}
defer secretFile.Close()

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

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

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

return buf.String(), nil
}

// Creates a file at the give path to be used for storing of the generated Secret config
// If any child dicrectories do not exist, it will be created.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment correct? I think you mean "it will not be created".

func createSecretFile(path string) (*os.File, error) {
// Split the outut 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(&outputFlag, "output", "o", "akita-secret.yml", "File to output the generated secret.")

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

import (
_ "embed"
"github.com/stretchr/testify/assert"
"os"
"path/filepath"
"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"
)

dir := t.TempDir()
actualOutput := filepath.Join(dir, "akita-secret.yml")

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

generatedFile, err := os.ReadFile(actualOutput)
if err != nil {
t.Errorf("Failed to read generated generatedFile: %v", err)
}

// THEN
assert.Equal(t, string(testAkitaSecretYAML), string(generatedFile))
assert.Equal(t, string(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
2 changes: 2 additions & 0 deletions learn/parse_http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package learn
import (
"bytes"
"compress/flate"
"github.com/akitasoftware/akita-cli/telemetry"
"net/http"
"strings"
"testing"
Expand Down Expand Up @@ -127,6 +128,7 @@ type parseTest struct {
}

func TestParseHTTPRequest(t *testing.T) {
telemetry.Init(false)
standardMethodMeta := &as.MethodMeta{
Meta: &as.MethodMeta_Http{
Http: &as.HTTPMethodMeta{
Expand Down
2 changes: 2 additions & 0 deletions pcap/stream_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package pcap

import (
"fmt"
"github.com/akitasoftware/akita-cli/telemetry"
"net"
"testing"
"time"
Expand Down Expand Up @@ -127,6 +128,7 @@ func runTCPFlowTestCase(c tcpFlowTestCase) error {
}

func TestTCPFlow(t *testing.T) {
telemetry.Init(false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I understand the need here and in pase_http_test since the error functions in parsing send telemetry.

Is there a fix to telemetry that checks whether uninitialized instead? If it's not easy to do, we can keep this, it just seems a bit odd to be initializing telemetry in a situation where we don't really want it.

testCases := []tcpFlowTestCase{
{
name: "unparsable single byte",
Expand Down
Loading