From e7a8a921c0d56f5dce80dcb98d4b246e780672a6 Mon Sep 17 00:00:00 2001 From: Richard Kosegi Date: Tue, 12 Aug 2025 18:55:44 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20minimal=20support=20for=20fir?= =?UTF-8?q?ewalls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change adds support to get firewall by ID or to list firewalls by selector Downstream issue: https://github.com/jenkinsci/hetzner-cloud-plugin/issues/97 Signed-off-by: Richard Kosegi --- .../dnation/hetznerclient/HetznerApi.java | 20 +++ src/main/resources/api.yaml | 78 +++++++++- .../dnation/hetznerclient/BasicTest.java | 34 +++++ .../resources/get-firewall-by-id-invalid.json | 7 + src/test/resources/get-firewall-by-id.json | 86 +++++++++++ .../resources/get-firewalls-by-selector.json | 140 ++++++++++++++++++ 6 files changed, 364 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/get-firewall-by-id-invalid.json create mode 100644 src/test/resources/get-firewall-by-id.json create mode 100644 src/test/resources/get-firewalls-by-selector.json diff --git a/src/main/java/cloud/dnation/hetznerclient/HetznerApi.java b/src/main/java/cloud/dnation/hetznerclient/HetznerApi.java index cb67122..233d271 100644 --- a/src/main/java/cloud/dnation/hetznerclient/HetznerApi.java +++ b/src/main/java/cloud/dnation/hetznerclient/HetznerApi.java @@ -182,6 +182,26 @@ Call getServersBySelector(@Query("label_selector") @GET("/v1/networks/{id}") Call getNetworkById(@Path("id") long id); + /** + * Get all firewalls matching given label selector. + * + * @param selector label selector used to match firewalls + * @return list of firewalls + * see API reference + */ + @GET("/v1/firewalls") + Call getFirewallsBySelector(@Query("label_selector") String selector); + + /** + * Get single firewall by ID. + * + * @param id firewall ID + * @return firewall detail + * see API reference + */ + @GET("/v1/firewalls/{id}") + Call getFirewallById(@Path("id") long id); + /** * Get placement groups, optionally filtered using label expression. * diff --git a/src/main/resources/api.yaml b/src/main/resources/api.yaml index 1f28370..8e8567c 100644 --- a/src/main/resources/api.yaml +++ b/src/main/resources/api.yaml @@ -127,6 +127,16 @@ components: $ref: "#/components/schemas/NetworkDetail" type: object + GetFirewallsBySelectorResponse: + allOf: + - $ref: "#/components/schemas/AbstractSearchResponse" + properties: + firewalls: + type: array + items: + $ref: "#/components/schemas/FirewallDetail" + type: object + GetServersBySelectorResponse: allOf: - $ref: "#/components/schemas/AbstractSearchResponse" @@ -205,6 +215,12 @@ components: $ref: "#/components/schemas/NetworkDetail" type: object + GetFirewallByIdResponse: + properties: + firewall: + $ref: "#/components/schemas/FirewallDetail" + type: object + GetPlacementGroupByIdResponse: properties: placement_group: @@ -236,7 +252,10 @@ components: ssh_key: $ref: "#/components/schemas/SshKeyDetail" type: object - + CreateServerFirewallsRequest: + properties: + firewall: + $ref: "#/components/schemas/Identifier" CreateServerRequest: properties: automount: @@ -247,6 +266,11 @@ components: not be used together with location) example: nbg1-dc3 type: string + firewalls: + description: Firewalls which should be applied on the Server's public network interface at creation time. + type: array + items: + $ref: "#/components/schemas/CreateServerFirewallsRequest" image: description: ID or name of the Image the Server is created from example: ubuntu-20.04 @@ -685,6 +709,58 @@ components: example: false type: boolean type: object + FirewallRule: + properties: + description: + description: Description of the rule. + type: string + direction: + description: Traffic direction in which the rule should be applied to. + type: string + protocol: + description: Network protocol to apply the rule for. + type: string + port: + description: Port or port range to apply the rule for. + type: string + destination_ips: + description: List of permitted IPv4/IPv6 addresses for outgoing traffic. + type: array + items: + type: string + source_ips: + description: List of permitted IPv4/IPv6 addresses for incoming traffic. + type: array + items: + type: string + FirewallAppliedToDetail: + properties: + type: + type: string + FirewallDetail: + allOf: + - $ref: "#/components/schemas/IdentifiableResource" + properties: + created: + description: Point in time when the Resource was created (in + ISO-8601 format) + example: '2016-01-30T23:55:00+00:00' + type: string + name: + type: string + description: Name of the Firewall. Must be unique per Project. + rules: + type: array + items: + $ref: "#/components/schemas/FirewallRule" + applied_to: + type: array + items: + $ref: "#/components/schemas/FirewallAppliedToDetail" + labels: + description: User-defined labels (key-value pairs) + $ref: "#/components/schemas/Labeled" + VolumeDetail: allOf: - $ref: "#/components/schemas/IdentifiableResource" diff --git a/src/test/java/cloud/dnation/hetznerclient/BasicTest.java b/src/test/java/cloud/dnation/hetznerclient/BasicTest.java index cbe84e8..aa54411 100644 --- a/src/test/java/cloud/dnation/hetznerclient/BasicTest.java +++ b/src/test/java/cloud/dnation/hetznerclient/BasicTest.java @@ -102,4 +102,38 @@ public void testGetPrimaryIpsBySelector() throws IOException { assertEquals("1.2.3.4", result.getPrimaryIps().get(0).getIp()); assertNull(result.getPrimaryIps().get(0).getAssigneeId()); } + + @Test + public void testGetFirewallBySelector() throws IOException { + ws.enqueue(new MockResponse().setBody(resourceAsString("get-firewalls-by-selector.json"))); + final Call call = api.getFirewallsBySelector("any"); + final GetFirewallsBySelectorResponse result = call.execute().body(); + assertEquals(3029857349L, (long) result.getFirewalls().get(0).getId()); + assertEquals("::/0", result.getFirewalls().get(0).getRules().get(1).getSourceIps().get(1)); + assertEquals("in", result.getFirewalls().get(0).getRules().get(1).getDirection()); + assertEquals("22", result.getFirewalls().get(0).getRules().get(0).getPort()); + } + + @Test + public void testGetFirewallById() throws IOException { + ws.enqueue(new MockResponse().setBody(resourceAsString("get-firewall-by-id.json"))); + final Call call = api.getFirewallById(345676L); + final GetFirewallByIdResponse result = call.execute().body(); + assertEquals(345676, (long) result.getFirewall().getId()); + assertEquals("::/0", result.getFirewall().getRules().get(1).getSourceIps().get(1)); + assertEquals("in", result.getFirewall().getRules().get(1).getDirection()); + assertEquals("22", result.getFirewall().getRules().get(0).getPort()); + } + + + @Test + public void testGetFirewallByIdInvalid() throws IOException { + ws.enqueue(new MockResponse() + .setBody(resourceAsString("get-firewall-by-id-invalid.json")) + .setResponseCode(404) + ); + Call call = api.getNetworkById(11); + Response response = call.execute(); + assertEquals(404, response.code()); + } } diff --git a/src/test/resources/get-firewall-by-id-invalid.json b/src/test/resources/get-firewall-by-id-invalid.json new file mode 100644 index 0000000..77be85a --- /dev/null +++ b/src/test/resources/get-firewall-by-id-invalid.json @@ -0,0 +1,7 @@ +{ + "error": { + "message": "firewall with ID XYZ not found", + "code": "not_found", + "details": null + } +} diff --git a/src/test/resources/get-firewall-by-id.json b/src/test/resources/get-firewall-by-id.json new file mode 100644 index 0000000..512b116 --- /dev/null +++ b/src/test/resources/get-firewall-by-id.json @@ -0,0 +1,86 @@ +{ + "firewall": { + "id": 345676, + "name": "firewall-worker", + "labels": {}, + "created": "2022-11-23T12:36:58+00:00", + "rules": [ + { + "direction": "in", + "protocol": "tcp", + "port": "22", + "source_ips": [ + "0.0.0.0/0", + "::/0" + ], + "destination_ips": [], + "description": null + }, + { + "direction": "in", + "protocol": "icmp", + "port": null, + "source_ips": [ + "0.0.0.0/0", + "::/0" + ], + "destination_ips": [], + "description": null + }, + { + "direction": "in", + "protocol": "tcp", + "port": "80", + "source_ips": [ + "0.0.0.0/0", + "::/0" + ], + "destination_ips": [], + "description": null + }, + { + "direction": "in", + "protocol": "tcp", + "port": "9090", + "source_ips": [ + "0.0.0.0/0", + "::/0" + ], + "destination_ips": [], + "description": null + }, + { + "direction": "in", + "protocol": "tcp", + "port": "443", + "source_ips": [ + "0.0.0.0/0", + "::/0" + ], + "destination_ips": [], + "description": null + }, + { + "direction": "in", + "protocol": "tcp", + "port": "any", + "source_ips": [ + "10.1.0.0/16" + ], + "destination_ips": [], + "description": null + }, + { + "direction": "in", + "protocol": "udp", + "port": "any", + "source_ips": [ + "10.1.0.0/16" + ], + "destination_ips": [], + "description": null + } + ], + "applied_to": [] + } +} diff --git a/src/test/resources/get-firewalls-by-selector.json b/src/test/resources/get-firewalls-by-selector.json new file mode 100644 index 0000000..ec242f3 --- /dev/null +++ b/src/test/resources/get-firewalls-by-selector.json @@ -0,0 +1,140 @@ +{ + "firewalls": [ + { + "id": 3029857349, + "name": "firewall-control-plane", + "labels": {}, + "created": "2022-11-23T12:32:55+00:00", + "rules": [ + { + "direction": "in", + "protocol": "tcp", + "port": "22", + "source_ips": [ + "0.0.0.0/0", + "::/0" + ], + "destination_ips": [], + "description": null + }, + { + "direction": "in", + "protocol": "icmp", + "port": null, + "source_ips": [ + "0.0.0.0/0", + "::/0" + ], + "destination_ips": [], + "description": null + }, + { + "direction": "in", + "protocol": "tcp", + "port": "6443", + "source_ips": [ + "0.0.0.0/0", + "::/0" + ], + "destination_ips": [], + "description": null + } + ], + "applied_to": [] + }, + { + "id": 9990809810, + "name": "firewall-worker", + "labels": {}, + "created": "2022-11-23T12:36:58+00:00", + "rules": [ + { + "direction": "in", + "protocol": "tcp", + "port": "22", + "source_ips": [ + "0.0.0.0/0", + "::/0" + ], + "destination_ips": [], + "description": null + }, + { + "direction": "in", + "protocol": "icmp", + "port": null, + "source_ips": [ + "0.0.0.0/0", + "::/0" + ], + "destination_ips": [], + "description": null + }, + { + "direction": "in", + "protocol": "tcp", + "port": "80", + "source_ips": [ + "0.0.0.0/0", + "::/0" + ], + "destination_ips": [], + "description": null + }, + { + "direction": "in", + "protocol": "tcp", + "port": "9090", + "source_ips": [ + "0.0.0.0/0", + "::/0" + ], + "destination_ips": [], + "description": null + }, + { + "direction": "in", + "protocol": "tcp", + "port": "443", + "source_ips": [ + "0.0.0.0/0", + "::/0" + ], + "destination_ips": [], + "description": null + }, + { + "direction": "in", + "protocol": "tcp", + "port": "any", + "source_ips": [ + "10.1.0.0/16" + ], + "destination_ips": [], + "description": null + }, + { + "direction": "in", + "protocol": "udp", + "port": "any", + "source_ips": [ + "10.0.0.0/8" + ], + "destination_ips": [], + "description": null + } + ], + "applied_to": [] + } + ], + "meta": { + "pagination": { + "page": 1, + "per_page": 25, + "previous_page": null, + "next_page": null, + "last_page": 1, + "total_entries": 2 + } + } +}