diff --git a/cmd/paasta-metastatus/main.go b/cmd/paasta-metastatus/main.go index 7a32604..b8dc5d8 100644 --- a/cmd/paasta-metastatus/main.go +++ b/cmd/paasta-metastatus/main.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "io" "net/url" "os" "strings" @@ -17,6 +18,7 @@ import ( apiclient "github.com/Yelp/paasta-tools-go/pkg/paasta_api/client" "github.com/Yelp/paasta-tools-go/pkg/paasta_api/client/operations" + "github.com/go-openapi/runtime" httptransport "github.com/go-openapi/runtime/client" "github.com/go-openapi/strfmt" ) @@ -44,7 +46,7 @@ func parseFlags(opts *PaastaMetastatusOptions) error { return nil } -func printDashboards( +func writeDashboards( cluster string, dashboards map[string]interface{}, sb *strings.Builder, ) { if dashboards == nil { @@ -64,13 +66,15 @@ func printDashboards( switch d := dashboard.(type) { case string: sb.WriteString(aurora.Cyan(d).String()) - case []interface{}: + case []string: if len(d) > 1 { for _, url := range d { - sb.WriteString(fmt.Sprintf("\n %v", aurora.Cyan(url.(string)))) + sb.WriteString( + fmt.Sprintf("\n %v", aurora.Cyan(url)), + ) } - } else { - sb.WriteString(aurora.Cyan(d[0].(string)).String()) + } else if len(d) == 1 { + sb.WriteString(aurora.Cyan(d[0]).String()) } } sb.WriteString("\n") @@ -78,7 +82,7 @@ func printDashboards( } } -func getMetastatusCmdArgs(opts *PaastaMetastatusOptions) ([]string, time.Duration) { +func buildMetastatusCmdArgs(opts *PaastaMetastatusOptions) ([]string, time.Duration) { cmdArgs := []string{} verbosity := 0 timeout := time.Duration(20) @@ -108,7 +112,15 @@ func getMetastatusCmdArgs(opts *PaastaMetastatusOptions) ([]string, time.Duratio return cmdArgs, timeout } -func printAPIStatus( +type ctxKey int + +const ( + ctxKeyTransport ctxKey = iota + ctxKeyOut + ctxKeyErr +) + +func writeAPIStatus( ctx context.Context, cluster, endpoint string, cmdArgs []string, @@ -119,11 +131,15 @@ func printAPIStatus( return fmt.Errorf("Failed to parse API endpoint %v: %v", endpoint, err) } - var ( + var transport runtime.ClientTransport + ctxTransport := ctx.Value(ctxKeyTransport) + if ctxTransport != nil { + transport = ctxTransport.(runtime.ClientTransport) + } else { transport = httptransport.New(url.Host, apiclient.DefaultBasePath, []string{url.Scheme}) - client = apiclient.New(transport, strfmt.Default) - ) + } + client := apiclient.New(transport, strfmt.Default) mp := &operations.MetastatusParams{CmdArgs: cmdArgs, Context: ctx} resp, err := client.Operations.Metastatus(mp) if err != nil { @@ -141,54 +157,30 @@ func getClusterStatus( ) (*strings.Builder, error) { sb := &strings.Builder{} sb.WriteString(fmt.Sprintf("Cluster: %v\n", cluster)) - printDashboards(cluster, dashboards, sb) - err := printAPIStatus(ctx, cluster, endpoint, cmdArgs, sb) + writeDashboards(cluster, dashboards, sb) + err := writeAPIStatus(ctx, cluster, endpoint, cmdArgs, sb) if err != nil { return sb, fmt.Errorf("Failed to get status for cluster %v: %v", cluster, err) } return sb, nil } -func metastatus(opts *PaastaMetastatusOptions) (bool, error) { - if opts.AutoscalingInfo { - if opts.Verbosity < 2 { - opts.Verbosity = 2 - } - } - sysStore := configstore.NewStore(opts.SysDir, nil) - - apiEndpoints := map[string]string{} - ok, err := sysStore.Load("api_endpoints", &apiEndpoints) - if !ok || err != nil { - return false, fmt.Errorf("Failed to load api_endpoints from configs: found=%v, error=%v", ok, err) - } - - dashboardLinks := map[string]map[string]interface{}{} - ok, err = sysStore.Load("dashboard_links", &dashboardLinks) - if !ok || err != nil { - return false, fmt.Errorf("Failed to load dashboard_links from configs: found=%v, error=%v", ok, err) - } - - var clusters []string - if opts.Cluster != "" { - clusters = []string{opts.Cluster} - } else { - ok, err := sysStore.Load("clusters", &clusters) - if !ok || err != nil { - return false, fmt.Errorf("Failed to load clusters from configs: found=%v, error=%v", ok, err) - } - } - - cmdArgs, timeout := getMetastatusCmdArgs(opts) - ctx, cancel := context.WithTimeout(context.Background(), timeout*time.Second) - defer cancel() +func metastatus( + ctx context.Context, + clusters []string, + apiEndpoints map[string]string, + dashboardLinks map[string]map[string]interface{}, + cmdArgs []string, +) (bool, error) { + outf := ctx.Value(ctxKeyOut).(io.Writer) + errf := ctx.Value(ctxKeyErr).(io.Writer) var wg sync.WaitGroup var success bool = true for _, cluster := range clusters { endpoint, ok := apiEndpoints[cluster] if !ok { - fmt.Printf("WARN: api endpoint not found for %v\n", cluster) + fmt.Fprintf(errf, "WARN: api endpoint not found for %v\n", cluster) continue } dashboards, _ := dashboardLinks[cluster] @@ -196,9 +188,9 @@ func metastatus(opts *PaastaMetastatusOptions) (bool, error) { go func(cluster, endpoint string) { defer wg.Done() sb, err := getClusterStatus(ctx, cluster, endpoint, dashboards, cmdArgs) - fmt.Print(sb) + fmt.Fprint(outf, sb) if err != nil { - fmt.Fprint(os.Stderr, err.Error()) + fmt.Fprint(errf, err.Error()) } }(cluster, endpoint) } @@ -219,7 +211,47 @@ func main() { flag.PrintDefaults() os.Exit(0) } - success, err := metastatus(options) + + if options.AutoscalingInfo { + if options.Verbosity < 2 { + options.Verbosity = 2 + } + } + sysStore := configstore.NewStore(options.SysDir, nil) + + apiEndpoints := map[string]string{} + ok, err := sysStore.Load("api_endpoints", &apiEndpoints) + if !ok || err != nil { + fmt.Fprintf(os.Stderr, "Failed to load api_endpoints from configs: found=%v, error=%v", ok, err) + os.Exit(1) + } + + dashboardLinks := map[string]map[string]interface{}{} + ok, err = sysStore.Load("dashboard_links", &dashboardLinks) + if !ok || err != nil { + fmt.Fprintf(os.Stderr, "Failed to load dashboard_links from configs: found=%v, error=%v", ok, err) + os.Exit(1) + } + + var clusters []string + if options.Cluster != "" { + clusters = []string{options.Cluster} + } else { + ok, err := sysStore.Load("clusters", &clusters) + if !ok || err != nil { + fmt.Fprintf(os.Stderr, "Failed to load clusters from configs: found=%v, error=%v", ok, err) + os.Exit(1) + } + } + + cmdArgs, timeout := buildMetastatusCmdArgs(options) + ctx, cancel := context.WithTimeout(context.Background(), timeout*time.Second) + defer cancel() + + ctx = context.WithValue(ctx, ctxKeyOut, os.Stdout) + ctx = context.WithValue(ctx, ctxKeyErr, os.Stderr) + + success, err := metastatus(ctx, clusters, apiEndpoints, dashboardLinks, cmdArgs) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/paasta-metastatus/main_test.go b/cmd/paasta-metastatus/main_test.go new file mode 100644 index 0000000..c66a032 --- /dev/null +++ b/cmd/paasta-metastatus/main_test.go @@ -0,0 +1,145 @@ +package main + +import ( + "context" + "reflect" + "regexp" + "strings" + "testing" + "time" + + "github.com/Yelp/paasta-tools-go/pkg/cli" + + "github.com/Yelp/paasta-tools-go/pkg/paasta_api/models" + + "github.com/go-openapi/runtime" + "github.com/stretchr/testify/assert" + + operations "github.com/Yelp/paasta-tools-go/pkg/paasta_api/client/operations" +) + +type MockTransport struct { + Ops []*runtime.ClientOperation +} + +func (m *MockTransport) Submit(co *runtime.ClientOperation) (interface{}, error) { + m.Ops = append(m.Ops, co) + return &operations.MetastatusOK{ + Payload: &models.MetaStatus{ + Output: "foo", + }, + }, nil +} + +func (m *MockTransport) Reset() { + m.Ops = []*runtime.ClientOperation{} +} + +func makeTestContext() context.Context { + t := &MockTransport{} + err := &strings.Builder{} + out := &strings.Builder{} + ctx := context.WithValue(context.Background(), ctxKeyTransport, t) + ctx = context.WithValue(ctx, ctxKeyOut, out) + ctx = context.WithValue(ctx, ctxKeyErr, err) + return ctx +} + +func TestMetastatus(test *testing.T) { + ctx := makeTestContext() + mockCmdArgs := []string{"foo", "bar"} + metastatus( + ctx, + []string{"cluster-foo"}, + map[string]string{"cluster-foo": "endpoint-foo"}, + map[string]map[string]interface{}{ + "cluster-foo": { + "dashboard-foo-1": "dashboard-foo-1-content", + }, + }, + mockCmdArgs, + ) + + transport := ctx.Value(ctxKeyTransport).(*MockTransport) + if len(transport.Ops) != 1 { + test.Logf("opes: %v", transport.Ops) + test.Errorf("expected number of operations: 1, got: %v", len(transport.Ops)) + } + + metastatusOp := transport.Ops[0] + if metastatusOp.PathPattern != "/metastatus" { + test.Errorf("unexpected path: %v", metastatusOp.PathPattern) + } + + params := metastatusOp.Params.(*operations.MetastatusParams) + if !reflect.DeepEqual(params.CmdArgs, mockCmdArgs) { + test.Errorf("expected mock args: %v, actual: %v", mockCmdArgs, params.CmdArgs) + } + + errf := ctx.Value(ctxKeyErr).(*strings.Builder) + if errf.String() != "" { + test.Errorf("error stream not empty: %v", errf) + } + + outf := ctx.Value(ctxKeyOut).(*strings.Builder) + outs := outf.String() + ok, _ := regexp.MatchString(`Cluster: cluster-foo`, outs) + if !ok { + test.Errorf("out doesn't match `Cluster: cluster-foo`:\n%v", outs) + } +} + +func Test_writeDashboards(test *testing.T) { + sb := &strings.Builder{} + writeDashboards("cluster-foo", nil, sb) + assert.Regexp(test, `No dashboards configured`, sb.String()) + + sb = &strings.Builder{} + writeDashboards( + "cluster-foo", + map[string]interface{}{ + "one": "two", + "three": []string{"four"}, + "five": []string{"six", "seven"}, + }, + sb, + ) + assert.Regexp(test, `one:.*two`, sb.String()) + assert.Regexp(test, `three:.*four`, sb.String()) + assert.Regexp(test, `five:.*\n.*six.*\n.*seven`, sb.String()) +} +func Test_buildMetastatusCmdArgs(test *testing.T) { + args, timeout := buildMetastatusCmdArgs(&PaastaMetastatusOptions{}) + assert.Equal(test, args, []string{}) + assert.Equal(test, timeout, time.Duration(20)) + + args, timeout = buildMetastatusCmdArgs(&PaastaMetastatusOptions{ + PaastaOptions: cli.PaastaOptions{Verbosity: 5}, + }) + assert.Equal(test, args, []string{"-vvvvv"}) + assert.Equal(test, timeout, time.Duration(120)) + + args, timeout = buildMetastatusCmdArgs(&PaastaMetastatusOptions{ + AutoscalingInfo: true, + }) + assert.Equal(test, args, []string{"-a", "-vv"}) + assert.Equal(test, timeout, time.Duration(120)) + + args, _ = buildMetastatusCmdArgs(&PaastaMetastatusOptions{ + Groupings: []string{"foo", "bar"}, + }) + assert.Equal(test, args, []string{"-g", "foo", "bar"}) + + args, _ = buildMetastatusCmdArgs(&PaastaMetastatusOptions{ + PaastaOptions: cli.PaastaOptions{UseMesosCache: true}, + }) + assert.Equal(test, args, []string{"--use-mesos-cache"}) +} + +func Test_writeAPIStatus(test *testing.T) { + // TODO: more tests +} + +func Test_getClusterStatus(test *testing.T) { + // TODO: more tests +} diff --git a/go.sum b/go.sum index 7cbe847..c1b5528 100644 --- a/go.sum +++ b/go.sum @@ -365,6 +365,7 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yelp/paasta-tools-go v0.0.0-20200427191252-351d9715626f h1:rdWD6ESbIWIm5ZZRToe4P4QpMsW95JdhtXyClEBjFrA= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=