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

use k6 cloud credentials #98

Merged
merged 3 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,17 @@ Since k6exec tries to emulate the `k6` command line, the `help` command or the `

### Prerequisites

k6exec tries to provide the appropriate k6 executable after detecting the extension dependencies using a build service.
k6exec tries to provide the appropriate k6 executable after detecting the extension dependencies. This can be done using a build service or a native builder.

#### Build Service

No additional installation is required to use the build service, just provide the build service URL.

The build service URL can be specified in the `K6_BUILD_SERVICE_URL` environment variable or by using the `--build-service-url` flag.

There is no default URL for the build service, otherwise k6exec will automatically provide k6 with the native builder.
If the build service requires authentication, you can specify the authentication token using the `K6_BUILD_SERVICE_AUTH` environment variable.

If the `k6_BUILD_SERVICE_URL` is not specified, `k6exec` tries to use the build service provided by Grafana Cloud K6 using the credential obtained from the [k6 cloud login](https://grafana.com/docs/grafana-cloud/testing/k6/author-run/tokens-and-cli-authentication/) command. You can also provide this credentials using the `K6_CLOUD_TOKEN` environment variable.

### Dependencies

Expand Down
23 changes: 23 additions & 0 deletions cmd/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package cmd

import (
"context"
"fmt"
"strings"

"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -45,3 +47,24 @@ func getArgs(cmd *cobra.Command) []string {

return nil
}

// getFlagValue returns the value of the flag from the command arguments given its name and (optional) shortName.
// If the flag is not found it returns an empty string.
// if it is found but the next element is not its value, it returns an error.
func getFlagValue(cmd *cobra.Command, fullName string, shortName string) (string, error) {
args := getArgs(cmd)

for i, arg := range args {
if arg == fullName || (shortName != "" && arg == shortName) {
// if this is the last argument or the next element is a flag, return an empty string
// this is an error in thc CLI arguments
if i+1 == len(args) || strings.HasPrefix(args[i+1], "-") {
return "", fmt.Errorf("flag %s is missing a value", arg)
}

return args[i+1], nil
}
}

return "", nil
}
4 changes: 3 additions & 1 deletion cmd/help.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ No additional installation is required to use the build service, just provide th

The build service URL can be specified in the `K6_BUILD_SERVICE_URL` environment variable or by using the `--build-service-url` flag.

There is no default URL for the build service, otherwise k6exec will automatically provide k6 with the native builder.
If the build service requires authentication, you can specify the authentication token using the `K6_BUILD_SERVICE_AUTH` environment variable.

If the `k6_BUILD_SERVICE_URL` is not specified, `k6exec` tries to use the build service provided by Grafana Cloud K6 using the credential obtained from the [k6 cloud login](https://grafana.com/docs/grafana-cloud/testing/k6/author-run/tokens-and-cli-authentication/) command. You can also provide this credentials using the `K6_CLOUD_TOKEN` environment variable.

### Dependencies

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

import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
)

// structure of the config file with the fields that are used by k6exec
type k6configFile struct {
Collectors struct {
Cloud struct {
Token string `json:"token"`
} `json:"cloud"`
Copy link
Collaborator

Choose a reason for hiding this comment

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

More FYI and not sure that we actually need workaround that, but until the k6 v0.50 the structure was different and had more layers, like ext.loadimpact was what currently cloud is.

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'll open an issue and we can discuss if this is required.

} `json:"collectors"`
}

// loadConfig loads the k6 config file from the given path or the default location.
// if using the default location and the file does not exist, it returns an empty config.
func loadConfig(configPath string) (k6configFile, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Will be it difficult to add the debug logs here for success path, which we can enable by increasing verbosity (-v). Something like Resolved config: /home/oleg/.config/k6/config.json` which could be useful for us if we need to investigate the things?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That will require some changes in the code as we are not logging anything at the moment. I will open an issue an address this in a follow-up PR.

var (
config k6configFile
usingDefault bool
homeDir string
err error
)

if configPath == "" {
usingDefault = true
homeDir, err = os.UserConfigDir() //nolint:forbidigo
if err != nil {
return config, fmt.Errorf("failed to get user home directory: %w", err)
}
configPath = filepath.Join(homeDir, "loadimpact", "k6", "config.json")
}

buffer, err := os.ReadFile(configPath) //nolint:forbidigo,gosec
if err != nil {
if errors.Is(err, os.ErrNotExist) && usingDefault { //nolint:forbidigo
return config, nil
}
return config, fmt.Errorf("failed to read config file %q: %w", configPath, err)
}

err = json.Unmarshal(buffer, &config)
if err != nil {
return config, fmt.Errorf("failed to parse config file %q: %w", configPath, err)
}

return config, nil
}
34 changes: 33 additions & 1 deletion cmd/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import (
"github.com/spf13/cobra"
)

const (
defaultBuildServiceURL = "https://ingest.k6.io/builder/api/v1"
)

type state struct {
k6exec.Options
buildServiceURL string
Expand All @@ -21,6 +25,7 @@ type state struct {
levelVar *slog.LevelVar
cmd *exec.Cmd
cleanup func() error
configFile string
}

func newState(levelVar *slog.LevelVar) *state {
Expand All @@ -31,11 +36,38 @@ func newState(levelVar *slog.LevelVar) *state {
return s
}

func (s *state) persistentPreRunE(_ *cobra.Command, _ []string) error {
func (s *state) persistentPreRunE(cmd *cobra.Command, _ []string) error {
var err error

s.Options.BuildServiceURL = defaultBuildServiceURL
if len(s.buildServiceURL) > 0 {
s.Options.BuildServiceURL = s.buildServiceURL
}

// get authorization token for the build service
auth := os.Getenv("K6_CLOUD_TOKEN") //nolint:forbidigo

if len(auth) == 0 {
// allow overriding the config file for testing
configFile := s.configFile
if configFile == "" {
// check if the command has a 'config' flag and get the value
configFile, err = getFlagValue(cmd, "--config", "-c")
if err != nil {
return err
}
}

config, err := loadConfig(configFile)
if err != nil {
return err
}

auth = config.Collectors.Cloud.Token
}

s.Options.BuildServiceToken = auth

if s.verbose && s.levelVar != nil {
s.levelVar.Set(slog.LevelDebug)
}
Expand Down
39 changes: 34 additions & 5 deletions cmd/state_internal_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"context"
"io"
"log/slog"
"os"
Expand All @@ -9,6 +10,7 @@ import (

"github.com/grafana/k6build/pkg/testutils"
"github.com/grafana/k6exec"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
)

Expand All @@ -31,25 +33,52 @@ func Test_interal_state(t *testing.T) { //nolint:tparallel
})

t.Run("Test_persistentPreRunE", func(t *testing.T) { //nolint:paralleltest
cmd := &cobra.Command{}
st := &state{levelVar: new(slog.LevelVar)}

require.NoError(t, st.persistentPreRunE(nil, nil))
require.Empty(t, st.BuildServiceURL)
require.NoError(t, st.persistentPreRunE(cmd, nil))
require.Equal(t, defaultBuildServiceURL, st.BuildServiceURL)
require.Equal(t, slog.LevelInfo, st.levelVar.Level())

st.buildServiceURL = "http://example.com"

require.NoError(t, st.persistentPreRunE(nil, nil))
require.NoError(t, st.persistentPreRunE(cmd, nil))
require.Equal(t, "http://example.com", st.BuildServiceURL)

st.buildServiceURL = "http://example.com"
st.verbose = true

require.NoError(t, st.persistentPreRunE(nil, nil))
require.NoError(t, st.persistentPreRunE(cmd, nil))
require.Equal(t, slog.LevelDebug, st.levelVar.Level())

st.levelVar = nil
require.NoError(t, st.persistentPreRunE(nil, nil))
require.NoError(t, st.persistentPreRunE(cmd, nil))
})

t.Run("Test_loadConfig", func(t *testing.T) { //nolint:paralleltest
st := &state{
levelVar: new(slog.LevelVar),
configFile: filepath.Join("testdata", "config", "valid.json"),
}

require.NoError(t, st.persistentPreRunE(&cobra.Command{}, nil))
require.Equal(t, "token", st.Options.BuildServiceToken)

st = &state{
levelVar: new(slog.LevelVar),
configFile: filepath.Join("testdata", "config", "empty.json"),
}

require.NoError(t, st.persistentPreRunE(&cobra.Command{}, nil))
require.Empty(t, st.Options.BuildServiceToken)

// test config override from flag
cmd := &cobra.Command{Use: "test"}
cmd.SetContext(context.WithValue(context.Background(), argsKey{}, []string{"test", "--config", "no_such_file.json"}))
st = &state{
levelVar: new(slog.LevelVar),
}
require.Error(t, st.persistentPreRunE(cmd, nil))
})

t.Run("Test_preRunE", func(t *testing.T) { //nolint:paralleltest
Expand Down
1 change: 1 addition & 0 deletions cmd/testdata/config/empty.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
7 changes: 7 additions & 0 deletions cmd/testdata/config/valid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"collectors": {
"cloud": {
"token": "token"
}
}
}
3 changes: 3 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@ type Options struct {
// BuildServiceURL contains the URL of the k6 build service to be used.
// If the value is not nil, the k6 binary is built using the build service instead of the local build.
BuildServiceURL string
// BuildServiceToken contains the token to be used to authenticate with the build service.
// Defaults to K6_CLOUD_TOKEN environment variable is set, or the value stored in the k6 config file.
BuildServiceToken string
}
1 change: 1 addition & 0 deletions provision.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ func provision(ctx context.Context, deps k6deps.Dependencies, opts *Options) (st

if opts != nil {
config.BuildServiceURL = opts.BuildServiceURL
config.BuildServiceAuth = opts.BuildServiceToken
}

provider, err := k6provider.NewProvider(config)
Expand Down
Loading