| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/hostclaim" |
| 9 | "github.com/carlosframework/platform/internal/hostguard" |
| 10 | "github.com/carlosframework/platform/internal/provision" |
| 11 | "github.com/carlosframework/platform/internal/release" |
| 12 | "github.com/carlosframework/platform/internal/rootstate" |
| 13 | ) |
| 14 | |
| 15 | type generationRecord struct { |
| 16 | Key, ETag string |
| 17 | Record provision.Record |
| 18 | } |
| 19 | |
| 20 | func generationRequiredInventory(ctx context.Context, store release.Store, snapshot generationInventory, fresh func(hostguard.RootInstallation) error) (generationInventory, error) { |
| 21 | var zero generationInventory |
| 22 | if store == nil { |
| 23 | return zero, os.ErrPermission |
| 24 | } |
| 25 | retirements, err := generationRetirements(ctx, snapshot) |
| 26 | if err != nil { |
| 27 | return zero, err |
| 28 | } |
| 29 | snapshot.Records = nil |
| 30 | for _, view := range snapshot.Installations { |
| 31 | if err := ctx.Err(); err != nil { |
| 32 | return zero, err |
| 33 | } |
| 34 | in := view.Installation |
| 35 | if _, err := inventoryTimerNames(in.Artifacts); err != nil { |
| 36 | return zero, err |
| 37 | } |
| 38 | if in.Record != provision.RecordKey(in.Account, in.App, in.Host) { |
| 39 | return zero, os.ErrPermission |
| 40 | } |
| 41 | retired := false |
| 42 | for _, old := range view.Retired { |
| 43 | if old.Incarnation == in.Incarnation && old.Stamp == in.Stamp { |
| 44 | retired = true |
| 45 | } |
| 46 | } |
| 47 | if retired { |
| 48 | for i, payload := range snapshot.Payloads { |
| 49 | if payload.Inventory.Host == in.Host && len(payload.Inventory.Entries) != 0 && retirements[i] == "" { |
| 50 | return zero, os.ErrPermission |
| 51 | } |
| 52 | } |
| 53 | continue |
| 54 | } |
| 55 | rec, etag, err := provision.GetRecordETag(ctx, store, in.Account, in.App, in.Host) |
| 56 | if err != nil { |
| 57 | return zero, err |
| 58 | } |
| 59 | if rec.Schema != 1 || !release.ValidAccount(rec.Account) || !release.ValidApp(rec.App) || provision.ValidateShape(*rec) != nil { |
| 60 | return zero, os.ErrPermission |
| 61 | } |
| 62 | inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt) |
| 63 | if err != nil || etag == "" || inc != in.Incarnation { |
| 64 | return zero, os.ErrPermission |
| 65 | } |
| 66 | snapshot.Records = append(snapshot.Records, generationRecord{Key: in.Record, ETag: etag, Record: *rec}) |
| 67 | |
| 68 | } |
| 69 | return generationLocalObligations(ctx, snapshot, fresh) |
| 70 | } |
| 71 | |
| 72 | func generationLocalObligations(ctx context.Context, snapshot generationInventory, fresh func(hostguard.RootInstallation) error) (generationInventory, error) { |
| 73 | var zero generationInventory |
| 74 | snapshot.Unlaunched = map[string]string{} |
| 75 | installs := map[string]hostguard.FenceView{} |
| 76 | for _, view := range snapshot.Installations { |
| 77 | installs[view.Host] = view |
| 78 | } |
| 79 | if err := generationPayloadBindings(snapshot.Payloads, installs); err != nil { |
| 80 | return zero, err |
| 81 | } |
| 82 | if err := generationRetainedTimers(snapshot); err != nil { |
| 83 | return zero, err |
| 84 | } |
| 85 | for _, view := range snapshot.Installations { |
| 86 | if err := ctx.Err(); err != nil { |
| 87 | return zero, err |
| 88 | } |
| 89 | in := view.Installation |
| 90 | if !release.ValidAccount(in.Account) || !release.ValidApp(in.App) || in.Record != provision.RecordKey(in.Account, in.App, in.Host) { |
| 91 | return zero, os.ErrPermission |
| 92 | } |
| 93 | if _, err := hostclaim.Key(in.Host); err != nil { |
| 94 | return zero, err |
| 95 | } |
| 96 | unlaunched, err := generationUnlaunched(view, snapshot, fresh) |
| 97 | if err != nil { |
| 98 | return zero, err |
| 99 | } |
| 100 | if unlaunched { |
| 101 | snapshot.Unlaunched[in.Host] = rootHistoryInstallation(in) |
| 102 | } |
| 103 | for _, record := range snapshot.Records { |
| 104 | if record.Key == in.Record && record.Record.DerivesPaths() { |
| 105 | in.Artifacts.StateDirectory = true |
| 106 | } |
| 107 | } |
| 108 | for _, p := range snapshot.Payloads { |
| 109 | payload := p.Inventory |
| 110 | if payload.Host != in.Host { |
| 111 | continue |
| 112 | } |
| 113 | required := len(payload.Entries) > 0 || !in.Empty || payload.InstallationStamp != in.Stamp |
| 114 | for _, launch := range snapshot.Launches { |
| 115 | t := launch.Target |
| 116 | if t.Host == payload.Host && t.Incarnation == payload.Incarnation && t.Stamp == payload.InstallationStamp { |
| 117 | required = true |
| 118 | } |
| 119 | } |
| 120 | if !required { |
| 121 | // Retained predecessors are checked below as nonempty obligations. |
| 122 | // Without one, an empty payload needs root's creation evidence. |
| 123 | if len(view.Retired) == 0 { |
| 124 | if fresh == nil { |
| 125 | return zero, os.ErrPermission |
| 126 | } |
| 127 | if err := fresh(in); err != nil { |
| 128 | return zero, err |
| 129 | } |
| 130 | } |
| 131 | continue |
| 132 | } |
| 133 | state, err := filepath.Rel(dataDir(), provision.InstanceDir(dataDir(), in.Host)) |
| 134 | if err != nil || !filepath.IsLocal(state) || state == "." { |
| 135 | return zero, os.ErrPermission |
| 136 | } |
| 137 | routes, directories := 0, 0 |
| 138 | for _, entry := range payload.Entries { |
| 139 | if entry.Kind == "route" { |
| 140 | if entry.ManagedRoot != "registry" || entry.RelativePath != in.Host || entry.RowIdentity == "" { |
| 141 | return zero, os.ErrPermission |
| 142 | } |
| 143 | routes++ |
| 144 | } |
| 145 | if entry.Kind == "path" && entry.ManagedRoot == "data" && entry.RelativePath == state { |
| 146 | if entry.Type != 0040000 || entry.Device == 0 || entry.Inode == 0 { |
| 147 | return zero, os.ErrPermission |
| 148 | } |
| 149 | directories++ |
| 150 | } |
| 151 | } |
| 152 | if routes != 1 { |
| 153 | return zero, os.ErrPermission |
| 154 | } |
| 155 | if in.Artifacts.StateDirectory && directories != 1 { |
| 156 | if directories != 0 || payload.InstallationStamp != in.Stamp || len(view.Retired) != 0 || !unlaunched { |
| 157 | return zero, os.ErrPermission |
| 158 | } |
| 159 | if _, err := os.Lstat(filepath.Join(dataDir(), state)); !os.IsNotExist(err) { |
| 160 | return zero, os.ErrPermission |
| 161 | } |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | return snapshot, ctx.Err() |
| 166 | } |
| 167 | |
| 168 | func generationUnlaunched(view hostguard.FenceView, snapshot generationInventory, fresh func(hostguard.RootInstallation) error) (bool, error) { |
| 169 | if fresh == nil || view.Installation.Empty { |
| 170 | return false, nil |
| 171 | } |
| 172 | for _, launch := range snapshot.Launches { |
| 173 | if launch.Target.Host == view.Host { |
| 174 | return false, nil |
| 175 | } |
| 176 | } |
| 177 | in := view.Installation |
| 178 | for _, old := range view.Retired { |
| 179 | if old.Incarnation == in.Incarnation && old.Stamp < in.Stamp { |
| 180 | in.Stamp = old.Stamp |
| 181 | } |
| 182 | } |
| 183 | if err := fresh(in); err != nil { |
| 184 | return false, err |
| 185 | } |
| 186 | return true, nil |
| 187 | } |
| 188 | |
| 189 | func productionGenerationCreation(in hostguard.RootInstallation) error { |
| 190 | state, err := rootstate.Open(release.RootHistoryDir, false, false) |
| 191 | if err != nil { |
| 192 | return err |
| 193 | } |
| 194 | defer state.Close() |
| 195 | fresh, err := (&release.RootHistory{Store: state}).FreshInstallation(release.HistoryBinding{Host: in.Host, Account: in.Account, App: in.App}, rootHistoryInstallation(in)) |
| 196 | if err != nil { |
| 197 | return err |
| 198 | } |
| 199 | if !fresh { |
| 200 | return os.ErrPermission |
| 201 | } |
| 202 | return nil |
| 203 | } |
| 204 | |