Skip to content

Commit

Permalink
feat: comple api-resources using go routine
Browse files Browse the repository at this point in the history
- 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
yjinjo committed Nov 4, 2024
1 parent 343f064 commit 32aea81
Show file tree
Hide file tree
Showing 3 changed files with 334 additions and 2 deletions.
202 changes: 202 additions & 0 deletions cmd/apiResources.go
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
}
16 changes: 16 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,45 @@ module github.com/cloudforet-io/cfctl
go 1.23.1

require (
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.33.0
)

require (
atomicgo.dev/cursor v0.2.0 // indirect
atomicgo.dev/keyboard v0.2.9 // indirect
atomicgo.dev/schedule v0.1.0 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/golang/protobuf v1.5.3 // 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
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.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
)
Loading

0 comments on commit 32aea81

Please sign in to comment.