github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/worker/uniter/relation/state.go (about) 1 // Copyright 2012-2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 // relation implements persistent local storage of a unit's relation state, and 5 // translation of relation changes into hooks that need to be run. 6 package relation 7 8 import ( 9 "fmt" 10 "io/ioutil" 11 "os" 12 "path/filepath" 13 "strconv" 14 "strings" 15 16 "github.com/juju/errors" 17 "github.com/juju/utils" 18 "gopkg.in/juju/charm.v6/hooks" 19 20 "github.com/juju/juju/worker/uniter/hook" 21 ) 22 23 // State describes the state of a relation. 24 type State struct { 25 // RelationId identifies the relation. 26 RelationId int 27 28 // Members is a map from unit name to the last change version 29 // for which a hook.Info was delivered on the output channel. 30 Members map[string]int64 31 32 // ChangedPending indicates that a "relation-changed" hook for the given 33 // unit name must be the first hook.Info to be sent to the output channel. 34 ChangedPending string 35 } 36 37 // copy returns an independent copy of the state. 38 func (s *State) copy() *State { 39 copy := &State{ 40 RelationId: s.RelationId, 41 ChangedPending: s.ChangedPending, 42 } 43 if s.Members != nil { 44 copy.Members = map[string]int64{} 45 for m, v := range s.Members { 46 copy.Members[m] = v 47 } 48 } 49 return copy 50 } 51 52 // Validate returns an error if the supplied hook.Info does not represent 53 // a valid change to the relation state. Hooks must always be validated 54 // against the current state before they are run, to ensure that the system 55 // meets its guarantees about hook execution order. 56 func (s *State) Validate(hi hook.Info) (err error) { 57 defer errors.DeferredAnnotatef(&err, "inappropriate %q for %q", hi.Kind, hi.RemoteUnit) 58 if hi.RelationId != s.RelationId { 59 return fmt.Errorf("expected relation %d, got relation %d", s.RelationId, hi.RelationId) 60 } 61 if s.Members == nil { 62 return fmt.Errorf(`relation is broken and cannot be changed further`) 63 } 64 unit, kind := hi.RemoteUnit, hi.Kind 65 if kind == hooks.RelationBroken { 66 if len(s.Members) == 0 { 67 return nil 68 } 69 return fmt.Errorf(`cannot run "relation-broken" while units still present`) 70 } 71 if s.ChangedPending != "" { 72 if unit != s.ChangedPending || kind != hooks.RelationChanged { 73 return fmt.Errorf(`expected "relation-changed" for %q`, s.ChangedPending) 74 } 75 } else if _, joined := s.Members[unit]; joined && kind == hooks.RelationJoined { 76 return fmt.Errorf("unit already joined") 77 } else if !joined && kind != hooks.RelationJoined { 78 return fmt.Errorf("unit has not joined") 79 } 80 return nil 81 } 82 83 // StateDir is a filesystem-backed representation of the state of a 84 // relation. Concurrent modifications to the underlying state directory 85 // will have undefined consequences. 86 type StateDir struct { 87 // path identifies the directory holding persistent state. 88 path string 89 90 // state is the cached state of the directory, which is guaranteed 91 // to be synchronized with the true state so long as no concurrent 92 // changes are made to the directory. 93 state State 94 } 95 96 // State returns the current state of the relation. 97 func (d *StateDir) State() *State { 98 return d.state.copy() 99 } 100 101 // ReadStateDir loads a StateDir from the subdirectory of dirPath named 102 // for the supplied RelationId. If the directory does not exist, no error 103 // is returned, 104 func ReadStateDir(dirPath string, relationId int) (d *StateDir, err error) { 105 d = &StateDir{ 106 filepath.Join(dirPath, strconv.Itoa(relationId)), 107 State{relationId, map[string]int64{}, ""}, 108 } 109 defer errors.DeferredAnnotatef(&err, "cannot load relation state from %q", d.path) 110 if _, err := os.Stat(d.path); os.IsNotExist(err) { 111 return d, nil 112 } else if err != nil { 113 return nil, err 114 } 115 fis, err := ioutil.ReadDir(d.path) 116 if err != nil { 117 return nil, err 118 } 119 for _, fi := range fis { 120 // Entries with names ending in "-" followed by an integer must be 121 // files containing valid unit data; all other names are ignored. 122 name := fi.Name() 123 i := strings.LastIndex(name, "-") 124 if i == -1 { 125 continue 126 } 127 svcName := name[:i] 128 unitId := name[i+1:] 129 if _, err := strconv.Atoi(unitId); err != nil { 130 continue 131 } 132 unitName := svcName + "/" + unitId 133 var info diskInfo 134 if err = utils.ReadYaml(filepath.Join(d.path, name), &info); err != nil { 135 return nil, fmt.Errorf("invalid unit file %q: %v", name, err) 136 } 137 if info.ChangeVersion == nil { 138 return nil, fmt.Errorf(`invalid unit file %q: "changed-version" not set`, name) 139 } 140 d.state.Members[unitName] = *info.ChangeVersion 141 if info.ChangedPending { 142 if d.state.ChangedPending != "" { 143 return nil, fmt.Errorf("%q and %q both have pending changed hooks", d.state.ChangedPending, unitName) 144 } 145 d.state.ChangedPending = unitName 146 } 147 } 148 return d, nil 149 } 150 151 // ReadAllStateDirs loads and returns every StateDir persisted directly inside 152 // the supplied dirPath. If dirPath does not exist, no error is returned. 153 func ReadAllStateDirs(dirPath string) (dirs map[int]*StateDir, err error) { 154 defer errors.DeferredAnnotatef(&err, "cannot load relations state from %q", dirPath) 155 if _, err := os.Stat(dirPath); os.IsNotExist(err) { 156 return nil, nil 157 } else if err != nil { 158 return nil, err 159 } 160 fis, err := ioutil.ReadDir(dirPath) 161 if err != nil { 162 return nil, err 163 } 164 dirs = map[int]*StateDir{} 165 for _, fi := range fis { 166 // Entries with integer names must be directories containing StateDir 167 // data; all other names will be ignored. 168 relationId, err := strconv.Atoi(fi.Name()) 169 if err != nil { 170 // This doesn't look like a relation. 171 continue 172 } 173 dir, err := ReadStateDir(dirPath, relationId) 174 if err != nil { 175 return nil, err 176 } 177 dirs[relationId] = dir 178 } 179 return dirs, nil 180 } 181 182 // Ensure creates the directory if it does not already exist. 183 func (d *StateDir) Ensure() error { 184 return os.MkdirAll(d.path, 0755) 185 } 186 187 // Exists returns true if the directory for this state exists. 188 func (d *StateDir) Exists() bool { 189 _, err := os.Stat(d.path) 190 return err == nil 191 } 192 193 // Write atomically writes to disk the relation state change in hi. 194 // It must be called after the respective hook was executed successfully. 195 // Write doesn't validate hi but guarantees that successive writes of 196 // the same hi are idempotent. 197 func (d *StateDir) Write(hi hook.Info) (err error) { 198 defer errors.DeferredAnnotatef(&err, "failed to write %q hook info for %q on state directory", hi.Kind, hi.RemoteUnit) 199 if hi.Kind == hooks.RelationBroken { 200 return d.Remove() 201 } 202 name := strings.Replace(hi.RemoteUnit, "/", "-", 1) 203 path := filepath.Join(d.path, name) 204 if hi.Kind == hooks.RelationDeparted { 205 if err = os.Remove(path); err != nil && !os.IsNotExist(err) { 206 return err 207 } 208 // If atomic delete succeeded, update own state. 209 delete(d.state.Members, hi.RemoteUnit) 210 return nil 211 } 212 di := diskInfo{&hi.ChangeVersion, hi.Kind == hooks.RelationJoined} 213 if err := utils.WriteYaml(path, &di); err != nil { 214 return err 215 } 216 // If write was successful, update own state. 217 d.state.Members[hi.RemoteUnit] = hi.ChangeVersion 218 if hi.Kind == hooks.RelationJoined { 219 d.state.ChangedPending = hi.RemoteUnit 220 } else { 221 d.state.ChangedPending = "" 222 } 223 return nil 224 } 225 226 // Remove removes the directory if it exists and is empty. 227 func (d *StateDir) Remove() error { 228 if err := os.Remove(d.path); err != nil && !os.IsNotExist(err) { 229 return err 230 } 231 // If atomic delete succeeded, update own state. 232 d.state.Members = nil 233 return nil 234 } 235 236 // diskInfo defines the relation unit data serialization. 237 type diskInfo struct { 238 ChangeVersion *int64 `yaml:"change-version"` 239 ChangedPending bool `yaml:"changed-pending,omitempty"` 240 }