sdk.go raw

   1  package ycsdk
   2  
   3  import (
   4  	"context"
   5  	"crypto/tls"
   6  	"fmt"
   7  	"runtime/debug"
   8  
   9  	"go.uber.org/zap"
  10  	"google.golang.org/grpc"
  11  	grpccreds "google.golang.org/grpc/credentials"
  12  	"google.golang.org/grpc/credentials/insecure"
  13  	"google.golang.org/protobuf/reflect/protoreflect"
  14  
  15  	endpointpb "github.com/yandex-cloud/go-genproto/yandex/cloud/endpoint"
  16  	"github.com/yandex-cloud/go-sdk/v2/credentials"
  17  	"github.com/yandex-cloud/go-sdk/v2/pkg/authentication"
  18  	"github.com/yandex-cloud/go-sdk/v2/pkg/endpoints"
  19  	"github.com/yandex-cloud/go-sdk/v2/pkg/log"
  20  	"github.com/yandex-cloud/go-sdk/v2/pkg/options"
  21  	"github.com/yandex-cloud/go-sdk/v2/pkg/options/retry"
  22  	"github.com/yandex-cloud/go-sdk/v2/pkg/transport"
  23  	transportgrpc "github.com/yandex-cloud/go-sdk/v2/pkg/transport/grpc"
  24  	transportauth "github.com/yandex-cloud/go-sdk/v2/pkg/transport/middleware/authentication"
  25  	endpointsdk "github.com/yandex-cloud/go-sdk/v2/services/endpoint"
  26  	endpointssdk "github.com/yandex-cloud/go-sdk/v2/services/endpoints"
  27  )
  28  
  29  var IamTokenCreateEndpoint = protoreflect.FullName("yandex.cloud.iam.v1.IamTokenService.Create")
  30  
  31  var _ transport.Connector = (*SDK)(nil)
  32  
  33  // SDK provides a client connection wrapper managing connection pooling and endpoint resolution for gRPC services.
  34  type SDK struct {
  35  	ctx              context.Context
  36  	connPool         *transportgrpc.ConnPool
  37  	conn             transport.Connector
  38  	authenticator    authentication.Authenticator
  39  	endpointResolver endpoints.EndpointsResolver
  40  }
  41  
  42  // Build initializes and configures an SDK instance with the provided options and context.
  43  // It applies default configurations and validates necessary parameters like credentials and endpoints.
  44  // Returns an SDK instance or an error if initialization fails.
  45  func Build(ctx context.Context, opts ...options.Option) (*SDK, error) {
  46  	buildOptions := options.DefaultOptions()
  47  	for _, opt := range opts {
  48  		opt(buildOptions)
  49  	}
  50  	if buildOptions.Credentials == nil {
  51  		return nil, fmt.Errorf("credentials must be provided")
  52  	}
  53  
  54  	logger := zap.NewNop()
  55  	if buildOptions.Logger != nil {
  56  		logger = buildOptions.Logger
  57  	}
  58  
  59  	if injector, ok := buildOptions.Credentials.(log.LogInjector); ok {
  60  		injector.InjectLogger(logger)
  61  	}
  62  
  63  	var err error
  64  	if buildOptions.EndpointsResolver == nil {
  65  		buildOptions.EndpointsResolver, err = buildEndpointsResolver(ctx, buildOptions.DiscoveryEndpoint)
  66  		if err != nil {
  67  			return nil, fmt.Errorf("failed to get endpointsResolver: %w", err)
  68  		}
  69  	}
  70  
  71  	if buildOptions.Authenticator == nil {
  72  		buildOptions.Authenticator, err = defaultAuthenticator(ctx, logger, buildOptions.Credentials, buildOptions.EndpointsResolver)
  73  		if err != nil {
  74  			return nil, fmt.Errorf("failed to create authenticator: %w", err)
  75  		}
  76  	}
  77  
  78  	if injector, ok := buildOptions.Authenticator.(log.LogInjector); ok {
  79  		injector.InjectLogger(
  80  			logger,
  81  		)
  82  	}
  83  
  84  	dialOpts := []grpc.DialOption{
  85  		grpc.WithUserAgent(userAgent()),
  86  	}
  87  
  88  	if _, ok := buildOptions.Credentials.(*credentials.NoCredentials); !ok {
  89  		tokenMiddleware := transportauth.NewIAMTokenMiddleware(buildOptions.Authenticator, transportauth.WithLogger(logger))
  90  		dialOpts = append(dialOpts,
  91  			grpc.WithUnaryInterceptor(tokenMiddleware.InterceptUnary),
  92  			grpc.WithStreamInterceptor(tokenMiddleware.InterceptStream),
  93  		)
  94  	}
  95  
  96  	if buildOptions.Plaintext {
  97  		dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
  98  	} else {
  99  		tlsConfig := buildOptions.TlsConfig
 100  		if tlsConfig == nil {
 101  			tlsConfig = &tls.Config{}
 102  		}
 103  		dialOpts = append(dialOpts, grpc.WithTransportCredentials(grpccreds.NewTLS(tlsConfig)))
 104  	}
 105  
 106  	if buildOptions.DefaultRetryOptions {
 107  		retryOpt, err := retry.DefaultRetryDialOption()
 108  		if err != nil {
 109  			return nil, fmt.Errorf("failed to apply default retry options: %w", err)
 110  		}
 111  		dialOpts = append(dialOpts, retryOpt)
 112  	}
 113  
 114  	if len(buildOptions.RetryOptions) > 0 {
 115  		retryOpt, err := retry.RetryDialOption(buildOptions.RetryOptions...)
 116  		if err != nil {
 117  			return nil, fmt.Errorf("failed to apply retry options: %w", err)
 118  		}
 119  		dialOpts = append(dialOpts, retryOpt)
 120  	}
 121  
 122  	dialOpts = append(dialOpts, buildOptions.CustomDialOpts...)
 123  
 124  	connectionPool := transportgrpc.NewConnPool(dialOpts)
 125  	return &SDK{
 126  		ctx:              ctx,
 127  		conn:             transport.NewConnector(buildOptions.EndpointsResolver, connectionPool),
 128  		endpointResolver: buildOptions.EndpointsResolver,
 129  		connPool:         connectionPool,
 130  		authenticator:    buildOptions.Authenticator,
 131  	}, nil
 132  }
 133  
 134  // GetConnection retrieves a gRPC client connection for the specified method with optional call options.
 135  func (sdk *SDK) GetConnection(ctx context.Context, method protoreflect.FullName, opts ...grpc.CallOption) (grpc.ClientConnInterface, error) {
 136  	return sdk.conn.GetConnection(ctx, method, opts...)
 137  }
 138  
 139  // Shutdown gracefully terminates the SDK by closing all active gRPC connections in the connection pool.
 140  func (sdk *SDK) Shutdown(ctx context.Context) error {
 141  	return sdk.connPool.Shutdown(ctx)
 142  }
 143  
 144  // userAgent returns the User-Agent string that includes the SDK name and its version, read from the build info.
 145  func userAgent() string {
 146  	cloudUserAgent := "yandex-cloud/go-sdk-v2"
 147  
 148  	build, _ := debug.ReadBuildInfo()
 149  	version := "unknown"
 150  
 151  	if build.Main.Version != "" {
 152  		version = build.Main.Version
 153  	}
 154  
 155  	return fmt.Sprintf("%s/%s", cloudUserAgent, version)
 156  }
 157  
 158  // defaultAuthenticator initializes an Authenticator using provided credentials and an endpoint resolver in the given context.
 159  func defaultAuthenticator(ctx context.Context, logger *zap.Logger, creds credentials.Credentials, resolver endpoints.EndpointsResolver) (authentication.Authenticator, error) {
 160  	authEndpoint, err := resolver.Endpoint(ctx, IamTokenCreateEndpoint)
 161  	if err != nil {
 162  		return nil, fmt.Errorf("failed to get auth endpoint: %w", err)
 163  	}
 164  
 165  	authernticator, err := authentication.NewAuthenticatorFromEndpoint(logger, creds, authEndpoint)
 166  	if err != nil {
 167  		return nil, fmt.Errorf("failed to create authenticator: %w", err)
 168  	}
 169  	return authernticator, nil
 170  }
 171  
 172  // BuildEndpointsResolver creates an EndpointsResolver using a discovery endpoint to dynamically map service prefixes.
 173  func buildEndpointsResolver(ctx context.Context, discoveryEndpoint string) (endpoints.EndpointsResolver, error) {
 174  	conn := transport.NewSingleConnector(discoveryEndpoint, grpc.WithTransportCredentials(grpccreds.NewTLS(&tls.Config{})))
 175  	client := endpointsdk.NewApiEndpointClient(conn)
 176  
 177  	resp, err := client.List(ctx, &endpointpb.ListApiEndpointsRequest{})
 178  	if err != nil {
 179  		return nil, fmt.Errorf("failed to list endpoints: %w", err)
 180  	}
 181  
 182  	// Map endpoint IDs to addresses
 183  	endpointMap := make(map[string]string, len(resp.Endpoints))
 184  	for _, ep := range resp.Endpoints {
 185  		endpointMap[ep.Id] = ep.Address
 186  	}
 187  
 188  	// Build resolver from dynamic endpoints
 189  	p2e := make(endpoints.PrefixToEndpoint, len(endpointssdk.DynamicEndpoints))
 190  	for prefix, id := range endpointssdk.DynamicEndpoints {
 191  		if addr, ok := endpointMap[id]; ok {
 192  			p2e[prefix] = endpoints.NewEndpointParams(addr)
 193  		}
 194  	}
 195  	p2e["yandex.cloud.endpoint"] = endpoints.NewEndpointParams(discoveryEndpoint)
 196  
 197  	return endpoints.NewPrefixEndpointsResolver(p2e), nil
 198  }
 199  
 200  func (sdk *SDK) CreateIAMToken(ctx context.Context) (authentication.IamToken, error) {
 201  	return sdk.authenticator.CreateIAMToken(ctx)
 202  }
 203  
 204  func (sdk *SDK) GetEndpoint(method protoreflect.FullName) (*endpoints.Endpoint, error) {
 205  	return sdk.endpointResolver.Endpoint(sdk.ctx, method)
 206  }
 207