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
15package libcni
16
17import (
18	"encoding/json"
19	"fmt"
20	"io/ioutil"
21	"os"
22	"path/filepath"
23	"sort"
24)
25
26type NotFoundError struct {
27	Dir  string
28	Name string
29}
30
31func (e NotFoundError) Error() string {
32	return fmt.Sprintf(`no net configuration with name "%s" in %s`, e.Name, e.Dir)
33}
34
35type NoConfigsFoundError struct {
36	Dir string
37}
38
39func (e NoConfigsFoundError) Error() string {
40	return fmt.Sprintf(`no net configurations found in %s`, e.Dir)
41}
42
43func ConfFromBytes(bytes []byte) (*NetworkConfig, error) {
44	conf := &NetworkConfig{Bytes: bytes}
45	if err := json.Unmarshal(bytes, &conf.Network); err != nil {
46		return nil, fmt.Errorf("error parsing configuration: %s", err)
47	}
48	if conf.Network.Type == "" {
49		return nil, fmt.Errorf("error parsing configuration: missing 'type'")
50	}
51	return conf, nil
52}
53
54func ConfFromFile(filename string) (*NetworkConfig, error) {
55	bytes, err := ioutil.ReadFile(filename)
56	if err != nil {
57		return nil, fmt.Errorf("error reading %s: %s", filename, err)
58	}
59	return ConfFromBytes(bytes)
60}
61
62func ConfListFromBytes(bytes []byte) (*NetworkConfigList, error) {
63	rawList := make(map[string]interface{})
64	if err := json.Unmarshal(bytes, &rawList); err != nil {
65		return nil, fmt.Errorf("error parsing configuration list: %s", err)
66	}
67
68	rawName, ok := rawList["name"]
69	if !ok {
70		return nil, fmt.Errorf("error parsing configuration list: no name")
71	}
72	name, ok := rawName.(string)
73	if !ok {
74		return nil, fmt.Errorf("error parsing configuration list: invalid name type %T", rawName)
75	}
76
77	var cniVersion string
78	rawVersion, ok := rawList["cniVersion"]
79	if ok {
80		cniVersion, ok = rawVersion.(string)
81		if !ok {
82			return nil, fmt.Errorf("error parsing configuration list: invalid cniVersion type %T", rawVersion)
83		}
84	}
85
86	disableCheck := false
87	if rawDisableCheck, ok := rawList["disableCheck"]; ok {
88		disableCheck, ok = rawDisableCheck.(bool)
89		if !ok {
90			return nil, fmt.Errorf("error parsing configuration list: invalid disableCheck type %T", rawDisableCheck)
91		}
92	}
93
94	list := &NetworkConfigList{
95		Name:         name,
96		DisableCheck: disableCheck,
97		CNIVersion:   cniVersion,
98		Bytes:        bytes,
99	}
100
101	var plugins []interface{}
102	plug, ok := rawList["plugins"]
103	if !ok {
104		return nil, fmt.Errorf("error parsing configuration list: no 'plugins' key")
105	}
106	plugins, ok = plug.([]interface{})
107	if !ok {
108		return nil, fmt.Errorf("error parsing configuration list: invalid 'plugins' type %T", plug)
109	}
110	if len(plugins) == 0 {
111		return nil, fmt.Errorf("error parsing configuration list: no plugins in list")
112	}
113
114	for i, conf := range plugins {
115		newBytes, err := json.Marshal(conf)
116		if err != nil {
117			return nil, fmt.Errorf("Failed to marshal plugin config %d: %v", i, err)
118		}
119		netConf, err := ConfFromBytes(newBytes)
120		if err != nil {
121			return nil, fmt.Errorf("Failed to parse plugin config %d: %v", i, err)
122		}
123		list.Plugins = append(list.Plugins, netConf)
124	}
125
126	return list, nil
127}
128
129func ConfListFromFile(filename string) (*NetworkConfigList, error) {
130	bytes, err := ioutil.ReadFile(filename)
131	if err != nil {
132		return nil, fmt.Errorf("error reading %s: %s", filename, err)
133	}
134	return ConfListFromBytes(bytes)
135}
136
137func ConfFiles(dir string, extensions []string) ([]string, error) {
138	// In part, adapted from rkt/networking/podenv.go#listFiles
139	files, err := ioutil.ReadDir(dir)
140	switch {
141	case err == nil: // break
142	case os.IsNotExist(err):
143		return nil, nil
144	default:
145		return nil, err
146	}
147
148	confFiles := []string{}
149	for _, f := range files {
150		if f.IsDir() {
151			continue
152		}
153		fileExt := filepath.Ext(f.Name())
154		for _, ext := range extensions {
155			if fileExt == ext {
156				confFiles = append(confFiles, filepath.Join(dir, f.Name()))
157			}
158		}
159	}
160	return confFiles, nil
161}
162
163func LoadConf(dir, name string) (*NetworkConfig, error) {
164	files, err := ConfFiles(dir, []string{".conf", ".json"})
165	switch {
166	case err != nil:
167		return nil, err
168	case len(files) == 0:
169		return nil, NoConfigsFoundError{Dir: dir}
170	}
171	sort.Strings(files)
172
173	for _, confFile := range files {
174		conf, err := ConfFromFile(confFile)
175		if err != nil {
176			return nil, err
177		}
178		if conf.Network.Name == name {
179			return conf, nil
180		}
181	}
182	return nil, NotFoundError{dir, name}
183}
184
185func LoadConfList(dir, name string) (*NetworkConfigList, error) {
186	files, err := ConfFiles(dir, []string{".conflist"})
187	if err != nil {
188		return nil, err
189	}
190	sort.Strings(files)
191
192	for _, confFile := range files {
193		conf, err := ConfListFromFile(confFile)
194		if err != nil {
195			return nil, err
196		}
197		if conf.Name == name {
198			return conf, nil
199		}
200	}
201
202	// Try and load a network configuration file (instead of list)
203	// from the same name, then upconvert.
204	singleConf, err := LoadConf(dir, name)
205	if err != nil {
206		// A little extra logic so the error makes sense
207		if _, ok := err.(NoConfigsFoundError); len(files) != 0 && ok {
208			// Config lists found but no config files found
209			return nil, NotFoundError{dir, name}
210		}
211
212		return nil, err
213	}
214	return ConfListFromConf(singleConf)
215}
216
217func InjectConf(original *NetworkConfig, newValues map[string]interface{}) (*NetworkConfig, error) {
218	config := make(map[string]interface{})
219	err := json.Unmarshal(original.Bytes, &config)
220	if err != nil {
221		return nil, fmt.Errorf("unmarshal existing network bytes: %s", err)
222	}
223
224	for key, value := range newValues {
225		if key == "" {
226			return nil, fmt.Errorf("keys cannot be empty")
227		}
228
229		if value == nil {
230			return nil, fmt.Errorf("key '%s' value must not be nil", key)
231		}
232
233		config[key] = value
234	}
235
236	newBytes, err := json.Marshal(config)
237	if err != nil {
238		return nil, err
239	}
240
241	return ConfFromBytes(newBytes)
242}
243
244// ConfListFromConf "upconverts" a network config in to a NetworkConfigList,
245// with the single network as the only entry in the list.
246func ConfListFromConf(original *NetworkConfig) (*NetworkConfigList, error) {
247	// Re-deserialize the config's json, then make a raw map configlist.
248	// This may seem a bit strange, but it's to make the Bytes fields
249	// actually make sense. Otherwise, the generated json is littered with
250	// golang default values.
251
252	rawConfig := make(map[string]interface{})
253	if err := json.Unmarshal(original.Bytes, &rawConfig); err != nil {
254		return nil, err
255	}
256
257	rawConfigList := map[string]interface{}{
258		"name":       original.Network.Name,
259		"cniVersion": original.Network.CNIVersion,
260		"plugins":    []interface{}{rawConfig},
261	}
262
263	b, err := json.Marshal(rawConfigList)
264	if err != nil {
265		return nil, err
266	}
267	return ConfListFromBytes(b)
268}
269