Skip to content

Commit c80bd55

Browse files
committed
Add Cloud.ru KMS support
Signed-off-by: Дмитрий Иванов <[email protected]>
1 parent 8019097 commit c80bd55

File tree

11 files changed

+785
-117
lines changed

11 files changed

+785
-117
lines changed

cloudru/client.go

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package cloudru
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
"net/http"
10+
"net/url"
11+
"os"
12+
"time"
13+
14+
iamAuthV1 "github.com/cloudru-tech/iam-sdk/api/auth/v1"
15+
kmsV1 "github.com/cloudru-tech/key-manager-sdk/api/v1"
16+
"google.golang.org/grpc"
17+
"google.golang.org/grpc/credentials"
18+
"google.golang.org/grpc/keepalive"
19+
"google.golang.org/grpc/metadata"
20+
)
21+
22+
// EndpointsResponse is a response from the Cloud.ru API.
23+
type EndpointsResponse struct {
24+
// Endpoints contains the list of actual API addresses of Cloud.ru products.
25+
Endpoints []Endpoint `json:"endpoints"`
26+
}
27+
28+
// Endpoint is a product API address.
29+
type Endpoint struct {
30+
ID string `json:"id"`
31+
Address string `json:"address"`
32+
}
33+
34+
type Client struct {
35+
KMS kmsV1.KeyManagerServiceClient
36+
kmsConn *grpc.ClientConn
37+
}
38+
39+
func provideClient() (*Client, error) {
40+
discoveryURL := DiscoveryURL
41+
42+
if du, ok := os.LookupEnv(EnvDiscoveryURL); ok {
43+
u, err := url.Parse(discoveryURL)
44+
if err != nil {
45+
return nil, fmt.Errorf("invalid %s param: %w", EnvDiscoveryURL, err)
46+
}
47+
48+
switch {
49+
case u.Host == "":
50+
return nil, fmt.Errorf("invalid %s param: missing host", EnvDiscoveryURL)
51+
case u.Scheme != "http", u.Scheme != "https":
52+
return nil, fmt.Errorf("invalid %s param: scheme must be http or https", EnvDiscoveryURL)
53+
}
54+
55+
discoveryURL = du
56+
}
57+
58+
var ok bool
59+
var akID, akSecret string
60+
if akID, ok = os.LookupEnv(EnvAccessKeyID); !ok {
61+
return nil, fmt.Errorf("missing %s env param", EnvAccessKeyID)
62+
}
63+
if akSecret, ok = os.LookupEnv(EnvAccessKeySecret); !ok {
64+
return nil, fmt.Errorf("missing %s env param", EnvAccessKeySecret)
65+
}
66+
67+
endpoints, err := getEndpoints(discoveryURL)
68+
if err != nil {
69+
return nil, err
70+
}
71+
72+
kmsEndpoint := endpoints.Get("key-manager")
73+
if kmsEndpoint == nil {
74+
return nil, errors.New("key-manager API is not available")
75+
}
76+
77+
iamEndpoint := endpoints.Get("iam")
78+
if iamEndpoint == nil {
79+
return nil, errors.New("iam API is not available")
80+
}
81+
82+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
83+
defer cancel()
84+
85+
iamConn, err := grpc.NewClient(iamEndpoint.Address,
86+
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS13})),
87+
grpc.WithKeepaliveParams(keepalive.ClientParameters{
88+
Time: time.Second * 30,
89+
Timeout: time.Second * 5,
90+
PermitWithoutStream: false,
91+
}),
92+
grpc.WithUserAgent("sops"),
93+
)
94+
if err != nil {
95+
return nil, fmt.Errorf("initiate IAM gRPC connection: %w", err)
96+
}
97+
defer iamConn.Close()
98+
99+
iam := iamAuthV1.NewAuthServiceClient(iamConn)
100+
token, err := iam.GetToken(ctx, &iamAuthV1.GetTokenRequest{
101+
KeyId: akID,
102+
Secret: akSecret,
103+
})
104+
if err != nil {
105+
return nil, fmt.Errorf("get token: %w", err)
106+
}
107+
108+
kmsConn, err := grpc.NewClient(kmsEndpoint.Address,
109+
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS13})),
110+
grpc.WithKeepaliveParams(keepalive.ClientParameters{
111+
Time: time.Second * 30,
112+
Timeout: time.Second * 5,
113+
PermitWithoutStream: false,
114+
}),
115+
grpc.WithUserAgent("sops"),
116+
grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
117+
md, ok := metadata.FromOutgoingContext(ctx)
118+
if !ok {
119+
md = metadata.New(map[string]string{})
120+
}
121+
md.Set("authorization", "Bearer "+token.AccessToken)
122+
123+
return invoker(metadata.NewOutgoingContext(ctx, md), method, req, reply, cc, opts...)
124+
}),
125+
)
126+
if err != nil {
127+
return nil, fmt.Errorf("initiate KMS gRPC connection: %w", err)
128+
}
129+
130+
return &Client{
131+
KMS: kmsV1.NewKeyManagerServiceClient(kmsConn),
132+
kmsConn: kmsConn,
133+
}, nil
134+
}
135+
136+
// getEndpoints returns the actual Cloud.ru API endpoints.
137+
func getEndpoints(url string) (*EndpointsResponse, error) {
138+
req, err := http.NewRequest(http.MethodGet, url, http.NoBody)
139+
if err != nil {
140+
return nil, fmt.Errorf("construct HTTP request for cloud.ru endpoints: %w", err)
141+
}
142+
143+
resp, err := http.DefaultClient.Do(req)
144+
if err != nil {
145+
return nil, fmt.Errorf("get cloud.ru endpoints: %w", err)
146+
}
147+
defer resp.Body.Close()
148+
149+
if resp.StatusCode != http.StatusOK {
150+
return nil, fmt.Errorf("get cloud.ru endpoints: unexpected status code %d", resp.StatusCode)
151+
}
152+
153+
var endpoints EndpointsResponse
154+
if err = json.NewDecoder(resp.Body).Decode(&endpoints); err != nil {
155+
return nil, fmt.Errorf("decode cloud.ru endpoints: %w", err)
156+
}
157+
158+
return &endpoints, nil
159+
}
160+
161+
// Get returns the API address of the product by its ID.
162+
// If the product is not found, the function returns nil.
163+
func (er *EndpointsResponse) Get(id string) *Endpoint {
164+
for i := range er.Endpoints {
165+
if er.Endpoints[i].ID == id {
166+
return &er.Endpoints[i]
167+
}
168+
}
169+
170+
return nil
171+
}
172+
173+
// Close closes the KMS gRPC client connection.
174+
func (c *Client) Close() error { return c.kmsConn.Close() }

0 commit comments

Comments
 (0)