File: yaml.go

package info (click to toggle)
golang-github-juju-utils 0.0~git20171220.f38c0b0-5
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 1,748 kB
  • sloc: makefile: 20
file content (113 lines) | stat: -rw-r--r-- 3,303 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// Copyright 2012, 2013 Canonical Ltd.
// Licensed under the LGPLv3, see LICENCE file for details.

package utils

import (
	"io/ioutil"
	"os"
	"path/filepath"

	"github.com/juju/errors"

	"gopkg.in/yaml.v2"
)

// WriteYaml marshals obj as yaml to a temporary file in the same directory
// as path, than atomically replaces path with the temporary file.
func WriteYaml(path string, obj interface{}) error {
	data, err := yaml.Marshal(obj)
	if err != nil {
		return errors.Trace(err)
	}
	dir := filepath.Dir(path)
	f, err := ioutil.TempFile(dir, "juju")
	if err != nil {
		return errors.Trace(err)
	}
	tmp := f.Name()
	if _, err := f.Write(data); err != nil {
		f.Close()      // don't leak file handle
		os.Remove(tmp) // don't leak half written files on disk
		return errors.Trace(err)
	}

	if err := f.Sync(); err != nil {
		f.Close()      // don't leak file handle
		os.Remove(tmp) // don't leak half written files on disk
		return errors.Trace(err)
	}
	// Explicitly close the file before moving it. This is needed on Windows
	// where the OS will not allow us to move a file that still has an open
	// file handle. Must check the error on close because filesystems can delay
	// reporting errors until the file is closed.
	if err := f.Close(); err != nil {
		os.Remove(tmp) // don't leak half written files on disk
		return errors.Trace(err)
	}

	// ioutils.TempFile creates files 0600, but this function has a contract
	// that files will be world readable, 0644 after replacement.
	if err := os.Chmod(tmp, 0644); err != nil {
		os.Remove(tmp) // remove file with incorrect permissions.
		return errors.Trace(err)
	}

	return ReplaceFile(tmp, path)
}

// ReadYaml unmarshals the yaml contained in the file at path into obj. See
// goyaml.Unmarshal. If path is not found, the error returned will be compatible
// with os.IsNotExist.
func ReadYaml(path string, obj interface{}) error {
	data, err := ioutil.ReadFile(path)
	if err != nil {
		return err // cannot wrap here because callers check for NotFound.
	}
	return yaml.Unmarshal(data, obj)
}

// ConformYAML ensures all keys of any nested maps are strings.  This is
// necessary because YAML unmarshals map[interface{}]interface{} in nested
// maps, which cannot be serialized by json or bson. Also, handle
// []interface{}. cf. gopkg.in/juju/charm.v4/actions.go cleanse
func ConformYAML(input interface{}) (interface{}, error) {
	switch typedInput := input.(type) {

	case map[string]interface{}:
		newMap := make(map[string]interface{})
		for key, value := range typedInput {
			newValue, err := ConformYAML(value)
			if err != nil {
				return nil, err
			}
			newMap[key] = newValue
		}
		return newMap, nil

	case map[interface{}]interface{}:
		newMap := make(map[string]interface{})
		for key, value := range typedInput {
			typedKey, ok := key.(string)
			if !ok {
				return nil, errors.New("map keyed with non-string value")
			}
			newMap[typedKey] = value
		}
		return ConformYAML(newMap)

	case []interface{}:
		newSlice := make([]interface{}, len(typedInput))
		for i, sliceValue := range typedInput {
			newSliceValue, err := ConformYAML(sliceValue)
			if err != nil {
				return nil, errors.New("map keyed with non-string value")
			}
			newSlice[i] = newSliceValue
		}
		return newSlice, nil

	default:
		return input, nil
	}
}