admission.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. /*
  2. Copyright 2014 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package serviceaccount
  14. import (
  15. "context"
  16. "fmt"
  17. "io"
  18. "math/rand"
  19. "strconv"
  20. "strings"
  21. "time"
  22. corev1 "k8s.io/api/core/v1"
  23. "k8s.io/apimachinery/pkg/api/errors"
  24. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  25. "k8s.io/apimachinery/pkg/labels"
  26. "k8s.io/apimachinery/pkg/runtime/schema"
  27. "k8s.io/apimachinery/pkg/util/sets"
  28. "k8s.io/apiserver/pkg/admission"
  29. genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer"
  30. "k8s.io/apiserver/pkg/storage/names"
  31. "k8s.io/client-go/informers"
  32. "k8s.io/client-go/kubernetes"
  33. corev1listers "k8s.io/client-go/listers/core/v1"
  34. "k8s.io/component-base/featuregate"
  35. podutil "k8s.io/kubernetes/pkg/api/pod"
  36. api "k8s.io/kubernetes/pkg/apis/core"
  37. "k8s.io/kubernetes/pkg/features"
  38. "k8s.io/kubernetes/pkg/serviceaccount"
  39. )
  40. const (
  41. // DefaultServiceAccountName is the name of the default service account to set on pods which do not specify a service account
  42. DefaultServiceAccountName = "default"
  43. // EnforceMountableSecretsAnnotation is a default annotation that indicates that a service account should enforce mountable secrets.
  44. // The value must be true to have this annotation take effect
  45. EnforceMountableSecretsAnnotation = "kubernetes.io/enforce-mountable-secrets"
  46. // ServiceAccountVolumeName is the prefix name that will be added to volumes that mount ServiceAccount secrets
  47. ServiceAccountVolumeName = "kube-api-access"
  48. // DefaultAPITokenMountPath is the path that ServiceAccountToken secrets are automounted to.
  49. // The token file would then be accessible at /var/run/secrets/kubernetes.io/serviceaccount
  50. DefaultAPITokenMountPath = "/var/run/secrets/kubernetes.io/serviceaccount"
  51. // PluginName is the name of this admission plugin
  52. PluginName = "ServiceAccount"
  53. )
  54. // Register registers a plugin
  55. func Register(plugins *admission.Plugins) {
  56. plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
  57. serviceAccountAdmission := NewServiceAccount()
  58. return serviceAccountAdmission, nil
  59. })
  60. }
  61. var _ = admission.Interface(&Plugin{})
  62. // Plugin contains the client used by the admission controller
  63. type Plugin struct {
  64. *admission.Handler
  65. // LimitSecretReferences rejects pods that reference secrets their service accounts do not reference
  66. LimitSecretReferences bool
  67. // RequireAPIToken determines whether pod creation attempts are rejected if no API token exists for the pod's service account
  68. RequireAPIToken bool
  69. // MountServiceAccountToken creates Volume and VolumeMounts for the first referenced ServiceAccountToken for the pod's service account
  70. MountServiceAccountToken bool
  71. client kubernetes.Interface
  72. serviceAccountLister corev1listers.ServiceAccountLister
  73. secretLister corev1listers.SecretLister
  74. generateName func(string) string
  75. boundServiceAccountTokenVolume bool
  76. }
  77. var _ admission.MutationInterface = &Plugin{}
  78. var _ admission.ValidationInterface = &Plugin{}
  79. var _ genericadmissioninitializer.WantsFeatures = &Plugin{}
  80. var _ = genericadmissioninitializer.WantsExternalKubeClientSet(&Plugin{})
  81. var _ = genericadmissioninitializer.WantsExternalKubeInformerFactory(&Plugin{})
  82. // NewServiceAccount returns an admission.Interface implementation which limits admission of Pod CREATE requests based on the pod's ServiceAccount:
  83. // 1. If the pod does not specify a ServiceAccount, it sets the pod's ServiceAccount to "default"
  84. // 2. It ensures the ServiceAccount referenced by the pod exists
  85. // 3. If LimitSecretReferences is true, it rejects the pod if the pod references Secret objects which the pod's ServiceAccount does not reference
  86. // 4. If the pod does not contain any ImagePullSecrets, the ImagePullSecrets of the service account are added.
  87. // 5. If MountServiceAccountToken is true, it adds a VolumeMount with the pod's ServiceAccount's api token secret to containers
  88. func NewServiceAccount() *Plugin {
  89. return &Plugin{
  90. Handler: admission.NewHandler(admission.Create),
  91. // TODO: enable this once we've swept secret usage to account for adding secret references to service accounts
  92. LimitSecretReferences: false,
  93. // Auto mount service account API token secrets
  94. MountServiceAccountToken: true,
  95. // Reject pod creation until a service account token is available
  96. RequireAPIToken: true,
  97. generateName: names.SimpleNameGenerator.GenerateName,
  98. }
  99. }
  100. // InspectFeatureGates allows setting bools without taking a dep on a global variable
  101. func (s *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
  102. s.boundServiceAccountTokenVolume = featureGates.Enabled(features.BoundServiceAccountTokenVolume)
  103. }
  104. // SetExternalKubeClientSet sets the client for the plugin
  105. func (s *Plugin) SetExternalKubeClientSet(cl kubernetes.Interface) {
  106. s.client = cl
  107. }
  108. // SetExternalKubeInformerFactory registers informers with the plugin
  109. func (s *Plugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
  110. serviceAccountInformer := f.Core().V1().ServiceAccounts()
  111. s.serviceAccountLister = serviceAccountInformer.Lister()
  112. secretInformer := f.Core().V1().Secrets()
  113. s.secretLister = secretInformer.Lister()
  114. s.SetReadyFunc(func() bool {
  115. return serviceAccountInformer.Informer().HasSynced() && secretInformer.Informer().HasSynced()
  116. })
  117. }
  118. // ValidateInitialization ensures an authorizer is set.
  119. func (s *Plugin) ValidateInitialization() error {
  120. if s.client == nil {
  121. return fmt.Errorf("missing client")
  122. }
  123. if s.secretLister == nil {
  124. return fmt.Errorf("missing secretLister")
  125. }
  126. if s.serviceAccountLister == nil {
  127. return fmt.Errorf("missing serviceAccountLister")
  128. }
  129. return nil
  130. }
  131. // Admit verifies if the pod should be admitted
  132. func (s *Plugin) Admit(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
  133. if shouldIgnore(a) {
  134. return nil
  135. }
  136. pod := a.GetObject().(*api.Pod)
  137. // Don't modify the spec of mirror pods.
  138. // That makes the kubelet very angry and confused, and it immediately deletes the pod (because the spec doesn't match)
  139. // That said, don't allow mirror pods to reference ServiceAccounts or SecretVolumeSources either
  140. if _, isMirrorPod := pod.Annotations[api.MirrorPodAnnotationKey]; isMirrorPod {
  141. return s.Validate(ctx, a, o)
  142. }
  143. // Set the default service account if needed
  144. if len(pod.Spec.ServiceAccountName) == 0 {
  145. pod.Spec.ServiceAccountName = DefaultServiceAccountName
  146. }
  147. serviceAccount, err := s.getServiceAccount(a.GetNamespace(), pod.Spec.ServiceAccountName)
  148. if err != nil {
  149. return admission.NewForbidden(a, fmt.Errorf("error looking up service account %s/%s: %v", a.GetNamespace(), pod.Spec.ServiceAccountName, err))
  150. }
  151. if s.MountServiceAccountToken && shouldAutomount(serviceAccount, pod) {
  152. if err := s.mountServiceAccountToken(serviceAccount, pod); err != nil {
  153. if _, ok := err.(errors.APIStatus); ok {
  154. return err
  155. }
  156. return admission.NewForbidden(a, err)
  157. }
  158. }
  159. if len(pod.Spec.ImagePullSecrets) == 0 {
  160. pod.Spec.ImagePullSecrets = make([]api.LocalObjectReference, len(serviceAccount.ImagePullSecrets))
  161. for i := 0; i < len(serviceAccount.ImagePullSecrets); i++ {
  162. pod.Spec.ImagePullSecrets[i].Name = serviceAccount.ImagePullSecrets[i].Name
  163. }
  164. }
  165. return s.Validate(ctx, a, o)
  166. }
  167. // Validate the data we obtained
  168. func (s *Plugin) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
  169. if shouldIgnore(a) {
  170. return nil
  171. }
  172. pod := a.GetObject().(*api.Pod)
  173. // Mirror pods have restrictions on what they can reference
  174. if _, isMirrorPod := pod.Annotations[api.MirrorPodAnnotationKey]; isMirrorPod {
  175. if len(pod.Spec.ServiceAccountName) != 0 {
  176. return admission.NewForbidden(a, fmt.Errorf("a mirror pod may not reference service accounts"))
  177. }
  178. hasSecrets := false
  179. podutil.VisitPodSecretNames(pod, func(name string) bool {
  180. hasSecrets = true
  181. return false
  182. })
  183. if hasSecrets {
  184. return admission.NewForbidden(a, fmt.Errorf("a mirror pod may not reference secrets"))
  185. }
  186. for _, v := range pod.Spec.Volumes {
  187. if proj := v.Projected; proj != nil {
  188. for _, projSource := range proj.Sources {
  189. if projSource.ServiceAccountToken != nil {
  190. return admission.NewForbidden(a, fmt.Errorf("a mirror pod may not use ServiceAccountToken volume projections"))
  191. }
  192. }
  193. }
  194. }
  195. return nil
  196. }
  197. // Ensure the referenced service account exists
  198. serviceAccount, err := s.getServiceAccount(a.GetNamespace(), pod.Spec.ServiceAccountName)
  199. if err != nil {
  200. return admission.NewForbidden(a, fmt.Errorf("error looking up service account %s/%s: %v", a.GetNamespace(), pod.Spec.ServiceAccountName, err))
  201. }
  202. if s.enforceMountableSecrets(serviceAccount) {
  203. if err := s.limitSecretReferences(serviceAccount, pod); err != nil {
  204. return admission.NewForbidden(a, err)
  205. }
  206. }
  207. return nil
  208. }
  209. func shouldIgnore(a admission.Attributes) bool {
  210. if a.GetResource().GroupResource() != api.Resource("pods") {
  211. return true
  212. }
  213. if a.GetSubresource() != "" {
  214. return true
  215. }
  216. obj := a.GetObject()
  217. if obj == nil {
  218. return true
  219. }
  220. _, ok := obj.(*api.Pod)
  221. if !ok {
  222. return true
  223. }
  224. return false
  225. }
  226. func shouldAutomount(sa *corev1.ServiceAccount, pod *api.Pod) bool {
  227. // Pod's preference wins
  228. if pod.Spec.AutomountServiceAccountToken != nil {
  229. return *pod.Spec.AutomountServiceAccountToken
  230. }
  231. // Then service account's
  232. if sa.AutomountServiceAccountToken != nil {
  233. return *sa.AutomountServiceAccountToken
  234. }
  235. // Default to true for backwards compatibility
  236. return true
  237. }
  238. // enforceMountableSecrets indicates whether mountable secrets should be enforced for a particular service account
  239. // A global setting of true will override any flag set on the individual service account
  240. func (s *Plugin) enforceMountableSecrets(serviceAccount *corev1.ServiceAccount) bool {
  241. if s.LimitSecretReferences {
  242. return true
  243. }
  244. if value, ok := serviceAccount.Annotations[EnforceMountableSecretsAnnotation]; ok {
  245. enforceMountableSecretCheck, _ := strconv.ParseBool(value)
  246. return enforceMountableSecretCheck
  247. }
  248. return false
  249. }
  250. // getServiceAccount returns the ServiceAccount for the given namespace and name if it exists
  251. func (s *Plugin) getServiceAccount(namespace string, name string) (*corev1.ServiceAccount, error) {
  252. serviceAccount, err := s.serviceAccountLister.ServiceAccounts(namespace).Get(name)
  253. if err == nil {
  254. return serviceAccount, nil
  255. }
  256. if !errors.IsNotFound(err) {
  257. return nil, err
  258. }
  259. // Could not find in cache, attempt to look up directly
  260. numAttempts := 1
  261. if name == DefaultServiceAccountName {
  262. // If this is the default serviceaccount, attempt more times, since it should be auto-created by the controller
  263. numAttempts = 10
  264. }
  265. retryInterval := time.Duration(rand.Int63n(100)+int64(100)) * time.Millisecond
  266. for i := 0; i < numAttempts; i++ {
  267. if i != 0 {
  268. time.Sleep(retryInterval)
  269. }
  270. serviceAccount, err := s.client.CoreV1().ServiceAccounts(namespace).Get(context.TODO(), name, metav1.GetOptions{})
  271. if err == nil {
  272. return serviceAccount, nil
  273. }
  274. if !errors.IsNotFound(err) {
  275. return nil, err
  276. }
  277. }
  278. return nil, errors.NewNotFound(api.Resource("serviceaccount"), name)
  279. }
  280. // getReferencedServiceAccountToken returns the name of the first referenced secret which is a ServiceAccountToken for the service account
  281. func (s *Plugin) getReferencedServiceAccountToken(serviceAccount *corev1.ServiceAccount) (string, error) {
  282. if len(serviceAccount.Secrets) == 0 {
  283. return "", nil
  284. }
  285. tokens, err := s.getServiceAccountTokens(serviceAccount)
  286. if err != nil {
  287. return "", err
  288. }
  289. accountTokens := sets.NewString()
  290. for _, token := range tokens {
  291. accountTokens.Insert(token.Name)
  292. }
  293. // Prefer secrets in the order they're referenced.
  294. for _, secret := range serviceAccount.Secrets {
  295. if accountTokens.Has(secret.Name) {
  296. return secret.Name, nil
  297. }
  298. }
  299. return "", nil
  300. }
  301. // getServiceAccountTokens returns all ServiceAccountToken secrets for the given ServiceAccount
  302. func (s *Plugin) getServiceAccountTokens(serviceAccount *corev1.ServiceAccount) ([]*corev1.Secret, error) {
  303. secrets, err := s.secretLister.Secrets(serviceAccount.Namespace).List(labels.Everything())
  304. if err != nil {
  305. return nil, err
  306. }
  307. tokens := []*corev1.Secret{}
  308. for _, secret := range secrets {
  309. if secret.Type != corev1.SecretTypeServiceAccountToken {
  310. continue
  311. }
  312. if serviceaccount.IsServiceAccountToken(secret, serviceAccount) {
  313. tokens = append(tokens, secret)
  314. }
  315. }
  316. return tokens, nil
  317. }
  318. func (s *Plugin) limitSecretReferences(serviceAccount *corev1.ServiceAccount, pod *api.Pod) error {
  319. // Ensure all secrets the pod references are allowed by the service account
  320. mountableSecrets := sets.NewString()
  321. for _, s := range serviceAccount.Secrets {
  322. mountableSecrets.Insert(s.Name)
  323. }
  324. for _, volume := range pod.Spec.Volumes {
  325. source := volume.VolumeSource
  326. if source.Secret == nil {
  327. continue
  328. }
  329. secretName := source.Secret.SecretName
  330. if !mountableSecrets.Has(secretName) {
  331. return fmt.Errorf("volume with secret.secretName=\"%s\" is not allowed because service account %s does not reference that secret", secretName, serviceAccount.Name)
  332. }
  333. }
  334. for _, container := range pod.Spec.InitContainers {
  335. for _, env := range container.Env {
  336. if env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil {
  337. if !mountableSecrets.Has(env.ValueFrom.SecretKeyRef.Name) {
  338. return fmt.Errorf("init container %s with envVar %s referencing secret.secretName=\"%s\" is not allowed because service account %s does not reference that secret", container.Name, env.Name, env.ValueFrom.SecretKeyRef.Name, serviceAccount.Name)
  339. }
  340. }
  341. }
  342. }
  343. for _, container := range pod.Spec.Containers {
  344. for _, env := range container.Env {
  345. if env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil {
  346. if !mountableSecrets.Has(env.ValueFrom.SecretKeyRef.Name) {
  347. return fmt.Errorf("container %s with envVar %s referencing secret.secretName=\"%s\" is not allowed because service account %s does not reference that secret", container.Name, env.Name, env.ValueFrom.SecretKeyRef.Name, serviceAccount.Name)
  348. }
  349. }
  350. }
  351. }
  352. // limit pull secret references as well
  353. pullSecrets := sets.NewString()
  354. for _, s := range serviceAccount.ImagePullSecrets {
  355. pullSecrets.Insert(s.Name)
  356. }
  357. for i, pullSecretRef := range pod.Spec.ImagePullSecrets {
  358. if !pullSecrets.Has(pullSecretRef.Name) {
  359. return fmt.Errorf(`imagePullSecrets[%d].name="%s" is not allowed because service account %s does not reference that imagePullSecret`, i, pullSecretRef.Name, serviceAccount.Name)
  360. }
  361. }
  362. return nil
  363. }
  364. func (s *Plugin) mountServiceAccountToken(serviceAccount *corev1.ServiceAccount, pod *api.Pod) error {
  365. var (
  366. // serviceAccountToken holds the name of a secret containing a legacy service account token
  367. serviceAccountToken string
  368. err error
  369. )
  370. if !s.boundServiceAccountTokenVolume {
  371. // Find the name of a referenced ServiceAccountToken secret we can mount
  372. serviceAccountToken, err = s.getReferencedServiceAccountToken(serviceAccount)
  373. if err != nil {
  374. return fmt.Errorf("Error looking up service account token for %s/%s: %v", serviceAccount.Namespace, serviceAccount.Name, err)
  375. }
  376. }
  377. if len(serviceAccountToken) == 0 && !s.boundServiceAccountTokenVolume {
  378. // We don't have an API token to mount, so return
  379. if s.RequireAPIToken {
  380. // If a token is required, this is considered an error
  381. err := errors.NewServerTimeout(schema.GroupResource{Resource: "serviceaccounts"}, "create pod", 1)
  382. err.ErrStatus.Message = fmt.Sprintf("No API token found for service account %q, retry after the token is automatically created and added to the service account", serviceAccount.Name)
  383. return err
  384. }
  385. return nil
  386. }
  387. // Find the volume and volume name for the ServiceAccountTokenSecret if it already exists
  388. tokenVolumeName := ""
  389. hasTokenVolume := false
  390. allVolumeNames := sets.NewString()
  391. for _, volume := range pod.Spec.Volumes {
  392. allVolumeNames.Insert(volume.Name)
  393. if (!s.boundServiceAccountTokenVolume && volume.Secret != nil && volume.Secret.SecretName == serviceAccountToken) ||
  394. (s.boundServiceAccountTokenVolume && strings.HasPrefix(volume.Name, ServiceAccountVolumeName+"-")) {
  395. tokenVolumeName = volume.Name
  396. hasTokenVolume = true
  397. break
  398. }
  399. }
  400. // Determine a volume name for the ServiceAccountTokenSecret in case we need it
  401. if len(tokenVolumeName) == 0 {
  402. if s.boundServiceAccountTokenVolume {
  403. tokenVolumeName = s.generateName(ServiceAccountVolumeName + "-")
  404. } else {
  405. // Try naming the volume the same as the serviceAccountToken, and uniquify if needed
  406. tokenVolumeName = serviceAccountToken
  407. if allVolumeNames.Has(tokenVolumeName) {
  408. tokenVolumeName = s.generateName(fmt.Sprintf("%s-", serviceAccountToken))
  409. }
  410. }
  411. }
  412. // Create the prototypical VolumeMount
  413. volumeMount := api.VolumeMount{
  414. Name: tokenVolumeName,
  415. ReadOnly: true,
  416. MountPath: DefaultAPITokenMountPath,
  417. }
  418. // Ensure every container mounts the APISecret volume
  419. needsTokenVolume := false
  420. for i, container := range pod.Spec.InitContainers {
  421. existingContainerMount := false
  422. for _, volumeMount := range container.VolumeMounts {
  423. // Existing mounts at the default mount path prevent mounting of the API token
  424. if volumeMount.MountPath == DefaultAPITokenMountPath {
  425. existingContainerMount = true
  426. break
  427. }
  428. }
  429. if !existingContainerMount {
  430. pod.Spec.InitContainers[i].VolumeMounts = append(pod.Spec.InitContainers[i].VolumeMounts, volumeMount)
  431. needsTokenVolume = true
  432. }
  433. }
  434. for i, container := range pod.Spec.Containers {
  435. existingContainerMount := false
  436. for _, volumeMount := range container.VolumeMounts {
  437. // Existing mounts at the default mount path prevent mounting of the API token
  438. if volumeMount.MountPath == DefaultAPITokenMountPath {
  439. existingContainerMount = true
  440. break
  441. }
  442. }
  443. if !existingContainerMount {
  444. pod.Spec.Containers[i].VolumeMounts = append(pod.Spec.Containers[i].VolumeMounts, volumeMount)
  445. needsTokenVolume = true
  446. }
  447. }
  448. // Add the volume if a container needs it
  449. if !hasTokenVolume && needsTokenVolume {
  450. pod.Spec.Volumes = append(pod.Spec.Volumes, s.createVolume(tokenVolumeName, serviceAccountToken))
  451. }
  452. return nil
  453. }
  454. func (s *Plugin) createVolume(tokenVolumeName, secretName string) api.Volume {
  455. if s.boundServiceAccountTokenVolume {
  456. return api.Volume{
  457. Name: tokenVolumeName,
  458. VolumeSource: api.VolumeSource{
  459. Projected: &api.ProjectedVolumeSource{
  460. Sources: []api.VolumeProjection{
  461. {
  462. ServiceAccountToken: &api.ServiceAccountTokenProjection{
  463. Path: "token",
  464. ExpirationSeconds: 60 * 60,
  465. },
  466. },
  467. {
  468. ConfigMap: &api.ConfigMapProjection{
  469. LocalObjectReference: api.LocalObjectReference{
  470. Name: "kube-root-ca.crt",
  471. },
  472. Items: []api.KeyToPath{
  473. {
  474. Key: "ca.crt",
  475. Path: "ca.crt",
  476. },
  477. },
  478. },
  479. },
  480. {
  481. DownwardAPI: &api.DownwardAPIProjection{
  482. Items: []api.DownwardAPIVolumeFile{
  483. {
  484. Path: "namespace",
  485. FieldRef: &api.ObjectFieldSelector{
  486. APIVersion: "v1",
  487. FieldPath: "metadata.namespace",
  488. },
  489. },
  490. },
  491. },
  492. },
  493. },
  494. },
  495. },
  496. }
  497. }
  498. return api.Volume{
  499. Name: tokenVolumeName,
  500. VolumeSource: api.VolumeSource{
  501. Secret: &api.SecretVolumeSource{
  502. SecretName: secretName,
  503. },
  504. },
  505. }
  506. }