Skip to content

Commit a9fd02e

Browse files
authored
Support for empty Firewall Inbound Rule (#29)
* ecma2020 * node16 * support for empty FW * 0.3.1 * wait for FW change to be succeeded * wait for DO to update the FW on all droplets * silent * Log FW details on error (#1) * retry ip lookup * 0.3.2 * omit the v from the tags
1 parent 3afa03c commit a9fd02e

File tree

10 files changed

+9309
-159
lines changed

10 files changed

+9309
-159
lines changed

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"extends": ["plugin:github/es6"],
44
"parser": "@typescript-eslint/parser",
55
"parserOptions": {
6-
"ecmaVersion": 9,
6+
"ecmaVersion": 2020,
77
"sourceType": "module",
88
"project": "./tsconfig.json"
99
},

.npmrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# remove the v prefix from the tag
2+
tag-version-prefix=""

action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,5 @@ inputs:
3434
default: false
3535

3636
runs:
37-
using: "node12"
37+
using: "node16"
3838
main: "dist/index.js"

dist/index.js

Lines changed: 135 additions & 121 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 9081 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "digitalocean-doorkeeper",
3-
"version": "0.3.0",
3+
"version": "0.3.2",
44
"description": "This Github action allows you to open or close an specific port in your DigitalOcean firewall. It's really useful for deploy in your instances from Github Actions, as they don't provide a list of IPs to add to your security groups.",
55
"main": "src/main.ts",
66
"scripts": {
@@ -44,4 +44,4 @@
4444
"ts-node": "^8.10.2",
4545
"typescript": "^3.9.10"
4646
}
47-
}
47+
}

src/digitalocean.ts

Lines changed: 76 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import {createApiClient, modules} from "dots-wrapper";
2-
import {IListRequest} from "dots-wrapper/dist/types/list-request";
1+
import { createApiClient, modules } from "dots-wrapper";
2+
import { IListRequest } from "dots-wrapper/dist/types/list-request";
33

4-
import {ActionConfig} from "./utils";
4+
import { ActionConfig } from "./utils";
5+
6+
import { IFirewallInboundRule, IFirewallOutboundRule } from "dots-wrapper/dist/modules/firewall";
7+
import { config } from "process";
58

6-
import {IFirewallInboundRule, IFirewallOutboundRule} from "dots-wrapper/dist/modules/firewall";
79

810
interface ClientInterface {
911
firewall: Readonly<{
@@ -36,53 +38,70 @@ export async function getFirewall({firewall: firewallClient}: ClientInterface, n
3638
throw new Error(`The firewall with name '${name}', doesn't exist.`);
3739
}
3840

41+
// in case the firewall has no inbound rules
42+
firewall.inbound_rules = firewall.inbound_rules || [];
43+
3944
return firewall;
4045
}
4146

42-
function applyRule(config: ActionConfig, rule: IFirewallInboundRule = {protocol: '', ports: '', sources: {}}) {
47+
function applyRule(config: ActionConfig, rule: IFirewallInboundRule = { protocol: '', ports: '', sources: {} }): IFirewallInboundRule | null {
4348
const cloneRule = { ...rule };
4449
const { port, action, protocol, IP } = config;
4550

46-
if (rule.ports != port.toString() || rule.protocol != protocol)
47-
return cloneRule;
4851

52+
if (!cloneRule.protocol) {
53+
cloneRule.protocol = protocol;
54+
}
55+
if (!cloneRule.ports) {
56+
cloneRule.ports = port.toString();
57+
}
4958
if (!cloneRule.sources.addresses) {
5059
cloneRule.sources.addresses = [];
5160
}
61+
5262
const addresses = cloneRule.sources.addresses;
5363
if (action == "add") {
5464
if (!addresses.includes(IP)) {
5565
addresses.push(IP);
5666
}
5767
} else if (action == "remove") {
5868
cloneRule.sources.addresses = addresses.filter(address => address != IP);
69+
5970
}
60-
61-
71+
72+
if(cloneRule.sources?.addresses.length == 0) {
73+
return null;
74+
}
75+
6276
return cloneRule;
6377
}
6478

65-
export function generateInboundRules(oldRules: IFirewallInboundRule[], config: ActionConfig): IFirewallInboundRule[] {
79+
export function generateInboundRules(oldRules: IFirewallInboundRule[] = [], config: ActionConfig): IFirewallInboundRule[] {
6680
const { port, action, protocol } = config;
6781
const existingRules = oldRules.filter(r => r.ports == port.toString() && r.protocol == protocol);
6882

6983
if (!existingRules.length) {
70-
oldRules.push(applyRule(config));
84+
const newRule = applyRule(config);
85+
if (newRule) {
86+
oldRules.push(newRule);
87+
}
7188
return oldRules;
7289
}
7390

74-
return oldRules.map((r, index) => {
91+
return oldRules.reduce((out, r, index) => {
7592
if (action == "remove" || (action == "add" && index == 0)) {
76-
return applyRule(config, r)
93+
const newRule = applyRule(config, r);
94+
if (newRule)
95+
out.push(newRule)
7796
} else {
78-
return r;
97+
out.push(r);
7998
}
80-
});
81-
99+
return out;
100+
}, [] as IFirewallInboundRule[]);
82101
}
83102

84103
export async function updateInboundRules(
85-
{firewall: firewallClient}: ClientInterface,
104+
{ firewall: firewallClient }: ClientInterface,
86105
firewall: modules.firewall.IFirewall,
87106
inboundRules: IFirewallInboundRule[],
88107
dryrun = true
@@ -95,27 +114,59 @@ export async function updateInboundRules(
95114

96115
const updated = {
97116
...firewall,
98-
inbound_rules: inboundRules,
117+
inbound_rules: inboundRules.length ? inboundRules : [],
99118
outbound_rules: prepareOutboundRules(firewall.outbound_rules)
100119
};
101120

102-
const {
103-
data: {firewall: response}
104-
} = await firewallClient.updateFirewall(updated);
121+
try {
122+
123+
let maxRetries = 10;
124+
const { data: { firewall: response } } = await firewallClient.updateFirewall(updated);
125+
let status = response.status;
126+
const firewallId = (response.id as string);
127+
128+
/*
129+
wait for DO to update the droplets using this firewall
130+
*/
131+
while (true) {
132+
133+
maxRetries--;
134+
if (maxRetries < 0) {
135+
break; // give up
136+
}
137+
console.log(`DO status: ${status}`);
138+
if (status != "waiting") {
139+
break;
140+
}
141+
142+
console.log(" waiting for DO to update the droplets using this firewall...");
143+
await new Promise(resolve => setTimeout(resolve, 2000));
144+
145+
const { data: { firewall: fw } } = await firewallClient.getFirewall({ firewall_id: firewallId });
146+
status = fw?.status || "errored";
147+
148+
}
105149

106-
console.log((response as any).status);
150+
} catch (e) {
151+
console.error("FW Update failed. updated : %j", updated);
152+
console.error("FW Update failed. inboundRules: %j", inboundRules);
153+
console.error(e);
154+
}
107155
}
108156

109-
export function printFirewallRules(inboundRules: IFirewallInboundRule[], title = "") {
157+
export function printFirewallRules(inboundRules: IFirewallInboundRule[] = [], title = "") {
110158
console.log("----------------------");
111159
console.log(`Firewall Inbound Rules ${title}`);
112160
console.log("----------------------");
161+
if(inboundRules.length == 0) {
162+
console.log("** no rules defined **");
163+
}
113164
inboundRules.forEach(rule => {
114-
console.log(`${rule.ports}::${rule.protocol} - ${rule.sources.addresses}`);
165+
console.log(`${rule.ports}::${rule.protocol} - ${rule.sources?.addresses}`);
115166
});
116167
}
117168

118-
function prepareOutboundRules(outboundRules: IFirewallOutboundRule[]): IFirewallOutboundRule[] {
169+
function prepareOutboundRules(outboundRules: IFirewallOutboundRule[] = []): IFirewallOutboundRule[] {
119170
return outboundRules.map(rule => {
120171
const clonedRule = {...rule};
121172
if (clonedRule.ports == "all") {

src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ async function main() {
1818
if (config.dryrun) {
1919
console.log("Running in DryRun MODE...");
2020
}
21+
console.log(`Action: '${config.action}' on '${config.protocol}' port '${config.port}' to firewall '${config.firewallName}'`)
2122

2223
const client = getDOClient(config);
2324
const firewall = await getFirewall(client, config.firewallName);
2425
printFirewallRules(firewall.inbound_rules, "(original)");
2526

2627
const newRules = generateInboundRules(firewall.inbound_rules, config);
28+
2729
await updateInboundRules(client, firewall, newRules, config.dryrun);
2830
}
2931

src/utils.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,13 @@ export async function getConfig(): Promise<ActionConfig> {
4040
const dryrun = core.getInput("dryRun") == "true";
4141

4242
// TODO: try/catch for getting the IP
43-
const IP = await getLocalIP();
43+
let IP;
44+
try {
45+
IP = await getLocalIP();
46+
} catch (error) {
47+
// try again
48+
IP = await getLocalIP();
49+
}
4450

4551
return {
4652
DO_TOKEN: token,
@@ -56,5 +62,6 @@ export async function getConfig(): Promise<ActionConfig> {
5662
// TODO: remove the export here and test the full configuration
5763
export async function getLocalIP(): Promise<string> {
5864
const response = await fetch("https://ifconfig.me/ip");
65+
if (!response.ok) throw new Error(`Error getting the IP address: ${response.statusText}`);
5966
return response.text();
6067
}

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"compilerOptions": {
3-
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
3+
"target": "es2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
44
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
55
"outDir": "./lib" /* Redirect output structure to the directory. */,
66
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,

0 commit comments

Comments
 (0)