-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: comple api-resources using go routine
- Implemented sorting of API resources by service name using `sort.Slice` to enhance the organization of the output table. - Maintained existing concurrency with goroutines to ensure efficient parallel processing of multiple endpoints. - Improved error handling and data collection mechanisms to support the new sorting feature. Signed-off-by: Youngjin Jo <[email protected]>
- Loading branch information
Showing
3 changed files
with
334 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
package cmd | ||
|
||
import ( | ||
"context" | ||
"crypto/tls" | ||
"fmt" | ||
"log" | ||
"os" | ||
"path/filepath" | ||
"sort" | ||
"strings" | ||
"sync" | ||
|
||
"github.com/pterm/pterm" | ||
"github.com/spf13/cobra" | ||
"github.com/spf13/viper" | ||
"google.golang.org/grpc" | ||
"google.golang.org/grpc/credentials" | ||
"google.golang.org/grpc/credentials/insecure" | ||
"google.golang.org/grpc/reflection/grpc_reflection_v1alpha" | ||
"google.golang.org/protobuf/proto" | ||
"google.golang.org/protobuf/types/descriptorpb" | ||
) | ||
|
||
var apiResourcesCmd = &cobra.Command{ | ||
Use: "api-resources", | ||
Short: "Displays supported API resources", | ||
Run: func(cmd *cobra.Command, args []string) { | ||
// Load configuration file | ||
cfgFile := viper.GetString("config") | ||
if cfgFile == "" { | ||
home, err := os.UserHomeDir() | ||
if err != nil { | ||
log.Fatalf("Failed to get user home directory: %v", err) | ||
} | ||
cfgFile = filepath.Join(home, ".spaceone", "cfctl.yaml") | ||
} | ||
|
||
viper.SetConfigFile(cfgFile) | ||
if err := viper.ReadInConfig(); err != nil { | ||
log.Fatalf("Error reading config file: %v", err) | ||
} | ||
|
||
endpoints := viper.GetStringMapString("endpoints") | ||
|
||
var wg sync.WaitGroup | ||
dataChan := make(chan [][]string, len(endpoints)) | ||
errorChan := make(chan error, len(endpoints)) | ||
|
||
for service, endpoint := range endpoints { | ||
wg.Add(1) | ||
go func(service, endpoint string) { | ||
defer wg.Done() | ||
result, err := fetchServiceResources(service, endpoint) | ||
if err != nil { | ||
errorChan <- fmt.Errorf("Error processing service %s: %v", service, err) | ||
return | ||
} | ||
dataChan <- result | ||
}(service, endpoint) | ||
} | ||
|
||
// Wait for all goroutines to finish | ||
wg.Wait() | ||
close(dataChan) | ||
close(errorChan) | ||
|
||
// Handle errors | ||
if len(errorChan) > 0 { | ||
for err := range errorChan { | ||
log.Println(err) | ||
} | ||
// Optionally exit the program | ||
// log.Fatal("Failed to process one or more endpoints.") | ||
} | ||
|
||
// Collect data | ||
var allData [][]string | ||
for data := range dataChan { | ||
allData = append(allData, data...) | ||
} | ||
|
||
// Sort the data by Service name | ||
sort.Slice(allData, func(i, j int) bool { | ||
return allData[i][0] < allData[j][0] | ||
}) | ||
|
||
// Render table | ||
table := pterm.TableData{{"Service", "Resource", "Short Names", "Verb"}} | ||
table = append(table, allData...) | ||
|
||
pterm.DefaultTable.WithHasHeader().WithData(table).Render() | ||
}, | ||
} | ||
|
||
func init() { | ||
rootCmd.AddCommand(apiResourcesCmd) | ||
} | ||
|
||
func fetchServiceResources(service, endpoint string) ([][]string, error) { | ||
// Configure gRPC connection based on TLS usage | ||
parts := strings.Split(endpoint, "://") | ||
if len(parts) != 2 { | ||
return nil, fmt.Errorf("invalid endpoint format: %s", endpoint) | ||
} | ||
|
||
scheme := parts[0] | ||
hostPort := strings.SplitN(parts[1], "/", 2)[0] | ||
|
||
var opts []grpc.DialOption | ||
if scheme == "grpc+ssl" { | ||
tlsConfig := &tls.Config{ | ||
InsecureSkipVerify: false, // Enable server certificate verification | ||
} | ||
creds := credentials.NewTLS(tlsConfig) | ||
opts = append(opts, grpc.WithTransportCredentials(creds)) | ||
} else { | ||
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) | ||
} | ||
|
||
conn, err := grpc.Dial(hostPort, opts...) | ||
if err != nil { | ||
return nil, fmt.Errorf("connection failed: unable to connect to %s: %v", endpoint, err) | ||
} | ||
defer conn.Close() | ||
|
||
client := grpc_reflection_v1alpha.NewServerReflectionClient(conn) | ||
stream, err := client.ServerReflectionInfo(context.Background()) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to create reflection client: %v", err) | ||
} | ||
|
||
// List all services | ||
req := &grpc_reflection_v1alpha.ServerReflectionRequest{ | ||
MessageRequest: &grpc_reflection_v1alpha.ServerReflectionRequest_ListServices{ListServices: ""}, | ||
} | ||
|
||
if err := stream.Send(req); err != nil { | ||
return nil, fmt.Errorf("failed to send reflection request: %v", err) | ||
} | ||
|
||
resp, err := stream.Recv() | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to receive reflection response: %v", err) | ||
} | ||
|
||
services := resp.GetListServicesResponse().Service | ||
data := [][]string{} | ||
for _, s := range services { | ||
if strings.HasPrefix(s.Name, "grpc.reflection.v1alpha.") { | ||
continue | ||
} | ||
resourceName := s.Name[strings.LastIndex(s.Name, ".")+1:] | ||
verbs := getServiceMethods(client, s.Name) | ||
data = append(data, []string{service, resourceName, "", strings.Join(verbs, ", ")}) | ||
} | ||
|
||
return data, nil | ||
} | ||
|
||
func getServiceMethods(client grpc_reflection_v1alpha.ServerReflectionClient, serviceName string) []string { | ||
stream, err := client.ServerReflectionInfo(context.Background()) | ||
if err != nil { | ||
log.Fatalf("Failed to create reflection client: %v", err) | ||
} | ||
|
||
req := &grpc_reflection_v1alpha.ServerReflectionRequest{ | ||
MessageRequest: &grpc_reflection_v1alpha.ServerReflectionRequest_FileContainingSymbol{FileContainingSymbol: serviceName}, | ||
} | ||
|
||
if err := stream.Send(req); err != nil { | ||
log.Fatalf("Failed to send reflection request: %v", err) | ||
} | ||
|
||
resp, err := stream.Recv() | ||
if err != nil { | ||
log.Fatalf("Failed to receive reflection response: %v", err) | ||
} | ||
|
||
fileDescriptor := resp.GetFileDescriptorResponse() | ||
if fileDescriptor == nil { | ||
return []string{} | ||
} | ||
|
||
// Extract method names from file descriptor | ||
methods := []string{} | ||
for _, fdBytes := range fileDescriptor.FileDescriptorProto { | ||
fd := &descriptorpb.FileDescriptorProto{} | ||
if err := proto.Unmarshal(fdBytes, fd); err != nil { | ||
log.Fatalf("Failed to unmarshal file descriptor: %v", err) | ||
} | ||
for _, service := range fd.GetService() { | ||
if service.GetName() == serviceName[strings.LastIndex(serviceName, ".")+1:] { | ||
for _, method := range service.GetMethod() { | ||
methods = append(methods, method.GetName()) | ||
} | ||
} | ||
} | ||
} | ||
|
||
return methods | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.