Skip to content
This repository was archived by the owner on Jul 10, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 10 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
}
8 changes: 5 additions & 3 deletions cmd/internal/ecs/ecs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ 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"
Expand Down Expand Up @@ -84,7 +83,10 @@ func init() {

func addAgentToECS(cmd *cobra.Command, args []string) error {
// Check for API key
key, secret := cfg.GetAPIKeyAndSecret()
key, secret, err := cmderr.RequireAPICredentials("The Akita agent must have an API key in order to capture traces.")
if err != nil {
return err
}
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() {
Expand All @@ -100,7 +102,7 @@ func addAgentToECS(cmd *cobra.Command, args []string) error {
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") {
Expand Down
15 changes: 15 additions & 0 deletions cmd/internal/kube/kube.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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: "Gateway to Kubernetes related utilities",
RunE: func(_ *cobra.Command, _ []string) error {
return cmderr.AkitaErr{Err: errors.New("no subcommand specified")}
},
}
127 changes: 127 additions & 0 deletions cmd/internal/kube/secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package kube

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

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

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

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

var secretCmd = &cobra.Command{
Use: "secret",
Short: "Generate a Kubernetes secret config for Akita",
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
}

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

printer.Infoln("Generated Kubernetes secret config to ", output)
return nil
},
}

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

// Generates a Kubernetes secret config file for Akita
func handleSecretGeneration(namespace, key, secret, output string) error {

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()

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

return 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 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 directory if it doesn't exist
err = os.MkdirAll(absOutputDir, 0755)
if err != nil {
return nil, errors.Wrapf(err, "failed to create the output directory")
}

// 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() {
var err error

secretTemplate, err = template.ParseFS(templateFS, "template/akita-secret.tmpl")
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm mostly OK with using a template here. But, I think this will be much harder to pull off for the next command, and I would like the two implementations to be consistent. If we have to have YAML parsing and output, let's start here with the easy case. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently, there's a discrepancy between the Kubernetes openapi spec and the Go packages API model representation. Here's a link to the issue that covers it: kubernetes/kubernetes#109427

I've opened a separate PR to address using the Kubernetes API for generating secrets: #204

if err != nil {
log.Fatalf("unable to parse kube secret template: %v", err)
}

secretCmd.Flags().StringVarP(
&namespace,
"namespace",
"n",
"",
"The Kuberenetes namespace the secret should be applied to",
)
_ = secretCmd.MarkFlagRequired("namespace")

secretCmd.Flags().StringVarP(&output, "output", "o", "akita-secret.yml", "File to output the generated secret.")
Copy link
Contributor

Choose a reason for hiding this comment

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

This should have come up in the design review -- sorry. I think the correct default, which is most idiomatic to Kubernetes tools, is to print to standard output.

The idiomatic usage we are aiming for is something like "akita kube secret | kubectl apply -f -"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've implemented printing in 5acfe06.

To avoid printing pre-run info logs, I've overriden the root command's PersistentPreRun handler. I've also had to make some updates to how we initialize telemetry which I've added as part of this commit: df81990


Cmd.AddCommand(secretCmd)
}
38 changes: 38 additions & 0 deletions cmd/internal/kube/secret_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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, "configurations", "akita-secret.yml")

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

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

assert.Equal(t, string(testAkitaSecretYAML), string(actualFile))
}
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}}
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==
2 changes: 2 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 @@ -279,6 +280,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