diff --git a/cmd/exec.go b/cmd/exec.go index 931cfd4..2759ba1 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -1,32 +1,35 @@ -/* -Copyright © 2024 NAME HERE -*/ package cmd import ( + "bytes" "context" "crypto/tls" + "encoding/csv" + "encoding/json" "fmt" - "io/ioutil" "log" + "net/url" "os" "strings" - "github.com/golang/protobuf/ptypes/empty" - - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/descriptorpb" - - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" - + "github.com/atotto/clipboard" + "github.com/jhump/protoreflect/dynamic" + "github.com/jhump/protoreflect/grpcreflect" + "github.com/pterm/pterm" "github.com/spf13/cobra" "google.golang.org/grpc" + "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" - "gopkg.in/yaml.v2" + "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" + "gopkg.in/yaml.v3" ) var parameters []string +var jsonParameter string +var fileParameter string +var apiVersion string +var outputFormat string +var copyToClipboard bool // Config structure to parse environment files type Config struct { @@ -34,10 +37,28 @@ type Config struct { Endpoints map[string]string `yaml:"endpoints"` } -// Load environment configuration +var execCmd = &cobra.Command{ + Use: "exec [rpc] [service].[resource]", + Short: "Execute a gRPC request to a specified service and message", + Long: `Executes a gRPC command to a given service and message based on environment configuration. + For example: cfctl exec list identity.User`, + Args: cobra.ExactArgs(2), + Run: runExecCommand, +} + +func init() { + rootCmd.AddCommand(execCmd) + execCmd.Flags().StringArrayVarP(¶meters, "parameter", "p", []string{}, "Input Parameter (-p = -p ...)") + execCmd.Flags().StringVarP(&jsonParameter, "json-parameter", "j", "", "JSON type parameter") + execCmd.Flags().StringVarP(&fileParameter, "file-parameter", "f", "", "YAML file parameter") + execCmd.Flags().StringVarP(&apiVersion, "api-version", "v", "v1", "API Version") + execCmd.Flags().StringVarP(&outputFormat, "output", "o", "yaml", "Output format (yaml, json, table, csv)") + execCmd.Flags().BoolVarP(©ToClipboard, "copy", "c", false, "Copy the output to the clipboard (copies any output format)") +} + func loadConfig(environment string) (*Config, error) { configPath := fmt.Sprintf("%s/.spaceone/environments/%s.yml", os.Getenv("HOME"), environment) - data, err := ioutil.ReadFile(configPath) + data, err := os.ReadFile(configPath) if err != nil { return nil, fmt.Errorf("could not read config file: %w", err) } @@ -50,11 +71,9 @@ func loadConfig(environment string) (*Config, error) { return &config, nil } -// Load current environment from environment.yml func fetchCurrentEnvironment() (string, error) { envPath := fmt.Sprintf("%s/.spaceone/environment.yml", os.Getenv("HOME")) - - data, err := ioutil.ReadFile(envPath) + data, err := os.ReadFile(envPath) if err != nil { return "", fmt.Errorf("could not read environment file: %w", err) } @@ -70,153 +89,332 @@ func fetchCurrentEnvironment() (string, error) { return envConfig.Environment, nil } -// execCmd represents the exec command -var execCmd = &cobra.Command{ - Use: "exec [verb] [service].[resource]", - Short: "Execute a gRPC request to a specified service and resource", - Long: `Executes a gRPC command to a given service and resource based on environment configuration. - For example: cfctl exec list identity.User`, - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - // Verb and service resource extraction - serviceResource := args[1] - rpcVerb := args[0] +func runExecCommand(cmd *cobra.Command, args []string) { + environment, err := fetchCurrentEnvironment() + if err != nil { + log.Fatalf("Failed to get current environment: %v", err) + } - // Load environment - environment, err := fetchCurrentEnvironment() - if err != nil { - log.Fatalf("Failed to get current environment: %v", err) - } + config, err := loadConfig(environment) + if err != nil { + log.Fatalf("Failed to load config for environment %s: %v", environment, err) + } + + verbName := args[0] + serviceResource := args[1] + parts := strings.Split(serviceResource, ".") + + if len(parts) != 2 { + log.Fatalf("Invalid service and resource format. Use [service].[resource]") + } + + serviceName := parts[0] + resourceName := parts[1] - config, err := loadConfig(environment) + endpoint, ok := config.Endpoints[serviceName] + if !ok { + log.Fatalf("Service endpoint not found for service: %s", serviceName) + } + + parsedURL, err := url.Parse(endpoint) + if err != nil { + log.Fatalf("Invalid endpoint URL %s: %v", endpoint, err) + } + + grpcEndpoint := fmt.Sprintf("%s:%s", parsedURL.Hostname(), parsedURL.Port()) + + // Set up secure connection + conn, err := grpc.Dial(grpcEndpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))) + if err != nil { + log.Fatalf("Failed to connect to gRPC server: %v", err) + } + defer conn.Close() + + ctx := metadata.AppendToOutgoingContext(context.Background(), "token", config.Token) + + // Set up reflection client + refClient := grpcreflect.NewClient(ctx, grpc_reflection_v1alpha.NewServerReflectionClient(conn)) + defer refClient.Reset() + + // Find the package name dynamically + packageName := "" + serviceList, err := refClient.ListServices() + if err != nil { + log.Fatalf("Failed to list services: %v", err) + } + + for _, fullServiceName := range serviceList { + serviceDesc, err := refClient.ResolveService(fullServiceName) if err != nil { - log.Fatalf("Failed to load config for environment %s: %v", environment, err) + log.Printf("Failed to resolve service %s: %v", fullServiceName, err) + continue } - // Extract service name - parts := strings.Split(serviceResource, ".") - if len(parts) != 2 { - log.Fatalf("Invalid service format. Use [service].[resource]") + if serviceDesc.GetName() == resourceName { + for _, method := range serviceDesc.GetMethods() { + if method.GetName() == verbName { + packageName = serviceDesc.GetFullyQualifiedName() + break + } + } } - serviceName := parts[0] - resourceName := parts[1] + if packageName != "" { + break + } + } - // Modify endpoint format - endpoint := config.Endpoints[serviceName] - endpoint = strings.Replace(strings.Replace(endpoint, "grpc+ssl://", "", 1), "/v1", "", 1) + if packageName == "" { + log.Fatalf("Service and method not found for verb: %s", verbName) + } - // Set up secure connection - conn, err := grpc.Dial(endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))) - if err != nil { - log.Fatalf("Failed to connect to gRPC server: %v", err) + // Get the service descriptor + serviceDesc, err := refClient.ResolveService(packageName) + if err != nil { + log.Fatalf("Failed to resolve service %s: %v", packageName, err) + } + + // Find the method descriptor + methodDesc := serviceDesc.FindMethodByName(verbName) + if methodDesc == nil { + log.Fatalf("Method %s not found in service %s", verbName, packageName) + } + + // Create a dynamic message for the request + inputType := methodDesc.GetInputType() + reqMsg := dynamic.NewMessage(inputType) + + // Parse the input parameters into a map + inputParams := parseParameters(fileParameter, jsonParameter, parameters) + for key, value := range inputParams { + if err := reqMsg.TrySetFieldByName(key, value); err != nil { + log.Fatalf("Failed to set field %s: %v", key, err) } - defer conn.Close() + } - // Set up gRPC reflection client - refClient := grpc_reflection_v1alpha.NewServerReflectionClient(conn) - ctx := metadata.AppendToOutgoingContext(context.Background(), "token", config.Token) + // Prepare response placeholder + outputType := methodDesc.GetOutputType() + respMsg := dynamic.NewMessage(outputType) - stream, err := refClient.ServerReflectionInfo(ctx) + // Make the RPC call using the client connection + err = conn.Invoke(ctx, fmt.Sprintf("/%s/%s", serviceDesc.GetFullyQualifiedName(), methodDesc.GetName()), reqMsg, respMsg) + if err != nil { + log.Fatalf("Failed to call method %s: %v", verbName, err) + } + + // Convert the response to a map and maintain UTF-8 decoding + respMap, err := messageToMap(respMsg) + if err != nil { + log.Fatalf("Failed to convert response message to map: %v", err) + } + + // Convert response to JSON to properly decode UTF-8 characters + jsonData, err := json.Marshal(respMap) + if err != nil { + log.Fatalf("Failed to marshal response to JSON: %v", err) + } + + // Unmarshal JSON data back into a map to maintain UTF-8 decoding + var prettyMap map[string]interface{} + if err := json.Unmarshal(jsonData, &prettyMap); err != nil { + log.Fatalf("Failed to unmarshal JSON data: %v", err) + } + + // Print the response + printData(prettyMap, outputFormat) +} + +func parseParameters(fileParameter, jsonParameter string, params []string) map[string]interface{} { + parsed := make(map[string]interface{}) + + // Load from file parameter if provided + if fileParameter != "" { + data, err := os.ReadFile(fileParameter) if err != nil { - log.Fatalf("Failed to create reflection stream: %v", err) + log.Fatalf("Failed to read file parameter: %v", err) } - // Request service list - req := &grpc_reflection_v1alpha.ServerReflectionRequest{ - MessageRequest: &grpc_reflection_v1alpha.ServerReflectionRequest_ListServices{ - ListServices: "*", - }, + if err := yaml.Unmarshal(data, &parsed); err != nil { + log.Fatalf("Failed to unmarshal YAML file: %v", err) } + } - if err := stream.Send(req); err != nil { - log.Fatalf("Failed to send reflection request: %v", err) + // Load from JSON parameter if provided + if jsonParameter != "" { + if err := json.Unmarshal([]byte(jsonParameter), &parsed); err != nil { + log.Fatalf("Failed to unmarshal JSON parameter: %v", err) } + } - // Receive and search for the specific service - resp, err := stream.Recv() - if err != nil { - log.Fatalf("Failed to receive reflection response: %v", err) + // Parse key=value parameters + for _, param := range params { + parts := strings.SplitN(param, "=", 2) + if len(parts) != 2 { + log.Fatalf("Invalid parameter format. Use key=value") } + parsed[parts[0]] = parts[1] + } - serviceFound := false - for _, svc := range resp.GetListServicesResponse().Service { - if strings.Contains(svc.Name, resourceName) { - serviceFound = true + return parsed +} - // Request file descriptor for the specific service - fileDescriptorReq := &grpc_reflection_v1alpha.ServerReflectionRequest{ - MessageRequest: &grpc_reflection_v1alpha.ServerReflectionRequest_FileContainingSymbol{ - FileContainingSymbol: svc.Name, - }, - } +func messageToMap(msg *dynamic.Message) (map[string]interface{}, error) { + result := make(map[string]interface{}) + fields := msg.GetKnownFields() - if err := stream.Send(fileDescriptorReq); err != nil { - log.Fatalf("Failed to send file descriptor request: %v", err) - } + for _, fd := range fields { + val := msg.GetField(fd) - fileResp, err := stream.Recv() + switch v := val.(type) { + case *dynamic.Message: + subMap, err := messageToMap(v) + if err != nil { + return nil, err + } + result[fd.GetName()] = subMap + case []*dynamic.Message: + var subList []map[string]interface{} + for _, subMsg := range v { + subMap, err := messageToMap(subMsg) if err != nil { - log.Fatalf("Failed to receive file descriptor response: %v", err) + return nil, err } + subList = append(subList, subMap) + } + result[fd.GetName()] = subList + case map[interface{}]interface{}: + // Convert map[interface{}]interface{} to map[string]interface{} + formattedMap := make(map[string]interface{}) + for key, value := range v { + formattedMap[fmt.Sprintf("%v", key)] = value + } + result[fd.GetName()] = formattedMap + case string: + result[fd.GetName()] = v + default: + result[fd.GetName()] = v + } + } - // Parse the file descriptor response - fd := fileResp.GetFileDescriptorResponse() - if fd == nil { - log.Fatalf("No file descriptor found for service %s", svc.Name) - } + return result, nil +} - // Extract methods from the file descriptor - fmt.Printf("Available methods for service %s:\n", svc.Name) - methodFound := false - for _, b := range fd.FileDescriptorProto { - protoDescriptor := &descriptorpb.FileDescriptorProto{} - if err := proto.Unmarshal(b, protoDescriptor); err != nil { - log.Fatalf("Failed to unmarshal file descriptor proto: %v", err) - } +func printData(data map[string]interface{}, format string) { + var output string - for _, service := range protoDescriptor.Service { - if service.GetName() == resourceName { - for _, method := range service.Method { - fmt.Printf("- %s\n", method.GetName()) - if method.GetName() == rpcVerb { - methodFound = true - // Call the method if it matches - fmt.Printf("Calling method %s on service %s...\n", rpcVerb, svc.Name) - - // Assuming the list method has no parameters - // Prepare the request message (in this case, an empty request) - req := &empty.Empty{} - response := new(empty.Empty) // Create a response placeholder - - // Make the RPC call using the client connection - err = conn.Invoke(ctx, fmt.Sprintf("/%s/%s", svc.Name, method.GetName()), req, response) - if err != nil { - log.Fatalf("Failed to call method %s: %v", rpcVerb, err) - } - - // Print the response - fmt.Printf("Response: %+v\n", response) - } - } - } - } - } + switch format { + case "json": + dataBytes, err := json.MarshalIndent(data, "", " ") + if err != nil { + log.Fatalf("Failed to marshal response to JSON: %v", err) + } + output = string(dataBytes) + fmt.Println(output) + + case "yaml": + var buf bytes.Buffer + encoder := yaml.NewEncoder(&buf) + encoder.SetIndent(2) + err := encoder.Encode(data) + if err != nil { + log.Fatalf("Failed to marshal response to YAML: %v", err) + } + output = buf.String() + fmt.Printf("---\n%s\n", output) + + case "table": + output = printTable(data) + + case "csv": + output = printCSV(data) + + default: + log.Fatalf("Unsupported output format: %s", format) + } - if !methodFound { - log.Fatalf("Method %s not found in service %s", rpcVerb, resourceName) + // Copy to clipboard if requested + if copyToClipboard && output != "" { + if err := clipboard.WriteAll(output); err != nil { + log.Fatalf("Failed to copy to clipboard: %v", err) + } + pterm.Success.Println("The output has been copied to your clipboard.") + } +} + +func printTable(data map[string]interface{}) string { + var output string + if results, ok := data["results"].([]interface{}); ok { + tableData := pterm.TableData{} + + // Extract headers + headers := []string{} + if len(results) > 0 { + if row, ok := results[0].(map[string]interface{}); ok { + for key := range row { + headers = append(headers, key) } + } + } + + // Append headers to table data + tableData = append(tableData, headers) - break + // Extract rows + for _, result := range results { + if row, ok := result.(map[string]interface{}); ok { + rowData := []string{} + for _, key := range headers { + rowData = append(rowData, fmt.Sprintf("%v", row[key])) + } + tableData = append(tableData, rowData) } } - if !serviceFound { - log.Fatalf("Service %s not found", resourceName) + // Disable styling only for the table output + pterm.DisableStyling() + renderedOutput, err := pterm.DefaultTable.WithHasHeader(true).WithData(tableData).Srender() + pterm.EnableStyling() // Re-enable styling for other outputs + if err != nil { + log.Fatalf("Failed to render table: %v", err) } - }, + output = renderedOutput + fmt.Println(output) // Print to console + } + return output } -func init() { - rootCmd.AddCommand(execCmd) - execCmd.Flags().StringArrayVarP(¶meters, "parameter", "p", []string{}, "Input Parameter (-p = -p ...)") +func printCSV(data map[string]interface{}) string { + var buf bytes.Buffer + if results, ok := data["results"].([]interface{}); ok { + writer := csv.NewWriter(&buf) + var headers []string + + // Extract headers + for _, result := range results { + if row, ok := result.(map[string]interface{}); ok { + if headers == nil { + for key := range row { + headers = append(headers, key) + } + writer.Write(headers) + } + + // Extract row values + var rowValues []string + for _, key := range headers { + if val, ok := row[key]; ok { + rowValues = append(rowValues, fmt.Sprintf("%v", val)) + } else { + rowValues = append(rowValues, "") + } + } + writer.Write(rowValues) + } + } + + writer.Flush() + output := buf.String() + fmt.Print(output) // Print to console + return output + } + return "" } diff --git a/go.mod b/go.mod index 7f853ea..65f0fd5 100644 --- a/go.mod +++ b/go.mod @@ -3,22 +3,25 @@ module github.com/cloudforet-io/cfctl go 1.23.1 require ( - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc - github.com/golang/protobuf v1.5.4 + github.com/atotto/clipboard v0.1.4 + github.com/jhump/protoreflect v1.17.0 github.com/pterm/pterm v0.12.79 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 google.golang.org/grpc v1.62.1 - google.golang.org/protobuf v1.34.2 + google.golang.org/protobuf v1.35.1 gopkg.in/yaml.v2 v2.2.4 + gopkg.in/yaml.v3 v3.0.1 ) require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect + github.com/bufbuild/protocompile v0.14.1 // indirect github.com/containerd/console v1.0.3 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/gookit/color v1.5.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -45,5 +48,4 @@ require ( golang.org/x/text v0.15.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index fc9213e..2ba4436 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,10 @@ github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYew github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -39,6 +43,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= @@ -136,6 +142,8 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -171,8 +179,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=