summaryrefslogtreecommitdiffstats
path: root/client
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2021-09-01 09:08:56 +0200
committerMartin Polden <mpolden@mpolden.no>2021-09-01 09:09:06 +0200
commit689fa4af9a00c1897837ce081eb1ca20c8d4672e (patch)
treea461c661bc31953e3d779107e164cde14d99efc1 /client
parent33d2f7ad62958addde4e115f96e53a8108110cf2 (diff)
Implement vespa api-key
Diffstat (limited to 'client')
-rw-r--r--client/go/cmd/api_key.go56
-rw-r--r--client/go/cmd/api_key_test.go21
-rw-r--r--client/go/cmd/cert.go2
-rw-r--r--client/go/cmd/command_tester.go46
-rw-r--r--client/go/cmd/config_test.go27
-rw-r--r--client/go/go.mod1
-rw-r--r--client/go/vespa/crypto.go13
-rw-r--r--client/go/vespa/crypto_test.go22
8 files changed, 143 insertions, 45 deletions
diff --git a/client/go/cmd/api_key.go b/client/go/cmd/api_key.go
new file mode 100644
index 00000000000..a4e1cff40e3
--- /dev/null
+++ b/client/go/cmd/api_key.go
@@ -0,0 +1,56 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// vespa api-key command
+// Author: mpolden
+package cmd
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/spf13/cobra"
+ "github.com/vespa-engine/vespa/util"
+ "github.com/vespa-engine/vespa/vespa"
+)
+
+var overwriteKey bool
+
+func init() {
+ rootCmd.AddCommand(apiKeyCmd)
+ apiKeyCmd.Flags().BoolVarP(&overwriteKey, "force", "f", false, "Force overwrite of existing API key")
+ apiKeyCmd.MarkPersistentFlagRequired(applicationFlag)
+}
+
+var apiKeyCmd = &cobra.Command{
+ Use: "api-key",
+ Short: "Create a new user API key for authentication with Vespa Cloud",
+ Example: "$ vespa api-key -a my-tenant.my-app.my-instance",
+ Args: cobra.ExactArgs(0),
+ Run: func(cmd *cobra.Command, args []string) {
+ configDir := configDir("")
+ if configDir == "" {
+ return
+ }
+ app, err := vespa.ApplicationFromString(getApplication())
+ if err != nil {
+ printErr(err, "Could not parse application")
+ return
+ }
+ apiKeyFile := filepath.Join(configDir, app.Tenant+".api-key.pem")
+ if util.PathExists(apiKeyFile) && !overwriteKey {
+ printErrHint(fmt.Errorf("File %s already exists", apiKeyFile), "Use -f to overwrite it")
+ return
+ }
+ apiKey, err := vespa.CreateAPIKey()
+ if err != nil {
+ printErr(err, "Could not create API key")
+ return
+ }
+ if err := os.WriteFile(apiKeyFile, apiKey, 0600); err == nil {
+ printSuccess("API key written to ", apiKeyFile)
+ } else {
+ printErr(err, "Failed to write ", apiKeyFile)
+
+ }
+ },
+}
diff --git a/client/go/cmd/api_key_test.go b/client/go/cmd/api_key_test.go
new file mode 100644
index 00000000000..736d5a33744
--- /dev/null
+++ b/client/go/cmd/api_key_test.go
@@ -0,0 +1,21 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// Author: mpolden
+
+package cmd
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIKey(t *testing.T) {
+ configDir := t.TempDir()
+ keyFile := configDir + "/.vespa/t1.api-key.pem"
+
+ out := execute(command{args: []string{"api-key", "-a", "t1.a1.i1"}, configDir: configDir}, t, nil)
+ assert.Equal(t, "Success: API key written to "+keyFile+"\n", out)
+
+ out = execute(command{args: []string{"api-key", "-a", "t1.a1.i1"}, configDir: configDir}, t, nil)
+ assert.Equal(t, "Error: File "+keyFile+" already exists\nHint: Use -f to overwrite it\n", out)
+}
diff --git a/client/go/cmd/cert.go b/client/go/cmd/cert.go
index 3d2bd37affd..04c5fc2d272 100644
--- a/client/go/cmd/cert.go
+++ b/client/go/cmd/cert.go
@@ -23,7 +23,7 @@ func init() {
var certCmd = &cobra.Command{
Use: "cert",
- Short: "Create a new private key and self-signed certificate for a cloud deployment",
+ Short: "Create a new private key and self-signed certificate for Vespa Cloud deployment",
Example: "$ vespa cert -a my-tenant.my-app.my-instance",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
diff --git a/client/go/cmd/command_tester.go b/client/go/cmd/command_tester.go
index 46c37e0fbd2..074089b399d 100644
--- a/client/go/cmd/command_tester.go
+++ b/client/go/cmd/command_tester.go
@@ -15,12 +15,19 @@ import (
"time"
"github.com/logrusorgru/aurora"
+ "github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/vespa-engine/vespa/util"
)
-func executeCommand(t *testing.T, client *mockHttpClient, args []string, moreArgs []string) string {
+type command struct {
+ configDir string
+ args []string
+ moreArgs []string
+}
+
+func execute(cmd command, t *testing.T, client *mockHttpClient) string {
if client != nil {
util.ActiveHttpClient = client
}
@@ -28,27 +35,40 @@ func executeCommand(t *testing.T, client *mockHttpClient, args []string, moreArg
// Never print colors in tests
color = aurora.NewAurora(false)
- // Use a separate config dir for each test
- os.Setenv("VESPA_CLI_HOME", t.TempDir())
- if len(args) > 0 && args[0] != "config" {
- viper.Reset() // Reset config unless we're testing the config sub-command
+ // Set config dir. Use a separate one per test if none is specified
+ if cmd.configDir == "" {
+ cmd.configDir = t.TempDir()
+ viper.Reset()
}
+ os.Setenv("VESPA_CLI_HOME", cmd.configDir)
- // Reset to default target - persistent flags in Cobra persists over tests
- log.SetOutput(bytes.NewBufferString(""))
- rootCmd.SetArgs([]string{"status", "-t", "local"})
- rootCmd.Execute()
+ // Reset flags to their default value - persistent flags in Cobra persists over tests
+ rootCmd.Flags().VisitAll(func(f *pflag.Flag) {
+ switch v := f.Value.(type) {
+ case pflag.SliceValue:
+ _ = v.Replace([]string{})
+ default:
+ switch v.Type() {
+ case "bool", "string", "int":
+ _ = v.Set(f.DefValue)
+ }
+ }
+ })
// Capture stdout and execute command
- b := bytes.NewBufferString("")
- log.SetOutput(b)
- rootCmd.SetArgs(append(args, moreArgs...))
+ var b bytes.Buffer
+ log.SetOutput(&b)
+ rootCmd.SetArgs(append(cmd.args, cmd.moreArgs...))
rootCmd.Execute()
- out, err := ioutil.ReadAll(b)
+ out, err := ioutil.ReadAll(&b)
assert.Empty(t, err, "No error")
return string(out)
}
+func executeCommand(t *testing.T, client *mockHttpClient, args []string, moreArgs []string) string {
+ return execute(command{args: args, moreArgs: moreArgs}, t, client)
+}
+
type mockHttpClient struct {
// The HTTP status code that will be returned from the next invocation. Default: 200
nextStatus int
diff --git a/client/go/cmd/config_test.go b/client/go/cmd/config_test.go
index ff97157ba07..caa116c0441 100644
--- a/client/go/cmd/config_test.go
+++ b/client/go/cmd/config_test.go
@@ -7,19 +7,20 @@ import (
)
func TestConfig(t *testing.T) {
- assert.Equal(t, "invalid option or value: \"foo\": \"bar\"\n", executeCommand(t, nil, []string{"config", "set", "foo", "bar"}, nil))
- assert.Equal(t, "foo = <unset>\n", executeCommand(t, nil, []string{"config", "get", "foo"}, nil))
- assert.Equal(t, "target = local\n", executeCommand(t, nil, []string{"config", "get", "target"}, nil))
- assert.Equal(t, "", executeCommand(t, nil, []string{"config", "set", "target", "cloud"}, nil))
- assert.Equal(t, "target = cloud\n", executeCommand(t, nil, []string{"config", "get", "target"}, nil))
- assert.Equal(t, "", executeCommand(t, nil, []string{"config", "set", "target", "http://127.0.0.1:8080"}, nil))
- assert.Equal(t, "", executeCommand(t, nil, []string{"config", "set", "target", "https://127.0.0.1"}, nil))
- assert.Equal(t, "target = https://127.0.0.1\n", executeCommand(t, nil, []string{"config", "get", "target"}, nil))
+ configDir := t.TempDir()
+ assert.Equal(t, "invalid option or value: \"foo\": \"bar\"\n", execute(command{configDir: configDir, args: []string{"config", "set", "foo", "bar"}}, t, nil))
+ assert.Equal(t, "foo = <unset>\n", execute(command{configDir: configDir, args: []string{"config", "get", "foo"}}, t, nil))
+ assert.Equal(t, "target = local\n", execute(command{configDir: configDir, args: []string{"config", "get", "target"}}, t, nil))
+ assert.Equal(t, "", execute(command{configDir: configDir, args: []string{"config", "set", "target", "cloud"}}, t, nil))
+ assert.Equal(t, "target = cloud\n", execute(command{configDir: configDir, args: []string{"config", "get", "target"}}, t, nil))
+ assert.Equal(t, "", execute(command{configDir: configDir, args: []string{"config", "set", "target", "http://127.0.0.1:8080"}}, t, nil))
+ assert.Equal(t, "", execute(command{configDir: configDir, args: []string{"config", "set", "target", "https://127.0.0.1"}}, t, nil))
+ assert.Equal(t, "target = https://127.0.0.1\n", execute(command{configDir: configDir, args: []string{"config", "get", "target"}}, t, nil))
- assert.Equal(t, "invalid application: \"foo\"\n", executeCommand(t, nil, []string{"config", "set", "application", "foo"}, nil))
- assert.Equal(t, "application = <unset>\n", executeCommand(t, nil, []string{"config", "get", "application"}, nil))
- assert.Equal(t, "", executeCommand(t, nil, []string{"config", "set", "application", "t1.a1.i1"}, nil))
- assert.Equal(t, "application = t1.a1.i1\n", executeCommand(t, nil, []string{"config", "get", "application"}, nil))
+ assert.Equal(t, "invalid application: \"foo\"\n", execute(command{configDir: configDir, args: []string{"config", "set", "application", "foo"}}, t, nil))
+ assert.Equal(t, "application = <unset>\n", execute(command{configDir: configDir, args: []string{"config", "get", "application"}}, t, nil))
+ assert.Equal(t, "", execute(command{configDir: configDir, args: []string{"config", "set", "application", "t1.a1.i1"}}, t, nil))
+ assert.Equal(t, "application = t1.a1.i1\n", execute(command{configDir: configDir, args: []string{"config", "get", "application"}}, t, nil))
- assert.Equal(t, "target = https://127.0.0.1\napplication = t1.a1.i1\n", executeCommand(t, nil, []string{"config", "get"}, nil))
+ assert.Equal(t, "target = https://127.0.0.1\napplication = t1.a1.i1\n", execute(command{configDir: configDir, args: []string{"config", "get"}}, t, nil))
}
diff --git a/client/go/go.mod b/client/go/go.mod
index ff444215332..4b72925a0d1 100644
--- a/client/go/go.mod
+++ b/client/go/go.mod
@@ -7,6 +7,7 @@ require (
github.com/mattn/go-colorable v0.0.9
github.com/mattn/go-isatty v0.0.3
github.com/spf13/cobra v1.2.1
+ github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.7.0
)
diff --git a/client/go/vespa/crypto.go b/client/go/vespa/crypto.go
index fd28a95b3c4..a6f52aa77e2 100644
--- a/client/go/vespa/crypto.go
+++ b/client/go/vespa/crypto.go
@@ -88,6 +88,19 @@ func CreateKeyPair() (PemKeyPair, error) {
return PemKeyPair{Certificate: pemCertificate, PrivateKey: pemPrivateKey}, nil
}
+// CreateAPIKey creates a EC private key encoded as PEM
+func CreateAPIKey() ([]byte, error) {
+ privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ return nil, fmt.Errorf("failed to generate private key: %w", err)
+ }
+ privateKeyDER, err := x509.MarshalECPrivateKey(privateKey)
+ if err != nil {
+ return nil, err
+ }
+ return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: privateKeyDER}), nil
+}
+
type RequestSigner struct {
now func() time.Time
rnd io.Reader
diff --git a/client/go/vespa/crypto_test.go b/client/go/vespa/crypto_test.go
index 693da04f70b..00be4298392 100644
--- a/client/go/vespa/crypto_test.go
+++ b/client/go/vespa/crypto_test.go
@@ -1,12 +1,7 @@
package vespa
import (
- "crypto/ecdsa"
- "crypto/elliptic"
- "crypto/x509"
"encoding/base64"
- "encoding/pem"
- "io"
"math/rand"
"net/http"
"strings"
@@ -26,7 +21,10 @@ func TestCreateKeyPair(t *testing.T) {
func TestSignRequest(t *testing.T) {
fixedTime := time.Unix(0, 0)
rnd := rand.New(rand.NewSource(0)) // Fixed seed for testing purposes
- privateKey := pemECPrivateKey(t, rnd)
+ privateKey, err := CreateAPIKey()
+ if err != nil {
+ t.Fatal(err)
+ }
rs := RequestSigner{
now: func() time.Time { return fixedTime },
rnd: rnd,
@@ -54,15 +52,3 @@ func TestSignRequest(t *testing.T) {
_, err = base64.StdEncoding.DecodeString(auth)
assert.Nil(t, err)
}
-
-func pemECPrivateKey(t *testing.T, rnd io.Reader) []byte {
- key, err := ecdsa.GenerateKey(elliptic.P256(), rnd)
- if err != nil {
- t.Fatal(err)
- }
- der, err := x509.MarshalECPrivateKey(key)
- if err != nil {
- t.Fatal(err)
- }
- return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der})
-}