From 34f785dc539c6795104230499c6db7a9cde2767f Mon Sep 17 00:00:00 2001 From: Adam Brightwell Date: Wed, 29 May 2024 10:47:15 -0400 Subject: [PATCH] Add support for managing VPC peering. Add support for managing VPC peering via the `cb network` command. These changes provide the following subcommands: * `cb network create-peering` - create a new VPC peering in the target network. * `cb network delete-peering` - delete an existing VPC peering. * `cb network get-peering` - show details of an existing VPC peering. * `cb network list-peerings` - list all existing VPC peerings for the target network. --- CHANGELOG.md | 4 +- spec/cb/completion_spec.cr | 71 ++++++- spec/cb/peering_spec.cr | 425 +++++++++++++++++++++++++++++++++++++ spec/support/factory.cr | 14 ++ src/cb/completion.cr | 90 +++++++- src/cb/peering.cr | 178 ++++++++++++++++ src/cli.cr | 95 ++++++++- src/client/peering.cr | 30 +++ src/models/peering.cr | 10 + 9 files changed, 905 insertions(+), 12 deletions(-) create mode 100644 spec/cb/peering_spec.cr create mode 100644 src/cb/peering.cr create mode 100644 src/client/peering.cr create mode 100644 src/models/peering.cr diff --git a/CHANGELOG.md b/CHANGELOG.md index 171e4d8..ac45e29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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` + `remove-firewall-rule` and `update-firewall-rule`. +- `cb network` now manages VPC peerings and supports the following subcommands: + `create-peering`, `delete-peering`, `get-peering` and `list-peerings`. ### Deprecated - `cb firewall` deprecated in favor of `cb network`. diff --git a/spec/cb/completion_spec.cr b/spec/cb/completion_spec.cr index 6a7c1b7..cf4f256 100644 --- a/spec/cb/completion_spec.cr +++ b/spec/cb/completion_spec.cr @@ -5,16 +5,23 @@ private class CompletionTestClient < CB::Client [Factory.cluster] end - def get_teams - [Factory.team(name: "my team", role: "manager")] + 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 + + def get_log_destinations(id) + [ + Factory.log_destination(id: "logid", host: "host", port: 2020, + template: "template", description: "logdest descr"), + ] 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")] + def list_peerings(network) + [Factory.peering] end def get_providers @@ -40,11 +47,8 @@ private class CompletionTestClient < CB::Client ] end - def get_log_destinations(id) - [ - Factory.log_destination(id: "logid", host: "host", port: 2020, - template: "template", description: "logdest descr"), - ] + def get_teams + [Factory.team(name: "my team", role: "manager")] end end @@ -704,6 +708,55 @@ Spectator.describe CB::Completion do expect(result).to have_option "--firewall-rule" expect(result).to have_option "--rule" + # Network Peering Managments + result = parse("cb network create-peering ") + expect(result).to have_option "--format" + expect(result).to have_option "--network" + expect(result).to have_option "--platform" + + result = parse("cb network create-peering --platform ") + expect(result).to eq ["aws", "gcp"] + + result = parse("cb network create-peering --network abc --platform aws ") + expect(result).to have_option "--aws-account-id" + expect(result).to have_option "--aws-vpc-id" + + result = parse("cb network create-peering --network abc --platform gcp ") + expect(result).to have_option "--gcp-project-id" + expect(result).to have_option "--gcp-vpc-name" + + result = parse("cb network delete-peering ") + expect(result).to have_option "--format" + expect(result).to have_option "--network" + expect(result).to have_option "--peering" + + result = parse("cb network delete-peering --network abc ") + expect(result).to have_option "--peering" + + result = parse("cb network delete-peering --network abc --peering ") + expect(result).to eq ["yydi4alkebgsfldibaoo4kliii\tExample Peering"] + + result = parse("cb network delete-peering --network abc --peering abc --format json ") + expect(result).to eq [] of String + + result = parse("cb network get-peering ") + expect(result).to have_option "--format" + expect(result).to have_option "--network" + expect(result).to have_option "--peering" + + result = parse("cb network get-peering --network abc ") + expect(result).to have_option "--peering" + + result = parse("cb network get-peering --network abc --peering abc --format json ") + expect(result).to eq [] of String + + result = parse("cb network list-peerings ") + expect(result).to have_option "--format" + expect(result).to have_option "--network" + + result = parse("cb network list-peerings --network abc --format json ") + expect(result).to eq [] of String + # Network Management result = parse("cb network info ") expect(result).to have_option "--network" diff --git a/spec/cb/peering_spec.cr b/spec/cb/peering_spec.cr new file mode 100644 index 0000000..90a2083 --- /dev/null +++ b/spec/cb/peering_spec.cr @@ -0,0 +1,425 @@ +require "../spec_helper" + +Spectator.describe PeeringCreate 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.platform = "aws" + expect(&.validate).to raise_error Program::Error, /Missing required argument/ + + action.aws_account_id = "abc" + expect(&.validate).to raise_error Program::Error, /Missing required argument/ + + action.aws_vpc_id = "abc" + expect(&.validate).to be_true + end + + it "ensures aws parameters" do + action.network_id = network.id + action.platform = "aws" + action.aws_account_id = "0123456789" + action.aws_vpc_id = "vpc-12345" + + action.gcp_project_id = "gcp-project" + expect(&.validate).to raise_error Program::Error, /Cannot use '--gcp-project-id'/ + + action.gcp_project_id = nil + action.gcp_vpc_name = "gcp-vpc" + expect(&.validate).to raise_error Program::Error, /Cannot use '--gcp-project-id'/ + end + + it "ensure gcp parameters" do + action.network_id = network.id + action.platform = "gcp" + action.gcp_project_id = "gcp-project" + action.gcp_vpc_name = "gcp-vpc" + + action.aws_account_id = "0123456789" + expect(&.validate).to raise_error Program::Error, /Cannot use '--aws-account-id'/ + + action.aws_account_id = nil + action.aws_vpc_id = "vpc-12345" + expect(&.validate).to raise_error Program::Error, /Cannot use '--aws-account-id'/ + end + end + + describe "#call" do + before_each { + action.output.flush + action.network_id = network.id + action.platform = "aws" + action.aws_account_id = "aws123" + action.aws_vpc_id = "vpc123" + + expect(client).to receive(:get_network).and_return network + expect(client).to receive(:create_peering).and_return Factory.peering + } + + it "outputs table with header" do + action.call + + expected = <<-EXPECTED + ID Name Network ID Peer ID CIDR Status + yydi4alkebgsfldibaoo4kliii Example Peering arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 10.0.0.0/24 active + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs table without header" do + action.no_header = true + action.call + + expected = <<-EXPECTED + yydi4alkebgsfldibaoo4kliii Example Peering arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 10.0.0.0/24 active + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs json" do + action.format = Format::JSON + action.call + + expected = <<-EXPECTED + { + "peerings": [ + { + "id": "yydi4alkebgsfldibaoo4kliii", + "cidr4": "10.0.0.0/24", + "name": "Example Peering", + "network_id": "oap3kavluvgm7cwtzgaaixzfoi", + "network_identifier": "arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789", + "peer_identifier": "arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789", + "status": "active" + } + ] + } + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs list" do + action.format = Format::List + action.call + + expected = <<-EXPECTED + -- Peering #1 -- + ID: yydi4alkebgsfldibaoo4kliii + Name: Example Peering + Network ID: arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 + Peer ID: arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 + CIDR: 10.0.0.0/24 + Status: active + EXPECTED + + expect(&.output.to_s).to look_like expected + end + end + + describe "#make_peer_identifier" do + it "generates AWS ARN" do + action.platform = "aws" + action.aws_account_id = "0123456789" + action.aws_vpc_id = "vcp-12345" + + expect(client).to receive(:get_network).and_return network + + expect(&.make_peer_identifier).to eq "arn:aws:ec2:us-east-1:0123456789:vpc/vcp-12345" + end + + it "generates GCP URL" do + action.platform = "gcp" + action.gcp_project_id = "0123456789" + action.gcp_vpc_name = "example-vpc" + + expect(&.make_peer_identifier).to eq "https://www.googleapis.com/compute/v1/projects/0123456789/global/networks/example-vpc" + end + end +end + +Spectator.describe PeeringDelete do + subject(action) { described_class.new client: client, output: IO::Memory.new } + + mock_client + + let(network) { Factory.network } + let(peering) { Factory.peering } + + 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.peering_id = peering.id + expect(&.validate).to be_true + end + end + + describe "#call" do + before_each { + action.output = IO::Memory.new + action.network_id = network.id + action.peering_id = peering.id + + expect(client).to receive(:delete_peering).and_return peering + } + + it "outputs table with header" do + action.call + + expected = <<-EXPECTED + ID Name Network ID Peer ID CIDR Status + yydi4alkebgsfldibaoo4kliii Example Peering arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 10.0.0.0/24 active + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs table without header" do + action.no_header = true + action.call + + expected = <<-EXPECTED + yydi4alkebgsfldibaoo4kliii Example Peering arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 10.0.0.0/24 active + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs json" do + action.format = Format::JSON + action.call + + expected = <<-EXPECTED + { + "peerings": [ + { + "id": "yydi4alkebgsfldibaoo4kliii", + "cidr4": "10.0.0.0/24", + "name": "Example Peering", + "network_id": "oap3kavluvgm7cwtzgaaixzfoi", + "network_identifier": "arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789", + "peer_identifier": "arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789", + "status": "active" + } + ] + } + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs list" do + action.format = Format::List + action.call + + expected = <<-EXPECTED + -- Peering #1 -- + ID: yydi4alkebgsfldibaoo4kliii + Name: Example Peering + Network ID: arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 + Peer ID: arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 + CIDR: 10.0.0.0/24 + Status: active + EXPECTED + + expect(&.output.to_s).to look_like expected + end + end +end + +Spectator.describe PeeringGet do + subject(action) { described_class.new client: client, output: IO::Memory.new } + + mock_client + + let(network) { Factory.network } + let(peering) { Factory.peering } + + 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.peering_id = peering.id + expect(&.validate).to be_true + end + end + + describe "#call" do + before_each { + action.output = IO::Memory.new + action.network_id = network.id + action.peering_id = peering.id + + expect(client).to receive(:get_peering).and_return peering + } + + it "outputs table with header" do + action.call + + expected = <<-EXPECTED + ID Name Network ID Peer ID CIDR Status + yydi4alkebgsfldibaoo4kliii Example Peering arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 10.0.0.0/24 active + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs table without header" do + action.no_header = true + action.call + + expected = <<-EXPECTED + yydi4alkebgsfldibaoo4kliii Example Peering arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 10.0.0.0/24 active + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs json" do + action.format = Format::JSON + action.call + + expected = <<-EXPECTED + { + "peerings": [ + { + "id": "yydi4alkebgsfldibaoo4kliii", + "cidr4": "10.0.0.0/24", + "name": "Example Peering", + "network_id": "oap3kavluvgm7cwtzgaaixzfoi", + "network_identifier": "arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789", + "peer_identifier": "arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789", + "status": "active" + } + ] + } + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs list" do + action.format = Format::List + action.call + + expected = <<-EXPECTED + -- Peering #1 -- + ID: yydi4alkebgsfldibaoo4kliii + Name: Example Peering + Network ID: arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 + Peer ID: arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 + CIDR: 10.0.0.0/24 + Status: active + EXPECTED + + expect(&.output.to_s).to look_like expected + end + end +end + +Spectator.describe PeeringList do + subject(action) { described_class.new client: client, output: IO::Memory.new } + + mock_client + + let(network) { Factory.network } + let(peering) { Factory.peering } + + 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(:list_peerings).and_return [peering] + } + + it "outputs table with header" do + action.call + + expected = <<-EXPECTED + ID Name Network ID Peer ID CIDR Status + yydi4alkebgsfldibaoo4kliii Example Peering arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 10.0.0.0/24 active + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs table without header" do + action.no_header = true + action.call + + expected = <<-EXPECTED + yydi4alkebgsfldibaoo4kliii Example Peering arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 10.0.0.0/24 active + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs json" do + action.format = Format::JSON + action.call + + expected = <<-EXPECTED + { + "peerings": [ + { + "id": "yydi4alkebgsfldibaoo4kliii", + "cidr4": "10.0.0.0/24", + "name": "Example Peering", + "network_id": "oap3kavluvgm7cwtzgaaixzfoi", + "network_identifier": "arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789", + "peer_identifier": "arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789", + "status": "active" + } + ] + } + EXPECTED + + expect(&.output.to_s).to look_like expected + end + + it "outputs list" do + action.format = Format::List + action.call + + expected = <<-EXPECTED + -- Peering #1 -- + ID: yydi4alkebgsfldibaoo4kliii + Name: Example Peering + Network ID: arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 + Peer ID: arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789 + CIDR: 10.0.0.0/24 + Status: active + 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 2b366c3..44a521e 100644 --- a/spec/support/factory.cr +++ b/spec/support/factory.cr @@ -157,6 +157,20 @@ module Factory CB::Model::Operation.new **params end + def peering(**params) + params = { + id: "yydi4alkebgsfldibaoo4kliii", + cidr4: "10.0.0.0/24", + name: "Example Peering", + network_id: "oap3kavluvgm7cwtzgaaixzfoi", + network_identifier: "arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789", + peer_identifier: "arn:aws:ec2:us-east-1:0123456789:vpc/vpc-0123456789", + status: "active", + }.merge(params) + + CB::Model::Peering.new **params + end + def plan(**params) params = { id: "jkon7qbrzzccrgwn346w3niloe", diff --git a/src/cb/completion.cr b/src/cb/completion.cr index 42d9e00..200fe42 100644 --- a/src/cb/completion.cr +++ b/src/cb/completion.cr @@ -157,7 +157,7 @@ class CB::Completion def network_suggestions teams = client.get_teams - networks = client.get_networks(teams) + networks = client.get_networks(nil) networks.map do |n| team_name = teams.find { |t| t.id == n.team_id }.try(&.name) || "unknown_team" @@ -165,6 +165,11 @@ class CB::Completion end end + def peering_suggestions(network_id : String?) + peerings = client.list_peerings(network_id) + peerings.map { |p| "#{p.id}\t#{p.name}" } + end + def firewall_rule_suggestions(network_id : String?) rules = client.get_firewall_rules(network_id) rules.map { |r| "#{r.id}\t#{r.description}" } @@ -571,8 +576,16 @@ class CB::Completion case @args[1] when "add-firewall-rule" network_add_firewall_rule + when "create-peering" + network_create_peering + when "delete-peering" + network_delete_peering + when "get-peering" + network_get_peering when "list-firewall-rules" network_list_firewall_rules + when "list-peerings" + network_list_peerings when "remove-firewall-rule" network_remove_firewall_rule when "update-firewall-rule" @@ -584,8 +597,12 @@ class CB::Completion else [ "add-firewall-rule\tadd firewall rule", + "create-peering\tcreate vpc peering", + "delete-peering\tdelete vpc peering", + "get-peering\tget vpc peering details", "remove-firewall-rule\tremove firewall rule", "list-firewall-rules\tlist firewall rules", + "list-peerings\tlist vpc peerings", "update-firewall-rule\tupdate firewall rule", "info\tdetailed network information", "list\tlist available networks", @@ -604,6 +621,71 @@ class CB::Completion suggest end + def network_create_peering + return ["json", "list", "table"] if last_arg?("--format") + return ["aws", "gcp"] if last_arg?("--platform") + return network_suggestions if last_arg?("--network") + + suggest = [] of String + suggest << "--format\tchoose output format" unless has_full_flag? :format + suggest << "--network\tnetwork id" unless has_full_flag? :network + suggest << "--platform\tcloud provider" unless has_full_flag? :platform + + if has_full_flag?(:platform) + case find_arg_value("--platform") + when "aws" + suggest << "--aws-account-id" unless has_full_flag? :aws_account_id + suggest << "--aws-vpc-id" unless has_full_flag? :aws_vpc_id + when "gcp" + suggest << "--gcp-project-id" unless has_full_flag? :gcp_project_id + suggest << "--gcp-vpc-name" unless has_full_flag? :gcp_vpc_name + end + end + + suggest + end + + def network_delete_peering + return ["json", "list", "table"] if last_arg?("--format") + return network_suggestions if last_arg?("--network") + + if last_arg?("--peering") && has_full_flag?(:network) + network = find_arg_value "--network" + return peering_suggestions(network) + end + + suggest = [] of String + suggest << "--format\tchoose output format" unless has_full_flag? :format + suggest << "--network\tchoose network" unless has_full_flag? :network + suggest << "--peering\tchoose peering" unless has_full_flag? :peering + suggest + end + + def network_get_peering + return ["json", "list", "table"] if last_arg?("--format") + return network_suggestions if last_arg?("--network") + + if last_arg?("--peering") && has_full_flag?(:network) + network = find_arg_value "--network" + return peering_suggestions(network) + end + + suggest = [] of String + suggest << "--format\tchoose output format" unless has_full_flag? :format + suggest << "--network\tchoose network" unless has_full_flag? :network + suggest << "--peering\tchoose peering" unless has_full_flag? :peering + suggest + end + + def network_list_peerings + return ["json", "list", "table"] 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_remove_firewall_rule if last_arg?("--firewall-rule") && has_full_flag?(:network) network = find_arg_value "--network" @@ -1261,7 +1343,12 @@ class CB::Completion def find_full_flags full = Set(Symbol).new full << :allow_restart if has_full_flag? "--allow-restart" + full << :aws_account_id if has_full_flag? "--aws-account-id" + full << :aws_vpc_id if has_full_flag? "--aws-vpc-id" + full << :gcp_project_id if has_full_flag? "--gcp-project-id" + full << :gcp_vpc_name if has_full_flag? "--gcp-vpc-name" full << :ha if has_full_flag? "--ha" + full << :ha if has_full_flag? "--peering" full << :plan if has_full_flag? "--plan" full << :name if has_full_flag? "--name", "-n" full << :team if has_full_flag? "--team", "-t" @@ -1295,6 +1382,7 @@ class CB::Completion full << :now if has_full_flag? "--now" full << :use_cluster_maintenance_window if has_full_flag? "use-cluster-maintenance-window" full << :no_header if has_full_flag? "--no-header" + full << :peering if has_full_flag? "--peering" full end diff --git a/src/cb/peering.cr b/src/cb/peering.cr new file mode 100644 index 0000000..732232a --- /dev/null +++ b/src/cb/peering.cr @@ -0,0 +1,178 @@ +require "./action" + +module CB + abstract class PeeringAction < APIAction + format_setter format + eid_setter network_id + property? no_header : Bool = false + + abstract def run + + def validate + check_required_args do |missing| + missing << "network" unless @network_id + end + end + + def display(peerings : Array(Model::Peering)) + case @format + when Format::Default, Format::Table + output_table(peerings) + when Format::List + output_list(peerings) + when Format::JSON + output_json(peerings) + end + end + + def output_json(peerings : Array(Model::Peering)) + output << { + "peerings": peerings, + }.to_pretty_json << '\n' + end + + def output_list(peerings : Array(Model::Peering)) + if peerings.empty? + output << "No peerings exist for network #{@network_id.colorize.t_id}.\n" + return + end + + peerings.each_with_index(offset: 1) do |peering, index| + output << "-- Peering ##{index} --\n" + table = Table::TableBuilder.new(border: :none) do + row ["ID:", peering.id.colorize.t_id] + row ["Name:", peering.name.colorize.t_name] + row ["Network ID:", peering.network_identifier] + row ["Peer ID:", peering.peer_identifier] + row ["CIDR:", peering.cidr4] + row ["Status:", peering.status] + end + output << table.render << '\n' + end + end + + def output_table(peerings : Array(Model::Peering)) + if peerings.empty? + output << "No peerings exist for network #{@network_id.colorize.t_id}.\n" + return + end + + table = Table::TableBuilder.new(border: :none) do + columns do + add "ID" + add "Name" + add "Network ID" + add "Peer ID" + add "CIDR" + add "Status" + end + + header unless @no_header + + rows peerings.map { |p| + [ + p.id, + p.name, + p.network_identifier, + p.peer_identifier, + p.cidr4, + p.status, + ] + } + end + + output << table.render << '\n' + end + end + + class PeeringCreate < PeeringAction + property platform : String? + property aws_account_id : String? + property aws_vpc_id : String? + property gcp_project_id : String? + property gcp_vpc_name : String? + + def validate + super + + raise Program::Error.new("Cannot use '--gcp-project-id' or '--gcp-vpc-name' if '--platform' is #{"aws".colorize.t_name}") if @platform == "aws" && (gcp_project_id || gcp_vpc_name) + raise Program::Error.new("Cannot use '--aws-account-id' or '--aws-vpc-id' if '--platform' is #{"gcp".colorize.t_name}") if @platform == "gcp" && (aws_account_id || aws_vpc_id) + + check_required_args do |missing| + case platform + when "aws" + missing << "aws-account-id" unless aws_account_id + missing << "aws-vpc-id" unless aws_vpc_id + when "gcp" + missing << "gcp-project-id" unless gcp_project_id + missing << "gcp-vpc-name" unless gcp_vpc_name + when nil + missing << "platform" unless platform + end + end + end + + def run + validate + + peer_identifier = make_peer_identifier.to_s + params = CB::Client::PeeringCreateParams.new peer_identifier: peer_identifier + peering = client.create_peering(@network_id, params) + + display([peering]) + end + + def make_peer_identifier + case @platform + when "aws" + region = client.get_network(@network_id).region_id + "arn:aws:ec2:#{region}:#{@aws_account_id}:vpc/#{@aws_vpc_id}" + when "gcp" + "https://www.googleapis.com/compute/v1/projects/#{gcp_project_id}/global/networks/#{gcp_vpc_name}" + end + end + end + + class PeeringDelete < PeeringAction + eid_setter peering_id + + def validate + check_required_args do |missing| + missing << "peering" unless peering_id + end + end + + def run + validate + + peering = client.delete_peering(@network_id, @peering_id) + display([peering]) + end + end + + class PeeringGet < PeeringAction + eid_setter peering_id + + def validate + check_required_args do |missing| + missing << "peering" unless peering_id + end + end + + def run + validate + + peering = client.get_peering(@network_id, @peering_id) + display([peering]) + end + end + + class PeeringList < PeeringAction + def run + validate + + peerings = client.list_peerings(@network_id) + display(peerings) + end + end +end diff --git a/src/cli.cr b/src/cli.cr index 8394216..55b9e11 100755 --- a/src/cli.cr +++ b/src/cli.cr @@ -492,7 +492,7 @@ op = OptionParser.new do |parser| # parser.on("network", "Manage networks") do - parser.banner = "cb network " + parser.banner = "cb network" parser.on("add-firewall-rule", "Add a firewall rule to a network") do add = set_action FirewallRuleAdd @@ -520,6 +520,99 @@ op = OptionParser.new do |parser| EXAMPLES end + parser.on("create-peering", "Create a new peering for a network") do + create = set_action PeeringCreate + + parser.banner = "cb network create-peering <--network> <--platform>" + + parser.on("--platform NAME", "Cloud provider") { |arg| create.platform = arg } + + # AWS peering options. + parser.on("--aws-account-id ID", "The AWS account id") { |arg| create.aws_account_id = arg } + parser.on("--aws-vpc-id ID", "The AWS VPC id") { |arg| create.aws_vpc_id = arg } + + # GCP peering options. + parser.on("--gcp-project-id ID", "The GCP project id") { |arg| create.gcp_project_id = arg } + parser.on("--gcp-vpc-name NAME", "The GCP VPC name") { |arg| create.gcp_vpc_name = arg } + + parser.on("--format FORMAT", "Output format (default: table)") { |arg| create.format = arg } + parser.on("--network ID", "The target network") { |arg| create.network_id = arg } + parser.on("--no-header", "Do not display table header") { create.no_header = true } + + parser.examples = <<-EXAMPLES + Create a new peering with AWS VPC. + $ cb network create-peering --platform aws --network --aws-account-id --aws-vpc-id + + Create a new peering with GCP VPC. + $ cb network create-peering --platform gcp --network --gcp-project-id --gcp-vpc-name + EXAMPLES + end + + parser.on("delete-peering", "Delete an existing peering for a network") do + delete = set_action PeeringDelete + + parser.banner = "cb network delete-peering <--network> <--peering>" + + parser.on("--format FORMAT", "Output format (default: table)") { |arg| delete.format = arg } + parser.on("--network ID", "The target network") { |arg| delete.network_id = arg } + parser.on("--no-header", "Do not display table header") { delete.no_header = true } + parser.on("--peering ID", "The ID of the peering") { |arg| delete.peering_id = arg } + + parser.examples = <<-EXAMPLES + Delete an existing peering. + $ cb network delete-peering --network --peering + EXAMPLES + end + + parser.on("get-peering", "Get an existing peering for a network") do + get = set_action PeeringGet + + parser.banner = "cb network get-peering <--network> <--peering>" + + parser.on("--format FORMAT", "Output format (default: table)") { |arg| get.format = arg } + parser.on("--network ID", "The target network") { |arg| get.network_id = arg } + parser.on("--no-header", "Do not display table header") { get.no_header = true } + parser.on("--peering ID", "The ID of the peering") { |arg| get.peering_id = arg } + + parser.examples = <<-EXAMPLES + Get an existing peering. Output: table + $ cb network get-peering --network --peering + + Get an existing peering. Output: table without header + $ cb network get-peering --network --peering --no-header + + Get an existing peering. Output: list + $ cb network get-peering --network --peering --format list + + Get an existing peering. Output: json + $ cb network get-peering --network --peering --format json + EXAMPLES + end + + parser.on("list-peerings", "List existing peerings for a network") do + list = set_action PeeringList + + parser.banner = "cb network list-peerings <--network>" + + 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 existing peerings. Output: table + $ cb network list-peerings --network + + List existing peerings. Output: table without header + $ cb network list-peerings --network --no-header + + List existing peerings. Output: list + $ cb network list-peerings --network --format list + + List existing peerings. Output: json + $ cb network list-peerings --network --format json + EXAMPLES + end + parser.on("list-firewall-rules", "List all firewall rules for a network") do list = set_action FirewallRuleList diff --git a/src/client/peering.cr b/src/client/peering.cr new file mode 100644 index 0000000..c6e9ffe --- /dev/null +++ b/src/client/peering.cr @@ -0,0 +1,30 @@ +require "json" + +require "./client" + +module CB + class Client + jrecord PeeringCreateParams, + peer_identifier : String + + def create_peering(network_id, params : PeeringCreateParams) + resp = post "networks/#{network_id}/peerings", params + CB::Model::Peering.from_json resp.body + end + + def delete_peering(network_id, peering_id) + resp = delete "networks/#{network_id}/peerings/#{peering_id}" + CB::Model::Peering.from_json resp.body + end + + def get_peering(network_id, peering_id) + resp = get "networks/#{network_id}/peerings/#{peering_id}" + CB::Model::Peering.from_json resp.body + end + + def list_peerings(network_id) + resp = get "networks/#{network_id}/peerings" + Array(CB::Model::Peering).from_json resp.body, root: "peerings" + end + end +end diff --git a/src/models/peering.cr b/src/models/peering.cr new file mode 100644 index 0000000..a17fee6 --- /dev/null +++ b/src/models/peering.cr @@ -0,0 +1,10 @@ +module CB::Model + jrecord Peering, + id : String, + cidr4 : String, + name : String?, + network_id : String, + network_identifier : String, + peer_identifier : String, + status : String +end