You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

storage_initializer_injector.go 10 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. /*
  2. Copyright 2021 The KubeEdge 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 runtime
  14. import (
  15. "net/url"
  16. "path/filepath"
  17. "strings"
  18. v1 "k8s.io/api/core/v1"
  19. "k8s.io/apimachinery/pkg/api/resource"
  20. "k8s.io/klog/v2"
  21. )
  22. const (
  23. downloadInitalizerContainerName = "storage-initializer"
  24. downloadInitalizerImage = "kubeedge/sedna-storage-initializer:v0.3.0"
  25. downloadInitalizerPrefix = "/downloads"
  26. downloadInitalizerVolumeName = "sedna-storage-initializer"
  27. hostPathPrefixEnvKey = "DATA_PATH_PREFIX"
  28. hostPathPrefix = "/home/data"
  29. urlsFieldSep = ";"
  30. indirectURLMark = "@"
  31. indirectURLMarkEnv = "INDIRECT_URL_MARK"
  32. defaultVolumeName = "sedna-default-volume-name"
  33. )
  34. var supportStorageInitializerURLSchemes = [...]string{
  35. // s3 compatible storage
  36. "s3",
  37. // http server, only for downloading
  38. "http", "https",
  39. }
  40. type MountURL struct {
  41. // URL is the url of dataset/model
  42. URL string
  43. // Indirect indicates the url is indirect, need to parse its content and download all,
  44. // and is used in dataset which has index url.
  45. //
  46. // when Indirect = true, URL could be in host path filesystem.
  47. // default: false
  48. Indirect bool
  49. // DownloadByInitializer indicates whether the url need to be download by initializer.
  50. DownloadByInitializer bool
  51. // IsDir indicates that url is directory
  52. IsDir bool
  53. // if true, only mounts when url is hostpath
  54. EnableIfHostPath bool
  55. // the container path
  56. ContainerPath string
  57. // indicates the path this url will be mounted into container.
  58. // can be ContainerPath or its parent dir
  59. MountPath string
  60. // for host path, we just need to mount without downloading
  61. HostPath string
  62. // for download
  63. DownloadSrcURL string
  64. DownloadDstDir string
  65. // if true, then no mount
  66. Disable bool
  67. // the relevant secret
  68. Secret *v1.Secret
  69. SecretEnvs []v1.EnvVar
  70. // parsed for the parent of url
  71. u *url.URL
  72. }
  73. func (m *MountURL) Parse() {
  74. u, _ := url.Parse(m.URL)
  75. m.u = u
  76. m.parseDownloadPath()
  77. m.parseHostPath()
  78. m.parseSecret()
  79. }
  80. func (m *MountURL) parseDownloadPath() {
  81. if !m.DownloadByInitializer {
  82. // no storage-initializer for write only
  83. // leave the write operation to worker
  84. return
  85. }
  86. for _, scheme := range supportStorageInitializerURLSchemes {
  87. if m.u.Scheme == scheme {
  88. m.MountPath = downloadInitalizerPrefix
  89. // here use u.Host + u.Path to avoid conflict
  90. m.ContainerPath = filepath.Join(m.MountPath, m.u.Host+m.u.Path)
  91. m.DownloadSrcURL = m.URL
  92. m.DownloadDstDir, _ = filepath.Split(m.ContainerPath)
  93. break
  94. }
  95. }
  96. }
  97. func (m *MountURL) parseHostPath() {
  98. // for compatibility, hostpath of a node is supported.
  99. // e.g. the url of a dataset: /datasets/d1/label.txt
  100. if m.u.Scheme != "" {
  101. if m.EnableIfHostPath {
  102. // not hostpath, so disable it
  103. m.Disable = true
  104. }
  105. return
  106. }
  107. if m.IsDir {
  108. m.HostPath = m.URL
  109. m.MountPath = filepath.Join(hostPathPrefix, m.u.Path)
  110. m.ContainerPath = m.MountPath
  111. } else {
  112. // if file, here mount its parent directory
  113. m.HostPath, _ = filepath.Split(m.URL)
  114. m.ContainerPath = filepath.Join(hostPathPrefix, m.u.Path)
  115. m.MountPath, _ = filepath.Split(m.ContainerPath)
  116. if m.Indirect {
  117. // we need to download it
  118. // TODO: mv these to download-related section
  119. m.DownloadSrcURL = m.ContainerPath
  120. m.ContainerPath = filepath.Join(downloadInitalizerPrefix, m.u.Host+m.u.Path)
  121. m.DownloadDstDir, _ = filepath.Split(m.ContainerPath)
  122. }
  123. }
  124. }
  125. func (m *MountURL) parseSecret() {
  126. if m.Secret == nil {
  127. return
  128. }
  129. if strings.ToLower(m.u.Scheme) == "s3" || m.Indirect {
  130. SecretEnvs, err := buildS3SecretEnvs(m.Secret)
  131. if err == nil {
  132. m.SecretEnvs = SecretEnvs
  133. }
  134. }
  135. }
  136. func injectHostPathMount(pod *v1.Pod, workerParam *WorkerParam) {
  137. var volumes []v1.Volume
  138. var volumeMounts []v1.VolumeMount
  139. var initContainerVolumeMounts []v1.VolumeMount
  140. uniqVolumeName := make(map[string]bool)
  141. hostPathType := v1.HostPathDirectory
  142. for _, mount := range workerParam.Mounts {
  143. for _, m := range mount.URLs {
  144. if m.HostPath == "" {
  145. continue
  146. }
  147. volumeName := ConvertK8SValidName(m.HostPath)
  148. if len(volumeName) == 0 {
  149. volumeName = defaultVolumeName
  150. klog.Warningf("failed to get name from url(%s), fallback to default name(%s)", m.URL, volumeName)
  151. }
  152. if _, ok := uniqVolumeName[volumeName]; !ok {
  153. volumes = append(volumes, v1.Volume{
  154. Name: volumeName,
  155. VolumeSource: v1.VolumeSource{
  156. HostPath: &v1.HostPathVolumeSource{
  157. Path: m.HostPath,
  158. Type: &hostPathType,
  159. },
  160. },
  161. })
  162. uniqVolumeName[volumeName] = true
  163. }
  164. vm := v1.VolumeMount{
  165. MountPath: m.MountPath,
  166. Name: volumeName,
  167. }
  168. if m.Indirect {
  169. initContainerVolumeMounts = append(initContainerVolumeMounts, vm)
  170. } else {
  171. volumeMounts = append(volumeMounts, vm)
  172. }
  173. }
  174. }
  175. injectVolume(pod, volumes, volumeMounts)
  176. if len(volumeMounts) > 0 {
  177. hostPathEnvs := []v1.EnvVar{
  178. {
  179. Name: hostPathPrefixEnvKey,
  180. Value: hostPathPrefix,
  181. },
  182. }
  183. injectEnvs(pod, hostPathEnvs)
  184. }
  185. if len(initContainerVolumeMounts) > 0 {
  186. initIdx := len(pod.Spec.InitContainers) - 1
  187. pod.Spec.InitContainers[initIdx].VolumeMounts = append(
  188. pod.Spec.InitContainers[initIdx].VolumeMounts,
  189. initContainerVolumeMounts...,
  190. )
  191. }
  192. }
  193. func injectWorkerSecrets(pod *v1.Pod, workerParam *WorkerParam) {
  194. var secretEnvs []v1.EnvVar
  195. for _, mount := range workerParam.Mounts {
  196. for _, m := range mount.URLs {
  197. if m.Disable || m.DownloadByInitializer {
  198. continue
  199. }
  200. if len(m.SecretEnvs) > 0 {
  201. secretEnvs = MergeSecretEnvs(secretEnvs, m.SecretEnvs, false)
  202. }
  203. }
  204. }
  205. injectEnvs(pod, secretEnvs)
  206. }
  207. func injectInitializerContainer(pod *v1.Pod, workerParam *WorkerParam) {
  208. var volumes []v1.Volume
  209. var volumeMounts []v1.VolumeMount
  210. var downloadPairs []string
  211. var secretEnvs []v1.EnvVar
  212. for _, mount := range workerParam.Mounts {
  213. for _, m := range mount.URLs {
  214. if m.Disable {
  215. continue
  216. }
  217. srcURL := m.DownloadSrcURL
  218. dstDir := m.DownloadDstDir
  219. if srcURL != "" && dstDir != "" {
  220. // need to add srcURL first: srcURL dstDir
  221. if m.Indirect {
  222. // here add indirectURLMark into dstDir which is controllable
  223. dstDir = indirectURLMark + dstDir
  224. }
  225. downloadPairs = append(downloadPairs, srcURL, dstDir)
  226. if len(m.SecretEnvs) > 0 {
  227. secretEnvs = MergeSecretEnvs(secretEnvs, m.SecretEnvs, false)
  228. }
  229. }
  230. }
  231. }
  232. // no need to download
  233. if len(downloadPairs) == 0 {
  234. return
  235. }
  236. envs := secretEnvs
  237. envs = append(envs, v1.EnvVar{
  238. Name: indirectURLMarkEnv,
  239. Value: indirectURLMark,
  240. })
  241. // use one empty directory
  242. storageVolume := v1.Volume{
  243. Name: downloadInitalizerVolumeName,
  244. VolumeSource: v1.VolumeSource{
  245. EmptyDir: &v1.EmptyDirVolumeSource{},
  246. },
  247. }
  248. storageVolumeMounts := v1.VolumeMount{
  249. Name: storageVolume.Name,
  250. MountPath: downloadInitalizerPrefix,
  251. ReadOnly: true,
  252. }
  253. volumes = append(volumes, storageVolume)
  254. volumeMounts = append(volumeMounts, storageVolumeMounts)
  255. initVolumeMounts := []v1.VolumeMount{
  256. {
  257. Name: storageVolume.Name,
  258. MountPath: downloadInitalizerPrefix,
  259. ReadOnly: false,
  260. },
  261. }
  262. initContainer := v1.Container{
  263. Name: downloadInitalizerContainerName,
  264. Image: downloadInitalizerImage,
  265. ImagePullPolicy: v1.PullIfNotPresent,
  266. Args: downloadPairs,
  267. TerminationMessagePolicy: v1.TerminationMessageFallbackToLogsOnError,
  268. Resources: v1.ResourceRequirements{
  269. Limits: map[v1.ResourceName]resource.Quantity{
  270. // limit one cpu
  271. v1.ResourceCPU: resource.MustParse("1"),
  272. // limit 1Gi memory
  273. v1.ResourceMemory: resource.MustParse("1Gi"),
  274. },
  275. },
  276. VolumeMounts: initVolumeMounts,
  277. Env: envs,
  278. }
  279. pod.Spec.InitContainers = append(pod.Spec.InitContainers, initContainer)
  280. injectVolume(pod, volumes, volumeMounts)
  281. }
  282. // InjectStorageInitializer injects these storage related volumes and envs into pod in-place
  283. func InjectStorageInitializer(pod *v1.Pod, workerParam *WorkerParam) {
  284. var mounts []WorkerMount
  285. // parse the mounts and environment key
  286. for _, mount := range workerParam.Mounts {
  287. var envPaths []string
  288. if mount.URL != nil {
  289. mount.URLs = append(mount.URLs, *mount.URL)
  290. }
  291. var mountURLs []MountURL
  292. for _, m := range mount.URLs {
  293. m.Parse()
  294. if m.Disable {
  295. continue
  296. }
  297. mountURLs = append(mountURLs, m)
  298. if m.ContainerPath != "" {
  299. envPaths = append(envPaths, m.ContainerPath)
  300. } else {
  301. // keep the original URL if no container path
  302. envPaths = append(envPaths, m.URL)
  303. }
  304. }
  305. if len(mountURLs) > 0 {
  306. mount.URLs = mountURLs
  307. mounts = append(mounts, mount)
  308. }
  309. if mount.EnvName != "" {
  310. workerParam.Env[mount.EnvName] = strings.Join(
  311. envPaths, urlsFieldSep,
  312. )
  313. }
  314. }
  315. workerParam.Mounts = mounts
  316. // need to call injectInitializerContainer before injectHostPathMount
  317. // since injectHostPathMount could inject volumeMount to init container
  318. injectInitializerContainer(pod, workerParam)
  319. injectHostPathMount(pod, workerParam)
  320. injectWorkerSecrets(pod, workerParam)
  321. }
  322. func injectVolume(pod *v1.Pod, volumes []v1.Volume, volumeMounts []v1.VolumeMount) {
  323. if len(volumes) > 0 {
  324. pod.Spec.Volumes = append(pod.Spec.Volumes, volumes...)
  325. }
  326. if len(volumeMounts) > 0 {
  327. for idx := range pod.Spec.Containers {
  328. // inject every containers
  329. pod.Spec.Containers[idx].VolumeMounts = append(
  330. pod.Spec.Containers[idx].VolumeMounts, volumeMounts...,
  331. )
  332. }
  333. }
  334. }
  335. func injectEnvs(pod *v1.Pod, envs []v1.EnvVar) {
  336. if len(envs) > 0 {
  337. for idx := range pod.Spec.Containers {
  338. // inject every containers
  339. pod.Spec.Containers[idx].Env = append(
  340. pod.Spec.Containers[idx].Env, envs...,
  341. )
  342. }
  343. }
  344. }