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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +15 -0
- data/CONTRIBUTING.md +14 -0
- data/LICENSE +21 -0
- data/README.md +239 -0
- data/Rakefile +30 -0
- data/contracts/additional_rest/network_policies.json +94 -0
- data/contracts/additional_rest/prometheus.json +103 -0
- data/contracts/graphql/fly_go_operations.json +948 -0
- data/contracts/graphql/flyctl_named_operations.graphql +316 -0
- data/contracts/graphql/flyctl_operations.json +336 -0
- data/contracts/graphql/official_examples.json +75 -0
- data/contracts/graphql/schema.graphql +10995 -0
- data/contracts/graphql/source.json +29 -0
- data/contracts/machines/openapi.headers +9 -0
- data/contracts/machines/openapi.json +1 -0
- data/contracts/machines/source.json +20 -0
- data/contracts/public_surface_inventory.json +164 -0
- data/contracts/research_metadata.json +17 -0
- data/contracts/sources/metrics.html.md +474 -0
- data/contracts/sources/network-policies.html.markerb +142 -0
- data/docs/API.md +149 -0
- data/docs/SURFACES.md +60 -0
- data/lib/fly_io/client.rb +52 -0
- data/lib/fly_io/configuration.rb +115 -0
- data/lib/fly_io/errors.rb +45 -0
- data/lib/fly_io/generated/additional_operations.json +123 -0
- data/lib/fly_io/generated/graphql_operations.json +1658 -0
- data/lib/fly_io/generated/operations.json +6130 -0
- data/lib/fly_io/generated/prometheus_operations.json +503 -0
- data/lib/fly_io/generated/schemas.json +4237 -0
- data/lib/fly_io/graphql_client.rb +103 -0
- data/lib/fly_io/model.rb +65 -0
- data/lib/fly_io/operation_registry.rb +79 -0
- data/lib/fly_io/redactor.rb +34 -0
- data/lib/fly_io/resources/base.rb +68 -0
- data/lib/fly_io/resources.rb +30 -0
- data/lib/fly_io/response.rb +45 -0
- data/lib/fly_io/schema_registry.rb +89 -0
- data/lib/fly_io/schema_validator.rb +56 -0
- data/lib/fly_io/transport.rb +282 -0
- data/lib/fly_io/version.rb +5 -0
- data/lib/fly_io.rb +23 -0
- data/script/check_api_coverage +60 -0
- data/script/fetch_openapi +18 -0
- data/script/generate_api +302 -0
- 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
|