Skip to content
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
2 changes: 1 addition & 1 deletion cmd/context/context_delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ func TestDeleteContext(t *testing.T) {
cfg, err := config.ReadConfig(cli.ConfigPath)
assert.NoError(t, err)

assert.Equal(t, 2, len(cfg.Contexts))
assert.Equal(t, 5, len(cfg.Contexts))
}
37 changes: 30 additions & 7 deletions cmd/context/context_save.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package context

import (
"encoding/base64"
"os"

"github.com/spf13/cobra"
stscobra "github.com/stackvista/stackstate-cli/internal/cobra"
"github.com/stackvista/stackstate-cli/internal/common"
Expand All @@ -13,12 +16,15 @@ const (
)

type SaveArgs struct {
Name string
URL string
APIToken string
ServiceToken string
APIPath string
SkipValidate bool
Name string
URL string
APIToken string
ServiceToken string
APIPath string
CaCertPath string
CaCertBase64Data string
SkipValidate bool
SkipSSLFlag bool
}

func SaveCommand(cli *di.Deps) *cobra.Command {
Expand All @@ -36,7 +42,9 @@ func SaveCommand(cli *di.Deps) *cobra.Command {
cmd.Flags().StringVar(&args.ServiceToken, common.ServiceTokenFlag, "", common.ServiceTokenFlagUse)
cmd.Flags().StringVar(&args.APIPath, APIPathFlag, "/api", "Specify the path of the API end-point, e.g. the part that comes after the URL")
cmd.Flags().BoolVar(&args.SkipValidate, "skip-validate", false, "Skip validation of the context")

cmd.Flags().StringVar(&args.CaCertPath, common.CaCertPathFlag, "", common.CaCertPathFlagUse)
cmd.Flags().StringVar(&args.CaCertBase64Data, common.CaCertBase64DataFlag, "", common.CaCertBase64DataFlagUse)
cmd.Flags().BoolVar(&args.SkipSSLFlag, common.SkipSSLFlag, false, common.SkipSSLFlagUse)
cmd.MarkFlagRequired(common.URLFlag) //nolint:errcheck
stscobra.MarkMutexFlags(cmd, []string{common.APITokenFlag, common.ServiceTokenFlag, common.K8sSATokenFlag}, "tokens", true)

Expand All @@ -57,8 +65,23 @@ func RunContextSaveCommand(args *SaveArgs) func(cli *di.Deps, cmd *cobra.Command
APIToken: args.APIToken,
ServiceToken: args.ServiceToken,
APIPath: args.APIPath,
SkipSSL: args.SkipSSLFlag,
},
}
// Use private CA only if SkipSSL is not enabled
if !args.SkipSSLFlag {
// Providing CA certificate from file takes precedence over providing from the command line argument.
if args.CaCertPath != "" {
data, serr := os.ReadFile(args.CaCertPath)
if serr != nil {
return common.NewReadFileError(serr, args.CaCertPath)
}
namedCtx.Context.CaCertBase64Data = base64.StdEncoding.EncodeToString(data)
namedCtx.Context.CaCertPath = ""
} else if args.CaCertBase64Data != "" {
namedCtx.Context.CaCertBase64Data = args.CaCertBase64Data
}
}

if !args.SkipValidate {
if _, err := ValidateContext(cli, cmd, namedCtx.Context); err != nil {
Expand Down
180 changes: 138 additions & 42 deletions cmd/context/context_save_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package context
import (
"testing"

"github.com/stretchr/testify/require"

"github.com/spf13/cobra"
"github.com/stackvista/stackstate-cli/internal/config"
"github.com/stackvista/stackstate-cli/internal/di"
Expand All @@ -16,50 +18,144 @@ func setupSaveCmd(t *testing.T) (*di.MockDeps, *cobra.Command) {
return &cli, cmd
}

func TestSaveNewContext(t *testing.T) {
cli, cmd := setupSaveCmd(t)
setupConfig(t, cli)
_, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, "--name", "baz", "--url", "http://baz.com", "--api-token", "my-token")
assert.NoError(t, err)

cfg, err := config.ReadConfig(cli.ConfigPath)
assert.NoError(t, err)
assert.Equal(t, "baz", cfg.CurrentContext)
assert.Len(t, cfg.Contexts, 4)

validateContext(t, cfg, cfg.CurrentContext, "http://baz.com", "my-token", "", "", "/api")
}

func TestSaveExistingContext(t *testing.T) {
cli, cmd := setupSaveCmd(t)
setupConfig(t, cli)
func TestSaveContext(t *testing.T) { //nolint:funlen
tests := []struct {
name string
args []string
expectedContext config.NamedContext
totalContextInConfig int
wantErr bool
errorMessage string
}{
{
name: "new context",
args: []string{"--name", "baz", "--url", "http://baz.com", "--api-token", "my-token"},
expectedContext: config.NamedContext{
Name: "baz",
Context: &config.StsContext{
URL: "http://baz.com",
APIToken: "my-token",
APIPath: "/api",
},
},
totalContextInConfig: 7,
wantErr: false,
},
{
name: "existing context",
args: []string{"--name", "bar", "--url", "http://bar.com", "--service-token", "my-token"},
expectedContext: config.NamedContext{
Name: "bar",
Context: &config.StsContext{
URL: "http://bar.com",
ServiceToken: "my-token",
APIPath: "/api",
},
},
totalContextInConfig: 6,
wantErr: false,
},
{
name: "existing context ca-cert is set with ca-cert-path",
args: []string{"--name", "bar", "--url", "http://bar.com", "--service-token", "my-token", "--ca-cert-path", "testdata/selfSignedCert.crt"},
expectedContext: config.NamedContext{
Name: "bar",
Context: &config.StsContext{
URL: "http://bar.com",
ServiceToken: "my-token",
APIPath: "/api",
CaCertBase64Data: selfSignedBase64Cert,
},
},
totalContextInConfig: 6,
wantErr: false,
},
{
name: "new context ca-cert is set with ca-cert-path",
args: []string{"--name", "cacertdata", "--url", "http://bar.com", "--service-token", "my-token", "--ca-cert-base64-data", privateCaBase64Cert},
expectedContext: config.NamedContext{
Name: "cacertdata",
Context: &config.StsContext{
URL: "http://bar.com",
ServiceToken: "my-token",
APIPath: "/api",
CaCertBase64Data: privateCaBase64Cert,
},
},
totalContextInConfig: 7,
wantErr: false,
},
{
name: "ca-cert-path takes precedence over ca-cert-base64-data",
args: []string{"--name", "cacertdata", "--url", "http://bar.com", "--service-token", "my-token", "--ca-cert-path", "testdata/selfSignedCert.crt", "--ca-cert-base64-data", privateCaBase64Cert},
expectedContext: config.NamedContext{
Name: "cacertdata",
Context: &config.StsContext{
URL: "http://bar.com",
ServiceToken: "my-token",
APIPath: "/api",
CaCertBase64Data: selfSignedBase64Cert,
},
},
totalContextInConfig: 7,
wantErr: false,
},
{
name: "ca-cert ignored if skip-ssl is set",
args: []string{"--name", "bar", "--url", "http://bar.com", "--service-token", "my-token", "--skip-ssl", "--ca-cert-path", "/path/to/ca.crt", "--ca-cert-base64-data", "base64-data"},
expectedContext: config.NamedContext{
Name: "bar",
Context: &config.StsContext{
URL: "http://bar.com",
ServiceToken: "my-token",
APIPath: "/api",
SkipSSL: true,
CaCertBase64Data: "",
CaCertPath: "",
},
},
totalContextInConfig: 6,
wantErr: false,
},
{
name: "no save on missing tokens",
args: []string{"--name", "bar", "--url", "http://my-bar.com"},
expectedContext: config.NamedContext{},
wantErr: true,
errorMessage: "one of the required flags {api-token | service-token} not set",
},
{
name: "ca-cert-path is not found",
args: []string{"--name", "bar", "--url", "http://my-bar.com", "--service-token", "my-token", "--ca-cert-path", "/path/to/ca.crt"},
expectedContext: config.NamedContext{},
wantErr: true,
errorMessage: "no such file or directory",
},
}

_, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, "--name", "bar", "--url", "http://bar.com", "--service-token", "my-token")
assert.NoError(t, err)

cfg, err := config.ReadConfig(cli.ConfigPath)
assert.NoError(t, err)
assert.Equal(t, "bar", cfg.CurrentContext)
assert.Len(t, cfg.Contexts, 3)
validateContext(t, cfg, cfg.CurrentContext, "http://bar.com", "", "my-token", "", "/api")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cli, cmd := setupSaveCmd(t)
setupConfig(t, cli)
_, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, tt.args...)
if tt.wantErr {
require.Error(t, err)
if tt.errorMessage != "" {
assert.Contains(t, err.Error(), tt.errorMessage)
}
} else {
cfg, err := config.ReadConfig(cli.ConfigPath)
assert.NoError(t, err)
assert.Equal(t, tt.expectedContext.Name, cfg.CurrentContext)
assert.Len(t, cfg.Contexts, tt.totalContextInConfig)
validateContext(t, cfg, tt.expectedContext)
}
})
}
}

func validateContext(t *testing.T, cfg *config.Config, name string, url string, apiToken, serviceToken, k8sSAToken string, apiPath string) {
ctx, err := cfg.GetContext(name)
func validateContext(t *testing.T, cfg *config.Config, expectedContext config.NamedContext) {
ctx, err := cfg.GetContext(expectedContext.Name)
assert.NoError(t, err)
assert.Equal(t, url, ctx.Context.URL)
assert.Equal(t, apiToken, ctx.Context.APIToken)
assert.Equal(t, serviceToken, ctx.Context.ServiceToken)
assert.Equal(t, k8sSAToken, ctx.Context.K8sSAToken)
assert.Equal(t, apiPath, ctx.Context.APIPath)
}

func TestNoSaveOnMissingTokens(t *testing.T) {
cli, cmd := setupSaveCmd(t)

_, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, "--name", "bar", "--url", "http://my-bar.com")
assert.Errorf(t, err, "missing required argument: --api-token")

// Should not have written config file
assert.NoFileExists(t, cli.ConfigPath)
assert.Equal(t, expectedContext.Context, ctx.Context)
}
30 changes: 21 additions & 9 deletions cmd/context/test_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,23 @@ import (
"github.com/stackvista/stackstate-cli/internal/di"
)

const (
//nolint:lll
selfSignedBase64Cert = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNKekNDQWRHZ0F3SUJBZ0lVVi9hSmoxZkVjQ2dOVTJGYWZZMHVSTHF5N21Bd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0tURW5NQ1VHQTFVRUF3d2VkbWxzYVdGcmIzWXVjMkZ1WkdKdmVDNXpkR0ZqYTNOMFlYUmxMbWx2TUNBWApEVEkxTURjeU1USXdORFUxTmxvWUR6SXhNalV3TmpJM01qQTBOVFUyV2pBcE1TY3dKUVlEVlFRRERCNTJhV3hwCllXdHZkaTV6WVc1a1ltOTRMbk4wWVdOcmMzUmhkR1V1YVc4d1hEQU5CZ2txaGtpRzl3MEJBUUVGQUFOTEFEQkkKQWtFQW9wUXVPSmZJa0xDV0pLVDcwaGdiSEpwVWtFQitaYTJwOXVBMUlOUktNNEFyN2RjVjltdXhOS09jSloycwpWdCtiK1lTS1c4cnRteE5QUVh1RTJENHRlUUlEQVFBQm80SE9NSUhMTUIwR0ExVWREZ1FXQkJRVTBPTFZRRzEyCndNb0VLSGdxSG1aeVhTelozekFmQmdOVkhTTUVHREFXZ0JRVTBPTFZRRzEyd01vRUtIZ3FIbVp5WFN6WjN6QVAKQmdOVkhSTUJBZjhFQlRBREFRSC9NSGdHQTFVZEVRUnhNRytDSG5acGJHbGhhMjkyTG5OaGJtUmliM2d1YzNSaApZMnR6ZEdGMFpTNXBiNElqYjNSc2NDMTJhV3hwWVd0dmRpNXpZVzVrWW05NExuTjBZV05yYzNSaGRHVXVhVytDCktHOTBiSEF0YUhSMGNDMTJhV3hwWVd0dmRpNXpZVzVrWW05NExuTjBZV05yYzNSaGRHVXVhVzh3RFFZSktvWkkKaHZjTkFRRUxCUUFEUVFBZllBVk1lTVJHbFcrR1prellPeGRIaVhYNEFISHA5SWxvWlBMbUJHNExtdlpDODBoVgpLNGNSVUVHSGtSeGdrMGgwYzl3RDhOZFZSM1FuRTBubjZXUEUKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
//nolint:lll
privateCaBase64Cert = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZiVENDQTFXZ0F3SUJBZ0lVVGdGVm56eFNpbGR6MC9VenQ2UVR0bGpyMWVZd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1JURUxNQWtHQTFVRUJoTUNRVlV4RXpBUkJnTlZCQWdNQ2xOdmJXVXRVM1JoZEdVeElUQWZCZ05WQkFvTQpHRWx1ZEdWeWJtVjBJRmRwWkdkcGRITWdVSFI1SUV4MFpEQWdGdzB5TlRBM01qRXlNRFEzTlRCYUdBOHlNVEkxCk1EWXlOekl3TkRjMU1Gb3dSVEVMTUFrR0ExVUVCaE1DUVZVeEV6QVJCZ05WQkFnTUNsTnZiV1V0VTNSaGRHVXgKSVRBZkJnTlZCQW9NR0VsdWRHVnlibVYwSUZkcFpHZHBkSE1nVUhSNUlFeDBaRENDQWlJd0RRWUpLb1pJaHZjTgpBUUVCQlFBRGdnSVBBRENDQWdvQ2dnSUJBSTRjbEJlRFNoeEpBZ09lWjIyaERiaUViTVArc1dtRCsxVTdlNkZqCjhRelVVMkFWRkdvWjAwbEdUSDlxZVN4T1ZDMittWlBmb3ZTcmR0S2xYYm9PdEV0TldBZmhxZ2twOGh1ODRZb2UKaWxLT3YybWYvS0N0SzBPeTVkNlEwK3FPb2RPZVlIYlBLQk9vVDUya1FZMWZYeFNlNG8zc0tyZFQ3eGRhUi8xYgpiSGVUeWxuZmlmV3d0NmNiVlpOb1IxYmZ6ZnJYdjhkYk94emVqNWJ3SlVCeDNiaFI0UHN4Tm9JRDUrVUZMeHdxCmhOT3FZMEhIcU13djN2clYwQ2ZnWWNkZmRWaVBZalJvejNNaTBDallMRllmeWQ2eDF4azM3RTZ5MnVXQVoxY2EKVXJjSGlORVp6c0sxQTd1Y1BLWDh5WTVjWkY5MHBUMmhHWnNGT2NjQmxyYTZQVVA5ZXFwTm1pYm1zbWNXdzBWQwp6WEswenpkMUVnMzRnWlplQjI5eko1MWJ0QlNoazZqc3pRaUFlSElEeWJnOEdzYWhob2NPUjhEd3dtL3Ezekw2CnRiY0ZKZS9TWDFrQTE2TFZHMzZMYTRnb3IrQ1E5b1Zxb3N0OU1sQzBvRktoUmpoYnM5ZGdSWlJ5TFhMMEZ0UysKTDJIQ0NyY2krcUpwT2hjSTZQMDhzR1owOWlBd3h2c1AzYjY5S0J0RVlFREJmL29QSVJWSmRBYithMnBocVc5QgpoUGFYVXpGOFQ5QkJLQzJHKytIeHlKcTU3QlQ3T3FpNXRQTW91ZlRMRXNiQlgrNkViTVZmOGV5SllONjFKak0xCmJMOUZ1MFkwNW9NRFFQcC83RWk0dGp3TFQ1S2VuWGJWbnZUN0s0aGo5MTlNbXpBbytOOWNWeklvOVZNMEF0U0gKbk52SEFnTUJBQUdqVXpCUk1CMEdBMVVkRGdRV0JCUk5ETFFMNnkvL213Wi83SEtEWEdIMnhwNHVqakFmQmdOVgpIU01FR0RBV2dCUk5ETFFMNnkvL213Wi83SEtEWEdIMnhwNHVqakFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQTBHCkNTcUdTSWIzRFFFQkN3VUFBNElDQVFBM1FsWThnM2NmdWJ3akRmWnpXbVowWWhBUEgwb2lXTkhZd25YOTQvY2sKaWRjQzUzblRuVC9yN3lnZlNsVk8wbllUelg2YS8rWXFXWFczT0ZBcXZUREZYVis2bVhTb3FWQ0ptRlAzUVh6TwpuTmthcmEzcWhIKzZHVVE2RnFVaEpza1hZNHdMT05FT2Q2T1VlNmcwZ1NTalZJUkVxVWVYeWZvYUlJR1owNVNhClNVRDRVQnczT0U4ZVhWaTIyWHVCaWpTMWVTRHd6a3RDdWc5MW9BeWVlUGRpSWp5UGNiMmVQdzMyZE1JcDZoYU4KR1lFMnNPR3l0aWtKTnBnbmNqR3RGdkRaSzFkaVNvQWxzM21FR3hjVTdXd05WMlFzN24vTGJqbUNENjQ1WXRFWgozVnJZNG10bEs1dEN3RURNcUFYK3ZScXJ2L09CL1R0Z3FvUG5HdmJkdWNoNThyMGIyUmtyZ3BtaUt1a3FxRUc3ClJiQmJNeWlSMXpjWmJoQm9SbnkxcXVEWm52MmxmVUJUdHVpV1JIUDNSRTRBNEIrYnp4bTI0UkVYZHRTSVUrUXAKaytZZjNuRGg5Y1Z2akpMWDZ5dmdmOUN5ZHIyQ2FVM015aTBCdmUyUnVJUm15VXlFYkE1MWUzV1F0NVF6emU2TApSS3A1a0JQR2ZjRTRTMmdDdi9DYktqQjV2V1doY2tieW9NL0pJMVFpSU94U1puOHFGWXg3NFdkMEJsYTNNaFhNClBOcXo3eDZxb3pWa1FzWTRBK1FEOUhnZE1Rbms3QlhKR01tbzJ3OSszdVB2SGJCdXJSV0FTMXRISVlUVlA0MVkKYXlXci9wTncwVWsrS2drMkdXQmx2T3VZSXMzN2RnMkw3RkNUeGR2bXU2dHNpK3QvUEpqcTNFWGkweDFzcE1aWAo5UT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
)

func setupConfig(t *testing.T, cli *di.MockDeps) {
cfg := &config.Config{
CurrentContext: "foo",
Contexts: []*config.NamedContext{
{Name: "foo", Context: newContext("http://foo.com", "apiToken", "", "", "/api")},
{Name: "bar", Context: newContext("http://bar.com", "", "svctok-xxxx", "", "/api/v1")},
{Name: "foobar", Context: newContext("http://bar.com", "", "", "eyJhbGc", "/api/v1")},
{Name: "foo", Context: newContext("http://foo.com", "apiToken", "", "", "/api", false, "")},
{Name: "bar", Context: newContext("http://bar.com", "", "svctok-xxxx", "", "/api/v1", false, "")},
{Name: "foobar", Context: newContext("http://bar.com", "", "", "eyJhbGc", "/api/v1", false, "")},
{Name: "skipssl", Context: newContext("http://bar.com", "", "", "eyJhbGc", "/api/v1", true, "")},
{Name: "privateca", Context: newContext("http://bar.com", "", "", "eyJhbGc", "/api/v1", false, privateCaBase64Cert)},
{Name: "selfsigned", Context: newContext("http://bar.com", "", "", "eyJhbGc", "/api/v1", false, selfSignedBase64Cert)},
},
}
cli.ConfigPath = filepath.Join(t.TempDir(), "config.yaml")
Expand All @@ -25,12 +35,14 @@ func setupConfig(t *testing.T, cli *di.MockDeps) {
}
}

func newContext(url, apiToken, serviceToken, k8sSAToken, apiPath string) *config.StsContext {
func newContext(url, apiToken, serviceToken, k8sSAToken, apiPath string, skipSSL bool, caCertBase64Data string) *config.StsContext {
return &config.StsContext{
URL: url,
APIToken: apiToken,
ServiceToken: serviceToken,
K8sSAToken: k8sSAToken,
APIPath: apiPath,
URL: url,
APIToken: apiToken,
ServiceToken: serviceToken,
K8sSAToken: k8sSAToken,
APIPath: apiPath,
SkipSSL: skipSSL,
CaCertBase64Data: caCertBase64Data,
}
}
14 changes: 14 additions & 0 deletions cmd/context/testdata/selfSignedCert.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICJzCCAdGgAwIBAgIUV/aJj1fEcCgNU2FafY0uRLqy7mAwDQYJKoZIhvcNAQEL
BQAwKTEnMCUGA1UEAwwedmlsaWFrb3Yuc2FuZGJveC5zdGFja3N0YXRlLmlvMCAX
DTI1MDcyMTIwNDU1NloYDzIxMjUwNjI3MjA0NTU2WjApMScwJQYDVQQDDB52aWxp
YWtvdi5zYW5kYm94LnN0YWNrc3RhdGUuaW8wXDANBgkqhkiG9w0BAQEFAANLADBI
AkEAopQuOJfIkLCWJKT70hgbHJpUkEB+Za2p9uA1INRKM4Ar7dcV9muxNKOcJZ2s
Vt+b+YSKW8rtmxNPQXuE2D4teQIDAQABo4HOMIHLMB0GA1UdDgQWBBQU0OLVQG12
wMoEKHgqHmZyXSzZ3zAfBgNVHSMEGDAWgBQU0OLVQG12wMoEKHgqHmZyXSzZ3zAP
BgNVHRMBAf8EBTADAQH/MHgGA1UdEQRxMG+CHnZpbGlha292LnNhbmRib3guc3Rh
Y2tzdGF0ZS5pb4Ijb3RscC12aWxpYWtvdi5zYW5kYm94LnN0YWNrc3RhdGUuaW+C
KG90bHAtaHR0cC12aWxpYWtvdi5zYW5kYm94LnN0YWNrc3RhdGUuaW8wDQYJKoZI
hvcNAQELBQADQQAfYAVMeMRGlW+GZkzYOxdHiXX4AHHp9IloZPLmBG4LmvZC80hV
K4cRUEGHkRxgk0h0c9wD8NdVR3QnE0nn6WPE
-----END CERTIFICATE-----
Loading
Loading