conf.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. // Copyright 2015 CNI authors
  2. //
  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. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package libcni
  15. import (
  16. "encoding/json"
  17. "errors"
  18. "fmt"
  19. "os"
  20. "path/filepath"
  21. "slices"
  22. "sort"
  23. "strings"
  24. "github.com/containernetworking/cni/pkg/types"
  25. "github.com/containernetworking/cni/pkg/version"
  26. )
  27. type NotFoundError struct {
  28. Dir string
  29. Name string
  30. }
  31. func (e NotFoundError) Error() string {
  32. return fmt.Sprintf(`no net configuration with name "%s" in %s`, e.Name, e.Dir)
  33. }
  34. type NoConfigsFoundError struct {
  35. Dir string
  36. }
  37. func (e NoConfigsFoundError) Error() string {
  38. return fmt.Sprintf(`no net configurations found in %s`, e.Dir)
  39. }
  40. // This will not validate that the plugins actually belong to the netconfig by ensuring
  41. // that they are loaded from a directory named after the networkName, relative to the network config.
  42. //
  43. // Since here we are just accepting raw bytes, the caller is responsible for ensuring that the plugin
  44. // config provided here actually "belongs" to the networkconfig in question.
  45. func NetworkPluginConfFromBytes(pluginConfBytes []byte) (*PluginConfig, error) {
  46. // TODO why are we creating a struct that holds both the byte representation and the deserialized
  47. // representation, and returning that, instead of just returning the deserialized representation?
  48. conf := &PluginConfig{Bytes: pluginConfBytes, Network: &types.PluginConf{}}
  49. if err := json.Unmarshal(pluginConfBytes, conf.Network); err != nil {
  50. return nil, fmt.Errorf("error parsing configuration: %w", err)
  51. }
  52. if conf.Network.Type == "" {
  53. return nil, fmt.Errorf("error parsing configuration: missing 'type'")
  54. }
  55. return conf, nil
  56. }
  57. // Given a path to a directory containing a network configuration, and the name of a network,
  58. // loads all plugin definitions found at path `networkConfPath/networkName/*.conf`
  59. func NetworkPluginConfsFromFiles(networkConfPath, networkName string) ([]*PluginConfig, error) {
  60. var pConfs []*PluginConfig
  61. pluginConfPath := filepath.Join(networkConfPath, networkName)
  62. pluginConfFiles, err := ConfFiles(pluginConfPath, []string{".conf"})
  63. if err != nil {
  64. return nil, fmt.Errorf("failed to read plugin config files in %s: %w", pluginConfPath, err)
  65. }
  66. for _, pluginConfFile := range pluginConfFiles {
  67. pluginConfBytes, err := os.ReadFile(pluginConfFile)
  68. if err != nil {
  69. return nil, fmt.Errorf("error reading %s: %w", pluginConfFile, err)
  70. }
  71. pluginConf, err := NetworkPluginConfFromBytes(pluginConfBytes)
  72. if err != nil {
  73. return nil, err
  74. }
  75. pConfs = append(pConfs, pluginConf)
  76. }
  77. return pConfs, nil
  78. }
  79. func NetworkConfFromBytes(confBytes []byte) (*NetworkConfigList, error) {
  80. rawList := make(map[string]interface{})
  81. if err := json.Unmarshal(confBytes, &rawList); err != nil {
  82. return nil, fmt.Errorf("error parsing configuration list: %w", err)
  83. }
  84. rawName, ok := rawList["name"]
  85. if !ok {
  86. return nil, fmt.Errorf("error parsing configuration list: no name")
  87. }
  88. name, ok := rawName.(string)
  89. if !ok {
  90. return nil, fmt.Errorf("error parsing configuration list: invalid name type %T", rawName)
  91. }
  92. var cniVersion string
  93. rawVersion, ok := rawList["cniVersion"]
  94. if ok {
  95. cniVersion, ok = rawVersion.(string)
  96. if !ok {
  97. return nil, fmt.Errorf("error parsing configuration list: invalid cniVersion type %T", rawVersion)
  98. }
  99. }
  100. rawVersions, ok := rawList["cniVersions"]
  101. if ok {
  102. // Parse the current package CNI version
  103. rvs, ok := rawVersions.([]interface{})
  104. if !ok {
  105. return nil, fmt.Errorf("error parsing configuration list: invalid type for cniVersions: %T", rvs)
  106. }
  107. vs := make([]string, 0, len(rvs))
  108. for i, rv := range rvs {
  109. v, ok := rv.(string)
  110. if !ok {
  111. return nil, fmt.Errorf("error parsing configuration list: invalid type for cniVersions index %d: %T", i, rv)
  112. }
  113. gt, err := version.GreaterThan(v, version.Current())
  114. if err != nil {
  115. return nil, fmt.Errorf("error parsing configuration list: invalid cniVersions entry %s at index %d: %w", v, i, err)
  116. } else if !gt {
  117. // Skip versions "greater" than this implementation of the spec
  118. vs = append(vs, v)
  119. }
  120. }
  121. // if cniVersion was already set, append it to the list for sorting.
  122. if cniVersion != "" {
  123. gt, err := version.GreaterThan(cniVersion, version.Current())
  124. if err != nil {
  125. return nil, fmt.Errorf("error parsing configuration list: invalid cniVersion %s: %w", cniVersion, err)
  126. } else if !gt {
  127. // ignore any versions higher than the current implemented spec version
  128. vs = append(vs, cniVersion)
  129. }
  130. }
  131. slices.SortFunc[[]string](vs, func(v1, v2 string) int {
  132. if v1 == v2 {
  133. return 0
  134. }
  135. if gt, _ := version.GreaterThan(v1, v2); gt {
  136. return 1
  137. }
  138. return -1
  139. })
  140. if len(vs) > 0 {
  141. cniVersion = vs[len(vs)-1]
  142. }
  143. }
  144. readBool := func(key string) (bool, error) {
  145. rawVal, ok := rawList[key]
  146. if !ok {
  147. return false, nil
  148. }
  149. if b, ok := rawVal.(bool); ok {
  150. return b, nil
  151. }
  152. s, ok := rawVal.(string)
  153. if !ok {
  154. return false, fmt.Errorf("error parsing configuration list: invalid type %T for %s", rawVal, key)
  155. }
  156. s = strings.ToLower(s)
  157. switch s {
  158. case "false":
  159. return false, nil
  160. case "true":
  161. return true, nil
  162. }
  163. return false, fmt.Errorf("error parsing configuration list: invalid value %q for %s", s, key)
  164. }
  165. disableCheck, err := readBool("disableCheck")
  166. if err != nil {
  167. return nil, err
  168. }
  169. disableGC, err := readBool("disableGC")
  170. if err != nil {
  171. return nil, err
  172. }
  173. loadOnlyInlinedPlugins, err := readBool("loadOnlyInlinedPlugins")
  174. if err != nil {
  175. return nil, err
  176. }
  177. list := &NetworkConfigList{
  178. Name: name,
  179. DisableCheck: disableCheck,
  180. DisableGC: disableGC,
  181. LoadOnlyInlinedPlugins: loadOnlyInlinedPlugins,
  182. CNIVersion: cniVersion,
  183. Bytes: confBytes,
  184. }
  185. var plugins []interface{}
  186. plug, ok := rawList["plugins"]
  187. // We can have a `plugins` list key in the main conf,
  188. // We can also have `loadOnlyInlinedPlugins == true`
  189. //
  190. // If `plugins` is there, then `loadOnlyInlinedPlugins` can be true
  191. //
  192. // If plugins is NOT there, then `loadOnlyInlinedPlugins` cannot be true
  193. //
  194. // We have to have at least some plugins.
  195. if !ok && loadOnlyInlinedPlugins {
  196. return nil, fmt.Errorf("error parsing configuration list: `loadOnlyInlinedPlugins` is true, and no 'plugins' key")
  197. } else if !ok && !loadOnlyInlinedPlugins {
  198. return list, nil
  199. }
  200. plugins, ok = plug.([]interface{})
  201. if !ok {
  202. return nil, fmt.Errorf("error parsing configuration list: invalid 'plugins' type %T", plug)
  203. }
  204. if len(plugins) == 0 {
  205. return nil, fmt.Errorf("error parsing configuration list: no plugins in list")
  206. }
  207. for i, conf := range plugins {
  208. newBytes, err := json.Marshal(conf)
  209. if err != nil {
  210. return nil, fmt.Errorf("failed to marshal plugin config %d: %w", i, err)
  211. }
  212. netConf, err := ConfFromBytes(newBytes)
  213. if err != nil {
  214. return nil, fmt.Errorf("failed to parse plugin config %d: %w", i, err)
  215. }
  216. list.Plugins = append(list.Plugins, netConf)
  217. }
  218. return list, nil
  219. }
  220. func NetworkConfFromFile(filename string) (*NetworkConfigList, error) {
  221. bytes, err := os.ReadFile(filename)
  222. if err != nil {
  223. return nil, fmt.Errorf("error reading %s: %w", filename, err)
  224. }
  225. conf, err := NetworkConfFromBytes(bytes)
  226. if err != nil {
  227. return nil, err
  228. }
  229. if !conf.LoadOnlyInlinedPlugins {
  230. plugins, err := NetworkPluginConfsFromFiles(filepath.Dir(filename), conf.Name)
  231. if err != nil {
  232. return nil, err
  233. }
  234. conf.Plugins = append(conf.Plugins, plugins...)
  235. }
  236. if len(conf.Plugins) == 0 {
  237. // Having 0 plugins for a given network is not necessarily a problem,
  238. // but return as error for caller to decide, since they tried to load
  239. return nil, fmt.Errorf("no plugin configs found")
  240. }
  241. return conf, nil
  242. }
  243. // Deprecated: This file format is no longer supported, use NetworkConfXXX and NetworkPluginXXX functions
  244. func ConfFromBytes(bytes []byte) (*NetworkConfig, error) {
  245. return NetworkPluginConfFromBytes(bytes)
  246. }
  247. // Deprecated: This file format is no longer supported, use NetworkConfXXX and NetworkPluginXXX functions
  248. func ConfFromFile(filename string) (*NetworkConfig, error) {
  249. bytes, err := os.ReadFile(filename)
  250. if err != nil {
  251. return nil, fmt.Errorf("error reading %s: %w", filename, err)
  252. }
  253. return ConfFromBytes(bytes)
  254. }
  255. func ConfListFromBytes(bytes []byte) (*NetworkConfigList, error) {
  256. return NetworkConfFromBytes(bytes)
  257. }
  258. func ConfListFromFile(filename string) (*NetworkConfigList, error) {
  259. return NetworkConfFromFile(filename)
  260. }
  261. // ConfFiles simply returns a slice of all files in the provided directory
  262. // with extensions matching the provided set.
  263. func ConfFiles(dir string, extensions []string) ([]string, error) {
  264. // In part, adapted from rkt/networking/podenv.go#listFiles
  265. files, err := os.ReadDir(dir)
  266. switch {
  267. case err == nil: // break
  268. case os.IsNotExist(err):
  269. // If folder not there, return no error - only return an
  270. // error if we cannot read contents or there are no contents.
  271. return nil, nil
  272. default:
  273. return nil, err
  274. }
  275. confFiles := []string{}
  276. for _, f := range files {
  277. if f.IsDir() {
  278. continue
  279. }
  280. fileExt := filepath.Ext(f.Name())
  281. for _, ext := range extensions {
  282. if fileExt == ext {
  283. confFiles = append(confFiles, filepath.Join(dir, f.Name()))
  284. }
  285. }
  286. }
  287. return confFiles, nil
  288. }
  289. // Deprecated: This file format is no longer supported, use NetworkConfXXX and NetworkPluginXXX functions
  290. func LoadConf(dir, name string) (*NetworkConfig, error) {
  291. files, err := ConfFiles(dir, []string{".conf", ".json"})
  292. switch {
  293. case err != nil:
  294. return nil, err
  295. case len(files) == 0:
  296. return nil, NoConfigsFoundError{Dir: dir}
  297. }
  298. sort.Strings(files)
  299. for _, confFile := range files {
  300. conf, err := ConfFromFile(confFile)
  301. if err != nil {
  302. return nil, err
  303. }
  304. if conf.Network.Name == name {
  305. return conf, nil
  306. }
  307. }
  308. return nil, NotFoundError{dir, name}
  309. }
  310. func LoadConfList(dir, name string) (*NetworkConfigList, error) {
  311. return LoadNetworkConf(dir, name)
  312. }
  313. // LoadNetworkConf looks at all the network configs in a given dir,
  314. // loads and parses them all, and returns the first one with an extension of `.conf`
  315. // that matches the provided network name predicate.
  316. func LoadNetworkConf(dir, name string) (*NetworkConfigList, error) {
  317. // TODO this .conflist/.conf extension thing is confusing and inexact
  318. // for implementors. We should pick one extension for everything and stick with it.
  319. files, err := ConfFiles(dir, []string{".conflist"})
  320. if err != nil {
  321. return nil, err
  322. }
  323. sort.Strings(files)
  324. for _, confFile := range files {
  325. conf, err := NetworkConfFromFile(confFile)
  326. if err != nil {
  327. return nil, err
  328. }
  329. if conf.Name == name {
  330. return conf, nil
  331. }
  332. }
  333. // Deprecated: Try and load a network configuration file (instead of list)
  334. // from the same name, then upconvert.
  335. singleConf, err := LoadConf(dir, name)
  336. if err != nil {
  337. // A little extra logic so the error makes sense
  338. var ncfErr NoConfigsFoundError
  339. if len(files) != 0 && errors.As(err, &ncfErr) {
  340. // Config lists found but no config files found
  341. return nil, NotFoundError{dir, name}
  342. }
  343. return nil, err
  344. }
  345. return ConfListFromConf(singleConf)
  346. }
  347. // InjectConf takes a PluginConfig and inserts additional values into it, ensuring the result is serializable.
  348. func InjectConf(original *PluginConfig, newValues map[string]interface{}) (*PluginConfig, error) {
  349. config := make(map[string]interface{})
  350. err := json.Unmarshal(original.Bytes, &config)
  351. if err != nil {
  352. return nil, fmt.Errorf("unmarshal existing network bytes: %w", err)
  353. }
  354. for key, value := range newValues {
  355. if key == "" {
  356. return nil, fmt.Errorf("keys cannot be empty")
  357. }
  358. if value == nil {
  359. return nil, fmt.Errorf("key '%s' value must not be nil", key)
  360. }
  361. config[key] = value
  362. }
  363. newBytes, err := json.Marshal(config)
  364. if err != nil {
  365. return nil, err
  366. }
  367. return NetworkPluginConfFromBytes(newBytes)
  368. }
  369. // ConfListFromConf "upconverts" a network config in to a NetworkConfigList,
  370. // with the single network as the only entry in the list.
  371. //
  372. // Deprecated: Non-conflist file formats are unsupported, use NetworkConfXXX and NetworkPluginXXX functions
  373. func ConfListFromConf(original *PluginConfig) (*NetworkConfigList, error) {
  374. // Re-deserialize the config's json, then make a raw map configlist.
  375. // This may seem a bit strange, but it's to make the Bytes fields
  376. // actually make sense. Otherwise, the generated json is littered with
  377. // golang default values.
  378. rawConfig := make(map[string]interface{})
  379. if err := json.Unmarshal(original.Bytes, &rawConfig); err != nil {
  380. return nil, err
  381. }
  382. rawConfigList := map[string]interface{}{
  383. "name": original.Network.Name,
  384. "cniVersion": original.Network.CNIVersion,
  385. "plugins": []interface{}{rawConfig},
  386. }
  387. b, err := json.Marshal(rawConfigList)
  388. if err != nil {
  389. return nil, err
  390. }
  391. return ConfListFromBytes(b)
  392. }