diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 152903f..276d5eb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -198,7 +198,7 @@ jobs: asset_name: cb-v${{ steps.version.outputs.version }}_macos_amd64.zip asset_content_type: application/zip - - name: Update release zip from macos arm64 + - name: Upload release zip from macos arm64 uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 262cf56..8bd2fbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- `cb network` now manages firewall rules and supports the following + subcommands: `add-firewall-rule`, `list-firewall-rules`, + `remove-firewall-rule` and `update-firewall-rule` + +### Deprecated +- `cb firewall` deprecated in favor of `cb network`. ## [3.5.1] - 2024-05-09 ### Fixed diff --git a/spec/cb/completion_spec.cr b/spec/cb/completion_spec.cr index ac98830..e7b98b5 100644 --- a/spec/cb/completion_spec.cr +++ b/spec/cb/completion_spec.cr @@ -9,6 +9,10 @@ private class CompletionTestClient < CB::Client [Factory.team(name: "my team", role: "manager")] end + def get_networks(team) + [Factory.network] + end + def get_firewall_rules(id) [Factory.firewall_rule(id: "f1", rule: "1.2.3.4/32"), Factory.firewall_rule(id: "f2", rule: "4.5.6.7/24")] end @@ -662,9 +666,44 @@ Spectator.describe CB::Completion do expect(result).to have_option "network" result = parse("cb network ") + expect(result).to have_option "add-firewall-rule" expect(result).to have_option "info" expect(result).to have_option "list" + expect(result).to have_option "list-firewall-rules" + expect(result).to have_option "remove-firewall-rule" + expect(result).to have_option "update-firewall-rule" + + # Network Firewall Rule Management + result = parse("cb network add-firewall-rule") + expect(result).to have_option "--format" + expect(result).to have_option "--network" + expect(result).to have_option "--rule" + + result = parse("cb network list-firewall-rules") + expect(result).to have_option "--format" + expect(result).to have_option "--network" + + result = parse("cb network remove-firewall-rule") + expect(result).to have_option "--format" + expect(result).to have_option "--network" + expect(result).to_not have_option "--firewall-rule" + + result = parse("cb network remove-firewall-rule --network abc ") + expect(result).to have_option "--firewall-rule" + + result = parse("cb network update-firewall-rule") + expect(result).to have_option "--description" + expect(result).to_not have_option "--firewall-rule" + expect(result).to have_option "--format" + expect(result).to have_option "--network" + expect(result).to have_option "--rule" + + result = parse("cb network update-firewall-rule --network abc ") + expect(result).to have_option "--description" + expect(result).to have_option "--firewall-rule" + expect(result).to have_option "--rule" + # Network Management result = parse("cb network info ") expect(result).to have_option "--network" expect(result).to have_option "--format" diff --git a/spec/cb/firewall_rule_spec.cr b/spec/cb/firewall_rule_spec.cr new file mode 100644 index 0000000..bb916ae --- /dev/null +++ b/spec/cb/firewall_rule_spec.cr @@ -0,0 +1,284 @@ +require "../spec_helper" + +Spectator.describe FirewallRuleAdd do + subject(action) { described_class.new client: client, output: IO::Memory.new } + + mock_client + + let(network) { Factory.network } + + describe "#validate" do + it "ensures required arguments are present" do + expect(&.validate).to raise_error Program::Error, /Missing required argument/ + + action.network_id = network.id + expect(&.validate).to raise_error Program::Error, /Missing required argument/ + + action.rule = "0.0.0.0/0" + expect(&.validate).to be_true + end + end + + describe "#call" do + before_each { + action.output = IO::Memory.new + action.network_id = network.id + action.rule = Factory.firewall_rule.rule + + expect(client).to receive(:create_firewall_rule).and_return Factory.firewall_rule + } + + it "outputs table with header" do + action.call + + expected = <<-EXPECTED + ID Rule Description + shofthj3fzaipie44lt6a5i3de 1.2.3.0/24 Example Description + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs table without header" do + action.no_header = true + action.call + + expected = <<-EXPECTED + shofthj3fzaipie44lt6a5i3de 1.2.3.0/24 Example Description + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs json" do + action.format = Format::JSON + action.call + + expected = <<-EXPECTED + { + "firewall_rules": [ + { + "id": "shofthj3fzaipie44lt6a5i3de", + "description": "Example Description", + "rule": "1.2.3.0/24" + } + ] + } + EXPECTED + + expect(&.output.to_s).to look_like expected + end + end +end + +Spectator.describe FirewallRuleList do + subject(action) { described_class.new client: client, output: IO::Memory.new } + + mock_client + + let(network) { Factory.network } + + describe "#validate" do + it "ensures required arguments are present" do + expect(&.validate).to raise_error Program::Error, /Missing required argument/ + + action.network_id = network.id + expect(&.validate).to be_true + end + end + + describe "#call" do + before_each { + action.output = IO::Memory.new + action.network_id = network.id + + expect(client).to receive(:get_firewall_rules).and_return [Factory.firewall_rule] + } + + it "outputs table with header" do + action.call + + expected = <<-EXPECTED + ID Rule Description + shofthj3fzaipie44lt6a5i3de 1.2.3.0/24 Example Description + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs table without header" do + action.no_header = true + action.call + + expected = <<-EXPECTED + shofthj3fzaipie44lt6a5i3de 1.2.3.0/24 Example Description + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs json" do + action.format = Format::JSON + action.call + + expected = <<-EXPECTED + { + "firewall_rules": [ + { + "id": "shofthj3fzaipie44lt6a5i3de", + "description": "Example Description", + "rule": "1.2.3.0/24" + } + ] + } + EXPECTED + + expect(&.output.to_s).to look_like expected + end + end +end + +Spectator.describe FirewallRuleRemove do + subject(action) { described_class.new client: client, output: IO::Memory.new } + + mock_client + + let(network) { Factory.network } + let(firewall_rule) { Factory.firewall_rule } + + describe "#validate" do + it "ensures required arguments are present" do + expect(&.validate).to raise_error Program::Error, /Missing required argument/ + + action.network_id = network.id + expect(&.validate).to raise_error Program::Error, /Missing required argument/ + + action.firewall_rule_id = firewall_rule.id + expect(&.validate).to be_true + end + end + + describe "#call" do + before_each { + action.output = IO::Memory.new + action.network_id = network.id + action.firewall_rule_id = firewall_rule.id + + expect(client).to receive(:destroy_firewall_rule).and_return Factory.firewall_rule + } + + it "outputs table with header" do + action.call + + expected = <<-EXPECTED + ID Rule Description + shofthj3fzaipie44lt6a5i3de 1.2.3.0/24 Example Description + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs table without header" do + action.no_header = true + action.call + + expected = <<-EXPECTED + shofthj3fzaipie44lt6a5i3de 1.2.3.0/24 Example Description + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs json" do + action.format = Format::JSON + action.call + + expected = <<-EXPECTED + { + "firewall_rules": [ + { + "id": "shofthj3fzaipie44lt6a5i3de", + "description": "Example Description", + "rule": "1.2.3.0/24" + } + ] + } + EXPECTED + + expect(&.output.to_s).to look_like expected + end + end +end + +Spectator.describe FirewallRuleUpdate do + subject(action) { described_class.new client: client, output: IO::Memory.new } + + mock_client + + let(network) { Factory.network } + let(firewall_rule) { Factory.firewall_rule } + + describe "#validate" do + it "ensures required arguments are present" do + expect(&.validate).to raise_error Program::Error, /Missing required argument/ + + action.network_id = network.id + expect(&.validate).to raise_error Program::Error, /Missing required argument/ + + action.firewall_rule_id = firewall_rule.id + expect(&.validate).to be_true + end + end + + describe "#call" do + before_each { + action.output = IO::Memory.new + action.network_id = network.id + action.firewall_rule_id = firewall_rule.id + action.rule = Factory.firewall_rule.rule + + expect(client).to receive(:update_firewall_rule).and_return Factory.firewall_rule + } + + it "outputs table with header" do + action.call + + expected = <<-EXPECTED + ID Rule Description + shofthj3fzaipie44lt6a5i3de 1.2.3.0/24 Example Description + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs table without header" do + action.no_header = true + action.call + + expected = <<-EXPECTED + shofthj3fzaipie44lt6a5i3de 1.2.3.0/24 Example Description + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs json" do + action.format = Format::JSON + action.call + + expected = <<-EXPECTED + { + "firewall_rules": [ + { + "id": "shofthj3fzaipie44lt6a5i3de", + "description": "Example Description", + "rule": "1.2.3.0/24" + } + ] + } + EXPECTED + + expect(&.output.to_s).to look_like expected + end + end +end diff --git a/spec/support/factory.cr b/spec/support/factory.cr index 8ca0e94..2b366c3 100644 --- a/spec/support/factory.cr +++ b/spec/support/factory.cr @@ -115,8 +115,9 @@ module Factory def firewall_rule(**params) params = { - id: "shofthj3fzaipie44lt6a5i3de", - rule: "1.2.3.0/24", + description: "Example Description", + id: "shofthj3fzaipie44lt6a5i3de", + rule: "1.2.3.0/24", }.merge(params) CB::Model::FirewallRule.new **params end diff --git a/src/cb/completion.cr b/src/cb/completion.cr index ffff1b7..28e5077 100644 --- a/src/cb/completion.cr +++ b/src/cb/completion.cr @@ -155,6 +155,21 @@ class CB::Completion end end + def network_suggestions + teams = client.get_teams + networks = client.get_networks(teams) + + networks.map do |n| + team_name = teams.find { |t| t.id == n.team_id }.try(&.name) || "unknown_team" + "#{n.id}\t#{n.name}" + end + end + + def firewall_rule_suggestions(network_id : String?) + rules = client.get_firewall_rules(network_id) + rules.map { |r| "#{r.id}\t#{r.description}" } + end + def teams client.get_teams.map { |t| "#{t.id}\t#{t.name}" } end @@ -311,8 +326,8 @@ class CB::Completion end end - def firewall_rules(cluster_id) - rules = client.get_firewall_rules(cluster_id) + def firewall_rules(network_id) + rules = client.get_firewall_rules(network_id) rules.map(&.rule) - @args rescue Client::Error [] of String @@ -549,18 +564,83 @@ class CB::Completion def network case @args[1] + when "add-firewall-rule" + network_add_firewall_rule + when "list-firewall-rules" + network_list_firewall_rules + when "remove-firewall-rule" + network_remove_firewall_rule + when "update-firewall-rule" + network_update_firewall_rule when "info" network_info when "list" network_list else [ + "add-firewall-rule\tadd firewall rule", + "remove-firewall-rule\tremove firewall rule", + "list-firewall-rules\tlist firewall rules", + "update-firewall-rule\tupdate firewall rule", "info\tdetailed network information", "list\tlist available networks", ] end end + def network_add_firewall_rule + return ["table", "json"] if last_arg?("--format") + return network_suggestions if last_arg?("--network") + suggest = [] of String + suggest << "--description\tdescription of rule to add" unless has_full_flag? :description + suggest << "--format\tchoose output format" unless has_full_flag? :format + suggest << "--network\tnetwork id" unless has_full_flag? :network + suggest << "--rule\tcidr of rule to add" unless has_full_flag? :rule + suggest + end + + def network_remove_firewall_rule + if last_arg?("--firewall-rule") + network = find_arg_value "--network" + return firewall_rule_suggestions(network) + end + + return ["table", "json"] if last_arg?("--format") + return network_suggestions if last_arg?("--network") + suggest = [] of String + suggest << "--firewall-rule\tchoose firewall rule" unless has_full_flag?(:firewall_rule) || !has_full_flag?(:network) + suggest << "--format\tchoose output format" unless has_full_flag? :format + suggest << "--network\tchoose network" unless has_full_flag? :network + suggest + end + + def network_list_firewall_rules + return ["table", "json"] if last_arg?("--format") + return network_suggestions if last_arg?("--network") + suggest = [] of String + suggest << "--format\tchoose output format" unless has_full_flag? :format + suggest << "--network\tchoose network" unless has_full_flag? :network + suggest + end + + def network_update_firewall_rule + if last_arg?("--firewall-rule") + network = find_arg_value "--network" + return firewall_rule_suggestions(network) + end + + return ["table", "json"] if last_arg?("--format") + return network_suggestions if last_arg?("--network") + + suggest = [] of String + suggest << "--description\tdescription of the rule" unless has_full_flag? :description + suggest << "--firewall-rule\tchoose firewall rule" unless has_full_flag?(:firewall_rule) || !has_full_flag?(:network) + suggest << "--format\tchoose output format" unless has_full_flag? :format + suggest << "--network\tchoose network" unless has_full_flag? :network + suggest << "--rule\tcidr of the rule" unless has_full_flag? :rule + suggest + end + def network_info if last_arg?("--format") return ["table", "json"] diff --git a/src/cb/firewall_rule.cr b/src/cb/firewall_rule.cr new file mode 100644 index 0000000..11a0fbf --- /dev/null +++ b/src/cb/firewall_rule.cr @@ -0,0 +1,156 @@ +require "./action" + +module CB + # API Action for network firewall rules. + # + # All network firewall rule actions must inherit this action. + abstract class FirewallRuleAction < APIAction + # The output format. The default format is `table` format. + format_setter format + + # The ID of the target network. + eid_setter network_id + + # Flag to indicate whether the output should include a header. This only + # has an effect when the output format is a table. + property? no_header : Bool = false + + def validate + check_required_args do |missing| + missing << "network" unless @network_id + end + end + + abstract def run + + def display(firewall_rules : Array(Model::FirewallRule)) + case @format + when Format::Default, Format::Table + output_table(firewall_rules) + when Format::JSON + output_json(firewall_rules) + end + end + + def output_json(firewall_rules : Array(Model::FirewallRule)) + output << { + "firewall_rules": firewall_rules, + }.to_pretty_json << '\n' + end + + def output_table(firewall_rules : Array(Model::FirewallRule)) + table = Table::TableBuilder.new(border: :none) do + columns do + add "ID" + add "Rule" + add "Description" + end + + header unless @no_header + + rows firewall_rules.map { |fwr| [fwr.id, fwr.rule, fwr.description] } + end + + output << table.render << '\n' + end + end + + # Action for adding a firewall rule to a network. + class FirewallRuleAdd < FirewallRuleAction + # The rule (required). + property rule : String = "" + + # The description of the rule. + property description : String? + + def validate + super + + check_required_args do |missing| + missing << "rule" if @rule.empty? + end + end + + def run + validate + + firewall_rule = client.create_firewall_rule( + network_id: @network_id, + params: CB::Client::FirewallRuleCreateParams.new( + description: @description, + rule: @rule.to_s + ) + ) + + display([firewall_rule]) + end + end + + # Action for listing existing firewall rules for a network. + class FirewallRuleList < FirewallRuleAction + def run + validate + + firewall_rules = client.get_firewall_rules(@network_id) + + display(firewall_rules) + end + end + + # Action for removing a firewall rule from a network + class FirewallRuleRemove < FirewallRuleAction + # The ID of the firewall rule to remove. + eid_setter firewall_rule_id + + def validate + super + + check_required_args do |missing| + missing << "firewall-rule" unless @firewall_rule_id + end + end + + def run + validate + + firewall_rule = client.destroy_firewall_rule(network_id, firewall_rule_id) + + display([firewall_rule]) + end + end + + # Action for updating an existing firewall rule for a network. + class FirewallRuleUpdate < FirewallRuleAction + # The ID of the firewall rule to update. + eid_setter firewall_rule_id + + # The rule. + property rule : String? + + # The description of the rule. + property description : String? + + def validate + super + + check_required_args do |missing| + missing << "firewall-rule" unless @firewall_rule_id + end + end + + def run + validate + + firewall_rule = client.update_firewall_rule( + network_id: @network_id, + firewall_rule_id: @firewall_rule_id, + params: CB::Client::FirewallRuleUpdateParams.new( + description: @description, + rule: @rule, + ) + ) + + display([firewall_rule]) + end + end +end diff --git a/src/cb/manage_firewall.cr b/src/cb/manage_firewall.cr index 749421c..75a22b2 100644 --- a/src/cb/manage_firewall.cr +++ b/src/cb/manage_firewall.cr @@ -58,14 +58,14 @@ class CB::ManageFirewall < CB::APIAction end def remove_rule(rule : CB::Model::FirewallRule) - @client.delete_firewall_rule @network_id, rule.id + @client.destroy_firewall_rule @network_id, rule.id "done".colorize.t_success rescue e : Client::Error output.print e end def add_rule(cidr : String) - @client.add_firewall_rule @network_id, cidr + @client.create_firewall_rule @network_id, CB::Client::FirewallRuleCreateParams.new(rule: cidr) "done".colorize.t_success rescue e : Client::Error output.print e diff --git a/src/cli.cr b/src/cli.cr index a33515e..8394216 100755 --- a/src/cli.cr +++ b/src/cli.cr @@ -126,7 +126,8 @@ op = OptionParser.new do |parser| positional_args psql.cluster_id end - parser.on("firewall", "Manage firewall rules") do + parser.on("firewall") do + show_deprecated("Prefer use of #{"cb network".colorize.bold} instead") manage = set_action ManageFirewall parser.banner = "cb firewall <--cluster> [--add] [--remove]" @@ -493,13 +494,99 @@ op = OptionParser.new do |parser| parser.on("network", "Manage networks") do parser.banner = "cb network " + parser.on("add-firewall-rule", "Add a firewall rule to a network") do + add = set_action FirewallRuleAdd + + parser.banner = "cb network add-firewall-rule <--network> <--rule>" + + parser.on("--description DESC", "A description for the rule") { |arg| add.description = arg } + parser.on("--format FORMAT", "Output format (default: table)") { |arg| add.format = arg } + parser.on("--network ID", "The target network for the rule") { |arg| add.network_id = arg } + parser.on("--no-header", "Do not display table header") { add.no_header = true } + parser.on("--rule CIDR", "A firewall rule") { |arg| add.rule = arg } + + parser.examples = <<-EXAMPLES + Add a firewall rule. Output: table + $ cb network add-firewall-rule --network --rule + + Add a firewall rule. Output: table without header + $ cb network add-firewall-rule --network --rule --no-header + + Add a firewall rule with a description. Output: table + $ cb network add-firewall-rule --network --rule --description + + Add a firewall rule. Output: json + $ cb network add-firewall-rule --network --rule --format json + EXAMPLES + end + + parser.on("list-firewall-rules", "List all firewall rules for a network") do + list = set_action FirewallRuleList + + parser.on("--format FORMAT", "Output format (default: table)") { |arg| list.format = arg } + parser.on("--network ID", "The target network") { |arg| list.network_id = arg } + parser.on("--no-header", "Do not display table header") { list.no_header = true } + + parser.examples = <<-EXAMPLES + List all firewall rules. Output: table + $ cb network list-firewall-rules --network + + List all firewall rules. Output: table without header + $ cb network list-firewall-rules --network --no-header + + List all firewall rules. Output: json + $ cb network list-firewall-rules --network --format json + EXAMPLES + end + + parser.on("remove-firewall-rule", "Remove a firewall rule from a network") do + remove = set_action FirewallRuleRemove + + parser.banner = "cb network remove-firewall-rule <--network> <--firewall-rule>" + + parser.on("--firewall-rule ID", "The id of the rule to remove") { |arg| remove.firewall_rule_id = arg } + parser.on("--format FORMAT", "Output format (default: table)") { |arg| remove.format = arg } + parser.on("--network ID", "The target network") { |arg| remove.network_id = arg } + + parser.examples = <<-EXAMPLES + Remove firewall rule. Output: table + $ cb network remove-firewall-rule --network --firewall-rule + + Remove firewall rule. Output: table without header + $ cb network remove-firewall-rule --network --firewall-rule --no-header + + Remove firewall rule. Ouptut: json + $ cb network remove-firewall-rule --network --firewall-rule --format json + EXAMPLES + end + + parser.on("update-firewall-rule", "Update a network firewall rule") do + update = set_action FirewallRuleUpdate + + parser.banner = "cb network update-firewall-rule <--network> <--firewall-rule>" + + parser.on("--description DESC", "The description for the rule") { |arg| update.description = arg } + parser.on("--firewall-rule ID", "The id of the rule to remove") { |arg| update.firewall_rule_id = arg } + parser.on("--format FORMAT", "Output format (default: table)") { |arg| update.format = arg } + parser.on("--network ID", "The target network") { |arg| update.network_id = arg } + parser.on("--rule CIDR", "The firewall rule") { |arg| update.rule = arg } + + parser.examples = <<-EXAMPLES + Update rule. + $ cb network update-firewall-rule --network --firewall-rule --rule + + Update description. + $ cb network update-firewall-rule --network --firewall-rule --description + EXAMPLES + end + parser.on("info", "Detailed network information") do info = set_action NetworkInfo parser.banner = "cb network info <--network>" - parser.on("--network ID", "Choose network") { |arg| info.network_id = arg } parser.on("--format FORMAT", "Choose output format (default: table)") { |arg| info.format = arg } + parser.on("--network ID", "Choose network") { |arg| info.network_id = arg } parser.on("--no-header", "Do not display table header") { info.no_header = true } parser.examples = <<-EXAMPLES diff --git a/src/client/firewall_rule.cr b/src/client/firewall_rule.cr index 80ef53b..bfd766e 100644 --- a/src/client/firewall_rule.cr +++ b/src/client/firewall_rule.cr @@ -1,27 +1,45 @@ +require "json" + require "./client" module CB class Client - # Add a firewall rule to a cluster. + jrecord FirewallRuleCreateParams, + description : String? = nil, + rule : String = "" + + # Add a firewall rule to a network. # - # TODO (abrightwell): Add docs reference. - def add_firewall_rule(network_id, cidr) - post "networks/#{network_id}/firewall-rules", {rule: cidr} + # https://docs.crunchybridge.com/api/network-firewall-rule#create-firewall-rule + def create_firewall_rule(network_id, params : FirewallRuleCreateParams) + resp = post "networks/#{network_id}/firewall-rules", params + CB::Model::FirewallRule.from_json resp.body end - # Remove a firewall rule from a cluster. + # Remove a firewall rule from a network. # - # TODO (abrightwell): Add docs reference. - def delete_firewall_rule(network_id, firewall_rule_id) - delete "networks/#{network_id}/firewall-rules/#{firewall_rule_id}" + # https://docs.crunchybridge.com/api/network-firewall-rule#destroy-firewall-rule + def destroy_firewall_rule(network_id, firewall_rule_id) + resp = delete "networks/#{network_id}/firewall-rules/#{firewall_rule_id}" + CB::Model::FirewallRule.from_json resp.body end - # List current firewall rules for a cluster. + # List current firewall rules for a network. # - # TODO (abrightwell): Add docs reference. + # https://docs.crunchybridge.com/api/network-firewall-rule#list-firewall-rules def get_firewall_rules(network_id) resp = get "networks/#{network_id}/firewall-rules" Array(CB::Model::FirewallRule).from_json resp.body, root: "firewall_rules" end + + jrecord FirewallRuleUpdateParams, description : String?, rule : String? + + # Update a firewall rule for a network. + # + # https://docs.crunchybridge.com/api/network-firewall-rule#update-firewall-rule + def update_firewall_rule(network_id, firewall_rule_id, params : FirewallRuleUpdateParams) + resp = patch "networks/#{network_id}/firewall-rules/#{firewall_rule_id}", params + CB::Model::FirewallRule.from_json resp.body + end end end diff --git a/src/client/network.cr b/src/client/network.cr index 1e564cc..adda08f 100644 --- a/src/client/network.cr +++ b/src/client/network.cr @@ -25,10 +25,15 @@ module CB else get "networks" end - Array(CB::Model::Network).from_json resp.body, root: "networks" end + def get_networks(teams : Array(CB::Model::Team)) + networks = [] of CB::Model::Network + teams.each { |team| networks.concat get_networks(Identifier.new team.id.to_s) } + networks + end + private def get_network_by_name(id : Identifier) network = get_networks(nil).find { |n| id == n.name } raise Program::Error.new "network #{id.to_s.colorize.t_name} does not exist." unless network diff --git a/src/models/firewall_rule.cr b/src/models/firewall_rule.cr index e81f971..6a9dfdf 100644 --- a/src/models/firewall_rule.cr +++ b/src/models/firewall_rule.cr @@ -1,5 +1,6 @@ module CB::Model jrecord FirewallRule, id : String, + description : String, rule : String end