-
Notifications
You must be signed in to change notification settings - Fork 172
Expand file tree
/
Copy pathserver.go
More file actions
362 lines (310 loc) · 13.4 KB
/
Copy pathserver.go
File metadata and controls
362 lines (310 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
/*
* Package responsible for reciving incomming mount requests from the driver.
*
* This package acts as the high level orchestrator; unpacking the message and
* calling the provider implementation to fetch the secrets.
*
*/
package server
import (
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"google.golang.org/grpc"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sv1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/klog/v2"
"sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1"
"github.com/aws/secrets-store-csi-driver-provider-aws/auth"
"github.com/aws/secrets-store-csi-driver-provider-aws/provider"
)
// Version filled in by Makefile during build.
var Version string
const (
namespaceAttrib = "csi.storage.k8s.io/pod.namespace"
acctAttrib = "csi.storage.k8s.io/serviceAccount.name"
podnameAttrib = "csi.storage.k8s.io/pod.name"
regionAttrib = "region" // The attribute name for the region in the SecretProviderClass
transAttrib = "pathTranslation" // Path translation char
regionLabel = "topology.kubernetes.io/region" // The node label giving the region
secProvAttrib = "objects" // The attribute used to pass the SecretProviderClass definition (with what to mount)
failoverRegionAttrib = "failoverRegion" // The attribute name for the failover region in the SecretProviderClass
usePodIdentityAttrib = "usePodIdentity" // The attribute used to indicate use Pod Identity for auth
preferredAddressTypeAttrib = "preferredAddressType" // The attribute used to indicate IP address preference (IPv4 or IPv6) for network connections. It controls whether connecting to the Pod Identity Agent IPv4 or IPv6 endpoint.
)
// A Secrets Store CSI Driver provider implementation for AWS Secrets Manager and SSM Parameter Store.
//
// This server receives mount requests and then retreives and stores the secrets
// from that request. The details of what secrets are required and where to
// store them are in the request. The secrets will be retrieved using the AWS
// credentials of the IAM role associated with the pod. If there is a failure
// during the mount of any one secret no secrets are written to the mount point.
type CSIDriverProviderServer struct {
*grpc.Server
secretProviderFactory provider.ProviderFactoryFactory
k8sClient k8sv1.CoreV1Interface
driverWriteSecrets bool
podIdentityHttpTimeout *time.Duration
eksAddonVersion string
}
// Factory function to create the server to handle incoming mount requests.
func NewServer(
secretProviderFact provider.ProviderFactoryFactory,
k8client k8sv1.CoreV1Interface,
driverWriteSecrets bool,
podIdentityHttpTimeout *time.Duration,
eksAddonVersion string,
) (srv *CSIDriverProviderServer, e error) {
return &CSIDriverProviderServer{
secretProviderFactory: secretProviderFact,
k8sClient: k8client,
driverWriteSecrets: driverWriteSecrets,
podIdentityHttpTimeout: podIdentityHttpTimeout,
eksAddonVersion: eksAddonVersion,
}, nil
}
// Mount handles each incomming mount request.
//
// The provider will fetch the secret value from the secret provider (Parameter
// Store or Secrets Manager) and write the secrets to the mount point. The
// version ids of the secrets are then returned to the driver.
func (s *CSIDriverProviderServer) Mount(ctx context.Context, req *v1alpha1.MountRequest) (response *v1alpha1.MountResponse, e error) {
// Log out the write mode
if s.driverWriteSecrets {
klog.Infof("Driver is configured to write secrets")
} else {
klog.Infof("Provider is configured to write secrets")
}
// Basic sanity check
if len(req.GetTargetPath()) == 0 {
return nil, fmt.Errorf("Missing mount path")
}
mountDir := req.GetTargetPath()
// Unpack the request.
var attrib map[string]string
err := json.Unmarshal([]byte(req.GetAttributes()), &attrib)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal attributes, error: %+v", err)
}
// Get the mount attributes.
nameSpace := attrib[namespaceAttrib]
svcAcct := attrib[acctAttrib]
podName := attrib[podnameAttrib]
region := attrib[regionAttrib]
translate := attrib[transAttrib]
failoverRegion := attrib[failoverRegionAttrib]
usePodIdentityStr := attrib[usePodIdentityAttrib]
assumeRoleArn := attrib["assumeRoleArn"]
assumeRoleDuration := attrib["assumeRoleDurationSeconds"]
assumeRoleExternalId := attrib["assumeRoleExternalId"]
preferredAddressType := attrib[preferredAddressTypeAttrib]
// Validate preferred address type
if preferredAddressType != "ipv4" && preferredAddressType != "ipv6" && preferredAddressType != "auto" && preferredAddressType != "" {
return nil, fmt.Errorf("invalid preferred address type: %s", preferredAddressType)
}
// Make a map of the currently mounted versions (if any)
curVersions := req.GetCurrentObjectVersion()
curVerMap := make(map[string]*v1alpha1.ObjectVersion)
for _, ver := range curVersions {
curVerMap[ver.Id] = ver
}
// Unpack the file permission to use.
var filePermission os.FileMode
err = json.Unmarshal([]byte(req.GetPermission()), &filePermission)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal file permission, error: %+v", err)
}
// Set the default file permission
provider.SetDefaultFilePermission(filePermission)
regions, err := s.getAwsRegions(ctx, region, failoverRegion, nameSpace, podName)
if err != nil {
klog.ErrorS(err, "Failed to initialize AWS session")
return nil, err
}
klog.Infof("Servicing mount request for pod %s in namespace %s using service account %s with region(s) %s", podName, nameSpace, svcAcct, strings.Join(regions, ", "))
// Default to use IRSA if usePodIdentity parameter is not set in the mount request
usePodIdentity := false
if usePodIdentityStr != "" {
var err error
usePodIdentity, err = strconv.ParseBool(usePodIdentityStr)
if err != nil {
return nil, fmt.Errorf("failed to parse usePodIdentity value, error: %+v", err)
}
}
awsConfigs, err := s.getAwsConfigs(ctx, nameSpace, svcAcct, s.eksAddonVersion, regions, usePodIdentity, podName, preferredAddressType, s.podIdentityHttpTimeout, assumeRoleArn, assumeRoleDuration, assumeRoleExternalId)
if err != nil {
return nil, err
}
if len(awsConfigs) > 2 {
klog.Errorf("Max number of region(s) exceeded: %s", strings.Join(regions, ", "))
return nil, err
}
// Get the list of secrets to mount. These will be grouped together by type
// in a map of slices (map[string][]*SecretDescriptor) keyed by secret type
// so that requests can be batched if the implementation allows it.
descriptors, err := provider.NewSecretDescriptorList(mountDir, translate, attrib[secProvAttrib], regions)
if err != nil {
klog.Errorf("Failure reading descriptor list: %s", err)
return nil, err
}
providerFactory := s.secretProviderFactory(awsConfigs, regions)
var fetchedSecrets []*provider.SecretValue
for sType := range descriptors { // Iterate over each secret type.
// Fetch all the secrets and update the curVerMap
provider := providerFactory.GetSecretProvider(sType)
secrets, err := provider.GetSecretValues(ctx, descriptors[sType], curVerMap)
if err != nil {
klog.Errorf("Failure getting secret values from provider type %s: %s", sType, err)
return nil, err
}
fetchedSecrets = append(fetchedSecrets, secrets...) // Build up the list of all secrets
}
// Write out the secrets to the mount point after everything is fetched.
var files []*v1alpha1.File
for _, secret := range fetchedSecrets {
file, err := s.writeFile(secret, secret.Descriptor.GetFilePermission())
if err != nil {
return nil, err
}
if file != nil {
files = append(files, file)
}
}
// Build the version response from the current version map and return it.
var ov []*v1alpha1.ObjectVersion
for id := range curVerMap {
ov = append(ov, curVerMap[id])
}
return &v1alpha1.MountResponse{Files: files, ObjectVersion: ov}, nil
}
// Private helper to get the aws lookup regions for a given pod.
//
// When a region in the mount request is available, the region is added as primary region to the lookup region list
// If a region is not specified in the mount request, we must lookup the region from node label and add as primary region to the lookup region list
// If both the region and node label region are not available, error will be thrown
// If backupRegion is provided and is equal to region/node region, error will be thrown else backupRegion is added to the lookup region list
func (s *CSIDriverProviderServer) getAwsRegions(ctx context.Context, region, backupRegion, nameSpace, podName string) (response []string, err error) {
var lookupRegionList []string
// Find primary region. Fall back to region node if unavailable.
if len(region) == 0 {
region, err = s.getRegionFromNode(ctx, nameSpace, podName)
if err != nil {
return nil, fmt.Errorf("failed to retrieve region from node. error %+v", err)
}
}
lookupRegionList = []string{region}
// Find backup region
if len(backupRegion) > 0 {
if region == backupRegion {
return nil, fmt.Errorf("%v: failover region cannot be the same as the primary region", region)
}
lookupRegionList = append(lookupRegionList, backupRegion)
}
return lookupRegionList, nil
}
// Private helper to get the aws configs for all the lookup regions for a given pod.
//
// Gets the pod's AWS creds for each lookup region
// Establishes the connection using Aws cred for each lookup region
// If at least one config is not created, error will be thrown
func (s *CSIDriverProviderServer) getAwsConfigs(ctx context.Context, nameSpace, svcAcct, eksAddonVersion string, lookupRegionList []string, usePodIdentity bool, podName string, preferredAddressType string, podIdentityHttpTimeout *time.Duration, assumeRoleArn string, assumeRoleDuration string, assumeRoleExternalId string) (response []aws.Config, err error) {
// Get the pod's AWS creds for each lookup region.
var awsConfigsList []aws.Config
for _, region := range lookupRegionList {
awsAuth, err := auth.NewAuth(region, nameSpace, svcAcct, podName, preferredAddressType, eksAddonVersion, usePodIdentity, podIdentityHttpTimeout, s.k8sClient, assumeRoleArn, assumeRoleDuration, assumeRoleExternalId)
if err != nil {
return nil, fmt.Errorf("%s: %s", region, err)
}
awsConfig, err := awsAuth.GetAWSConfig(ctx)
if err != nil {
return nil, fmt.Errorf("%s: %s", region, err)
}
awsConfigsList = append(awsConfigsList, awsConfig)
}
return awsConfigsList, nil
}
// Return the provider plugin version information to the driver.
func (s *CSIDriverProviderServer) Version(ctx context.Context, req *v1alpha1.VersionRequest) (*v1alpha1.VersionResponse, error) {
return &v1alpha1.VersionResponse{
Version: "v1alpha1",
RuntimeName: auth.ProviderName,
RuntimeVersion: Version,
}, nil
}
// Private helper to get the region information for a given pod.
//
// When a region is not specified in the mount request, we must lookup the
// region of the requesting pod by first descriing the pod to find the node and
// then describing the node to get the region label.
//
// See also: https://pkg.go.dev/k8s.io/client-go/kubernetes/typed/core/v1
func (s *CSIDriverProviderServer) getRegionFromNode(ctx context.Context, namespace string, podName string) (reg string, err error) {
// Check if AWS_REGION environment variable is set
if envRegion := os.Getenv("AWS_REGION"); envRegion != "" {
return envRegion, nil
}
// Describe the pod to find the node: kubectl -o yaml -n <namespace> get pod <podid>
pod, err := s.k8sClient.Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
if err != nil {
return "", err
}
// Describe node to get region: kubectl -o yaml -n <namespace> get node <nodeid>
nodeName := pod.Spec.NodeName
node, err := s.k8sClient.Nodes().Get(ctx, nodeName, metav1.GetOptions{})
if err != nil {
return "", err
}
labels := node.ObjectMeta.Labels
region := labels[regionLabel]
if len(region) == 0 {
return "", fmt.Errorf("Region not found")
}
return region, nil
}
// Private helper to write a new secret or perform an update on a previously mounted secret.
//
// If the driver writes the secrets just return the dirver data. Otherwise,
// we write the secret to a temp file and then rename in order to get as close
// to an atomic update as the file system supports. This is to avoid having
// pod applications inadvertantly reading an empty or partial files as it is
// being updated.
func (s *CSIDriverProviderServer) writeFile(secret *provider.SecretValue, mode os.FileMode) (*v1alpha1.File, error) {
// Don't write if the driver is supposed to do it.
if s.driverWriteSecrets {
return &v1alpha1.File{
Path: secret.Descriptor.GetFileName(),
Mode: int32(mode),
Contents: secret.Value,
}, nil
}
// Write to a tempfile first
tmpFile, err := os.CreateTemp(secret.Descriptor.GetMountDir(), secret.Descriptor.GetFileName())
if err != nil {
return nil, err
}
defer os.Remove(tmpFile.Name()) // Cleanup on fail
defer tmpFile.Close() // Don't leak file descriptors
err = tmpFile.Chmod(mode) // Set correct permissions
if err != nil {
return nil, err
}
_, err = tmpFile.Write(secret.Value) // Write the secret
if err != nil {
return nil, err
}
err = tmpFile.Sync() // Make sure to flush to disk
if err != nil {
return nil, err
}
// Swap out the old secret for the new
err = os.Rename(tmpFile.Name(), secret.Descriptor.GetMountPath())
if err != nil {
return nil, err
}
return nil, nil
}