Skip to content

Commit

Permalink
Refactor options/config file parsing
Browse files Browse the repository at this point in the history
Rather than having config file parsing be part of the logs package,
let's move it out so that it can easily be shared between additional
commands as they arise.
  • Loading branch information
grepory committed Nov 1, 2024
1 parent 2ebe42f commit 3b7216f
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 176 deletions.
20 changes: 17 additions & 3 deletions cmd/swo/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"github.com/solarwinds/swo-cli/config"
"log"
"os"

Expand All @@ -16,13 +17,26 @@ func main() {
Usage: "SolarWinds Observability Command-Line Interface",
Version: version,
Flags: []cli.Flag{
&cli.StringFlag{Name: "api-url", Usage: "URL of the SWO API", Value: logs.DefaultAPIURL},
&cli.StringFlag{Name: "api-token", Usage: "API token"},
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "path to config", Value: logs.DefaultConfigFile},
&cli.StringFlag{Name: config.APIURLContextKey, Usage: "URL of the SWO API", Value: config.DefaultAPIURL},
&cli.StringFlag{Name: config.TokenContextKey, Usage: "API token"},
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "path to config", Value: config.DefaultConfigFile},
},
Commands: []*cli.Command{
logs.NewLogsCommand(),
},
Before: func(cCtx *cli.Context) error {
cfg, err := config.Init(cCtx.String("config"), cCtx.String("api-url"), cCtx.String("api-token"))
if err != nil {
return err
}
if err = cCtx.Set(config.APIURLContextKey, cfg.APIURL); err != nil {
return err
}
if err = cCtx.Set(config.TokenContextKey, cfg.Token); err != nil {
return err
}
return nil
},
}

if err := app.Run(os.Args); err != nil {
Expand Down
80 changes: 80 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package config

import (
"errors"
"fmt"
"gopkg.in/yaml.v3"
"os"
"os/user"
"path/filepath"
"strings"
)

const (
DefaultConfigFile = "~/.swo-cli.yml"
DefaultAPIURL = "https://api.na-01.cloud.solarwinds.com"
APIURLContextKey = "api-url"
TokenContextKey = "token"
)

var (
errMissingToken = errors.New("failed to find token")
errMissingAPIURL = errors.New("failed to find API URL")
)

type Config struct {
APIURL string `yaml:"api-url"`
Token string `yaml:"token"`
}

/*
* Precedence: CLI flags, environment, config file
*/
func Init(configPath string, apiURL string, apiToken string) (*Config, error) {
config := &Config{
APIURL: apiURL,
Token: apiToken,
}

cwd, err := os.Getwd()
if err != nil {
return nil, err
}

localConfig := filepath.Join(cwd, ".swo-cli.yaml")
if _, err := os.Stat(localConfig); err == nil {
configPath = localConfig
} else if strings.HasPrefix(configPath, "~/") {
usr, err := user.Current()
if err != nil {
return nil, fmt.Errorf("error while resolving current user to read configuration file: %w", err)
}

configPath = filepath.Join(usr.HomeDir, configPath[2:])
}

if content, err := os.ReadFile(configPath); err == nil {
err = yaml.Unmarshal(content, config)
if err != nil {
return nil, fmt.Errorf("error while unmarshaling %s config file: %w", configPath, err)
}
}

if token := os.Getenv("SWO_API_TOKEN"); token != "" {
config.Token = token
}

if url := os.Getenv("SWO_API_URL"); url != "" {
config.APIURL = url
}

if config.Token == "" {
return nil, errMissingToken
}

if config.APIURL == "" {
return nil, errMissingAPIURL
}

return config, nil
}
95 changes: 95 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package config

import (
"errors"
"github.com/stretchr/testify/require"
"os"
"testing"
)

func createConfigFile(t *testing.T, content string) string {
f, err := os.CreateTemp(os.TempDir(), "swo-config-test")
require.NoError(t, err, "creating a temporary file should not fail")

n, err := f.Write([]byte(content))
require.Equal(t, n, len(content))
require.NoError(t, err)

t.Cleanup(func() {
os.Remove(f.Name())
})

return f.Name()
}

func TestLoadConfig(t *testing.T) {
testCases := []struct {
name string
configFile string
apiURL string
token string
expected Config
expectedError error
action func()
}{
{
name: "read full config file",
expected: Config{
APIURL: "https://api.solarwinds.com",
Token: "123456",
},
configFile: func() string {
yamlStr := `
token: 123456
api-url: https://api.solarwinds.com
`
return createConfigFile(t, yamlStr)
}(),
},
{
name: "read token from config file",
expected: Config{
APIURL: DefaultAPIURL,
Token: "123456",
},
configFile: func() string {
yamlStr := "token: 123456"
return createConfigFile(t, yamlStr)
}(),
},
{
name: "read token from env var",
expected: Config{
APIURL: DefaultAPIURL,
Token: "tokenFromEnvVar",
},
action: func() {
err := os.Setenv("SWO_API_TOKEN", "tokenFromEnvVar")
require.NoError(t, err)
},
},
{
name: "missing token",
expectedError: errMissingToken,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
os.Setenv("SWO_API_TOKEN", "")
os.Setenv("SWO_API_URL", "")

if tc.action != nil {
tc.action()
}

cfg, err := Init(tc.configFile, DefaultAPIURL, "")
require.True(t, errors.Is(err, tc.expectedError), "error: %v, expected: %v", err, tc.expectedError)
if tc.expectedError != nil {
return
}

require.Equal(t, &tc.expected, cfg)
})
}
}
5 changes: 0 additions & 5 deletions logs/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ import (
"time"
)

const (
DefaultConfigFile = "~/.swo-cli.yml"
DefaultAPIURL = "https://api.na-01.cloud.solarwinds.com"
)

var (
ErrInvalidAPIResponse = errors.New("Received non-2xx status code")
ErrInvalidDateTime = errors.New("Could not parse timestamp")
Expand Down
58 changes: 16 additions & 42 deletions logs/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/solarwinds/swo-cli/config"
"io"
"net"
"net/http"
Expand Down Expand Up @@ -41,30 +42,12 @@ var (
}
)

func createConfigFile(t *testing.T, filename, content string) {
_ = os.Remove(filename)
f, err := os.Create(filename)
require.NoError(t, err, "creating a temporary file should not fail")

n, err := f.Write([]byte(content))
require.Equal(t, n, len(content))
require.NoError(t, err)

t.Cleanup(func() {
os.Remove(filename)
})
}

func TestPrepareRequest(t *testing.T) {
location, err := time.LoadLocation("GMT")
require.NoError(t, err)

time.Local = location

token := "1234567"
yamlStr := fmt.Sprintf("token: %s", token)
createConfigFile(t, configFile, yamlStr)

fixedTime, err := time.Parse(time.DateTime, "2000-01-01 10:00:30")
require.NoError(t, err)
now = fixedTime
Expand All @@ -77,16 +60,16 @@ func TestPrepareRequest(t *testing.T) {
}{
{
name: "default request",
options: &Options{configFile: configFile, APIURL: DefaultAPIURL},
options: &Options{Token: "123456"},
expectedValues: map[string][]string{},
},
{
name: "custom count group startTime and endTime",
options: &Options{
configFile: configFile,
group: "groupValue",
minTime: "10 seconds ago",
maxTime: "2 seconds ago",
Token: "123456",
group: "groupValue",
minTime: "10 seconds ago",
maxTime: "2 seconds ago",
},
expectedValues: map[string][]string{
"group": {"groupValue"},
Expand All @@ -96,17 +79,17 @@ func TestPrepareRequest(t *testing.T) {
},
{
name: "system flag",
options: &Options{configFile: configFile, system: "systemValue"},
options: &Options{Token: "123456", system: "systemValue"},
expectedValues: map[string][]string{
"filter": {`host:"systemValue"`},
},
},
{
name: "system flag with filter",
options: &Options{
args: []string{`"access denied"`, "1.2.3.4", "-sshd"},
configFile: configFile,
system: "systemValue",
Token: "123456",
args: []string{`"access denied"`, "1.2.3.4", "-sshd"},
system: "systemValue",
},
expectedValues: map[string][]string{
"filter": func() []string {
Expand Down Expand Up @@ -140,14 +123,13 @@ func TestPrepareRequest(t *testing.T) {

header := request.Header
for k, v := range map[string][]string{
"Authorization": {fmt.Sprintf("Bearer %s", token)},
"Authorization": {"Bearer 123456"},
"Accept": {"application/json"},
} {
require.ElementsMatch(t, v, header[k])
}
})
}

}

func TestRun(t *testing.T) {
Expand Down Expand Up @@ -185,19 +167,13 @@ func TestRun(t *testing.T) {
err = server.Serve(listener)
}()

token := "1234567"
yamlStr := fmt.Sprintf(`
token: %s
api-url: %s
`, token, fmt.Sprintf("http://%s", listener.Addr().String()))
createConfigFile(t, configFile, yamlStr)

r, w, err := os.Pipe()
require.NoError(t, err)

opts := &Options{
configFile: configFile,
json: true,
Token: "123456",
APIURL: fmt.Sprintf("http://%s", listener.Addr().String()),
json: true,
}

err = opts.Init([]string{})
Expand Down Expand Up @@ -253,8 +229,7 @@ func TestPrintResultStandard(t *testing.T) {

time.Local = location

createConfigFile(t, configFile, "token: 1234567")
client, err := NewClient(&Options{configFile: configFile})
client, err := NewClient(&Options{Token: "123456", APIURL: config.DefaultAPIURL})
require.NoError(t, err)

r, w, err := os.Pipe()
Expand Down Expand Up @@ -290,8 +265,7 @@ func TestPrintResultJSON(t *testing.T) {

time.Local = location

createConfigFile(t, configFile, "token: 1234567")
client, err := NewClient(&Options{configFile: configFile, json: true})
client, err := NewClient(&Options{Token: "123456", APIURL: config.DefaultAPIURL, json: true})
require.NoError(t, err)

r, w, err := os.Pipe()
Expand Down
Loading

0 comments on commit 3b7216f

Please sign in to comment.