| 1 | package park |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "path" |
| 7 | "sort" |
| 8 | "strings" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/hostclaim" |
| 11 | "github.com/carlosframework/platform/internal/hostguard" |
| 12 | "github.com/carlosframework/platform/internal/release" |
| 13 | ) |
| 14 | |
| 15 | var ( |
| 16 | ErrLocalIdentity = errors.New("park: local retirement identity mismatch") |
| 17 | ErrLocalInventory = errors.New("park: missing or unsafe inventory") |
| 18 | ErrLocalJournal = errors.New("park: invalid or conflicting local journal") |
| 19 | ErrLocalUnsupported = errors.New("park: local retirement unsupported") |
| 20 | errLocalComplete = errors.New("park: local obligation already completed") |
| 21 | ) |
| 22 | |
| 23 | type Local struct { |
| 24 | Authorize func(context.Context, *hostclaim.Claim, hostclaim.Obligation) error |
| 25 | Guard hostguard.Guard |
| 26 | Claims release.Store |
| 27 | DataDir, Box string |
| 28 | afterRename func(string) error |
| 29 | } |
| 30 | |
| 31 | type localItem struct { |
| 32 | Path string |
| 33 | Stamp uint64 |
| 34 | Dev, Ino uint64 |
| 35 | Type uint32 |
| 36 | Missing, Done bool |
| 37 | } |
| 38 | |
| 39 | type localJournal struct { |
| 40 | V int |
| 41 | Host, Incarnation, Op, Obligation, Retirement, Mode string |
| 42 | Installation, ArtifactStamp uint64 |
| 43 | Items []localItem |
| 44 | } |
| 45 | |
| 46 | func localComponent(s string) bool { |
| 47 | return s != "" && s != "." && s != ".." && !strings.ContainsAny(s, "/\\\x00") |
| 48 | } |
| 49 | |
| 50 | func localPath(s string) bool { |
| 51 | if s == "" || path.IsAbs(s) || path.Clean(s) != s || strings.ContainsAny(s, "\\\x00") { |
| 52 | return false |
| 53 | } |
| 54 | for _, p := range strings.Split(s, "/") { |
| 55 | if !localComponent(p) { |
| 56 | return false |
| 57 | } |
| 58 | } |
| 59 | return s != "hostguard" && !strings.HasPrefix(s, "hostguard/") && s != ".parked" && !strings.HasPrefix(s, ".parked/") |
| 60 | } |
| 61 | |
| 62 | func (l Local) chore(c *hostclaim.Claim, captured hostguard.Captured, id, op string) (hostclaim.Obligation, error) { |
| 63 | if c == nil { |
| 64 | return hostclaim.Obligation{}, ErrLocalIdentity |
| 65 | } |
| 66 | var found *hostclaim.Obligation |
| 67 | for i := range c.Obligations { |
| 68 | if c.Obligations[i].ID == id && c.Obligations[i].Op == op { |
| 69 | found = &c.Obligations[i] |
| 70 | break |
| 71 | } |
| 72 | } |
| 73 | if found == nil { |
| 74 | return hostclaim.Obligation{}, errLocalComplete |
| 75 | } |
| 76 | if c.Holder == nil || c.Holder.Incarnation != captured.Incarnation || found.Kind != "park-local" || found.Box != l.Box || found.Owner != "box:"+l.Box || !localComponent(found.Retirement) { |
| 77 | return hostclaim.Obligation{}, ErrLocalIdentity |
| 78 | } |
| 79 | if _, ok := c.Inventory[l.Box]; !ok { |
| 80 | return hostclaim.Obligation{}, ErrLocalInventory |
| 81 | } |
| 82 | return *found, nil |
| 83 | } |
| 84 | |
| 85 | func (l Local) RetireArtifacts(ctx context.Context, host string, captured hostguard.Captured, id, op string, stamp uint64) error { |
| 86 | if stamp == 0 { |
| 87 | return ErrLocalIdentity |
| 88 | } |
| 89 | return l.retire(ctx, host, captured, id, op, "artifacts", stamp) |
| 90 | } |
| 91 | |
| 92 | // RetireSuperseded is retire-artifacts(S) of claim §6.4 for an admitted |
| 93 | // writer's own superseded local generation (claim Plan 9 D4): a restarted |
| 94 | // landing probe, or a cold wake after the open, over its predecessor's |
| 95 | // database. No chore is read, required or completed. The authority is the |
| 96 | // guard at the active installation captured, newer than stamp, together with |
| 97 | // check, which runs on every fresh claim under the guard and again in the |
| 98 | // inventory CAS. Exactly paths are parked, each of which the inventory must |
| 99 | // tag stamp; the same CAS tags them captured.Stamp, the ownership the |
| 100 | // successor records before it materializes them (§6.6 P6-D2), so its own |
| 101 | // successor can retire them in turn. No stamp moves and every other path and |
| 102 | // box is left as found. A replay of the same retirement resumes its journal. |
| 103 | func (l Local) RetireSuperseded(ctx context.Context, host string, captured hostguard.Captured, op, retirement string, stamp uint64, paths []string, check func(*hostclaim.Claim) error) (before, after hostclaim.Observed, resultErr error) { |
| 104 | if !localSupported() { |
| 105 | return before, after, ErrLocalUnsupported |
| 106 | } |
| 107 | if ctx == nil || check == nil || l.Claims == nil || l.Box == "" || l.DataDir == "" || l.Guard.DataDir != l.DataDir || captured.Incarnation == "" || captured.Stamp == 0 || |
| 108 | stamp == 0 || stamp >= captured.Stamp || !localComponent(op) || !localComponent(retirement) || len(paths) == 0 { |
| 109 | return before, after, ErrLocalIdentity |
| 110 | } |
| 111 | sorted := append([]string(nil), paths...) |
| 112 | sort.Strings(sorted) |
| 113 | for i := range sorted { |
| 114 | if !localPath(sorted[i]) { |
| 115 | return before, after, ErrLocalInventory |
| 116 | } |
| 117 | for k := i + 1; k < len(sorted); k++ { |
| 118 | if sorted[k] == sorted[i] || strings.HasPrefix(sorted[k], sorted[i]+"/") { |
| 119 | return before, after, ErrLocalInventory |
| 120 | } |
| 121 | } |
| 122 | } |
| 123 | selected := func(c *hostclaim.Claim) error { |
| 124 | if c == nil || c.Holder == nil || c.Holder.Incarnation != captured.Incarnation || c.Holder.Stamp != captured.Stamp { |
| 125 | return ErrLocalIdentity |
| 126 | } |
| 127 | return check(c) |
| 128 | } |
| 129 | var j localJournal |
| 130 | guarded := func(c *hostclaim.Claim) error { |
| 131 | if err := selected(c); err != nil { |
| 132 | return err |
| 133 | } |
| 134 | entry, ok := c.Inventory[l.Box] |
| 135 | if !ok { |
| 136 | return ErrLocalInventory |
| 137 | } |
| 138 | superseded, successor := 0, 0 |
| 139 | for _, p := range sorted { |
| 140 | switch entry.Paths[p] { |
| 141 | case stamp: |
| 142 | superseded++ |
| 143 | case captured.Stamp: |
| 144 | successor++ |
| 145 | default: |
| 146 | return ErrLocalInventory |
| 147 | } |
| 148 | } |
| 149 | if successor == len(sorted) { |
| 150 | return errLocalComplete |
| 151 | } |
| 152 | if superseded != len(sorted) { |
| 153 | return ErrLocalInventory |
| 154 | } |
| 155 | j = localJournal{V: 1, Host: host, Incarnation: captured.Incarnation, Op: op, Retirement: retirement, Mode: "superseded", Installation: captured.Stamp, ArtifactStamp: stamp} |
| 156 | for _, p := range sorted { |
| 157 | j.Items = append(j.Items, localItem{Path: p, Stamp: stamp}) |
| 158 | } |
| 159 | return nil |
| 160 | } |
| 161 | work := func() error { |
| 162 | if err := l.parkLocal(ctx, &j); err != nil { |
| 163 | return err |
| 164 | } |
| 165 | fresh, err := hostclaim.Read(ctx, l.Claims, host) |
| 166 | if err != nil { |
| 167 | return err |
| 168 | } |
| 169 | if fresh.Absent { |
| 170 | return ErrLocalIdentity |
| 171 | } |
| 172 | before = fresh |
| 173 | after, err = hostclaim.Update(ctx, l.Claims, fresh, func(c *hostclaim.Claim) error { |
| 174 | if err := selected(c); err != nil { |
| 175 | return err |
| 176 | } |
| 177 | entry, ok := c.Inventory[l.Box] |
| 178 | if !ok { |
| 179 | return ErrLocalInventory |
| 180 | } |
| 181 | for _, item := range j.Items { |
| 182 | if entry.Paths[item.Path] != item.Stamp { |
| 183 | return ErrLocalInventory |
| 184 | } |
| 185 | } |
| 186 | for _, item := range j.Items { |
| 187 | entry.Paths[item.Path] = captured.Stamp |
| 188 | } |
| 189 | c.Inventory[l.Box] = entry |
| 190 | return nil |
| 191 | }) |
| 192 | return err |
| 193 | } |
| 194 | err := l.Guard.With(ctx, host, captured, guarded, work) |
| 195 | if errors.Is(err, errLocalComplete) { |
| 196 | return before, after, nil |
| 197 | } |
| 198 | return before, after, err |
| 199 | } |
| 200 | |
| 201 | func (l Local) RetireInstallation(ctx context.Context, host string, captured hostguard.Captured, id, op string) error { |
| 202 | return l.retire(ctx, host, captured, id, op, "installation", 0) |
| 203 | } |
| 204 | |
| 205 | func (l Local) retire(ctx context.Context, host string, captured hostguard.Captured, id, op, mode string, stamp uint64) error { |
| 206 | if !localSupported() { |
| 207 | return ErrLocalUnsupported |
| 208 | } |
| 209 | if ctx == nil || l.Authorize == nil || l.Claims == nil || l.Box == "" || l.DataDir == "" || l.Guard.DataDir != l.DataDir || (captured.Stamp == 0 && mode != "empty") || id == "" || op == "" { |
| 210 | return ErrLocalIdentity |
| 211 | } |
| 212 | obs, err := hostclaim.Read(ctx, l.Claims, host) |
| 213 | if err != nil { |
| 214 | return err |
| 215 | } |
| 216 | if obs.Absent { |
| 217 | return ErrLocalIdentity |
| 218 | } |
| 219 | if _, err := l.chore(obs.Claim, captured, id, op); err != nil { |
| 220 | if errors.Is(err, errLocalComplete) { |
| 221 | return nil |
| 222 | } |
| 223 | return err |
| 224 | } |
| 225 | var j localJournal |
| 226 | var inventory map[string]uint64 |
| 227 | check := func(c *hostclaim.Claim) error { |
| 228 | o, err := l.chore(c, captured, id, op) |
| 229 | if err != nil { |
| 230 | return err |
| 231 | } |
| 232 | if err := l.Authorize(ctx, c, o); err != nil { |
| 233 | return err |
| 234 | } |
| 235 | inventory = c.Inventory[l.Box].Paths |
| 236 | if mode == "empty" && len(inventory) != 0 { |
| 237 | return ErrLocalInventory |
| 238 | } |
| 239 | paths := make([]string, 0, len(inventory)) |
| 240 | for p, s := range inventory { |
| 241 | if !localPath(p) || s == 0 { |
| 242 | return ErrLocalInventory |
| 243 | } |
| 244 | paths = append(paths, p) |
| 245 | } |
| 246 | sort.Strings(paths) |
| 247 | for i := range paths { |
| 248 | for k := i + 1; k < len(paths); k++ { |
| 249 | if strings.HasPrefix(paths[k], paths[i]+"/") { |
| 250 | return ErrLocalInventory |
| 251 | } |
| 252 | } |
| 253 | } |
| 254 | j = localJournal{V: 1, Host: host, Incarnation: captured.Incarnation, Op: op, Obligation: id, Retirement: o.Retirement, Mode: mode, Installation: captured.Stamp, ArtifactStamp: stamp} |
| 255 | for _, p := range paths { |
| 256 | if mode == "installation" || inventory[p] == stamp { |
| 257 | j.Items = append(j.Items, localItem{Path: p, Stamp: inventory[p]}) |
| 258 | } |
| 259 | } |
| 260 | return nil |
| 261 | } |
| 262 | work := func() error { |
| 263 | if err := l.parkLocal(ctx, &j); err != nil { |
| 264 | return err |
| 265 | } |
| 266 | fresh, err := hostclaim.Read(ctx, l.Claims, host) |
| 267 | if err != nil { |
| 268 | return err |
| 269 | } |
| 270 | if fresh.Absent { |
| 271 | return ErrLocalIdentity |
| 272 | } |
| 273 | _, err = hostclaim.Update(ctx, l.Claims, fresh, func(c *hostclaim.Claim) error { |
| 274 | o, err := l.chore(c, captured, id, op) |
| 275 | if err != nil { |
| 276 | return err |
| 277 | } |
| 278 | if err := l.Authorize(ctx, c, o); err != nil { |
| 279 | return err |
| 280 | } |
| 281 | entry := c.Inventory[l.Box] |
| 282 | for _, item := range j.Items { |
| 283 | if entry.Paths[item.Path] != item.Stamp { |
| 284 | return ErrLocalInventory |
| 285 | } |
| 286 | } |
| 287 | if (mode == "installation" || mode == "empty") && len(entry.Paths) != len(j.Items) { |
| 288 | return ErrLocalInventory |
| 289 | } |
| 290 | for _, item := range j.Items { |
| 291 | delete(entry.Paths, item.Path) |
| 292 | } |
| 293 | c.Inventory[l.Box] = entry |
| 294 | return hostclaim.CompleteObligation(op, id)(c) |
| 295 | }) |
| 296 | return err |
| 297 | } |
| 298 | if mode == "installation" { |
| 299 | err = l.Guard.RetireInstallationWith(ctx, host, captured, check, work) |
| 300 | } else if mode == "empty" { |
| 301 | err = l.Guard.WithNoInstallation(ctx, host, captured.Incarnation, check, work) |
| 302 | } else { |
| 303 | err = l.Guard.With(ctx, host, captured, check, work) |
| 304 | } |
| 305 | if errors.Is(err, errLocalComplete) { |
| 306 | return nil |
| 307 | } |
| 308 | return err |
| 309 | } |
| 310 | |
| 311 | func (l Local) RecordInventory(ctx context.Context, host string, captured hostguard.Captured, layout string, paths []string, check func(*hostclaim.Claim) error) error { |
| 312 | if !localSupported() { |
| 313 | return ErrLocalUnsupported |
| 314 | } |
| 315 | if ctx == nil || l.Claims == nil || l.Box == "" || l.Guard.DataDir != l.DataDir || l.DataDir == "" || check == nil || captured.Stamp == 0 || (layout != "flat" && layout != "nested") { |
| 316 | return ErrLocalInventory |
| 317 | } |
| 318 | for _, p := range paths { |
| 319 | if !localPath(p) { |
| 320 | return ErrLocalInventory |
| 321 | } |
| 322 | } |
| 323 | return l.Guard.With(ctx, host, captured, check, func() error { |
| 324 | obs, err := hostclaim.Read(ctx, l.Claims, host) |
| 325 | if err != nil { |
| 326 | return err |
| 327 | } |
| 328 | if obs.Absent { |
| 329 | return ErrLocalIdentity |
| 330 | } |
| 331 | _, err = hostclaim.Update(ctx, l.Claims, obs, func(c *hostclaim.Claim) error { |
| 332 | if c.Holder == nil || c.Holder.Incarnation != captured.Incarnation { |
| 333 | return ErrLocalIdentity |
| 334 | } |
| 335 | if err := check(c); err != nil { |
| 336 | return err |
| 337 | } |
| 338 | if c.Inventory == nil { |
| 339 | c.Inventory = make(map[string]hostclaim.InventoryEntry) |
| 340 | } |
| 341 | entry, exists := c.Inventory[l.Box] |
| 342 | if exists && entry.Layout != layout && len(entry.Paths) != 0 { |
| 343 | return ErrLocalInventory |
| 344 | } |
| 345 | entry.Layout = layout |
| 346 | if entry.Paths == nil { |
| 347 | entry.Paths = make(map[string]uint64) |
| 348 | } |
| 349 | for _, p := range paths { |
| 350 | if old := entry.Paths[p]; old != 0 && old != captured.Stamp { |
| 351 | return ErrLocalInventory |
| 352 | } |
| 353 | entry.Paths[p] = captured.Stamp |
| 354 | } |
| 355 | all := make([]string, 0, len(entry.Paths)) |
| 356 | for p, s := range entry.Paths { |
| 357 | if !localPath(p) || s == 0 { |
| 358 | return ErrLocalInventory |
| 359 | } |
| 360 | all = append(all, p) |
| 361 | } |
| 362 | sort.Strings(all) |
| 363 | for i := range all { |
| 364 | for k := i + 1; k < len(all); k++ { |
| 365 | if strings.HasPrefix(all[k], all[i]+"/") { |
| 366 | return ErrLocalInventory |
| 367 | } |
| 368 | } |
| 369 | } |
| 370 | c.Inventory[l.Box] = entry |
| 371 | return nil |
| 372 | }) |
| 373 | return err |
| 374 | }) |
| 375 | } |
| 376 | |
| 377 | func (l Local) AcknowledgeEmpty(ctx context.Context, host, incarnation, id, op string) error { |
| 378 | return l.retire(ctx, host, hostguard.Captured{Incarnation: incarnation}, id, op, "empty", 0) |
| 379 | } |
| 380 | |