• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kubernetes-sigs / blob-csi-driver / 6947043498

21 Nov 2023 04:52PM UTC coverage: 80.422%. Remained the same
6947043498

Pull #1123

github

web-flow
chore(deps): bump github.com/onsi/ginkgo/v2 from 2.13.0 to 2.13.1

Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.13.0 to 2.13.1.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.13.0...v2.13.1)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #1123: chore(deps): bump github.com/onsi/ginkgo/v2 from 2.13.0 to 2.13.1

1980 of 2462 relevant lines covered (80.42%)

7.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.26
/pkg/blob/blob.go
1
/*
2
Copyright 2019 The Kubernetes Authors.
3

4
Licensed under the Apache License, Version 2.0 (the "License");
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
7

8
    http://www.apache.org/licenses/LICENSE-2.0
9

10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16

17
package blob
18

19
import (
20
        "fmt"
21
        "os"
22
        "strconv"
23
        "strings"
24
        "sync"
25
        "time"
26

27
        "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2021-09-01/storage"
28
        azstorage "github.com/Azure/azure-sdk-for-go/storage"
29
        az "github.com/Azure/go-autorest/autorest/azure"
30
        "github.com/container-storage-interface/spec/lib/go/csi"
31
        "github.com/pborman/uuid"
32
        "golang.org/x/net/context"
33

34
        v1 "k8s.io/api/core/v1"
35
        "k8s.io/apimachinery/pkg/api/errors"
36
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
37
        "k8s.io/client-go/kubernetes"
38
        "k8s.io/klog/v2"
39
        k8sutil "k8s.io/kubernetes/pkg/volume/util"
40
        mount "k8s.io/mount-utils"
41
        utilexec "k8s.io/utils/exec"
42

43
        csicommon "sigs.k8s.io/blob-csi-driver/pkg/csi-common"
44
        "sigs.k8s.io/blob-csi-driver/pkg/util"
45
        azcache "sigs.k8s.io/cloud-provider-azure/pkg/cache"
46
        azure "sigs.k8s.io/cloud-provider-azure/pkg/provider"
47
)
48

49
const (
50
        // DefaultDriverName holds the name of the csi-driver
51
        DefaultDriverName              = "blob.csi.azure.com"
52
        blobCSIDriverName              = "blob_csi_driver"
53
        separator                      = "#"
54
        volumeIDTemplate               = "%s#%s#%s#%s#%s#%s"
55
        secretNameTemplate             = "azure-storage-account-%s-secret"
56
        serverNameField                = "server"
57
        storageEndpointSuffixField     = "storageendpointsuffix"
58
        tagsField                      = "tags"
59
        matchTagsField                 = "matchtags"
60
        protocolField                  = "protocol"
61
        accountNameField               = "accountname"
62
        accountKeyField                = "accountkey"
63
        storageAccountField            = "storageaccount"
64
        storageAccountTypeField        = "storageaccounttype"
65
        skuNameField                   = "skuname"
66
        subscriptionIDField            = "subscriptionid"
67
        resourceGroupField             = "resourcegroup"
68
        locationField                  = "location"
69
        secretNameField                = "secretname"
70
        secretNamespaceField           = "secretnamespace"
71
        containerNameField             = "containername"
72
        containerNamePrefixField       = "containernameprefix"
73
        storeAccountKeyField           = "storeaccountkey"
74
        getLatestAccountKeyField       = "getlatestaccountkey"
75
        isHnsEnabledField              = "ishnsenabled"
76
        softDeleteBlobsField           = "softdeleteblobs"
77
        softDeleteContainersField      = "softdeletecontainers"
78
        enableBlobVersioningField      = "enableblobversioning"
79
        getAccountKeyFromSecretField   = "getaccountkeyfromsecret"
80
        storageSPNClientIDField        = "azurestoragespnclientid"
81
        storageSPNTenantIDField        = "azurestoragespntenantid"
82
        storageAuthTypeField           = "azurestorageauthtype"
83
        storageIentityClientIDField    = "azurestorageidentityclientid"
84
        storageIdentityObjectIDField   = "azurestorageidentityobjectid"
85
        storageIdentityResourceIDField = "azurestorageidentityresourceid"
86
        msiEndpointField               = "msiendpoint"
87
        storageAADEndpointField        = "azurestorageaadendpoint"
88
        keyVaultURLField               = "keyvaulturl"
89
        keyVaultSecretNameField        = "keyvaultsecretname"
90
        keyVaultSecretVersionField     = "keyvaultsecretversion"
91
        storageAccountNameField        = "storageaccountname"
92
        allowBlobPublicAccessField     = "allowblobpublicaccess"
93
        requireInfraEncryptionField    = "requireinfraencryption"
94
        ephemeralField                 = "csi.storage.k8s.io/ephemeral"
95
        podNamespaceField              = "csi.storage.k8s.io/pod.namespace"
96
        mountOptionsField              = "mountoptions"
97
        falseValue                     = "false"
98
        trueValue                      = "true"
99
        defaultSecretAccountName       = "azurestorageaccountname"
100
        defaultSecretAccountKey        = "azurestorageaccountkey"
101
        accountSasTokenField           = "azurestorageaccountsastoken"
102
        msiSecretField                 = "msisecret"
103
        storageSPNClientSecretField    = "azurestoragespnclientsecret"
104
        Fuse                           = "fuse"
105
        Fuse2                          = "fuse2"
106
        NFS                            = "nfs"
107
        AZNFS                          = "aznfs"
108
        vnetResourceGroupField         = "vnetresourcegroup"
109
        vnetNameField                  = "vnetname"
110
        subnetNameField                = "subnetname"
111
        accessTierField                = "accesstier"
112
        networkEndpointTypeField       = "networkendpointtype"
113
        mountPermissionsField          = "mountpermissions"
114
        useDataPlaneAPIField           = "usedataplaneapi"
115

116
        // See https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names
117
        containerNameMinLength = 3
118
        containerNameMaxLength = 63
119

120
        accountNotProvisioned                   = "StorageAccountIsNotProvisioned"
121
        tooManyRequests                         = "TooManyRequests"
122
        clientThrottled                         = "client throttled"
123
        containerBeingDeletedDataplaneAPIError  = "ContainerBeingDeleted"
124
        containerBeingDeletedManagementAPIError = "container is being deleted"
125
        statusCodeNotFound                      = "StatusCode=404"
126
        httpCodeNotFound                        = "HTTPStatusCode: 404"
127

128
        // containerMaxSize is the max size of the blob container. See https://docs.microsoft.com/en-us/azure/storage/blobs/scalability-targets#scale-targets-for-blob-storage
129
        containerMaxSize = 100 * util.TiB
130

131
        subnetTemplate = "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/virtualNetworks/%s/subnets/%s"
132

133
        defaultNamespace = "default"
134

135
        pvcNameKey           = "csi.storage.k8s.io/pvc/name"
136
        pvcNamespaceKey      = "csi.storage.k8s.io/pvc/namespace"
137
        pvNameKey            = "csi.storage.k8s.io/pv/name"
138
        pvcNameMetadata      = "${pvc.metadata.name}"
139
        pvcNamespaceMetadata = "${pvc.metadata.namespace}"
140
        pvNameMetadata       = "${pv.metadata.name}"
141

142
        VolumeID = "volumeid"
143

144
        defaultStorageEndPointSuffix = "core.windows.net"
145
)
146

147
var (
148
        supportedProtocolList = []string{Fuse, Fuse2, NFS}
149
        retriableErrors       = []string{accountNotProvisioned, tooManyRequests, statusCodeNotFound, containerBeingDeletedDataplaneAPIError, containerBeingDeletedManagementAPIError, clientThrottled}
150
)
151

152
// DriverOptions defines driver parameters specified in driver deployment
153
type DriverOptions struct {
154
        NodeID                                 string
155
        DriverName                             string
156
        BlobfuseProxyEndpoint                  string
157
        EnableBlobfuseProxy                    bool
158
        BlobfuseProxyConnTimout                int
159
        EnableBlobMockMount                    bool
160
        AllowInlineVolumeKeyAccessWithIdentity bool
161
        EnableGetVolumeStats                   bool
162
        AppendTimeStampInCacheDir              bool
163
        AppendMountErrorHelpLink               bool
164
        MountPermissions                       uint64
165
        EnableAznfsMount                       bool
166
        VolStatsCacheExpireInMinutes           int
167
        SasTokenExpirationMinutes              int
168
}
169

170
// Driver implements all interfaces of CSI drivers
171
type Driver struct {
172
        csicommon.CSIDriver
173

174
        cloud                 *azure.Cloud
175
        blobfuseProxyEndpoint string
176
        // enableBlobMockMount is only for testing, DO NOT set as true in non-testing scenario
177
        enableBlobMockMount                    bool
178
        enableBlobfuseProxy                    bool
179
        enableGetVolumeStats                   bool
180
        allowInlineVolumeKeyAccessWithIdentity bool
181
        appendTimeStampInCacheDir              bool
182
        appendMountErrorHelpLink               bool
183
        blobfuseProxyConnTimout                int
184
        mountPermissions                       uint64
185
        enableAznfsMount                       bool
186
        mounter                                *mount.SafeFormatAndMount
187
        volLockMap                             *util.LockMap
188
        // A map storing all volumes with ongoing operations so that additional operations
189
        // for that same volume (as defined by VolumeID) return an Aborted error
190
        volumeLocks *volumeLocks
191
        // only for nfs feature
192
        subnetLockMap *util.LockMap
193
        // a map storing all volumes created by this driver <volumeName, accountName>
194
        volMap sync.Map
195
        // a timed cache storing all volumeIDs and storage accounts that are using data plane API
196
        dataPlaneAPIVolCache azcache.Resource
197
        // a timed cache storing account search history (solve account list throttling issue)
198
        accountSearchCache azcache.Resource
199
        // a timed cache storing volume stats <volumeID, volumeStats>
200
        volStatsCache azcache.Resource
201
        // sas expiry time for azcopy in volume clone
202
        sasTokenExpirationMinutes int
203
        // azcopy for provide exec mock for ut
204
        azcopy *util.Azcopy
205
}
206

207
// NewDriver Creates a NewCSIDriver object. Assumes vendor version is equal to driver version &
208
// does not support optional driver plugin info manifest field. Refer to CSI spec for more details.
209
func NewDriver(options *DriverOptions, cloud *azure.Cloud) *Driver {
113✔
210
        d := Driver{
113✔
211
                volLockMap:                             util.NewLockMap(),
113✔
212
                subnetLockMap:                          util.NewLockMap(),
113✔
213
                volumeLocks:                            newVolumeLocks(),
113✔
214
                blobfuseProxyEndpoint:                  options.BlobfuseProxyEndpoint,
113✔
215
                enableBlobfuseProxy:                    options.EnableBlobfuseProxy,
113✔
216
                allowInlineVolumeKeyAccessWithIdentity: options.AllowInlineVolumeKeyAccessWithIdentity,
113✔
217
                blobfuseProxyConnTimout:                options.BlobfuseProxyConnTimout,
113✔
218
                enableBlobMockMount:                    options.EnableBlobMockMount,
113✔
219
                enableGetVolumeStats:                   options.EnableGetVolumeStats,
113✔
220
                appendMountErrorHelpLink:               options.AppendMountErrorHelpLink,
113✔
221
                mountPermissions:                       options.MountPermissions,
113✔
222
                enableAznfsMount:                       options.EnableAznfsMount,
113✔
223
                sasTokenExpirationMinutes:              options.SasTokenExpirationMinutes,
113✔
224
                azcopy:                                 &util.Azcopy{},
113✔
225
        }
113✔
226
        d.Name = options.DriverName
113✔
227
        d.Version = driverVersion
113✔
228
        d.NodeID = options.NodeID
113✔
229

113✔
230
        var err error
113✔
231
        getter := func(key string) (interface{}, error) { return nil, nil }
118✔
232
        if d.accountSearchCache, err = azcache.NewTimedCache(time.Minute, getter, false); err != nil {
113✔
233
                klog.Fatalf("%v", err)
×
234
        }
×
235
        if d.dataPlaneAPIVolCache, err = azcache.NewTimedCache(10*time.Minute, getter, false); err != nil {
113✔
236
                klog.Fatalf("%v", err)
×
237
        }
×
238

239
        if options.VolStatsCacheExpireInMinutes <= 0 {
226✔
240
                options.VolStatsCacheExpireInMinutes = 10 // default expire in 10 minutes
113✔
241
        }
113✔
242
        if d.volStatsCache, err = azcache.NewTimedCache(time.Duration(options.VolStatsCacheExpireInMinutes)*time.Minute, getter, false); err != nil {
113✔
243
                klog.Fatalf("%v", err)
×
244
        }
×
245
        d.cloud = cloud
113✔
246
        d.mounter = &mount.SafeFormatAndMount{
113✔
247
                Interface: mount.New(""),
113✔
248
                Exec:      utilexec.New(),
113✔
249
        }
113✔
250

113✔
251
        // Initialize default library driver
113✔
252
        d.AddControllerServiceCapabilities(
113✔
253
                []csi.ControllerServiceCapability_RPC_Type{
113✔
254
                        csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME,
113✔
255
                        //csi.ControllerServiceCapability_RPC_CREATE_DELETE_SNAPSHOT,
113✔
256
                        //csi.ControllerServiceCapability_RPC_LIST_SNAPSHOTS,
113✔
257
                        csi.ControllerServiceCapability_RPC_EXPAND_VOLUME,
113✔
258
                        csi.ControllerServiceCapability_RPC_SINGLE_NODE_MULTI_WRITER,
113✔
259
                        csi.ControllerServiceCapability_RPC_CLONE_VOLUME,
113✔
260
                })
113✔
261
        d.AddVolumeCapabilityAccessModes([]csi.VolumeCapability_AccessMode_Mode{
113✔
262
                csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
113✔
263
                csi.VolumeCapability_AccessMode_SINGLE_NODE_READER_ONLY,
113✔
264
                csi.VolumeCapability_AccessMode_SINGLE_NODE_SINGLE_WRITER,
113✔
265
                csi.VolumeCapability_AccessMode_SINGLE_NODE_MULTI_WRITER,
113✔
266
                csi.VolumeCapability_AccessMode_MULTI_NODE_READER_ONLY,
113✔
267
                csi.VolumeCapability_AccessMode_MULTI_NODE_SINGLE_WRITER,
113✔
268
                csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER,
113✔
269
        })
113✔
270

113✔
271
        nodeCap := []csi.NodeServiceCapability_RPC_Type{
113✔
272
                csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME,
113✔
273
                csi.NodeServiceCapability_RPC_SINGLE_NODE_MULTI_WRITER,
113✔
274
        }
113✔
275
        if d.enableGetVolumeStats {
113✔
276
                nodeCap = append(nodeCap, csi.NodeServiceCapability_RPC_GET_VOLUME_STATS)
×
277
        }
×
278
        d.AddNodeServiceCapabilities(nodeCap)
113✔
279

113✔
280
        return &d
113✔
281
}
282

283
// Run driver initialization
284
func (d *Driver) Run(endpoint string, testBool bool) {
2✔
285
        versionMeta, err := GetVersionYAML(d.Name)
2✔
286
        if err != nil {
2✔
287
                klog.Fatalf("%v", err)
×
288
        }
×
289
        klog.Infof("\nDRIVER INFORMATION:\n-------------------\n%s\n\nStreaming logs below:", versionMeta)
2✔
290

2✔
291
        s := csicommon.NewNonBlockingGRPCServer()
2✔
292
        // Driver d act as IdentityServer, ControllerServer and NodeServer
2✔
293
        s.Start(endpoint, d, d, d, testBool)
2✔
294
        s.Wait()
2✔
295
}
296

297
// GetContainerInfo get container info according to volume id
298
// the format of VolumeId is: rg#accountName#containerName#uuid#secretNamespace#subsID
299
//
300
// e.g.
301
// input: "rg#f5713de20cde511e8ba4900#containerName#uuid#"
302
// output: rg, f5713de20cde511e8ba4900, containerName, "" , ""
303
// input: "rg#f5713de20cde511e8ba4900#containerName#uuid#namespace#"
304
// output: rg, f5713de20cde511e8ba4900, containerName, namespace, ""
305
// input: "rg#f5713de20cde511e8ba4900#containerName#uuid#namespace#subsID"
306
// output: rg, f5713de20cde511e8ba4900, containerName, namespace, subsID
307
func GetContainerInfo(id string) (string, string, string, string, string, error) {
39✔
308
        segments := strings.Split(id, separator)
39✔
309
        if len(segments) < 3 {
48✔
310
                return "", "", "", "", "", fmt.Errorf("error parsing volume id: %q, should at least contain two #", id)
9✔
311
        }
9✔
312
        var secretNamespace, subsID string
30✔
313
        if len(segments) > 4 {
36✔
314
                secretNamespace = segments[4]
6✔
315
        }
6✔
316
        if len(segments) > 5 {
33✔
317
                subsID = segments[5]
3✔
318
        }
3✔
319
        return segments[0], segments[1], segments[2], secretNamespace, subsID, nil
30✔
320
}
321

322
// A container name must be a valid DNS name, conforming to the following naming rules:
323
//  1. Container names must start with a letter or number, and can contain only letters, numbers, and the dash (-) character.
324
//  2. Every dash (-) character must be immediately preceded and followed by a letter or number; consecutive dashes are not permitted in container names.
325
//  3. All letters in a container name must be lowercase.
326
//  4. Container names must be from 3 through 63 characters long.
327
//
328
// See https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names
329
func getValidContainerName(volumeName, protocol string) string {
5✔
330
        containerName := strings.ToLower(volumeName)
5✔
331
        if len(containerName) > containerNameMaxLength {
6✔
332
                containerName = containerName[0:containerNameMaxLength]
1✔
333
        }
1✔
334
        if !checkContainerNameBeginAndEnd(containerName) || len(containerName) < containerNameMinLength {
5✔
335
                // now we set as 63 for maximum container name length
×
336
                // todo: get cluster name
×
337
                containerName = k8sutil.GenerateVolumeName(fmt.Sprintf("pvc-%s", protocol), uuid.NewUUID().String(), 63)
×
338
                klog.Warningf("requested volume name (%s) is invalid, regenerated as (%q)", volumeName, containerName)
×
339
        }
×
340
        return strings.Replace(containerName, "--", "-", -1)
5✔
341
}
342

343
func checkContainerNameBeginAndEnd(containerName string) bool {
11✔
344
        length := len(containerName)
11✔
345
        if (('a' <= containerName[0] && containerName[0] <= 'z') ||
11✔
346
                ('0' <= containerName[0] && containerName[0] <= '9')) &&
11✔
347
                (('a' <= containerName[length-1] && containerName[length-1] <= 'z') ||
11✔
348
                        ('0' <= containerName[length-1] && containerName[length-1] <= '9')) {
20✔
349
                return true
9✔
350
        }
9✔
351

352
        return false
2✔
353
}
354

355
// isSASToken checks if the key contains the patterns.
356
// SAS token format could refer to https://docs.microsoft.com/en-us/rest/api/eventhub/generate-sas-token
357
func isSASToken(key string) bool {
3✔
358
        return strings.HasPrefix(key, "?")
3✔
359
}
3✔
360

361
// GetAuthEnv return <accountName, containerName, authEnv, error>
362
func (d *Driver) GetAuthEnv(ctx context.Context, volumeID, protocol string, attrib, secrets map[string]string) (string, string, string, string, []string, error) {
10✔
363
        rgName, accountName, containerName, secretNamespace, _, err := GetContainerInfo(volumeID)
10✔
364
        if err != nil {
12✔
365
                // ignore volumeID parsing error
2✔
366
                klog.V(2).Infof("parsing volumeID(%s) return with error: %v", volumeID, err)
2✔
367
                err = nil
2✔
368
        }
2✔
369

370
        var (
10✔
371
                subsID                  string
10✔
372
                accountKey              string
10✔
373
                accountSasToken         string
10✔
374
                msiSecret               string
10✔
375
                storageSPNClientSecret  string
10✔
376
                storageSPNClientID      string
10✔
377
                storageSPNTenantID      string
10✔
378
                secretName              string
10✔
379
                pvcNamespace            string
10✔
380
                keyVaultURL             string
10✔
381
                keyVaultSecretName      string
10✔
382
                keyVaultSecretVersion   string
10✔
383
                azureStorageAuthType    string
10✔
384
                authEnv                 []string
10✔
385
                getAccountKeyFromSecret bool
10✔
386
                getLatestAccountKey     bool
10✔
387
        )
10✔
388

10✔
389
        for k, v := range attrib {
36✔
390
                switch strings.ToLower(k) {
26✔
391
                case subscriptionIDField:
1✔
392
                        subsID = v
1✔
393
                case resourceGroupField:
×
394
                        rgName = v
×
395
                case containerNameField:
3✔
396
                        containerName = v
3✔
397
                case keyVaultURLField:
1✔
398
                        keyVaultURL = v
1✔
399
                case keyVaultSecretNameField:
1✔
400
                        keyVaultSecretName = v
1✔
401
                case keyVaultSecretVersionField:
1✔
402
                        keyVaultSecretVersion = v
1✔
403
                case storageAccountField:
2✔
404
                        accountName = v
2✔
405
                case storageAccountNameField: // for compatibility
1✔
406
                        accountName = v
1✔
407
                case secretNameField:
1✔
408
                        secretName = v
1✔
409
                case secretNamespaceField:
1✔
410
                        secretNamespace = v
1✔
411
                case pvcNamespaceKey:
1✔
412
                        pvcNamespace = v
1✔
413
                case getAccountKeyFromSecretField:
1✔
414
                        getAccountKeyFromSecret = strings.EqualFold(v, trueValue)
1✔
415
                case storageAuthTypeField:
×
416
                        azureStorageAuthType = v
×
417
                        authEnv = append(authEnv, "AZURE_STORAGE_AUTH_TYPE="+v)
×
418
                case storageIentityClientIDField:
1✔
419
                        authEnv = append(authEnv, "AZURE_STORAGE_IDENTITY_CLIENT_ID="+v)
1✔
420
                case storageIdentityObjectIDField:
1✔
421
                        authEnv = append(authEnv, "AZURE_STORAGE_IDENTITY_OBJECT_ID="+v)
1✔
422
                case storageIdentityResourceIDField:
1✔
423
                        authEnv = append(authEnv, "AZURE_STORAGE_IDENTITY_RESOURCE_ID="+v)
1✔
424
                case msiEndpointField:
1✔
425
                        authEnv = append(authEnv, "MSI_ENDPOINT="+v)
1✔
426
                case storageSPNClientIDField:
1✔
427
                        storageSPNClientID = v
1✔
428
                case storageSPNTenantIDField:
1✔
429
                        storageSPNTenantID = v
1✔
430
                case storageAADEndpointField:
1✔
431
                        authEnv = append(authEnv, "AZURE_STORAGE_AAD_ENDPOINT="+v)
1✔
432
                case getLatestAccountKeyField:
1✔
433
                        if getLatestAccountKey, err = strconv.ParseBool(v); err != nil {
2✔
434
                                return rgName, accountName, accountKey, containerName, authEnv, fmt.Errorf("invalid %s: %s in volume context", getLatestAccountKeyField, v)
1✔
435
                        }
1✔
436
                }
437
        }
438
        klog.V(2).Infof("volumeID(%s) authEnv: %s", volumeID, authEnv)
9✔
439

9✔
440
        if protocol == NFS {
11✔
441
                // nfs protocol does not need account key, return directly
2✔
442
                return rgName, accountName, accountKey, containerName, authEnv, err
2✔
443
        }
2✔
444

445
        if secretNamespace == "" {
13✔
446
                if pvcNamespace == "" {
12✔
447
                        secretNamespace = defaultNamespace
6✔
448
                } else {
6✔
449
                        secretNamespace = pvcNamespace
×
450
                }
×
451
        }
452

453
        if rgName == "" {
8✔
454
                rgName = d.cloud.ResourceGroup
1✔
455
        }
1✔
456

457
        // 1. If keyVaultURL is not nil, preferentially use the key stored in key vault.
458
        // 2. Then if secrets map is not nil, use the key stored in the secrets map.
459
        // 3. Finally if both keyVaultURL and secrets map are nil, get the key from Azure.
460
        if keyVaultURL != "" {
8✔
461
                key, err := d.getKeyVaultSecretContent(ctx, keyVaultURL, keyVaultSecretName, keyVaultSecretVersion)
1✔
462
                if err != nil {
2✔
463
                        return rgName, accountName, accountKey, containerName, authEnv, err
1✔
464
                }
1✔
465
                if isSASToken(key) {
×
466
                        accountSasToken = key
×
467
                } else {
×
468
                        accountKey = key
×
469
                }
×
470
        } else {
6✔
471
                if len(secrets) == 0 {
11✔
472
                        if secretName == "" && accountName != "" {
9✔
473
                                secretName = fmt.Sprintf(secretNameTemplate, accountName)
4✔
474
                        }
4✔
475
                        if secretName != "" {
10✔
476
                                // read from k8s secret first
5✔
477
                                var name, spnClientID, spnTenantID string
5✔
478
                                name, accountKey, accountSasToken, msiSecret, storageSPNClientSecret, spnClientID, spnTenantID, err = d.GetInfoFromSecret(ctx, secretName, secretNamespace)
5✔
479
                                if name != "" {
5✔
480
                                        accountName = name
×
481
                                }
×
482
                                if spnClientID != "" {
5✔
483
                                        storageSPNClientID = spnClientID
×
484
                                }
×
485
                                if spnTenantID != "" {
5✔
486
                                        storageSPNTenantID = spnTenantID
×
487
                                }
×
488
                                if err != nil && strings.EqualFold(azureStorageAuthType, "msi") {
5✔
489
                                        klog.V(2).Infof("ignore error(%v) since secret is optional for auth type(%s)", err, azureStorageAuthType)
×
490
                                        err = nil
×
491
                                }
×
492
                                if err != nil && !getAccountKeyFromSecret && (azureStorageAuthType == "" || strings.EqualFold(azureStorageAuthType, "key")) {
10✔
493
                                        klog.V(2).Infof("get account(%s) key from secret(%s, %s) failed with error: %v, use cluster identity to get account key instead",
5✔
494
                                                accountName, secretNamespace, secretName, err)
5✔
495
                                        accountKey, err = d.cloud.GetStorageAccesskey(ctx, subsID, accountName, rgName, getLatestAccountKey)
5✔
496
                                        if err != nil {
7✔
497
                                                return rgName, accountName, accountKey, containerName, authEnv, fmt.Errorf("no key for storage account(%s) under resource group(%s), err %w", accountName, rgName, err)
2✔
498
                                        }
2✔
499
                                }
500
                        }
501
                } else {
1✔
502
                        for k, v := range secrets {
8✔
503
                                v = strings.TrimSpace(v)
7✔
504
                                switch strings.ToLower(k) {
7✔
505
                                case accountNameField:
1✔
506
                                        accountName = v
1✔
507
                                case defaultSecretAccountName: // for compatibility with built-in blobfuse plugin
1✔
508
                                        accountName = v
1✔
509
                                case accountKeyField:
1✔
510
                                        accountKey = v
1✔
511
                                case defaultSecretAccountKey: // for compatibility with built-in blobfuse plugin
1✔
512
                                        accountKey = v
1✔
513
                                case accountSasTokenField:
1✔
514
                                        accountSasToken = v
1✔
515
                                case msiSecretField:
1✔
516
                                        msiSecret = v
1✔
517
                                case storageSPNClientSecretField:
1✔
518
                                        storageSPNClientSecret = v
1✔
519
                                case storageSPNClientIDField:
×
520
                                        storageSPNClientID = v
×
521
                                case storageSPNTenantIDField:
×
522
                                        storageSPNTenantID = v
×
523
                                }
524
                        }
525
                }
526
        }
527

528
        if containerName == "" {
4✔
529
                err = fmt.Errorf("could not find containerName from attributes(%v) or volumeID(%v)", attrib, volumeID)
×
530
        }
×
531

532
        if accountKey != "" {
8✔
533
                authEnv = append(authEnv, "AZURE_STORAGE_ACCESS_KEY="+accountKey)
4✔
534
        }
4✔
535

536
        if accountSasToken != "" {
5✔
537
                klog.V(2).Infof("accountSasToken is not empty, use it to access storage account(%s), container(%s)", accountName, containerName)
1✔
538
                authEnv = append(authEnv, "AZURE_STORAGE_SAS_TOKEN="+accountSasToken)
1✔
539
        }
1✔
540

541
        if msiSecret != "" {
5✔
542
                klog.V(2).Infof("msiSecret is not empty, use it to access storage account(%s), container(%s)", accountName, containerName)
1✔
543
                authEnv = append(authEnv, "MSI_SECRET="+msiSecret)
1✔
544
        }
1✔
545

546
        if storageSPNClientSecret != "" {
5✔
547
                klog.V(2).Infof("storageSPNClientSecret is not empty, use it to access storage account(%s), container(%s)", accountName, containerName)
1✔
548
                authEnv = append(authEnv, "AZURE_STORAGE_SPN_CLIENT_SECRET="+storageSPNClientSecret)
1✔
549
        }
1✔
550

551
        if storageSPNClientID != "" {
4✔
552
                klog.V(2).Infof("storageSPNClientID(%s) is not empty, use it to access storage account(%s), container(%s)", storageSPNClientID, accountName, containerName)
×
553
                authEnv = append(authEnv, "AZURE_STORAGE_SPN_CLIENT_ID="+storageSPNClientID)
×
554
        }
×
555

556
        if storageSPNTenantID != "" {
4✔
557
                klog.V(2).Infof("storageSPNTenantID(%s) is not empty, use it to access storage account(%s), container(%s)", storageSPNTenantID, accountName, containerName)
×
558
                authEnv = append(authEnv, "AZURE_STORAGE_SPN_TENANT_ID="+storageSPNTenantID)
×
559
        }
×
560

561
        return rgName, accountName, accountKey, containerName, authEnv, err
4✔
562
}
563

564
// GetStorageAccountAndContainer get storage account and container info
565
// returns <accountName, accountKey, accountSasToken, containerName>
566
// only for e2e testing
567
func (d *Driver) GetStorageAccountAndContainer(ctx context.Context, volumeID string, attrib, secrets map[string]string) (string, string, string, string, error) {
3✔
568
        var (
3✔
569
                subsID                string
3✔
570
                accountName           string
3✔
571
                accountKey            string
3✔
572
                accountSasToken       string
3✔
573
                containerName         string
3✔
574
                keyVaultURL           string
3✔
575
                keyVaultSecretName    string
3✔
576
                keyVaultSecretVersion string
3✔
577
                getLatestAccountKey   bool
3✔
578
                err                   error
3✔
579
        )
3✔
580

3✔
581
        for k, v := range attrib {
8✔
582
                switch strings.ToLower(k) {
5✔
583
                case subscriptionIDField:
×
584
                        subsID = v
×
585
                case containerNameField:
1✔
586
                        containerName = v
1✔
587
                case keyVaultURLField:
×
588
                        keyVaultURL = v
×
589
                case keyVaultSecretNameField:
1✔
590
                        keyVaultSecretName = v
1✔
591
                case keyVaultSecretVersionField:
1✔
592
                        keyVaultSecretVersion = v
1✔
593
                case storageAccountField:
×
594
                        accountName = v
×
595
                case storageAccountNameField: // for compatibility
1✔
596
                        accountName = v
1✔
597
                case getLatestAccountKeyField:
1✔
598
                        if getLatestAccountKey, err = strconv.ParseBool(v); err != nil {
2✔
599
                                return "", "", "", "", fmt.Errorf("invalid %s: %s in volume context", getLatestAccountKeyField, v)
1✔
600
                        }
1✔
601
                }
602
        }
603

604
        // 1. If keyVaultURL is not nil, preferentially use the key stored in key vault.
605
        // 2. Then if secrets map is not nil, use the key stored in the secrets map.
606
        // 3. Finally if both keyVaultURL and secrets map are nil, get the key from Azure.
607
        if keyVaultURL != "" {
2✔
608
                key, err := d.getKeyVaultSecretContent(ctx, keyVaultURL, keyVaultSecretName, keyVaultSecretVersion)
×
609
                if err != nil {
×
610
                        return "", "", "", "", err
×
611
                }
×
612
                if isSASToken(key) {
×
613
                        accountSasToken = key
×
614
                } else {
×
615
                        accountKey = key
×
616
                }
×
617
        } else {
2✔
618
                if len(secrets) == 0 {
4✔
619
                        var rgName string
2✔
620
                        rgName, accountName, containerName, _, _, err = GetContainerInfo(volumeID)
2✔
621
                        if err != nil {
2✔
622
                                return "", "", "", "", err
×
623
                        }
×
624

625
                        if rgName == "" {
2✔
626
                                rgName = d.cloud.ResourceGroup
×
627
                        }
×
628

629
                        accountKey, err = d.cloud.GetStorageAccesskey(ctx, subsID, accountName, rgName, getLatestAccountKey)
2✔
630
                        if err != nil {
3✔
631
                                return "", "", "", "", fmt.Errorf("no key for storage account(%s) under resource group(%s), err %w", accountName, rgName, err)
1✔
632
                        }
1✔
633
                }
634
        }
635

636
        if containerName == "" {
1✔
637
                return "", "", "", "", fmt.Errorf("could not find containerName from attributes(%v) or volumeID(%v)", attrib, volumeID)
×
638
        }
×
639

640
        return accountName, accountKey, accountSasToken, containerName, nil
1✔
641
}
642

643
func IsCorruptedDir(dir string) bool {
4✔
644
        _, pathErr := mount.PathExists(dir)
4✔
645
        return pathErr != nil && mount.IsCorruptedMnt(pathErr)
4✔
646
}
4✔
647

648
func isRetriableError(err error) bool {
5✔
649
        if err != nil {
9✔
650
                for _, v := range retriableErrors {
19✔
651
                        if strings.Contains(strings.ToLower(err.Error()), strings.ToLower(v)) {
18✔
652
                                return true
3✔
653
                        }
3✔
654
                }
655
        }
656
        return false
2✔
657
}
658

659
func isSupportedProtocol(protocol string) bool {
17✔
660
        if protocol == "" {
18✔
661
                return true
1✔
662
        }
1✔
663
        for _, v := range supportedProtocolList {
40✔
664
                if protocol == v {
38✔
665
                        return true
14✔
666
                }
14✔
667
        }
668
        return false
2✔
669
}
670

671
func isSupportedAccessTier(accessTier string) bool {
20✔
672
        if accessTier == "" {
33✔
673
                return true
13✔
674
        }
13✔
675
        for _, tier := range storage.PossibleAccessTierValues() {
25✔
676
                if accessTier == string(tier) {
21✔
677
                        return true
3✔
678
                }
3✔
679
        }
680
        return false
4✔
681
}
682

683
// container names can contain only lowercase letters, numbers, and hyphens,
684
// and must begin and end with a letter or a number
685
func isSupportedContainerNamePrefix(prefix string) bool {
19✔
686
        if prefix == "" {
30✔
687
                return true
11✔
688
        }
11✔
689
        if len(prefix) > 20 {
9✔
690
                return false
1✔
691
        }
1✔
692
        if prefix[0] == '-' {
8✔
693
                return false
1✔
694
        }
1✔
695
        for _, v := range prefix {
19✔
696
                if v != '-' && (v < '0' || v > '9') && (v < 'a' || v > 'z') {
17✔
697
                        return false
4✔
698
                }
4✔
699
        }
700
        return true
2✔
701
}
702

703
// isNFSProtocol checks if the protocol is NFS or AZNFS
704
func isNFSProtocol(protocol string) bool {
21✔
705
        protocol = strings.ToLower(protocol)
21✔
706
        return protocol == NFS || protocol == AZNFS
21✔
707
}
21✔
708

709
// get storage account from secrets map
710
func getStorageAccount(secrets map[string]string) (string, string, error) {
18✔
711
        if secrets == nil {
19✔
712
                return "", "", fmt.Errorf("unexpected: getStorageAccount secrets is nil")
1✔
713
        }
1✔
714

715
        var accountName, accountKey string
17✔
716
        for k, v := range secrets {
53✔
717
                v = strings.TrimSpace(v)
36✔
718
                switch strings.ToLower(k) {
36✔
719
                case accountNameField:
7✔
720
                        accountName = v
7✔
721
                case defaultSecretAccountName: // for compatibility with built-in azurefile plugin
9✔
722
                        accountName = v
9✔
723
                case accountKeyField:
7✔
724
                        accountKey = v
7✔
725
                case defaultSecretAccountKey: // for compatibility with built-in azurefile plugin
9✔
726
                        accountKey = v
9✔
727
                }
728
        }
729

730
        if accountName == "" {
21✔
731
                return accountName, accountKey, fmt.Errorf("could not find %s or %s field in secrets", accountNameField, defaultSecretAccountName)
4✔
732
        }
4✔
733
        if accountKey == "" {
16✔
734
                return accountName, accountKey, fmt.Errorf("could not find %s or %s field in secrets", accountKeyField, defaultSecretAccountKey)
3✔
735
        }
3✔
736

737
        accountName = strings.TrimSpace(accountName)
10✔
738
        klog.V(4).Infof("got storage account(%s) from secret", accountName)
10✔
739
        return accountName, accountKey, nil
10✔
740
}
741

742
func getContainerReference(containerName string, secrets map[string]string, env az.Environment) (*azstorage.Container, error) {
9✔
743
        accountName, accountKey, rerr := getStorageAccount(secrets)
9✔
744
        if rerr != nil {
11✔
745
                return nil, rerr
2✔
746
        }
2✔
747
        client, err := azstorage.NewBasicClientOnSovereignCloud(accountName, accountKey, env)
7✔
748
        if err != nil {
13✔
749
                return nil, err
6✔
750
        }
6✔
751
        blobClient := client.GetBlobService()
1✔
752
        container := blobClient.GetContainerReference(containerName)
1✔
753
        if container == nil {
1✔
754
                return nil, fmt.Errorf("ContainerReference of %s is nil", containerName)
×
755
        }
×
756
        return container, nil
1✔
757
}
758

759
func setAzureCredentials(ctx context.Context, kubeClient kubernetes.Interface, accountName, accountKey, secretNamespace string) (string, error) {
6✔
760
        if kubeClient == nil {
8✔
761
                klog.Warningf("could not create secret: kubeClient is nil")
2✔
762
                return "", nil
2✔
763
        }
2✔
764
        if accountName == "" || accountKey == "" {
6✔
765
                return "", fmt.Errorf("the account info is not enough, accountName(%v), accountKey(%v)", accountName, accountKey)
2✔
766
        }
2✔
767
        secretName := fmt.Sprintf(secretNameTemplate, accountName)
2✔
768
        secret := &v1.Secret{
2✔
769
                ObjectMeta: metav1.ObjectMeta{
2✔
770
                        Namespace: secretNamespace,
2✔
771
                        Name:      secretName,
2✔
772
                },
2✔
773
                Data: map[string][]byte{
2✔
774
                        defaultSecretAccountName: []byte(accountName),
2✔
775
                        defaultSecretAccountKey:  []byte(accountKey),
2✔
776
                },
2✔
777
                Type: "Opaque",
2✔
778
        }
2✔
779
        _, err := kubeClient.CoreV1().Secrets(secretNamespace).Create(ctx, secret, metav1.CreateOptions{})
2✔
780
        if errors.IsAlreadyExists(err) {
3✔
781
                err = nil
1✔
782
        }
1✔
783
        if err != nil {
2✔
784
                return "", fmt.Errorf("couldn't create secret %w", err)
×
785
        }
×
786
        return secretName, err
2✔
787
}
788

789
// GetStorageAccesskey get Azure storage account key from
790
//  1. secrets (if not empty)
791
//  2. use k8s client identity to read from k8s secret
792
//  3. use cluster identity to get from storage account directly
793
func (d *Driver) GetStorageAccesskey(ctx context.Context, accountOptions *azure.AccountOptions, secrets map[string]string, secretName, secretNamespace string) (string, string, error) {
9✔
794
        if len(secrets) > 0 {
10✔
795
                return getStorageAccount(secrets)
1✔
796
        }
1✔
797

798
        // read from k8s secret first
799
        if secretName == "" {
14✔
800
                secretName = fmt.Sprintf(secretNameTemplate, accountOptions.Name)
6✔
801
        }
6✔
802
        _, accountKey, _, _, _, _, _, err := d.GetInfoFromSecret(ctx, secretName, secretNamespace) //nolint
8✔
803
        if err != nil {
14✔
804
                klog.V(2).Infof("could not get account(%s) key from secret(%s) namespace(%s), error: %v, use cluster identity to get account key instead", accountOptions.Name, secretName, secretNamespace, err)
6✔
805
                accountKey, err = d.cloud.GetStorageAccesskey(ctx, accountOptions.SubscriptionID, accountOptions.Name, accountOptions.ResourceGroup, accountOptions.GetLatestAccountKey)
6✔
806
        }
6✔
807
        return accountOptions.Name, accountKey, err
8✔
808
}
809

810
// GetInfoFromSecret get info from k8s secret
811
// return <accountName, accountKey, accountSasToken, msiSecret, spnClientSecret, spnClientID, spnTenantID, error>
812
func (d *Driver) GetInfoFromSecret(ctx context.Context, secretName, secretNamespace string) (string, string, string, string, string, string, string, error) {
17✔
813
        if d.cloud.KubeClient == nil {
28✔
814
                return "", "", "", "", "", "", "", fmt.Errorf("could not get account key from secret(%s): KubeClient is nil", secretName)
11✔
815
        }
11✔
816

817
        secret, err := d.cloud.KubeClient.CoreV1().Secrets(secretNamespace).Get(ctx, secretName, metav1.GetOptions{})
6✔
818
        if err != nil {
8✔
819
                return "", "", "", "", "", "", "", fmt.Errorf("could not get secret(%v): %w", secretName, err)
2✔
820
        }
2✔
821

822
        accountName := strings.TrimSpace(string(secret.Data[defaultSecretAccountName][:]))
4✔
823
        accountKey := strings.TrimSpace(string(secret.Data[defaultSecretAccountKey][:]))
4✔
824
        accountSasToken := strings.TrimSpace(string(secret.Data[accountSasTokenField][:]))
4✔
825
        msiSecret := strings.TrimSpace(string(secret.Data[msiSecretField][:]))
4✔
826
        spnClientSecret := strings.TrimSpace(string(secret.Data[storageSPNClientSecretField][:]))
4✔
827
        spnClientID := strings.TrimSpace(string(secret.Data[storageSPNClientIDField][:]))
4✔
828
        spnTenantID := strings.TrimSpace(string(secret.Data[storageSPNTenantIDField][:]))
4✔
829

4✔
830
        klog.V(4).Infof("got storage account(%s) from secret(%s) namespace(%s)", accountName, secretName, secretNamespace)
4✔
831
        return accountName, accountKey, accountSasToken, msiSecret, spnClientSecret, spnClientID, spnTenantID, nil
4✔
832
}
833

834
// getSubnetResourceID get default subnet resource ID from cloud provider config
835
func (d *Driver) getSubnetResourceID(vnetResourceGroup, vnetName, subnetName string) string {
6✔
836
        subsID := d.cloud.SubscriptionID
6✔
837
        if len(d.cloud.NetworkResourceSubscriptionID) > 0 {
10✔
838
                subsID = d.cloud.NetworkResourceSubscriptionID
4✔
839
        }
4✔
840

841
        if len(vnetResourceGroup) == 0 {
11✔
842
                vnetResourceGroup = d.cloud.ResourceGroup
5✔
843
                if len(d.cloud.VnetResourceGroup) > 0 {
8✔
844
                        vnetResourceGroup = d.cloud.VnetResourceGroup
3✔
845
                }
3✔
846
        }
847

848
        if len(vnetName) == 0 {
11✔
849
                vnetName = d.cloud.VnetName
5✔
850
        }
5✔
851

852
        if len(subnetName) == 0 {
11✔
853
                subnetName = d.cloud.SubnetName
5✔
854
        }
5✔
855
        return fmt.Sprintf(subnetTemplate, subsID, vnetResourceGroup, vnetName, subnetName)
6✔
856
}
857

858
func (d *Driver) useDataPlaneAPI(volumeID, accountName string) bool {
4✔
859
        cache, err := d.dataPlaneAPIVolCache.Get(volumeID, azcache.CacheReadTypeDefault)
4✔
860
        if err != nil {
4✔
861
                klog.Errorf("get(%s) from dataPlaneAPIVolCache failed with error: %v", volumeID, err)
×
862
        }
×
863
        if cache != nil {
7✔
864
                return true
3✔
865
        }
3✔
866
        cache, err = d.dataPlaneAPIVolCache.Get(accountName, azcache.CacheReadTypeDefault)
1✔
867
        if err != nil {
1✔
868
                klog.Errorf("get(%s) from dataPlaneAPIVolCache failed with error: %v", accountName, err)
×
869
        }
×
870
        if cache != nil {
1✔
871
                return true
×
872
        }
×
873
        return false
1✔
874
}
875

876
// appendDefaultMountOptions return mount options combined with mountOptions and defaultMountOptions
877
func appendDefaultMountOptions(mountOptions []string, tmpPath, containerName string) []string {
4✔
878
        var defaultMountOptions = map[string]string{
4✔
879
                "--pre-mount-validate": "true",
4✔
880
                "--use-https":          "true",
4✔
881
                "--tmp-path":           tmpPath,
4✔
882
                "--container-name":     containerName,
4✔
883
                // prevent billing charges on mounting
4✔
884
                "--cancel-list-on-mount-seconds": "10",
4✔
885
                // allow remounting using a non-empty tmp-path
4✔
886
                "--empty-dir-check": "false",
4✔
887
        }
4✔
888

4✔
889
        // stores the mount options already included in mountOptions
4✔
890
        included := make(map[string]bool)
4✔
891

4✔
892
        for _, mountOption := range mountOptions {
11✔
893
                for k := range defaultMountOptions {
49✔
894
                        if strings.HasPrefix(mountOption, k) {
46✔
895
                                included[k] = true
4✔
896
                        }
4✔
897
                }
898
        }
899

900
        allMountOptions := mountOptions
4✔
901

4✔
902
        for k, v := range defaultMountOptions {
28✔
903
                if _, isIncluded := included[k]; !isIncluded {
44✔
904
                        if v != "" {
40✔
905
                                allMountOptions = append(allMountOptions, fmt.Sprintf("%s=%s", k, v))
20✔
906
                        } else {
20✔
907
                                allMountOptions = append(allMountOptions, k)
×
908
                        }
×
909
                }
910
        }
911

912
        return allMountOptions
4✔
913
}
914

915
// chmodIfPermissionMismatch only perform chmod when permission mismatches
916
func chmodIfPermissionMismatch(targetPath string, mode os.FileMode) error {
3✔
917
        info, err := os.Lstat(targetPath)
3✔
918
        if err != nil {
4✔
919
                return err
1✔
920
        }
1✔
921
        perm := info.Mode() & os.ModePerm
2✔
922
        if perm != mode {
3✔
923
                klog.V(2).Infof("chmod targetPath(%s, mode:0%o) with permissions(0%o)", targetPath, info.Mode(), mode)
1✔
924
                if err := os.Chmod(targetPath, mode); err != nil {
1✔
925
                        return err
×
926
                }
×
927
        } else {
1✔
928
                klog.V(2).Infof("skip chmod on targetPath(%s) since mode is already 0%o)", targetPath, info.Mode())
1✔
929
        }
1✔
930
        return nil
2✔
931
}
932

933
func createStorageAccountSecret(account, key string) map[string]string {
1✔
934
        secret := make(map[string]string)
1✔
935
        secret[defaultSecretAccountName] = account
1✔
936
        secret[defaultSecretAccountKey] = key
1✔
937
        return secret
1✔
938
}
1✔
939

940
// setKeyValueInMap set key/value pair in map
941
// key in the map is case insensitive, if key already exists, overwrite existing value
942
func setKeyValueInMap(m map[string]string, key, value string) {
6✔
943
        if m == nil {
7✔
944
                return
1✔
945
        }
1✔
946
        for k := range m {
16✔
947
                if strings.EqualFold(k, key) {
13✔
948
                        m[k] = value
2✔
949
                        return
2✔
950
                }
2✔
951
        }
952
        m[key] = value
3✔
953
}
954

955
// replaceWithMap replace key with value for str
956
func replaceWithMap(str string, m map[string]string) string {
13✔
957
        for k, v := range m {
18✔
958
                if k != "" {
9✔
959
                        str = strings.ReplaceAll(str, k, v)
4✔
960
                }
4✔
961
        }
962
        return str
13✔
963
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc