Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 56 additions & 8 deletions src/cli/fleet.toit
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ class Status_:
return is-fully-updated and missed-checkins == 0

class FleetFile:
static JSON-SCHEMA ::= "https://toit.io/schemas/artemis/fleet/v2.json"

path/string
id/Uuid
organization-id/Uuid
Expand Down Expand Up @@ -105,7 +107,17 @@ class FleetFile:
ui.abort "Fleet file '$path' has invalid format."
if not fleet-contents.contains "id":
ui.abort "Fleet file '$path' does not contain an ID."
if not fleet-contents.contains "organization":

schema := fleet-contents.get "\$schema"
if schema and schema != JSON-SCHEMA:
ui.abort "Fleet file '$path' has an unsupported schema: $schema"
is-new-format := schema != null

// The legacy format had a top-level "organization" UUID; the new
// format drops it and stores a per-server "scope" inside each
// server entry instead. The "broker" field is a server-name string
// in both layouts.
if not is-new-format and not fleet-contents.contains "organization":
ui.abort "Fleet file '$path' does not contain an organization ID."

is-reference := fleet-contents.get "is-reference" --if-absent=: false
Expand All @@ -130,7 +142,20 @@ class FleetFile:
ui.abort "Fleet file '$path' has invalid format for 'pod' in group '$group-name'."
PodReference.parse entry["pod"] --cli=cli

broker-name := fleet-contents.get "broker"
// Broker reference is a string in both the new and legacy layouts.
// In the new layout the broker's scope lives inside its server entry
// (see below); in the legacy layout it's read from the top-level
// "organization" field.
broker-entry := fleet-contents.get "broker"
broker-name/string? := null
if broker-entry and broker-entry is not string:
ui.abort "Fleet file '$path' has invalid format for 'broker'."
broker-name = broker-entry

organization-id/Uuid? := null
if not is-new-format:
organization-id = Uuid.parse fleet-contents["organization"]

migrating-from-entry := fleet-contents.get "migrating-from"
servers-entry := fleet-contents.get "servers"

Expand All @@ -141,16 +166,33 @@ class FleetFile:
ui.abort "Fleet file '$path' has invalid format for 'broker' and 'servers'."
if servers-entry is not Map:
ui.abort "Fleet file '$path' has invalid format for 'servers'."
broker-entry := servers-entry.get broker-name
if not broker-entry:
broker-server-entry := servers-entry.get broker-name
if not broker-server-entry:
ui.abort "Fleet file '$path' does not contain a server entry for broker '$broker-name'."

// The new layout stores each server's scope alongside its
// connection info. Strip the scope field before handing the entry
// to ServerConfig.from-json. For now only the broker's scope is
// actually used (it populates organization-id); scopes on other
// server entries are read but ignored. They become load-bearing
// once we track per-server scope in memory.
servers = servers-entry.map: | server-name/string encoded-server |
if encoded-server is not Map:
ui.abort "Fleet file '$path' has invalid format for server '$server-name'."
ServerConfig.from-json server-name encoded-server
encoded-map := encoded-server as Map
cleaned := encoded-map
if encoded-map.contains "scope":
cleaned = encoded-map.copy
cleaned.remove "scope"
ServerConfig.from-json server-name cleaned
--der-deserializer=: base64.decode it

if is-new-format:
broker-scope-value := (broker-server-entry as Map).get "scope"
if broker-scope-value is not string:
ui.abort "Fleet file '$path' is missing 'scope' on broker server '$broker-name'."
organization-id = Uuid.parse broker-scope-value

if migrating-from-entry:
if migrating-from-entry is not List:
ui.abort "Fleet file '$path' has invalid format for 'migrating-from'."
Expand Down Expand Up @@ -187,7 +229,7 @@ class FleetFile:
return FleetFile
--path=path
--id=Uuid.parse fleet-contents["id"]
--organization-id=Uuid.parse fleet-contents["organization"]
--organization-id=organization-id
--group-pods=group-pods
--is-reference=is-reference
--broker-name=broker-name
Expand Down Expand Up @@ -236,8 +278,8 @@ class FleetFile:

to-json_ --reference/bool=false -> Map:
result := {
"\$schema": JSON-SCHEMA,
"id": "$id",
"organization": "$organization-id",
}
if reference:
result["is-reference"] = true
Expand All @@ -261,8 +303,14 @@ class FleetFile:
result["broker"] = broker-name
if migrating-from and not migrating-from.is-empty:
result["migrating-from"] = migrating-from
// Each server entry carries its own scope. For now every entry
// uses the fleet's single organization-id; this anticipates a
// future world where each server can be scoped independently.
scope-string := "$organization-id"
result["servers"] = servers.map: | server-name/string server-config/ServerConfig |
server-config.to-json --der-serializer=: base64.encode it
encoded := server-config.to-json --der-serializer=: base64.encode it
encoded["scope"] = scope-string
encoded
result["recovery-urls"] = recovery-urls
return result

Expand Down
33 changes: 33 additions & 0 deletions tests/fleet-root-test.toit
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (C) 2023 Toitware ApS.

import encoding.json
import host.file
import host.os
import expect show *
Expand Down Expand Up @@ -35,3 +36,35 @@ run-test tester/Tester:
expect (file.is-file "$fleet-tmp-dir/fleet.json")
expect (file.is-file "$fleet-tmp-dir/devices.json")
expect (file.is-file "$fleet-tmp-dir/my-pod.yaml")

// Verify that the reader still understands the legacy fleet.json
// shape (no $schema, no scope inside server entries, organization at
// the top level). We initialize a new fleet, rewrite the file in the
// legacy shape, then exercise a command that has to parse it.
with-tmp-directory: | fleet-tmp-dir |
tester.run [
"fleet",
"--fleet-root", fleet-tmp-dir,
"init",
"--organization-id", "$TEST-ORGANIZATION-UUID",
]
fleet-path := "$fleet-tmp-dir/fleet.json"
new-format := json.decode (file.read-contents fleet-path)
legacy := new-format.copy
legacy.remove "\$schema"
broker-name := legacy["broker"]
legacy-servers := (legacy["servers"] as Map).copy
legacy-servers.do: | name encoded-server |
cleaned := (encoded-server as Map).copy
if name == broker-name:
legacy["organization"] = cleaned["scope"]
cleaned.remove "scope"
legacy-servers[name] = cleaned
legacy["servers"] = legacy-servers
file.write-contents --path=fleet-path (json.encode legacy)
tester.run [
"fleet",
"--fleet-root", fleet-tmp-dir,
"group",
"list",
]
Loading