| 1 | // Package infraview is what an infrastructure inventory IS, in terms no |
| 2 | // cloud provider owns. It is deliberately free of every SDK: the AWS |
| 3 | // implementation (internal/awsinv) imports this package, never the other |
| 4 | // way round, so the console page that renders an inventory can be pointed |
| 5 | // at a second provider by writing one file rather than by rewriting the |
| 6 | // page. |
| 7 | // |
| 8 | // The wire shape here is written to a production bucket |
| 9 | // (InventoryKey), so it obeys the repo's additive rule: every field is |
| 10 | // optional, unknown fields parse, and an unknown Kind is carried through |
| 11 | // as itself. A reader that drops what it does not recognise would make the |
| 12 | // page LESS useful exactly where it is most useful — the resource nobody |
| 13 | // wrote code for is the one worth seeing. |
| 14 | package infraview |
| 15 | |
| 16 | import ( |
| 17 | "context" |
| 18 | "time" |
| 19 | ) |
| 20 | |
| 21 | // InventoryKey is where a deployment's inventory snapshot lives in its own |
| 22 | // bucket. One object, overwritten in place: this is a current-state |
| 23 | // snapshot, not a history, and a history would need a retention policy |
| 24 | // nobody has asked for. |
| 25 | const InventoryKey = "infra/inventory.json" |
| 26 | |
| 27 | // The kind vocabulary. Small and neutral on purpose — a provider maps its |
| 28 | // own types into these and carries the truth in NativeType, so the page's |
| 29 | // layout rules ("machines sit in the box layer") never have to learn a new |
| 30 | // cloud's nouns. KindOther is not a failure: it is the honest home for a |
| 31 | // discovered resource this vocabulary has no word for. |
| 32 | const ( |
| 33 | KindMachine = "machine" |
| 34 | KindIP = "ip" |
| 35 | KindDisk = "disk" |
| 36 | KindFirewall = "firewall" |
| 37 | KindNetwork = "network" |
| 38 | KindBucket = "bucket" |
| 39 | KindDNSZone = "dns_zone" |
| 40 | KindDNSRecord = "dns_record" |
| 41 | KindIdentity = "identity" |
| 42 | KindAlarm = "alarm" |
| 43 | KindCertificate = "certificate" |
| 44 | KindOther = "other" |
| 45 | ) |
| 46 | |
| 47 | // Resource is one thing a provider is carrying. Attrs is stringly on |
| 48 | // purpose: a typed struct per kind would need a migration every time a |
| 49 | // provider exposes a new field, and this object must stay parseable by |
| 50 | // readers written before that field existed. Keys the console renders are |
| 51 | // documented where it reads them; keys it does not know are shown verbatim. |
| 52 | type Resource struct { |
| 53 | Provider string `json:"provider"` |
| 54 | Kind string `json:"kind"` |
| 55 | NativeType string `json:"native_type,omitempty"` |
| 56 | ID string `json:"id"` |
| 57 | Name string `json:"name,omitempty"` |
| 58 | Region string `json:"region,omitempty"` |
| 59 | Attrs map[string]string `json:"attrs,omitempty"` |
| 60 | Tags map[string]string `json:"tags,omitempty"` |
| 61 | } |
| 62 | |
| 63 | // Attr reads one attribute without the caller having to nil-check the map. |
| 64 | func (r Resource) Attr(key string) string { |
| 65 | if r.Attrs == nil { |
| 66 | return "" |
| 67 | } |
| 68 | return r.Attrs[key] |
| 69 | } |
| 70 | |
| 71 | // Inventory is one provider's estate at one moment. Partial names the reads |
| 72 | // that FAILED, in words a person can act on ("security groups in eu-west-1: |
| 73 | // throttled"). It exists because "no security groups" and "the security |
| 74 | // group read failed" are different sentences and only one of them may be |
| 75 | // rendered as a fact. |
| 76 | type Inventory struct { |
| 77 | Provider string `json:"provider"` |
| 78 | Account string `json:"account,omitempty"` |
| 79 | TakenAt time.Time `json:"taken_at"` |
| 80 | Resources []Resource `json:"resources"` |
| 81 | Partial []string `json:"partial,omitempty"` |
| 82 | } |
| 83 | |
| 84 | // ByKind returns every resource of one kind, in inventory order. |
| 85 | func (i Inventory) ByKind(kind string) []Resource { |
| 86 | var out []Resource |
| 87 | for _, r := range i.Resources { |
| 88 | if r.Kind == kind { |
| 89 | out = append(out, r) |
| 90 | } |
| 91 | } |
| 92 | return out |
| 93 | } |
| 94 | |
| 95 | // Find returns the first resource of kind whose attr equals value, or nil. |
| 96 | // value == "" never matches: an empty attribute is absence, and matching on |
| 97 | // it would join every machine with no public IP to every record with no |
| 98 | // value. |
| 99 | func (i Inventory) Find(kind, attr, value string) *Resource { |
| 100 | if value == "" { |
| 101 | return nil |
| 102 | } |
| 103 | for idx := range i.Resources { |
| 104 | if i.Resources[idx].Kind == kind && i.Resources[idx].Attr(attr) == value { |
| 105 | return &i.Resources[idx] |
| 106 | } |
| 107 | } |
| 108 | return nil |
| 109 | } |
| 110 | |
| 111 | // Provider is one cloud (or one bare-metal estate) that can describe |
| 112 | // itself. Implementations return construction-class failures as an error — |
| 113 | // no credential, no reachable API — and everything else through |
| 114 | // Inventory.Partial, because a page that can show eleven resource classes |
| 115 | // must show eleven. |
| 116 | type Provider interface { |
| 117 | Inventory(ctx context.Context) (Inventory, error) |
| 118 | } |
| 119 | |