blob: 2d443a04c2727bd215596b7315f341047d2a5835 [file] [log] [blame]
David K. Bainbridge528b3182017-01-23 08:51:59 -08001// Copyright 2012, 2013 Canonical Ltd.
2// Licensed under the LGPLv3, see LICENCE file for details.
3
4package utils
5
6import (
7 "io/ioutil"
8 "os"
9 "path/filepath"
10
11 "github.com/juju/errors"
12
13 "gopkg.in/yaml.v2"
14)
15
16// WriteYaml marshals obj as yaml to a temporary file in the same directory
17// as path, than atomically replaces path with the temporary file.
18func WriteYaml(path string, obj interface{}) error {
19 data, err := yaml.Marshal(obj)
20 if err != nil {
21 return errors.Trace(err)
22 }
23 dir := filepath.Dir(path)
24 f, err := ioutil.TempFile(dir, "juju")
25 if err != nil {
26 return errors.Trace(err)
27 }
28 tmp := f.Name()
29 if _, err := f.Write(data); err != nil {
30 f.Close() // don't leak file handle
31 os.Remove(tmp) // don't leak half written files on disk
32 return errors.Trace(err)
33 }
34 // Explicitly close the file before moving it. This is needed on Windows
35 // where the OS will not allow us to move a file that still has an open
36 // file handle. Must check the error on close because filesystems can delay
37 // reporting errors until the file is closed.
38 if err := f.Close(); err != nil {
39 os.Remove(tmp) // don't leak half written files on disk
40 return errors.Trace(err)
41 }
42
43 // ioutils.TempFile creates files 0600, but this function has a contract
44 // that files will be world readable, 0644 after replacement.
45 if err := os.Chmod(tmp, 0644); err != nil {
46 os.Remove(tmp) // remove file with incorrect permissions.
47 return errors.Trace(err)
48 }
49
50 return ReplaceFile(tmp, path)
51}
52
53// ReadYaml unmarshals the yaml contained in the file at path into obj. See
54// goyaml.Unmarshal. If path is not found, the error returned will be compatible
55// with os.IsNotExist.
56func ReadYaml(path string, obj interface{}) error {
57 data, err := ioutil.ReadFile(path)
58 if err != nil {
59 return err // cannot wrap here because callers check for NotFound.
60 }
61 return yaml.Unmarshal(data, obj)
62}
63
64// ConformYAML ensures all keys of any nested maps are strings. This is
65// necessary because YAML unmarshals map[interface{}]interface{} in nested
66// maps, which cannot be serialized by json or bson. Also, handle
67// []interface{}. cf. gopkg.in/juju/charm.v4/actions.go cleanse
68func ConformYAML(input interface{}) (interface{}, error) {
69 switch typedInput := input.(type) {
70
71 case map[string]interface{}:
72 newMap := make(map[string]interface{})
73 for key, value := range typedInput {
74 newValue, err := ConformYAML(value)
75 if err != nil {
76 return nil, err
77 }
78 newMap[key] = newValue
79 }
80 return newMap, nil
81
82 case map[interface{}]interface{}:
83 newMap := make(map[string]interface{})
84 for key, value := range typedInput {
85 typedKey, ok := key.(string)
86 if !ok {
87 return nil, errors.New("map keyed with non-string value")
88 }
89 newMap[typedKey] = value
90 }
91 return ConformYAML(newMap)
92
93 case []interface{}:
94 newSlice := make([]interface{}, len(typedInput))
95 for i, sliceValue := range typedInput {
96 newSliceValue, err := ConformYAML(sliceValue)
97 if err != nil {
98 return nil, errors.New("map keyed with non-string value")
99 }
100 newSlice[i] = newSliceValue
101 }
102 return newSlice, nil
103
104 default:
105 return input, nil
106 }
107}