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