fly_io 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +15 -0
  3. data/CONTRIBUTING.md +14 -0
  4. data/LICENSE +21 -0
  5. data/README.md +239 -0
  6. data/Rakefile +30 -0
  7. data/contracts/additional_rest/network_policies.json +94 -0
  8. data/contracts/additional_rest/prometheus.json +103 -0
  9. data/contracts/graphql/fly_go_operations.json +948 -0
  10. data/contracts/graphql/flyctl_named_operations.graphql +316 -0
  11. data/contracts/graphql/flyctl_operations.json +336 -0
  12. data/contracts/graphql/official_examples.json +75 -0
  13. data/contracts/graphql/schema.graphql +10995 -0
  14. data/contracts/graphql/source.json +29 -0
  15. data/contracts/machines/openapi.headers +9 -0
  16. data/contracts/machines/openapi.json +1 -0
  17. data/contracts/machines/source.json +20 -0
  18. data/contracts/public_surface_inventory.json +164 -0
  19. data/contracts/research_metadata.json +17 -0
  20. data/contracts/sources/metrics.html.md +474 -0
  21. data/contracts/sources/network-policies.html.markerb +142 -0
  22. data/docs/API.md +149 -0
  23. data/docs/SURFACES.md +60 -0
  24. data/lib/fly_io/client.rb +52 -0
  25. data/lib/fly_io/configuration.rb +115 -0
  26. data/lib/fly_io/errors.rb +45 -0
  27. data/lib/fly_io/generated/additional_operations.json +123 -0
  28. data/lib/fly_io/generated/graphql_operations.json +1658 -0
  29. data/lib/fly_io/generated/operations.json +6130 -0
  30. data/lib/fly_io/generated/prometheus_operations.json +503 -0
  31. data/lib/fly_io/generated/schemas.json +4237 -0
  32. data/lib/fly_io/graphql_client.rb +103 -0
  33. data/lib/fly_io/model.rb +65 -0
  34. data/lib/fly_io/operation_registry.rb +79 -0
  35. data/lib/fly_io/redactor.rb +34 -0
  36. data/lib/fly_io/resources/base.rb +68 -0
  37. data/lib/fly_io/resources.rb +30 -0
  38. data/lib/fly_io/response.rb +45 -0
  39. data/lib/fly_io/schema_registry.rb +89 -0
  40. data/lib/fly_io/schema_validator.rb +56 -0
  41. data/lib/fly_io/transport.rb +282 -0
  42. data/lib/fly_io/version.rb +5 -0
  43. data/lib/fly_io.rb +23 -0
  44. data/script/check_api_coverage +60 -0
  45. data/script/fetch_openapi +18 -0
  46. data/script/generate_api +302 -0
  47. metadata +197 -0
@@ -0,0 +1,142 @@
1
+ ---
2
+ title: Network Policies
3
+ layout: docs
4
+ order: 25
5
+ nav: machines
6
+ ---
7
+
8
+ Network policies let you control traffic to and from your Machines. You can use policies to allow or restrict ingress and egress on specific ports and protocols. This is useful when you're running untrusted code or want to lock down what traffic is allowed.
9
+
10
+ <div class="callout">
11
+ Network policies only apply to traffic directly to and from Machines. They do not affect traffic routed through the Fly Proxy.
12
+
13
+ </div>
14
+
15
+ ## How it works
16
+
17
+ A network policy contains:
18
+
19
+ * A `selector` to match which Machines the policy applies to.
20
+ * A list of `rules` for ingress or egress, specifying allowed ports and protocols.
21
+
22
+ Once you create a rule for a direction (ingress or egress), the default for that direction becomes "deny all." Only explicitly allowed traffic will be permitted.
23
+
24
+ ## Restricting egress traffic to HTTP/HTTPS
25
+
26
+ For example, if you're running an app that should only make outbound HTTP and HTTPS requests, you can create a policy like this:
27
+
28
+ ```sh
29
+ curl --location 'https://api.machines.dev/v1/apps/my-app-name/network_policies' \
30
+ --header 'Authorization: Bearer <FLY_API_TOKEN>' \
31
+ --header 'Content-Type: application/json' \
32
+ --data '{
33
+ "name": "specific-egress",
34
+ "selector": {
35
+ "all": true
36
+ },
37
+ "rules": [
38
+ {
39
+ "action": "allow",
40
+ "direction": "egress",
41
+ "ports": [
42
+ { "protocol": "tcp", "port": 80 },
43
+ { "protocol": "tcp", "port": 443 }
44
+ ]
45
+ }
46
+ ]
47
+ }'
48
+ ```
49
+
50
+ Replace `<FLY_API_TOKEN>` and `my-app-name` with your actual values.
51
+
52
+ ## Selectors
53
+
54
+ Selectors determine which Machines your network policy applies to. You can target Machines using one or more of the following methods:
55
+
56
+ ### All Machines in an app
57
+
58
+ To apply a policy to every Machine in your app:
59
+
60
+ ```json
61
+ { "all": true }
62
+ ```
63
+
64
+ ### Specific Machine IDs
65
+
66
+ To target individual Machines by their unique identifiers:
67
+
68
+ ```json
69
+ { "machines": [ { "id": "abc" }, { "id": "def" } ] }
70
+ ```
71
+
72
+ ### Metadata matching
73
+
74
+ To select Machines based on their metadata attributes:
75
+
76
+ ```json
77
+ { "metadata": { "role": "web", "env": "production" } }
78
+ ```
79
+ These selection methods can be combined to create more precise targeting rules that match Machines satisfying all specified criteria.
80
+
81
+ ## Rules
82
+
83
+ Each rule has:
84
+
85
+ * `action`: Only `allow` is supported.
86
+ * `direction`: Either `ingress` or `egress`.
87
+ * `ports`: A list of port and protocol objects.
88
+
89
+ Example:
90
+
91
+ ```json
92
+ {
93
+ "action": "allow",
94
+ "direction": "egress",
95
+ "ports": [
96
+ { "protocol": "tcp", "port": 443 }
97
+ ]
98
+ }
99
+ ```
100
+
101
+ Once a rule is defined, all other traffic in that direction is denied by default.
102
+
103
+ ## API summary
104
+
105
+ ### Create or update a policy
106
+
107
+ ```http
108
+ POST /v1/apps/<app_name>/network_policies
109
+ ```
110
+
111
+ Request body:
112
+
113
+ ```json
114
+ {
115
+ "name": "policy-name",
116
+ "id": "optional-policy-id",
117
+ "selector": { ... },
118
+ "rules": [ ... ]
119
+ }
120
+ ```
121
+ Include the `id` field to update an existing policy.
122
+
123
+ ### List policies
124
+
125
+ ```http
126
+ GET /v1/apps/<app_name>/network_policies/
127
+ ```
128
+
129
+ ### Delete a policy
130
+
131
+ ```http
132
+ DELETE /v1/apps/<app_name>/network_policies/<policy_id>
133
+ ```
134
+
135
+ ## Troubleshooting
136
+
137
+ * After creating or updating a policy, restart or redeploy the Machines for changes to take effect.
138
+ * Use direct IP addresses (not hostnames) to test blocked traffic to avoid DNS masking.
139
+ * Make sure your `selector` is correct and matches the Machines you expect.
140
+ * If traffic is still allowed unexpectedly, check if it's going through the Fly Proxy.
141
+
142
+
data/docs/API.md ADDED
@@ -0,0 +1,149 @@
1
+ # Generated Fly API reference
2
+
3
+ Machines metadata is generated from `https://docs.machines.dev/openapi.json` (SHA-256 `9e6a180c40a38bf20b72a8577a8cd93866f47882627b608af753cc4c22f7758f`).
4
+ This file lists 98 Machines OpenAPI operations, 3 Network Policies
5
+ documentation-derived operations, and 7 Prometheus endpoint families. GraphQL's
6
+ experimental generated-operation adapter is inventoried separately in `generated/graphql_operations.json`.
7
+ Do not edit this file by hand.
8
+
9
+ ## Apps
10
+
11
+ - `GET /v1/apps` → `client.apps.list` (`Apps_list`)
12
+ - `POST /v1/apps` → `client.apps.create` (`Apps_create`)
13
+ - `GET /v1/apps/{app_name}` → `client.apps.get` (`Apps_show`)
14
+ - `DELETE /v1/apps/{app_name}` → `client.apps.destroy` (`Apps_delete`)
15
+ - `POST /v1/apps/{app_name}/deploy_token` → `client.apps.create_deploy_token` (`App_create_deploy_token`)
16
+ - `GET /v1/apps/{app_name}/ip_assignments` → `client.apps.list_ip_assignments` (`App_IPAssignments_list`)
17
+ - `POST /v1/apps/{app_name}/ip_assignments` → `client.apps.create_ip_assignment` (`App_IPAssignments_create`)
18
+ - `DELETE /v1/apps/{app_name}/ip_assignments/{ip}` → `client.apps.delete_ip_assignment` (`App_IPAssignments_delete`)
19
+
20
+ ## Machines
21
+
22
+ - `GET /v1/apps/{app_name}/machines` → `client.machines.list` (`Machines_list`)
23
+ - `POST /v1/apps/{app_name}/machines` → `client.machines.create` (`Machines_create`)
24
+ - `GET /v1/apps/{app_name}/machines/{machine_id}` → `client.machines.get` (`Machines_show`)
25
+ - `POST /v1/apps/{app_name}/machines/{machine_id}` → `client.machines.update` (`Machines_update`)
26
+ - `DELETE /v1/apps/{app_name}/machines/{machine_id}` → `client.machines.destroy` (`Machines_delete`)
27
+ - `POST /v1/apps/{app_name}/machines/{machine_id}/cordon` → `client.machines.cordon` (`Machines_cordon`)
28
+ - `GET /v1/apps/{app_name}/machines/{machine_id}/events` → `client.machines.events` (`Machines_list_events`)
29
+ - `POST /v1/apps/{app_name}/machines/{machine_id}/exec` → `client.machines.execute` (`Machines_exec`)
30
+ - `GET /v1/apps/{app_name}/machines/{machine_id}/lease` → `client.machines.lease` (`Machines_show_lease`)
31
+ - `POST /v1/apps/{app_name}/machines/{machine_id}/lease` → `client.machines.create_lease` (`Machines_create_lease`)
32
+ - `DELETE /v1/apps/{app_name}/machines/{machine_id}/lease` → `client.machines.release_lease` (`Machines_release_lease`)
33
+ - `GET /v1/apps/{app_name}/machines/{machine_id}/memory` → `client.machines.memory` (`Machines_get_memory`)
34
+ - `PUT /v1/apps/{app_name}/machines/{machine_id}/memory` → `client.machines.set_memory_limit` (`Machines_set_memory_limit`)
35
+ - `POST /v1/apps/{app_name}/machines/{machine_id}/memory/reclaim` → `client.machines.reclaim_memory` (`Machines_reclaim_memory`)
36
+ - `GET /v1/apps/{app_name}/machines/{machine_id}/metadata` → `client.machines.metadata` (`Machines_show_metadata`)
37
+ - `PUT /v1/apps/{app_name}/machines/{machine_id}/metadata` → `client.machines.replace_metadata` (`Machines_update_metadata`)
38
+ - `PATCH /v1/apps/{app_name}/machines/{machine_id}/metadata` → `client.machines.update_metadata` (`Machines_update_metadata`)
39
+ - `GET /v1/apps/{app_name}/machines/{machine_id}/metadata/{key}` → `client.machines.get_metadata` (`Machines_get_metadata_key`)
40
+ - `POST /v1/apps/{app_name}/machines/{machine_id}/metadata/{key}` → `client.machines.set_metadata` (`Machines_upsert_metadata`)
41
+ - `DELETE /v1/apps/{app_name}/machines/{machine_id}/metadata/{key}` → `client.machines.delete_metadata` (`Machines_delete_metadata`)
42
+ - `GET /v1/apps/{app_name}/machines/{machine_id}/ps` → `client.machines.processes` (`Machines_list_processes`)
43
+ - `POST /v1/apps/{app_name}/machines/{machine_id}/restart` → `client.machines.restart` (`Machines_restart`)
44
+ - `POST /v1/apps/{app_name}/machines/{machine_id}/signal` → `client.machines.signal` (`Machines_signal`)
45
+ - `POST /v1/apps/{app_name}/machines/{machine_id}/start` → `client.machines.start` (`Machines_start`)
46
+ - `POST /v1/apps/{app_name}/machines/{machine_id}/stop` → `client.machines.stop` (`Machines_stop`)
47
+ - `POST /v1/apps/{app_name}/machines/{machine_id}/suspend` → `client.machines.suspend` (`Machines_suspend`)
48
+ - `POST /v1/apps/{app_name}/machines/{machine_id}/uncordon` → `client.machines.uncordon` (`Machines_uncordon`)
49
+ - `GET /v1/apps/{app_name}/machines/{machine_id}/versions` → `client.machines.versions` (`Machines_list_versions`)
50
+ - `GET /v1/apps/{app_name}/machines/{machine_id}/wait` → `client.machines.wait` (`Machines_wait`)
51
+
52
+ ## Metrics
53
+
54
+ - `GET /prometheus/{org_slug}/api/v1/query` → `client.metrics.query` (`Prometheus_query`)
55
+ - `GET /prometheus/{org_slug}/api/v1/query_range` → `client.metrics.query_range` (`Prometheus_query_range`)
56
+ - `GET /prometheus/{org_slug}/api/v1/series` → `client.metrics.series` (`Prometheus_series`)
57
+ - `GET /prometheus/{org_slug}/api/v1/labels` → `client.metrics.labels` (`Prometheus_labels`)
58
+ - `GET /prometheus/{org_slug}/api/v1/label/{label_name}/values` → `client.metrics.label_values` (`Prometheus_label_values`)
59
+ - `GET /prometheus/{org_slug}/api/v1/status/tsdb` → `client.metrics.tsdb` (`Prometheus_tsdb`)
60
+ - `GET /prometheus/{org_slug}/federate` → `client.metrics.federate` (`Prometheus_federate`)
61
+
62
+ ## Network Policies
63
+
64
+ - `POST /v1/apps/{app_name}/network_policies` → `client.network_policies.upsert` (`NetworkPolicies_upsert`)
65
+ - `GET /v1/apps/{app_name}/network_policies/` → `client.network_policies.list` (`NetworkPolicies_list`)
66
+ - `DELETE /v1/apps/{app_name}/network_policies/{policy_id}` → `client.network_policies.delete` (`NetworkPolicies_delete`)
67
+
68
+ ## Organizations
69
+
70
+ - `GET /v1/orgs/{org_slug}/machines` → `client.organizations.list_machines` (`Machines_org_list`)
71
+ - `GET /v1/orgs/{org_slug}/volumes` → `client.organizations.list_volumes` (`Volumes_org_list`)
72
+
73
+ ## Platform
74
+
75
+ - `POST /v1/platform/placements` → `client.platform.placements` (`Platform_placements_post`)
76
+ - `GET /v1/platform/regions` → `client.platform.regions` (`Platform_regions_get`)
77
+
78
+ ## Postgres Clusters
79
+
80
+ - `GET /v1/postgres` → `client.postgres_clusters.list` (`Postgres_list`)
81
+ - `POST /v1/postgres` → `client.postgres_clusters.create` (`Postgres_create`)
82
+ - `GET /v1/postgres/{postgres_cluster_id}` → `client.postgres_clusters.show` (`Postgres_show`)
83
+ - `DELETE /v1/postgres/{postgres_cluster_id}` → `client.postgres_clusters.delete` (`Postgres_delete`)
84
+ - `POST /v1/postgres/{postgres_cluster_id}/attachments` → `client.postgres_clusters.attachments_create` (`Postgres_attachments_create`)
85
+ - `DELETE /v1/postgres/{postgres_cluster_id}/attachments/{app_name}` → `client.postgres_clusters.attachments_delete` (`Postgres_attachments_delete`)
86
+ - `GET /v1/postgres/{postgres_cluster_id}/backups` → `client.postgres_clusters.backups_list` (`Postgres_backups_list`)
87
+ - `POST /v1/postgres/{postgres_cluster_id}/backups` → `client.postgres_clusters.backups_create` (`Postgres_backups_create`)
88
+ - `GET /v1/postgres/{postgres_cluster_id}/databases` → `client.postgres_clusters.databases_list` (`Postgres_databases_list`)
89
+ - `POST /v1/postgres/{postgres_cluster_id}/databases` → `client.postgres_clusters.databases_create` (`Postgres_databases_create`)
90
+ - `DELETE /v1/postgres/{postgres_cluster_id}/databases/{database_name}` → `client.postgres_clusters.databases_delete` (`Postgres_databases_delete`)
91
+ - `GET /v1/postgres/{postgres_cluster_id}/databases/{database_name}/extensions` → `client.postgres_clusters.extensions_list` (`Postgres_extensions_list`)
92
+ - `POST /v1/postgres/{postgres_cluster_id}/databases/{database_name}/extensions` → `client.postgres_clusters.extensions_enable` (`Postgres_extensions_enable`)
93
+ - `DELETE /v1/postgres/{postgres_cluster_id}/databases/{database_name}/extensions/{extension_name}` → `client.postgres_clusters.extensions_disable` (`Postgres_extensions_disable`)
94
+ - `POST /v1/postgres/{postgres_cluster_id}/fork` → `client.postgres_clusters.fork` (`Postgres_fork`)
95
+ - `POST /v1/postgres/{postgres_cluster_id}/restore` → `client.postgres_clusters.restore` (`Postgres_restore`)
96
+ - `GET /v1/postgres/{postgres_cluster_id}/users` → `client.postgres_clusters.users_list` (`Postgres_users_list`)
97
+ - `POST /v1/postgres/{postgres_cluster_id}/users` → `client.postgres_clusters.users_create` (`Postgres_users_create`)
98
+ - `DELETE /v1/postgres/{postgres_cluster_id}/users/{username}` → `client.postgres_clusters.users_delete` (`Postgres_users_delete`)
99
+ - `PATCH /v1/postgres/{postgres_cluster_id}/users/{username}` → `client.postgres_clusters.users_update_role` (`Postgres_users_update_role`)
100
+ - `GET /v1/postgres/{postgres_cluster_id}/users/{username}/credentials` → `client.postgres_clusters.users_credentials` (`Postgres_users_credentials`)
101
+ - `POST /v1/postgres/{postgres_cluster_id}/users/{username}/rotate_password` → `client.postgres_clusters.users_rotate_password` (`Postgres_users_rotate_password`)
102
+
103
+ ## Secrets
104
+
105
+ - `GET /v1/apps/{app_name}/secretkeys` → `client.secrets.list_keys` (`Secretkeys_list`)
106
+ - `GET /v1/apps/{app_name}/secretkeys/{secret_name}` → `client.secrets.get_key` (`Secretkey_get`)
107
+ - `POST /v1/apps/{app_name}/secretkeys/{secret_name}` → `client.secrets.set_key` (`Secretkey_set`)
108
+ - `DELETE /v1/apps/{app_name}/secretkeys/{secret_name}` → `client.secrets.delete_key` (`Secretkey_delete`)
109
+ - `POST /v1/apps/{app_name}/secretkeys/{secret_name}/decrypt` → `client.secrets.decrypt_key` (`Secretkey_decrypt`)
110
+ - `POST /v1/apps/{app_name}/secretkeys/{secret_name}/encrypt` → `client.secrets.encrypt_key` (`Secretkey_encrypt`)
111
+ - `POST /v1/apps/{app_name}/secretkeys/{secret_name}/generate` → `client.secrets.generate_key` (`Secretkey_generate`)
112
+ - `POST /v1/apps/{app_name}/secretkeys/{secret_name}/sign` → `client.secrets.sign_key` (`Secretkey_sign`)
113
+ - `POST /v1/apps/{app_name}/secretkeys/{secret_name}/verify` → `client.secrets.verify_key` (`Secretkey_verify`)
114
+ - `GET /v1/apps/{app_name}/secrets` → `client.secrets.list` (`Secrets_list`)
115
+ - `POST /v1/apps/{app_name}/secrets` → `client.secrets.update` (`Secrets_update`)
116
+ - `GET /v1/apps/{app_name}/secrets/{secret_name}` → `client.secrets.get` (`Secret_get`)
117
+ - `POST /v1/apps/{app_name}/secrets/{secret_name}` → `client.secrets.set` (`Secret_create`)
118
+ - `DELETE /v1/apps/{app_name}/secrets/{secret_name}` → `client.secrets.delete` (`Secret_delete`)
119
+
120
+ ## TLS Certificates
121
+
122
+ - `GET /v1/apps/{app_name}/certificates` → `client.certificates.list` (`App_Certificates_list`)
123
+ - `POST /v1/apps/{app_name}/certificates/acme` → `client.certificates.request_acme` (`App_Certificates_acme_create`)
124
+ - `POST /v1/apps/{app_name}/certificates/custom` → `client.certificates.upload_custom` (`App_Certificates_custom_create`)
125
+ - `GET /v1/apps/{app_name}/certificates/{hostname}` → `client.certificates.get` (`App_Certificates_show`)
126
+ - `DELETE /v1/apps/{app_name}/certificates/{hostname}` → `client.certificates.remove` (`App_Certificates_delete`)
127
+ - `DELETE /v1/apps/{app_name}/certificates/{hostname}/acme` → `client.certificates.remove_acme` (`App_Certificates_acme_delete`)
128
+ - `POST /v1/apps/{app_name}/certificates/{hostname}/check` → `client.certificates.check` (`App_Certificates_check`)
129
+ - `DELETE /v1/apps/{app_name}/certificates/{hostname}/custom` → `client.certificates.remove_custom` (`App_Certificates_custom_delete`)
130
+
131
+ ## Tokens
132
+
133
+ - `POST /v1/tokens/authenticate` → `client.tokens.authenticate` (`Tokens_authenticate`)
134
+ - `POST /v1/tokens/authorize` → `client.tokens.authorize` (`Tokens_authorize`)
135
+ - `GET /v1/tokens/current` → `client.tokens.current` (`CurrentToken_show`)
136
+ - `POST /v1/tokens/kms` → `client.tokens.request_kms` (`Tokens_request_Kms`)
137
+ - `POST /v1/tokens/oidc` → `client.tokens.request_oidc` (`Tokens_request_OIDC`)
138
+
139
+ ## Volumes
140
+
141
+ - `GET /v1/apps/{app_name}/volumes` → `client.volumes.list` (`Volumes_list`)
142
+ - `POST /v1/apps/{app_name}/volumes` → `client.volumes.create` (`Volumes_create`)
143
+ - `GET /v1/apps/{app_name}/volumes/{volume_id}` → `client.volumes.get` (`Volumes_get_by_id`)
144
+ - `PUT /v1/apps/{app_name}/volumes/{volume_id}` → `client.volumes.update` (`Volumes_update`)
145
+ - `DELETE /v1/apps/{app_name}/volumes/{volume_id}` → `client.volumes.destroy` (`Volume_delete`)
146
+ - `PUT /v1/apps/{app_name}/volumes/{volume_id}/extend` → `client.volumes.extend` (`Volumes_extend`)
147
+ - `GET /v1/apps/{app_name}/volumes/{volume_id}/snapshots` → `client.volumes.list_snapshots` (`Volumes_list_snapshots`)
148
+ - `POST /v1/apps/{app_name}/volumes/{volume_id}/snapshots` → `client.volumes.create_snapshot` (`createVolumeSnapshot`)
149
+
data/docs/SURFACES.md ADDED
@@ -0,0 +1,60 @@
1
+ # Fly.io public API surface inventory
2
+
3
+ Retrieved 2026-08-26. The machine-readable source is
4
+ [`contracts/public_surface_inventory.json`](../contracts/public_surface_inventory.json).
5
+
6
+ ## Supported surfaces
7
+
8
+ | Surface | Base URL | Stability | Authoritative contract/evidence | Count | Ruby coverage |
9
+ |---|---|---|---|---:|---|
10
+ | Machines REST | `https://api.machines.dev` | Public documented | OpenAPI 3.0.1, `https://docs.machines.dev/openapi.json`, SHA-256 `9e6a180c40a38bf20b72a8577a8cd93866f47882627b608af753cc4c22f7758f` | 68 paths / 98 operations / 173 schemas | 98 generated resource methods and operation-ID adapter; 173 typed models |
11
+ | Network Policies REST | `https://api.machines.dev` | Public documented; no formal stability label | Official docs revision `1e8cac5f0fd7a82e66e3b01ece3a10a8ecb2b7e3`, source SHA-256 `fa64b69db56beb1da2de60e3ff91820b628a1238a120d103c2f3bae16e2fcc1f` | 3 operations | `client.network_policies`; responses raw because upstream documents none |
12
+ | Control-plane GraphQL | `https://api.fly.io/graphql` | **Internal/experimental; explicitly no stability guarantee** | flyctl revision `1f906765dee8e973f6d1dd8fff65b58212e7b425`; evidence schema SHA-256 `2f1a1d0f2f8940277e5e14002c78cb4f6404b3ea1ce9d0999d4d3b65c52f9480` | 39 query + 141 mutation schema root fields; 29 flyctl + 85 fly-go + 3 official observed documents | Raw first-class `GraphQLClient`; 117-operation experimental generated adapter; opt-in introspection |
13
+ | Prometheus metrics | `https://api.fly.io/prometheus/{org_slug}/` | Stable standard data API with qualified compatibility; not control-plane | Official docs revision `1e8cac5f0fd7a82e66e3b01ece3a10a8ecb2b7e3`, source SHA-256 `e22d7b34406b16c2ea75eed6cd92c51ec07d166963155623557295ef24f67e9f`; Prometheus/VictoriaMetrics contracts | 7 endpoint families | `client.metrics` typed response envelope and federation text |
14
+
15
+ Machines families are Apps (8), TLS Certificates (8), Machines (29), Secrets (14), Volumes (8), Organizations (2),
16
+ Platform (2), Postgres Clusters (22), and Tokens (5). Network Policies adds its own family outside OpenAPI.
17
+
18
+ ## Authentication decision
19
+
20
+ The official Machines guide specifies `Bearer`; the generated reference says `FlyV1` in prose but uses `Bearer` in
21
+ its curl example. flyctl selects schemes by token kind. The gem therefore maps raw tokens to `Bearer` and accepts an
22
+ explicit preformatted authorization value or explicit `:fly_v1` mode. It never guesses from token contents.
23
+
24
+ ## Audited exclusions
25
+
26
+ | Surface | Decision and reason |
27
+ |---|---|
28
+ | Extensions partner OAuth/webhooks | Partner-gated, prose-only integration requiring Fly-issued credentials; not a general account control-plane API. The audit records 4 prose operations but the gem does not imply public availability. |
29
+ | flyctl UIEX REST (`https://api.fly.io/api/v1`) | 52 current internal call sites, with no official public documentation or schema. Excluded rather than presenting observed implementation details as stable. |
30
+ | Tigris S3 | Adjacent beta data plane governed by the external S3 contract and separate credentials. |
31
+ | `registry.fly.io` | Adjacent image data plane accessed through Docker/OCI tooling and its external distribution contract. |
32
+
33
+ ## Known upstream gaps and follow-up checks
34
+
35
+ - `Machines_update_metadata` is duplicated for PUT and PATCH. Coverage identity is method + path + operation ID.
36
+ - OpenAPI has no security scheme and omits several used tags from its top-level declaration.
37
+ - Network Policies is absent from OpenAPI; its guide gives no response status/schema and shows a trailing slash only
38
+ for list. The committed documentation snapshot makes this ambiguity reviewable.
39
+ - Fly’s GraphQL schema is manually copied into flyctl from a private repository. It is evidence, never a stable
40
+ contract. Refresh all three GraphQL inventories when flyctl or fly-go revisions change.
41
+ - Fly describes Prometheus support as “most common” compatible endpoints; `api:check` asserts the seven listed
42
+ families, while semantics stay delegated to Prometheus/VictoriaMetrics.
43
+
44
+ Run `bundle exec rake api:check` offline for all committed contracts and `bundle exec rake api:fetch_check` to compare
45
+ the Machines snapshot with the live OpenAPI URL.
46
+
47
+ ## Authoritative starting links
48
+
49
+ - [Machines API overview](https://fly.io/docs/machines/api/)
50
+ - [Machines OpenAPI JSON](https://docs.machines.dev/openapi.json)
51
+ - [Generated Machines reference](https://docs.machines.dev/)
52
+ - [Connection, authentication, statuses, and rate limits](https://fly.io/docs/machines/api/working-with-machines-api)
53
+ - [Apps resource](https://fly.io/docs/machines/api/apps-resource)
54
+ - [Machines resource](https://fly.io/docs/machines/api/machines-resource)
55
+ - [Volumes resource](https://fly.io/docs/machines/api/volumes-resource)
56
+ - [Certificates resource](https://fly.io/docs/machines/api/certificates-resource)
57
+ - [Tokens resource](https://fly.io/docs/machines/api/tokens-resource)
58
+ - [REST and GraphQL stability guidance](https://fly.io/docs/machines/guides-examples/managing-machines-with-the-api)
59
+ - [Network Policies](https://fly.io/docs/machines/guides-examples/network-policies)
60
+ - [Prometheus metrics](https://fly.io/docs/monitoring/metrics)
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlyIO
4
+ class Client
5
+ RESOURCE_NAMES = %w[apps certificates machines secrets volumes organizations platform postgres_clusters tokens
6
+ network_policies metrics].freeze
7
+
8
+ attr_reader :configuration, :transport
9
+
10
+ def initialize(configuration = nil, **options)
11
+ if configuration && !options.empty?
12
+ raise ConfigurationError, "provide a Configuration or keyword options, not both"
13
+ end
14
+
15
+ @configuration = configuration || Configuration.new(**options)
16
+ unless @configuration.is_a?(Configuration)
17
+ raise ConfigurationError,
18
+ "configuration must be a FlyIO::Configuration"
19
+ end
20
+
21
+ @transport = Transport.new(@configuration)
22
+ @metrics_transport = Transport.new(@configuration.with(base_url: @configuration.metrics_url))
23
+ @resources = RESOURCE_NAMES.to_h do |name|
24
+ [name, Resources.class_for(name).new(self)]
25
+ end.freeze
26
+ freeze
27
+ end
28
+
29
+ RESOURCE_NAMES.each do |name|
30
+ define_method(name) { @resources.fetch(name) }
31
+ end
32
+
33
+ def graphql
34
+ GraphQLClient.new(configuration)
35
+ end
36
+
37
+ def transport_for(operation)
38
+ operation.surface == "prometheus_metrics" ? @metrics_transport : transport
39
+ end
40
+
41
+ def request(method:, path:, path_params: {}, query: {}, headers: {}, body: FlyIO::UNSET,
42
+ content_type: "application/json", accept: "application/json", retry_unsafe: nil, timeout: nil)
43
+ transport.request(method: method, path: path, path_params: path_params, query: query, headers: headers,
44
+ body: body, content_type: content_type, accept: accept, retry_unsafe: retry_unsafe, timeout: timeout)
45
+ end
46
+
47
+ def operation(operation_id, method: nil, path: nil, **arguments)
48
+ operation = OperationRegistry.fetch(operation_id, method: method, path: path)
49
+ @resources.fetch(operation.ruby_resource).operation(operation_id, method: method, path: path, **arguments)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module FlyIO
6
+ class Configuration
7
+ DEFAULT_BASE_URL = "https://api.machines.dev"
8
+ DEFAULT_GRAPHQL_URL = "https://api.fly.io/graphql"
9
+
10
+ ATTRIBUTES = %i[
11
+ base_url graphql_url metrics_url authorization_value open_timeout read_timeout write_timeout request_timeout
12
+ proxy user_agent max_retries base_retry_interval max_retry_interval retry_jitter logger adapter
13
+ sleeper random retry_unsafe introspection
14
+ ].freeze
15
+
16
+ attr_reader(*ATTRIBUTES)
17
+
18
+ def initialize(token: nil, authorization: nil, authorization_mode: :bearer,
19
+ base_url: DEFAULT_BASE_URL, graphql_url: DEFAULT_GRAPHQL_URL, metrics_url: "https://api.fly.io",
20
+ open_timeout: 10, read_timeout: 30, write_timeout: 30, request_timeout: nil,
21
+ proxy: nil, user_agent: "fly_io/#{FlyIO::VERSION}", max_retries: 2,
22
+ base_retry_interval: 0.25, max_retry_interval: 5.0, retry_jitter: 0.25,
23
+ logger: nil, adapter: nil, sleeper: ->(seconds) { sleep(seconds) },
24
+ random: Random.new, retry_unsafe: false, introspection: false)
25
+ @base_url = normalize_url(base_url, "base_url")
26
+ @graphql_url = normalize_url(graphql_url, "graphql_url")
27
+ @metrics_url = normalize_url(metrics_url, "metrics_url")
28
+ @authorization_value = build_authorization(token, authorization, authorization_mode)
29
+ @open_timeout = positive_number(open_timeout, "open_timeout")
30
+ @read_timeout = positive_number(read_timeout, "read_timeout")
31
+ @write_timeout = positive_number(write_timeout, "write_timeout")
32
+ @request_timeout = request_timeout && positive_number(request_timeout, "request_timeout")
33
+ @proxy = proxy
34
+ @user_agent = String(user_agent).dup.freeze
35
+ @max_retries = Integer(max_retries)
36
+ @base_retry_interval = Float(base_retry_interval)
37
+ @max_retry_interval = Float(max_retry_interval)
38
+ @retry_jitter = Float(retry_jitter)
39
+ @logger = logger
40
+ @adapter = adapter
41
+ @sleeper = sleeper
42
+ @random = random
43
+ @retry_unsafe = retry_unsafe
44
+ @introspection = introspection
45
+ validate_retry!
46
+ freeze
47
+ end
48
+
49
+ def with(**overrides)
50
+ current = ATTRIBUTES.to_h { |name| [name, public_send(name)] }
51
+ authorization_value = current.delete(:authorization_value)
52
+ current[:authorization] = authorization_value
53
+ self.class.new(**current, **overrides)
54
+ end
55
+
56
+ private
57
+
58
+ def build_authorization(token, authorization, mode)
59
+ if token && authorization
60
+ raise ConfigurationError, "provide token or authorization, not both"
61
+ end
62
+
63
+ if authorization
64
+ value = String(authorization).strip
65
+ raise ConfigurationError, "authorization must include a scheme and value" unless value.match?(/\A\S+\s+\S+/)
66
+
67
+ return value.freeze
68
+ end
69
+
70
+ raw = String(token || "").strip
71
+ raise ConfigurationError, "token is required" if raw.empty?
72
+ if raw.match?(/\A(?:Bearer|FlyV1)\s+/i)
73
+ raise ConfigurationError,
74
+ "token must be raw; use authorization: for a preformatted value"
75
+ end
76
+
77
+ scheme = {bearer: "Bearer", fly_v1: "FlyV1"}.fetch(mode.to_sym) do
78
+ raise ConfigurationError, "authorization_mode must be :bearer or :fly_v1"
79
+ end
80
+ "#{scheme} #{raw}".freeze
81
+ end
82
+
83
+ def normalize_url(value, name)
84
+ uri = URI.parse(String(value))
85
+ raise ConfigurationError, "#{name} must be an absolute HTTP(S) URL" unless %w[http
86
+ https].include?(uri.scheme) && uri.host
87
+ if uri.userinfo || uri.query || uri.fragment
88
+ raise ConfigurationError,
89
+ "#{name} must not contain credentials, query, or fragment"
90
+ end
91
+
92
+ uri.path = uri.path.sub(%r{/+\z}, "")
93
+ uri.to_s.freeze
94
+ rescue URI::InvalidURIError
95
+ raise ConfigurationError, "#{name} must be a valid URL"
96
+ end
97
+
98
+ def positive_number(value, name)
99
+ number = Float(value)
100
+ raise ConfigurationError, "#{name} must be positive" unless number.positive?
101
+
102
+ number
103
+ end
104
+
105
+ def validate_retry!
106
+ raise ConfigurationError, "max_retries must be non-negative" if max_retries.negative?
107
+ raise ConfigurationError, "retry intervals and jitter must be non-negative" if [base_retry_interval,
108
+ max_retry_interval, retry_jitter].any?(&:negative?)
109
+ return unless max_retry_interval < base_retry_interval
110
+
111
+ raise ConfigurationError,
112
+ "max_retry_interval must be at least base_retry_interval"
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlyIO
4
+ class Error < StandardError; end
5
+ class ConfigurationError < Error; end
6
+ class ArgumentError < Error; end
7
+
8
+ class APIError < Error
9
+ attr_reader :status, :headers, :details, :request_id, :request
10
+
11
+ def initialize(message = nil, status: nil, headers: {}, details: nil, request_id: nil, request: nil)
12
+ @status = status
13
+ @headers = headers.freeze
14
+ @details = details
15
+ @request_id = request_id
16
+ @request = request
17
+ super(Redactor.redact(message || default_message))
18
+ end
19
+
20
+ private
21
+
22
+ def default_message
23
+ ["Fly.io API request failed", ("(HTTP #{status})" if status),
24
+ ("request_id=#{request_id}" if request_id)].compact.join(" ")
25
+ end
26
+ end
27
+
28
+ class AuthenticationError < APIError; end
29
+ class AuthorizationError < APIError; end
30
+ class NotFoundError < APIError; end
31
+ class ValidationError < APIError; end
32
+ class RateLimitError < APIError; end
33
+ class RequestTimeoutError < APIError; end
34
+ class ServerError < APIError; end
35
+ class TransportError < APIError; end
36
+
37
+ class GraphQLError < APIError
38
+ attr_reader :graphql_errors
39
+
40
+ def initialize(message = "Fly.io GraphQL request failed", graphql_errors: [], **)
41
+ @graphql_errors = Redactor.redact_object(graphql_errors)
42
+ super(message, details: @graphql_errors, **)
43
+ end
44
+ end
45
+ end