From 96abf2b6a02efdf485be798f220e5e9af2a18249 Mon Sep 17 00:00:00 2001 From: Josh Russett Date: Wed, 2 Aug 2023 19:41:46 +0000 Subject: [PATCH] Bring `silk` src into `silk-release` We had originally setup silk in an enitre other repo with the intention that it could be imported, used, and extended by other projects. This never happened and now we have a separate repo for `silk` when it is only used in `silk-release. This commit takes [code.cloudfoundry.org/silk at commit 8e48b8d1e6812ccd3da7c93fb30728ad1d9000ed](https://github.com/cloudfoundry/silk/commit/8e48b8d1e6812ccd3da7c93fb30728ad1d9000ed), and folds it into `silk-release` under `src/code.cloudfoundry.org/silk`. Other changes worth mentioning: - `scripts/test.sh` and `scripts/start-db-helper` taken from `cf-networking`, e.g. https://github.com/cloudfoundry/cf-networking-release/blob/develop/scripts/test.sh - the `silk` tests now require some databases to run - removes scripts from the `silk` source repo - Updates bosh packaging specs for the new pakage import path - Consolidates `modules/modules.go` and `tools/tools.go` since they seem to do the same thing and we don't need to import the silk stuff Addresses: https://github.com/cloudfoundry/silk/issues/52 [#185149418](https://www.pivotaltracker.com/story/show/185149418) Signed-off-by: Renee Chu --- packages/silk-cni/spec | 20 +- packages/silk-controller/spec | 16 +- packages/silk-daemon/spec | 22 +- scripts/docker-shell | 9 +- scripts/start-db-helper | 30 + scripts/test.sh | 116 +- src/code.cloudfoundry.org/go.mod | 15 +- src/code.cloudfoundry.org/go.sum | 2 - src/code.cloudfoundry.org/modules/modules.go | 15 - src/code.cloudfoundry.org/silk/.gitignore | 4 + .../code.cloudfoundry.org => }/silk/LICENSE | 0 .../code.cloudfoundry.org => }/silk/NOTICE | 0 src/code.cloudfoundry.org/silk/README.md | 44 + .../silk/client/config/config.go | 0 .../silk/client/config/config_suite_test.go | 13 + .../silk/client/config/config_test.go | 110 ++ .../silk/cmd/silk-cni/main.go | 0 .../silk/cmd/silk-controller/main.go | 0 .../silk/cmd/silk-daemon/main.go | 0 .../silk/cmd/silk-teardown/main.go | 0 .../silk/cni/adapter/ip_adapter.go | 0 .../silk/cni/adapter/namespace_adapter.go | 0 .../silk/cni/adapter/sysctl_adapter.go | 0 .../silk/cni/config/config.go | 0 .../silk/cni/config/config_creator.go | 0 .../silk/cni/config/config_creator_test.go | 185 +++ .../silk/cni/config/config_suite_test.go | 13 + .../silk/cni/config/config_test.go | 69 ++ .../silk/cni/config/device_name_generator.go | 0 .../cni/config/device_name_generator_test.go | 62 + .../cni/config/fakes/deviceNameGenerator.go | 166 +++ .../config/fakes/hardwareAddressGenerator.go | 166 +++ .../silk/cni/config/fakes/namespaceAdapter.go | 157 +++ .../silk/cni/config/fakes/netNS.go | 300 +++++ .../cni/config/hardware_address_generator.go | 0 .../silk/cni/config/ipam.go | 0 .../silk/cni/config/ipam_test.go | 44 + .../silk/cni/integration/error_cases_test.go | 216 ++++ .../silk/cni/integration/fake_daemon/main.go | 44 + .../cni/integration/integration_suite_test.go | 65 + .../silk/cni/integration/integration_test.go | 832 +++++++++++++ .../silk/cni/lib/common.go | 0 .../silk/cni/lib/common_test.go | 222 ++++ .../silk/cni/lib/container_setup.go | 0 .../silk/cni/lib/container_setup_test.go | 148 +++ .../silk/cni/lib/fakes/common.go | 100 ++ .../silk/cni/lib/fakes/deviceNameGenerator.go | 100 ++ .../silk/cni/lib/fakes/linkOperations.go | 532 +++++++++ .../silk/cni/lib/fakes/namespaceAdapter.go | 157 +++ .../silk/cni/lib/fakes/netNS.go | 300 +++++ .../silk/cni/lib/fakes/netlinkAdapter.go | 1031 ++++++++++++++++ .../silk/cni/lib/fakes/sysctlAdapter.go | 101 ++ .../silk/cni/lib/host_setup.go | 0 .../silk/cni/lib/host_setup_test.go | 91 ++ .../silk/cni/lib/interface.go | 0 .../silk/cni/lib/lib_suite_test.go | 13 + .../silk/cni/lib/link_operations.go | 0 .../silk/cni/lib/link_operations_test.go | 369 ++++++ .../silk/cni/lib/pair_creator.go | 0 .../silk/cni/lib/pair_creator_test.go | 138 +++ .../silk/cni/netinfo/daemon.go | 0 .../silk/cni/netinfo/daemon_test.go | 56 + .../silk/cni/netinfo/discover.go | 0 .../silk/cni/netinfo/discover_test.go | 66 ++ .../silk/cni/netinfo/fakes/netinfo.go | 87 ++ .../silk/cni/netinfo/flannel.go | 0 .../silk/cni/netinfo/flannel_test.go | 91 ++ .../silk/cni/netinfo/netinfo_suite_test.go | 13 + .../silk/control-plane.png | Bin 0 -> 34142 bytes .../silk/controller/client.go | 0 .../silk/controller/client_test.go | 248 ++++ .../silk/controller/config/config.go | 0 .../controller/config/config_suite_test.go | 13 + .../silk/controller/config/config_test.go | 118 ++ .../silk/controller/controller_suite_test.go | 13 + .../controller/database/database_handler.go | 0 .../database/database_handler_test.go | 832 +++++++++++++ .../database/database_suite_test.go | 19 + .../database/fakes/database_migrator.go | 89 ++ .../silk/controller/database/fakes/db.go | 401 +++++++ .../database/fakes/migrateAdapter.go | 108 ++ .../controller/database/fakes/sqlResult.go | 145 +++ .../controller/database/migrate_adapter.go | 0 .../silk/controller/database/migrator.go | 0 .../silk/controller/database/migrator_test.go | 56 + .../handlers/fakes/database_checker.go | 84 ++ .../handlers/fakes/error_response.go | 147 +++ .../fakes/hardwareAddressGenerator.go | 96 ++ .../handlers/fakes/lease_acquirer.go | 103 ++ .../handlers/fakes/lease_releaser.go | 94 ++ .../handlers/fakes/lease_renewer.go | 96 ++ .../handlers/fakes/lease_repository.go | 91 ++ .../handlers/handlers_suite_test.go | 32 + .../silk/controller/handlers/health.go | 0 .../silk/controller/handlers/health_test.go | 73 ++ .../silk/controller/handlers/lease_acquire.go | 0 .../controller/handlers/lease_acquire_test.go | 214 ++++ .../silk/controller/handlers/leases_index.go | 0 .../controller/handlers/leases_index_test.go | 120 ++ .../handlers/loggable_handler_wrapper.go | 0 .../handlers/loggable_handler_wrapper_test.go | 57 + .../silk/controller/handlers/release_lease.go | 0 .../controller/handlers/release_lease_test.go | 124 ++ .../silk/controller/handlers/renew_lease.go | 0 .../controller/handlers/renew_lease_test.go | 148 +++ .../controller/integration/fixtures/ca.crl | 16 + .../controller/integration/fixtures/ca.crt | 28 + .../controller/integration/fixtures/ca.key | 51 + .../integration/fixtures/client.crt | 25 + .../integration/fixtures/client.csr | 16 + .../integration/fixtures/client.key | 27 + .../integration/fixtures/regenerate-certs.sh | 20 + .../integration/fixtures/server.crt | 25 + .../integration/fixtures/server.csr | 16 + .../integration/fixtures/server.key | 27 + .../controller/integration/helpers/helpers.go | 109 ++ .../integration/integration_suite_test.go | 44 + .../integration/integration_test.go | 542 +++++++++ .../silk/controller/leaser/cidrpool.go | 0 .../silk/controller/leaser/cidrpool_test.go | 213 ++++ .../silk/controller/leaser/fakes/cidr_pool.go | 226 ++++ .../leaser/fakes/database_handler.go | 716 +++++++++++ .../leaser/fakes/hardwareAddressGenerator.go | 100 ++ .../leaser/fakes/lease_validator.go | 96 ++ .../leaser/hardware_address_generator.go | 0 .../controller/leaser/lease_controller.go | 0 .../leaser/lease_controller_test.go | 593 ++++++++++ .../controller/leaser/lease_suite_test.go | 13 + .../silk/controller/leaser/validator.go | 0 .../silk/controller/leaser/validator_test.go | 60 + .../server_metrics/fakes/cidrPool.go | 84 ++ .../server_metrics/fakes/databaseHandler.go | 157 +++ .../server_metrics/server_metrics.go | 0 .../server_metrics_suite_test.go | 13 + .../server_metrics/server_metrics_test.go | 86 ++ .../silk/daemon/errors.go | 0 .../daemon/integration/error_cases_test.go | 317 +++++ .../integration/integration_suite_test.go | 75 ++ .../daemon/integration/integration_test.go | 695 +++++++++++ .../silk/daemon/network_info.go | 0 .../daemon/planner/fakes/controller_client.go | 148 +++ .../silk/daemon/planner/fakes/converger.go | 97 ++ .../planner/fakes/fatalErrorDetector.go | 115 ++ .../silk/daemon/planner/fakes/metricSender.go | 95 ++ .../daemon/planner/fatal_error_detector.go | 0 .../planner/fatal_error_detector_test.go | 66 ++ .../silk/daemon/planner/planner_suite_test.go | 13 + .../silk/daemon/planner/vxlan_planner.go | 0 .../silk/daemon/planner/vxlan_planner_test.go | 232 ++++ .../silk/daemon/poller/poller.go | 0 .../silk/daemon/poller/poller_suite_test.go | 13 + .../silk/daemon/poller/poller_test.go | 122 ++ .../silk/daemon/vtep/config_creator.go | 0 .../silk/daemon/vtep/config_creator_test.go | 218 ++++ .../silk/daemon/vtep/converger.go | 0 .../silk/daemon/vtep/converger_test.go | 439 +++++++ .../silk/daemon/vtep/factory.go | 0 .../silk/daemon/vtep/factory_test.go | 238 ++++ .../silk/daemon/vtep/fakes/netAdapter.go | 222 ++++ .../silk/daemon/vtep/fakes/netlinkAdapter.go | 1050 +++++++++++++++++ .../silk/daemon/vtep/vtep_suite_test.go | 13 + src/code.cloudfoundry.org/silk/data-plane.png | Bin 0 -> 147676 bytes .../silk/lib/adapter/net_adapter.go | 0 .../silk/lib/adapter/netlink_adapter.go | 0 .../silk/lib/datastore/datastore.go | 0 .../datastore/datastore_integration_test.go | 210 ++++ .../lib/datastore/datastore_suite_test.go | 13 + .../silk/lib/datastore/datastore_test.go | 234 ++++ .../silk/lib/fakes/datastore.go | 224 ++++ .../silk/lib/fakes/file_locker.go | 94 ++ .../silk/lib/fakes/overwriteable_file.go | 304 +++++ .../silk/lib/fakes/serializer.go | 160 +++ .../silk/lib/hwaddr/hwaddr.go | 0 .../silk/lib/hwaddr/hwaddr_suite_test.go | 13 + .../silk/lib/hwaddr/hwaddr_test.go | 41 + .../silk/lib/serial/file_serializer.go | 0 .../silk/lib/serial/file_serializer_test.go | 140 +++ .../silk/lib/serial/serializer_suite_test.go | 13 + .../teardown/integration/error_cases_test.go | 121 ++ .../integration/integration_suite_test.go | 74 ++ .../teardown/integration/integration_test.go | 133 +++ .../silk/testsupport/fakecontroller.go | 106 ++ src/code.cloudfoundry.org/tools/tools.go | 4 + .../fakes/http_client.go | 147 +++ .../fakes/json_client.go | 149 +++ .../cf-networking-helpers/fakes/marshaler.go | 116 ++ .../fakes/metrics_sender.go | 114 ++ .../fakes/mysql_adapter.go | 191 +++ .../cf-networking-helpers/fakes/sleeper.go | 73 ++ .../fakes/unmarshaler.go | 118 ++ .../plugins/pkg/testutils/bad_reader.go | 33 + .../plugins/pkg/testutils/cmd.go | 112 ++ .../plugins/pkg/testutils/dns.go | 59 + .../plugins/pkg/testutils/netns_linux.go | 176 +++ .../plugins/pkg/testutils/ping.go | 61 + .../plugins/pkg/testutils/testing.go | 54 + src/code.cloudfoundry.org/vendor/modules.txt | 27 +- 197 files changed, 20530 insertions(+), 126 deletions(-) create mode 100755 scripts/start-db-helper delete mode 100644 src/code.cloudfoundry.org/modules/modules.go create mode 100644 src/code.cloudfoundry.org/silk/.gitignore rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/LICENSE (100%) rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/NOTICE (100%) create mode 100644 src/code.cloudfoundry.org/silk/README.md rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/client/config/config.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/client/config/config_suite_test.go create mode 100644 src/code.cloudfoundry.org/silk/client/config/config_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cmd/silk-cni/main.go (100%) rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cmd/silk-controller/main.go (100%) rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cmd/silk-daemon/main.go (100%) rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cmd/silk-teardown/main.go (100%) rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cni/adapter/ip_adapter.go (100%) rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cni/adapter/namespace_adapter.go (100%) rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cni/adapter/sysctl_adapter.go (100%) rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cni/config/config.go (100%) rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cni/config/config_creator.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/cni/config/config_creator_test.go create mode 100644 src/code.cloudfoundry.org/silk/cni/config/config_suite_test.go create mode 100644 src/code.cloudfoundry.org/silk/cni/config/config_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cni/config/device_name_generator.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/cni/config/device_name_generator_test.go create mode 100644 src/code.cloudfoundry.org/silk/cni/config/fakes/deviceNameGenerator.go create mode 100644 src/code.cloudfoundry.org/silk/cni/config/fakes/hardwareAddressGenerator.go create mode 100644 src/code.cloudfoundry.org/silk/cni/config/fakes/namespaceAdapter.go create mode 100644 src/code.cloudfoundry.org/silk/cni/config/fakes/netNS.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cni/config/hardware_address_generator.go (100%) rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cni/config/ipam.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/cni/config/ipam_test.go create mode 100644 src/code.cloudfoundry.org/silk/cni/integration/error_cases_test.go create mode 100644 src/code.cloudfoundry.org/silk/cni/integration/fake_daemon/main.go create mode 100644 src/code.cloudfoundry.org/silk/cni/integration/integration_suite_test.go create mode 100644 src/code.cloudfoundry.org/silk/cni/integration/integration_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cni/lib/common.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/cni/lib/common_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cni/lib/container_setup.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/cni/lib/container_setup_test.go create mode 100644 src/code.cloudfoundry.org/silk/cni/lib/fakes/common.go create mode 100644 src/code.cloudfoundry.org/silk/cni/lib/fakes/deviceNameGenerator.go create mode 100644 src/code.cloudfoundry.org/silk/cni/lib/fakes/linkOperations.go create mode 100644 src/code.cloudfoundry.org/silk/cni/lib/fakes/namespaceAdapter.go create mode 100644 src/code.cloudfoundry.org/silk/cni/lib/fakes/netNS.go create mode 100644 src/code.cloudfoundry.org/silk/cni/lib/fakes/netlinkAdapter.go create mode 100644 src/code.cloudfoundry.org/silk/cni/lib/fakes/sysctlAdapter.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cni/lib/host_setup.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/cni/lib/host_setup_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cni/lib/interface.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/cni/lib/lib_suite_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cni/lib/link_operations.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/cni/lib/link_operations_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cni/lib/pair_creator.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/cni/lib/pair_creator_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cni/netinfo/daemon.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/cni/netinfo/daemon_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cni/netinfo/discover.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/cni/netinfo/discover_test.go create mode 100644 src/code.cloudfoundry.org/silk/cni/netinfo/fakes/netinfo.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/cni/netinfo/flannel.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/cni/netinfo/flannel_test.go create mode 100644 src/code.cloudfoundry.org/silk/cni/netinfo/netinfo_suite_test.go create mode 100644 src/code.cloudfoundry.org/silk/control-plane.png rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/controller/client.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/controller/client_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/controller/config/config.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/controller/config/config_suite_test.go create mode 100644 src/code.cloudfoundry.org/silk/controller/config/config_test.go create mode 100644 src/code.cloudfoundry.org/silk/controller/controller_suite_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/controller/database/database_handler.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/controller/database/database_handler_test.go create mode 100644 src/code.cloudfoundry.org/silk/controller/database/database_suite_test.go create mode 100644 src/code.cloudfoundry.org/silk/controller/database/fakes/database_migrator.go create mode 100644 src/code.cloudfoundry.org/silk/controller/database/fakes/db.go create mode 100644 src/code.cloudfoundry.org/silk/controller/database/fakes/migrateAdapter.go create mode 100644 src/code.cloudfoundry.org/silk/controller/database/fakes/sqlResult.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/controller/database/migrate_adapter.go (100%) rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/controller/database/migrator.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/controller/database/migrator_test.go create mode 100644 src/code.cloudfoundry.org/silk/controller/handlers/fakes/database_checker.go create mode 100644 src/code.cloudfoundry.org/silk/controller/handlers/fakes/error_response.go create mode 100644 src/code.cloudfoundry.org/silk/controller/handlers/fakes/hardwareAddressGenerator.go create mode 100644 src/code.cloudfoundry.org/silk/controller/handlers/fakes/lease_acquirer.go create mode 100644 src/code.cloudfoundry.org/silk/controller/handlers/fakes/lease_releaser.go create mode 100644 src/code.cloudfoundry.org/silk/controller/handlers/fakes/lease_renewer.go create mode 100644 src/code.cloudfoundry.org/silk/controller/handlers/fakes/lease_repository.go create mode 100644 src/code.cloudfoundry.org/silk/controller/handlers/handlers_suite_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/controller/handlers/health.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/controller/handlers/health_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/controller/handlers/lease_acquire.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/controller/handlers/lease_acquire_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/controller/handlers/leases_index.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/controller/handlers/leases_index_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/controller/handlers/loggable_handler_wrapper.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/controller/handlers/loggable_handler_wrapper_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/controller/handlers/release_lease.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/controller/handlers/release_lease_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/controller/handlers/renew_lease.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/controller/handlers/renew_lease_test.go create mode 100644 src/code.cloudfoundry.org/silk/controller/integration/fixtures/ca.crl create mode 100644 src/code.cloudfoundry.org/silk/controller/integration/fixtures/ca.crt create mode 100644 src/code.cloudfoundry.org/silk/controller/integration/fixtures/ca.key create mode 100644 src/code.cloudfoundry.org/silk/controller/integration/fixtures/client.crt create mode 100644 src/code.cloudfoundry.org/silk/controller/integration/fixtures/client.csr create mode 100644 src/code.cloudfoundry.org/silk/controller/integration/fixtures/client.key create mode 100755 src/code.cloudfoundry.org/silk/controller/integration/fixtures/regenerate-certs.sh create mode 100644 src/code.cloudfoundry.org/silk/controller/integration/fixtures/server.crt create mode 100644 src/code.cloudfoundry.org/silk/controller/integration/fixtures/server.csr create mode 100644 src/code.cloudfoundry.org/silk/controller/integration/fixtures/server.key create mode 100644 src/code.cloudfoundry.org/silk/controller/integration/helpers/helpers.go create mode 100644 src/code.cloudfoundry.org/silk/controller/integration/integration_suite_test.go create mode 100644 src/code.cloudfoundry.org/silk/controller/integration/integration_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/controller/leaser/cidrpool.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/controller/leaser/cidrpool_test.go create mode 100644 src/code.cloudfoundry.org/silk/controller/leaser/fakes/cidr_pool.go create mode 100644 src/code.cloudfoundry.org/silk/controller/leaser/fakes/database_handler.go create mode 100644 src/code.cloudfoundry.org/silk/controller/leaser/fakes/hardwareAddressGenerator.go create mode 100644 src/code.cloudfoundry.org/silk/controller/leaser/fakes/lease_validator.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/controller/leaser/hardware_address_generator.go (100%) rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/controller/leaser/lease_controller.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/controller/leaser/lease_controller_test.go create mode 100644 src/code.cloudfoundry.org/silk/controller/leaser/lease_suite_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/controller/leaser/validator.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/controller/leaser/validator_test.go create mode 100644 src/code.cloudfoundry.org/silk/controller/server_metrics/fakes/cidrPool.go create mode 100644 src/code.cloudfoundry.org/silk/controller/server_metrics/fakes/databaseHandler.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/controller/server_metrics/server_metrics.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/controller/server_metrics/server_metrics_suite_test.go create mode 100644 src/code.cloudfoundry.org/silk/controller/server_metrics/server_metrics_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/daemon/errors.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/daemon/integration/error_cases_test.go create mode 100644 src/code.cloudfoundry.org/silk/daemon/integration/integration_suite_test.go create mode 100644 src/code.cloudfoundry.org/silk/daemon/integration/integration_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/daemon/network_info.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/daemon/planner/fakes/controller_client.go create mode 100644 src/code.cloudfoundry.org/silk/daemon/planner/fakes/converger.go create mode 100644 src/code.cloudfoundry.org/silk/daemon/planner/fakes/fatalErrorDetector.go create mode 100644 src/code.cloudfoundry.org/silk/daemon/planner/fakes/metricSender.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/daemon/planner/fatal_error_detector.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/daemon/planner/fatal_error_detector_test.go create mode 100644 src/code.cloudfoundry.org/silk/daemon/planner/planner_suite_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/daemon/planner/vxlan_planner.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/daemon/planner/vxlan_planner_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/daemon/poller/poller.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/daemon/poller/poller_suite_test.go create mode 100644 src/code.cloudfoundry.org/silk/daemon/poller/poller_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/daemon/vtep/config_creator.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/daemon/vtep/config_creator_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/daemon/vtep/converger.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/daemon/vtep/converger_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/daemon/vtep/factory.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/daemon/vtep/factory_test.go create mode 100644 src/code.cloudfoundry.org/silk/daemon/vtep/fakes/netAdapter.go create mode 100644 src/code.cloudfoundry.org/silk/daemon/vtep/fakes/netlinkAdapter.go create mode 100644 src/code.cloudfoundry.org/silk/daemon/vtep/vtep_suite_test.go create mode 100644 src/code.cloudfoundry.org/silk/data-plane.png rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/lib/adapter/net_adapter.go (100%) rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/lib/adapter/netlink_adapter.go (100%) rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/lib/datastore/datastore.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/lib/datastore/datastore_integration_test.go create mode 100644 src/code.cloudfoundry.org/silk/lib/datastore/datastore_suite_test.go create mode 100644 src/code.cloudfoundry.org/silk/lib/datastore/datastore_test.go create mode 100644 src/code.cloudfoundry.org/silk/lib/fakes/datastore.go create mode 100644 src/code.cloudfoundry.org/silk/lib/fakes/file_locker.go create mode 100644 src/code.cloudfoundry.org/silk/lib/fakes/overwriteable_file.go create mode 100644 src/code.cloudfoundry.org/silk/lib/fakes/serializer.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/lib/hwaddr/hwaddr.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/lib/hwaddr/hwaddr_suite_test.go create mode 100644 src/code.cloudfoundry.org/silk/lib/hwaddr/hwaddr_test.go rename src/code.cloudfoundry.org/{vendor/code.cloudfoundry.org => }/silk/lib/serial/file_serializer.go (100%) create mode 100644 src/code.cloudfoundry.org/silk/lib/serial/file_serializer_test.go create mode 100644 src/code.cloudfoundry.org/silk/lib/serial/serializer_suite_test.go create mode 100644 src/code.cloudfoundry.org/silk/teardown/integration/error_cases_test.go create mode 100644 src/code.cloudfoundry.org/silk/teardown/integration/integration_suite_test.go create mode 100644 src/code.cloudfoundry.org/silk/teardown/integration/integration_test.go create mode 100644 src/code.cloudfoundry.org/silk/testsupport/fakecontroller.go create mode 100644 src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/http_client.go create mode 100644 src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/json_client.go create mode 100644 src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/marshaler.go create mode 100644 src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/metrics_sender.go create mode 100644 src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/mysql_adapter.go create mode 100644 src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/sleeper.go create mode 100644 src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/unmarshaler.go create mode 100644 src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/bad_reader.go create mode 100644 src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/cmd.go create mode 100644 src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/dns.go create mode 100644 src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/netns_linux.go create mode 100644 src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/ping.go create mode 100644 src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/testing.go diff --git a/packages/silk-cni/spec b/packages/silk-cni/spec index d267d236..67cf1ab1 100644 --- a/packages/silk-cni/spec +++ b/packages/silk-cni/spec @@ -28,16 +28,16 @@ files: - code.cloudfoundry.org/lib/rules/*.go # gosub-main-module - code.cloudfoundry.org/lib/serial/*.go # gosub-main-module - code.cloudfoundry.org/vendor/code.cloudfoundry.org/policy_client/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cmd/silk-cni/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/adapter/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/config/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/lib/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/netinfo/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/lib/adapter/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/lib/datastore/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/lib/hwaddr/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/lib/serial/*.go # gosub-main-module + - code.cloudfoundry.org/silk/cmd/silk-cni/*.go # gosub-main-module + - code.cloudfoundry.org/silk/cni/adapter/*.go # gosub-main-module + - code.cloudfoundry.org/silk/cni/config/*.go # gosub-main-module + - code.cloudfoundry.org/silk/cni/lib/*.go # gosub-main-module + - code.cloudfoundry.org/silk/cni/netinfo/*.go # gosub-main-module + - code.cloudfoundry.org/silk/daemon/*.go # gosub-main-module + - code.cloudfoundry.org/silk/lib/adapter/*.go # gosub-main-module + - code.cloudfoundry.org/silk/lib/datastore/*.go # gosub-main-module + - code.cloudfoundry.org/silk/lib/hwaddr/*.go # gosub-main-module + - code.cloudfoundry.org/silk/lib/serial/*.go # gosub-main-module - code.cloudfoundry.org/vendor/github.com/alexflint/go-filemutex/*.go # gosub-main-module - code.cloudfoundry.org/vendor/github.com/containernetworking/cni/pkg/invoke/*.go # gosub-main-module - code.cloudfoundry.org/vendor/github.com/containernetworking/cni/pkg/skel/*.go # gosub-main-module diff --git a/packages/silk-controller/spec b/packages/silk-controller/spec index a979b8f2..f0c3f8f8 100644 --- a/packages/silk-controller/spec +++ b/packages/silk-controller/spec @@ -20,14 +20,14 @@ files: - code.cloudfoundry.org/vendor/code.cloudfoundry.org/lager/v3/*.go # gosub-main-module - code.cloudfoundry.org/vendor/code.cloudfoundry.org/lager/v3/internal/truncate/*.go # gosub-main-module - code.cloudfoundry.org/vendor/code.cloudfoundry.org/lager/v3/lagerflags/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cmd/silk-controller/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/config/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/database/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/handlers/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/leaser/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/server_metrics/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/lib/hwaddr/*.go # gosub-main-module + - code.cloudfoundry.org/silk/cmd/silk-controller/*.go # gosub-main-module + - code.cloudfoundry.org/silk/controller/*.go # gosub-main-module + - code.cloudfoundry.org/silk/controller/config/*.go # gosub-main-module + - code.cloudfoundry.org/silk/controller/database/*.go # gosub-main-module + - code.cloudfoundry.org/silk/controller/handlers/*.go # gosub-main-module + - code.cloudfoundry.org/silk/controller/leaser/*.go # gosub-main-module + - code.cloudfoundry.org/silk/controller/server_metrics/*.go # gosub-main-module + - code.cloudfoundry.org/silk/lib/hwaddr/*.go # gosub-main-module - code.cloudfoundry.org/vendor/github.com/bmizerany/pat/*.go # gosub-main-module - code.cloudfoundry.org/vendor/github.com/cloudfoundry/dropsonde/*.go # gosub-main-module - code.cloudfoundry.org/vendor/github.com/cloudfoundry/dropsonde/emitter/*.go # gosub-main-module diff --git a/packages/silk-daemon/spec b/packages/silk-daemon/spec index 6eef2266..a8b9d74b 100644 --- a/packages/silk-daemon/spec +++ b/packages/silk-daemon/spec @@ -28,17 +28,17 @@ files: - code.cloudfoundry.org/silk-daemon-bootstrap/*.go # gosub-main-module - code.cloudfoundry.org/silk-daemon-bootstrap/config/*.go # gosub-main-module - code.cloudfoundry.org/silk-daemon-shutdown/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/client/config/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cmd/silk-daemon/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cmd/silk-teardown/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/planner/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/poller/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/vtep/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/lib/adapter/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/lib/datastore/*.go # gosub-main-module - - code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/lib/serial/*.go # gosub-main-module + - code.cloudfoundry.org/silk/client/config/*.go # gosub-main-module + - code.cloudfoundry.org/silk/cmd/silk-daemon/*.go # gosub-main-module + - code.cloudfoundry.org/silk/cmd/silk-teardown/*.go # gosub-main-module + - code.cloudfoundry.org/silk/controller/*.go # gosub-main-module + - code.cloudfoundry.org/silk/daemon/*.go # gosub-main-module + - code.cloudfoundry.org/silk/daemon/planner/*.go # gosub-main-module + - code.cloudfoundry.org/silk/daemon/poller/*.go # gosub-main-module + - code.cloudfoundry.org/silk/daemon/vtep/*.go # gosub-main-module + - code.cloudfoundry.org/silk/lib/adapter/*.go # gosub-main-module + - code.cloudfoundry.org/silk/lib/datastore/*.go # gosub-main-module + - code.cloudfoundry.org/silk/lib/serial/*.go # gosub-main-module - code.cloudfoundry.org/vendor/github.com/cloudfoundry/dropsonde/*.go # gosub-main-module - code.cloudfoundry.org/vendor/github.com/cloudfoundry/dropsonde/emitter/*.go # gosub-main-module - code.cloudfoundry.org/vendor/github.com/cloudfoundry/dropsonde/envelope_sender/*.go # gosub-main-module diff --git a/scripts/docker-shell b/scripts/docker-shell index 8a0e358a..6de1b492 100755 --- a/scripts/docker-shell +++ b/scripts/docker-shell @@ -20,7 +20,8 @@ docker run \ --rm \ -it \ --privileged \ - -v $PWD:/silk \ - -w /silk \ - $docker_image \ - /bin/bash "$@" + -v "${PWD}:/silk-release" \ + -e "DB=${db}" \ + -w /silk-release \ + "${docker_image}" \ + /bin/bash "${@}" diff --git a/scripts/start-db-helper b/scripts/start-db-helper new file mode 100755 index 00000000..9930f7a2 --- /dev/null +++ b/scripts/start-db-helper @@ -0,0 +1,30 @@ +#!/bin/bash + +function bootDB { + db="$1" + + if [ "${db}" = "postgres" ]; then + launchDB="(/postgres-entrypoint.sh postgres &> /var/log/postgres-boot.log) &" + testConnection="psql -h localhost -U postgres -c '\conninfo'" + elif [ "${db}" = "mysql" ] || [ "${db}" = "mysql-5.6" ] || [ "${db}" = "mysql8" ]; then + launchDB="(MYSQL_ROOT_PASSWORD=password /mysql-entrypoint.sh mysqld &> /var/log/mysql-boot.log) &" + testConnection="mysql -h localhost -u root -D mysql -e '\s;' --password='password'" + else + echo "skipping database" + return 0 + fi + + echo -n "booting ${db}" + eval "$launchDB" + for _ in $(seq 1 60); do + if eval "${testConnection}" &> /dev/null; then + echo "connection established to ${db}" + return 0 + fi + echo -n "." + sleep 1 + done + eval "${testConnection}" || true + echo "unable to connect to ${db}" + exit 1 +} diff --git a/scripts/test.sh b/scripts/test.sh index cee39141..2086d47e 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,74 +1,104 @@ #!/bin/bash -set -eu -set -o pipefail +specificied_package="${1}" + +set -e -u go version # so we see the version tested in CI -# In the cf-networking-and-silk-pr.yml pipeline, we need to run db-unit tests for cf-networking, but -# concourse doesn't have a way of conditionally adding jobs, so it will end up running db-unit tests -# against silk, which doesn't do anything other than run unit tests again, so we skip it here. -if [[ -n "${DB:-""}" ]]; then - echo "No DB specific silk tests have been defined. Skipping this step." - exit 0 -fi +SCRIPT_PATH="$(cd "$(dirname "${0}")" && pwd)" +. "${SCRIPT_PATH}/start-db-helper" + +cd "${SCRIPT_PATH}/.." -cd $(dirname $0)/.. +DB="${DB:-"notset"}" + +## Setting to other than 1 node will break cni-wrapper-plugin/integration +serial_nodes=1 declare -a serial_packages=( "src/code.cloudfoundry.org/cni-wrapper-plugin/integration" - "src/code.cloudfoundry.org/vxlan-policy-agent/integration/linux" - "src/code.cloudfoundry.org/silk-daemon-shutdown/integration" "src/code.cloudfoundry.org/silk-daemon-bootstrap/integration" + "src/code.cloudfoundry.org/silk-daemon-shutdown/integration" + "src/code.cloudfoundry.org/silk/cni/integration" + "src/code.cloudfoundry.org/vxlan-policy-agent/integration/linux" ) declare -a windows_packages=( "src/code.cloudfoundry.org/vxlan-policy-agent/integration/windows" ) -# get all git submodule paths | print only the path without the extra info | cut the "package root" for go | deduplicate -declare -a git_modules=($(git config --file .gitmodules --get-regexp path | awk '{ print $2 }' | cut -d'/' -f1,2 | sort -u)) +declare -a ignored_packages -declare -a packages=($(find src -type f -name "*_test.go" | xargs -L 1 -I{} dirname {} | sort -u)) +# gather ignored packages from exclude_packages +for pkg in $(echo "${exclude_packages:-""}" | jq -r .[]); do + ignored_packages+=("${pkg}") +done +# gather more ignored packages because they are windows code +for pkg in "${windows_packages[@]}"; do + ignored_packages+=("${pkg}") +done + +containsElement() { + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +test_package() { + local package=$1 + if [ ! -d "${package}" ]; then + return 0 + fi + shift + pushd "${package}" &>/dev/null + pwd + go run github.com/onsi/ginkgo/v2/ginkgo --race -randomize-all -randomize-suites -fail-fast \ + -ldflags="extldflags=-WL,--allow-multiple-definition" \ + "${@}"; + rc=$? + popd &>/dev/null + return "${rc}" +} + +bootDB "${DB}" + +declare -a packages +if [[ -n "${include_only:-""}" ]]; then + mapfile -t packages < <(echo "${include_only}" | jq -r .[]) +else + mapfile -t packages < <(find src -type f -name '*_test.go' -print0 | xargs -0 -L1 -I{} dirname {} | sort -u) +fi # filter out serial_packages from packages for i in "${serial_packages[@]}"; do - packages=(${packages[@]//*$i*}) + packages=("${packages[@]//*$i*}") done -# filter out windows_packages from packages -for i in "${windows_packages[@]}"; do - packages=(${packages[@]//*$i*}) +# filter out explicitly ignored packages +for i in "${ignored_packages[@]}"; do + packages=("${packages[@]//*$i*}") + serial_packages=("${serial_packages[@]//*$i*}") done -if [ "${1:-""}" = "" ]; then +if [[ -z "${specificied_package}" ]]; then + echo "testing packages: " "${packages[@]}" for dir in "${packages[@]}"; do - pushd "$dir" - go run github.com/onsi/ginkgo/v2/ginkgo -p --race --randomize-all --randomize-suites \ - -ldflags="-extldflags=-Wl,--allow-multiple-definition" \ - ${@:2} - popd + test_package "${dir}" -p done + echo "testing serial packages: " "${serial_packages[@]}" for dir in "${serial_packages[@]}"; do - pushd "$dir" - go run github.com/onsi/ginkgo/v2/ginkgo --race --randomize-all --randomize-suites --fail-fast \ - -ldflags="-extldflags=-Wl,--allow-multiple-definition" \ - ${@:2} - popd + test_package "${dir}" --nodes "${serial_nodes}" done else - dir="${@: -1}" - dir="${dir#./}" - for package in "${serial_packages[@]}"; do - if [[ "${dir##$package}" != "${dir}" ]]; then - go run github.com/onsi/ginkgo/v2/ginkgo --race --randomize-all --randomize-suites --fail-fast \ - -ldflags="-extldflags=-Wl,--allow-multiple-definition" \ - "${@}" - exit $? - fi - done - go run github.com/onsi/ginkgo/v2/ginkgo -p --race --randomize-all --randomize-suites --fail-fast --skip-package windows \ - -ldflags="-extldflags=-Wl,--allow-multiple-definition" \ - "${@}" + specificied_package="${specificied_package#./}" + if containsElement "${specificied_package}" "${serial_packages[@]}"; then + echo "testing serial package ${specificied_package}" + test_package "${specificied_package}" --nodes "${serial_nodes}" + else + echo "testing package ${specificied_package}" + test_package "${specificied_package}" -p + fi fi diff --git a/src/code.cloudfoundry.org/go.mod b/src/code.cloudfoundry.org/go.mod index c83f92a9..db09654f 100644 --- a/src/code.cloudfoundry.org/go.mod +++ b/src/code.cloudfoundry.org/go.mod @@ -17,19 +17,25 @@ require ( code.cloudfoundry.org/lager/v3 v3.0.2 code.cloudfoundry.org/policy_client v0.0.0-20230726190751-c4580e1b1f80 code.cloudfoundry.org/runtimeschema v0.0.0-20230323223330-5366865eed76 - code.cloudfoundry.org/silk v0.0.0-20230728161150-8e48b8d1e681 github.com/cloudfoundry/dropsonde v1.1.0 github.com/containernetworking/cni v1.1.2 github.com/containernetworking/plugins v1.3.0 github.com/coreos/go-iptables v0.6.0 + github.com/go-sql-driver/mysql v1.7.1 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/google/uuid v1.3.0 github.com/hashicorp/go-multierror v1.1.1 github.com/hpcloud/tail v1.0.0 + github.com/jmoiron/sqlx v1.3.5 + github.com/lib/pq v1.10.9 github.com/onsi/ginkgo/v2 v2.11.0 github.com/onsi/gomega v1.27.10 github.com/pivotal-cf-experimental/gomegamatchers v0.0.0-20180326192815-e36bfcc98c3a + github.com/rubenv/sql-migrate v1.5.2 github.com/tedsuo/ifrit v0.0.0-20230516164442-7862c310ad26 + github.com/tedsuo/rata v1.0.0 github.com/vishvananda/netlink v1.2.1-beta.2 + github.com/ziutek/utils v0.0.0-20190626152656-eb2a3b364d6c gopkg.in/validator.v2 v2.0.1 ) @@ -46,27 +52,20 @@ require ( github.com/cloudfoundry/sonde-go v0.0.0-20230710164515-a0a43d1dbbf8 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-logr/logr v1.2.4 // indirect - github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-test/deep v1.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/jackc/pgx v3.6.2+incompatible // indirect - github.com/jmoiron/sqlx v1.3.5 // indirect - github.com/lib/pq v1.10.9 // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/openzipkin/zipkin-go v0.4.1 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/rubenv/sql-migrate v1.5.2 // indirect github.com/safchain/ethtool v0.3.0 // indirect github.com/square/certstrap v1.3.0 // indirect - github.com/tedsuo/rata v1.0.0 // indirect github.com/vishvananda/netns v0.0.4 // indirect - github.com/ziutek/utils v0.0.0-20190626152656-eb2a3b364d6c // indirect go.step.sm/crypto v0.33.0 // indirect golang.org/x/crypto v0.11.0 // indirect golang.org/x/net v0.12.0 // indirect diff --git a/src/code.cloudfoundry.org/go.sum b/src/code.cloudfoundry.org/go.sum index f02b8ea9..28da7029 100644 --- a/src/code.cloudfoundry.org/go.sum +++ b/src/code.cloudfoundry.org/go.sum @@ -623,8 +623,6 @@ code.cloudfoundry.org/routing-info v0.0.0-20230612154656-079a27345e39 h1:oNbebj4 code.cloudfoundry.org/routing-info v0.0.0-20230612154656-079a27345e39/go.mod h1:ykLgqzJGV5PTkvxtfyOy8hcQy7wxPaoV5ZPyk74aqp8= code.cloudfoundry.org/runtimeschema v0.0.0-20180622181441-7dcd19348be6 h1:J08p1/LBnhv5BDDf0WLpHRyMJFCws3vd3fLCFL/iVnQ= code.cloudfoundry.org/runtimeschema v0.0.0-20180622181441-7dcd19348be6/go.mod h1:QSfnsqiGUdV3OOxqHxQkmUZwycGJNWYdredPgeGuGf4= -code.cloudfoundry.org/silk v0.0.0-20230728161150-8e48b8d1e681 h1:zSzoPyOnsPVsXtFMtHO6q8x7p/t/RuQBC4qfGJ/2fJ0= -code.cloudfoundry.org/silk v0.0.0-20230728161150-8e48b8d1e681/go.mod h1:t0kHfcURVs8b0c0IO+U4lQWC+jS7XZwOzugOLcUliQ4= code.cloudfoundry.org/tlsconfig v0.0.0-20200131000646-bbe0f8da39b3/go.mod h1:eTbFJpyXRGuFVyg5+oaj9B2eIbIc+0/kZjH8ftbtdew= code.cloudfoundry.org/tlsconfig v0.0.0-20230612153104-23c0622de227 h1:QYyb6Ur0Ys6FciDB3+8zCW3eVk7AxAs2++Foa5DAdt0= code.cloudfoundry.org/tlsconfig v0.0.0-20230612153104-23c0622de227/go.mod h1:C8SxvGRSutmgzV2FxH8Zwqz2Q8HsaAITQRQFKhlDzPw= diff --git a/src/code.cloudfoundry.org/modules/modules.go b/src/code.cloudfoundry.org/modules/modules.go deleted file mode 100644 index 186b1a08..00000000 --- a/src/code.cloudfoundry.org/modules/modules.go +++ /dev/null @@ -1,15 +0,0 @@ -// +build modules - -package modules - -import ( - _ "code.cloudfoundry.org/iptables-logger/cmd/iptables-logger" - _ "code.cloudfoundry.org/silk/cmd/silk-cni" - _ "code.cloudfoundry.org/silk/cmd/silk-controller" - _ "code.cloudfoundry.org/silk/cmd/silk-daemon" - _ "code.cloudfoundry.org/silk/cmd/silk-teardown" -) - -// imporing modules that are needed for building and testing this module -// these modules are not imported in code, but they build binaries -// that are needed at runtime diff --git a/src/code.cloudfoundry.org/silk/.gitignore b/src/code.cloudfoundry.org/silk/.gitignore new file mode 100644 index 00000000..8455cda9 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/.gitignore @@ -0,0 +1,4 @@ + +/pkg +/bin +bin diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/LICENSE b/src/code.cloudfoundry.org/silk/LICENSE similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/LICENSE rename to src/code.cloudfoundry.org/silk/LICENSE diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/NOTICE b/src/code.cloudfoundry.org/silk/NOTICE similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/NOTICE rename to src/code.cloudfoundry.org/silk/NOTICE diff --git a/src/code.cloudfoundry.org/silk/README.md b/src/code.cloudfoundry.org/silk/README.md new file mode 100644 index 00000000..f0f7e788 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/README.md @@ -0,0 +1,44 @@ +# Silk + +> Note: This repository should be imported as `code.cloudfoundry.org/silk`. + +Silk is an open-source, [CNI](https://github.com/containernetworking/cni/)-compatible container networking fabric. +It was inspired by the [flannel](https://github.com/coreos/flannel) VXLAN backend and designed to meet the strict +operational requirements of [Cloud Foundry](https://cloudfoundry.org/platform/). + +To see how Silk is used inside of Cloud Foundry, look at the [CF Networking Release](https://code.cloudfoundry.org/cf-networking-release). + + +## Architecture + +### Control plane + +Silk has three components: + +- `silk-controller` runs on at least one central node and manages IP subnet lease allocation across the cluster. + It is implemented as a stateless HTTP JSON API backed by a SQL database. + +- `silk-daemon` runs on each host in order to acquire and renew the subnet lease for the host by calling the `silk-controller` API. It also has an HTTP JSON API endpoint that serves the subnet lease information and also acts as a health check. + +- `silk-cni` is a short-lived program, executed by the container runner, to set up the network stack for a particular container. Before setting up the network stack for the container, it calls the `silk-daemon` API to check its health and retrieve the host's subnet information. + +![](control-plane.png) + + +### Data plane + +The Silk dataplane is a virtual L3 overlay network. Each container host is assigned a unique IP address range, +and each container gets a unique IP from that range. + +The virtual network is constructed from three primitives: +- Every host runs one virtual L3 router (via Linux routing). +- Each container on a host is connected to the host's virtual router via a dedicated virtual L2 segment, one segment per container (point-to-point over a virtual ethernet pair). +- A single shared [VXLAN](https://tools.ietf.org/html/rfc7348) segment connects all of the the virtual L3 routers. + +Although the shared VXLAN network carries L2 frames, containers are not connected to it directly. They only access the VXLAN segment via their host's virtual L3 router. Therefore, from a container's point of view, the container-to-container network carries L3 packets, not L2. + +![](data-plane.png) + +To provide multi-tenant network policy on top of this connectivity fabric, Cloud Foundry utilizes the +[VXLAN GBP](https://tools.ietf.org/html/draft-smith-vxlan-group-policy-03#section-2.1) extension to tag +egress packets with a policy identifier. Other network policy enforcement schemes are also possible. diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/client/config/config.go b/src/code.cloudfoundry.org/silk/client/config/config.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/client/config/config.go rename to src/code.cloudfoundry.org/silk/client/config/config.go diff --git a/src/code.cloudfoundry.org/silk/client/config/config_suite_test.go b/src/code.cloudfoundry.org/silk/client/config/config_suite_test.go new file mode 100644 index 00000000..3a00e569 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/client/config/config_suite_test.go @@ -0,0 +1,13 @@ +package config_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Client Config Suite") +} diff --git a/src/code.cloudfoundry.org/silk/client/config/config_test.go b/src/code.cloudfoundry.org/silk/client/config/config_test.go new file mode 100644 index 00000000..53b2a61c --- /dev/null +++ b/src/code.cloudfoundry.org/silk/client/config/config_test.go @@ -0,0 +1,110 @@ +package config_test + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + + "code.cloudfoundry.org/silk/client/config" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func cloneMap(original map[string]interface{}) map[string]interface{} { + new := map[string]interface{}{} + for k, v := range original { + new[k] = v + } + return new +} + +var _ = Describe("Config.LoadConfig", func() { + var ( + requiredFields map[string]interface{} + ) + + BeforeEach(func() { + requiredFields = map[string]interface{}{ + "underlay_ip": "1.2.3.4", + "subnet_prefix_length": 24, + "overlay_network": "10.255.0.0/16", + "health_check_port": 22222, + "vtep_name": "silk-vxlan", + "connectivity_server_url": "https://silk-controller.something", + "ca_cert_file": "/some/cert/file.pem", + "client_cert_file": "/some/client-cert/file.pem", + "client_key_file": "/some/client-key/file.pem", + "vni": 44, + "poll_interval": 5, + "debug_server_port": 22229, + "datastore": "/some/data-store-file.json", + "partition_tolerance_seconds": 25, + "client_timeout_seconds": 5, + "metron_port": 5435, + "vtep_port": 1234, + "log_prefix": "some-prefix", + } + }) + + It("does not error on a valid config", func() { + cfg := cloneMap(requiredFields) + + file, err := ioutil.TempFile(os.TempDir(), "config-") + Expect(err).NotTo(HaveOccurred()) + + Expect(json.NewEncoder(file).Encode(cfg)).To(Succeed()) + + _, err = config.LoadConfig(file.Name()) + Expect(err).NotTo(HaveOccurred()) + }) + + It("errors if a required field is not set", func() { + for fieldName, _ := range requiredFields { + cfg := cloneMap(requiredFields) + delete(cfg, fieldName) + + file, err := ioutil.TempFile(os.TempDir(), "config-") + Expect(err).NotTo(HaveOccurred()) + + Expect(json.NewEncoder(file).Encode(cfg)).To(Succeed()) + + By(fmt.Sprintf("checking that %s is required", fieldName)) + _, err = config.LoadConfig(file.Name()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(HavePrefix("invalid config:")) + } + }) + + Context("when single ip only is specified", func() { + It("sets SingleIPOnly", func() { + cfg := cloneMap(requiredFields) + cfg["single_ip_only"] = true + + file, err := ioutil.TempFile(os.TempDir(), "config-") + Expect(err).NotTo(HaveOccurred()) + + Expect(json.NewEncoder(file).Encode(cfg)).To(Succeed()) + + loadedConfig, err := config.LoadConfig(file.Name()) + Expect(err).NotTo(HaveOccurred()) + Expect(loadedConfig.SingleIPOnly).To(Equal(true)) + }) + }) + + Context("when vxlan_interface_name is specified", func() { + It("sets VxlanInterfaceName", func() { + cfg := cloneMap(requiredFields) + cfg["vxlan_interface_name"] = "something" + + file, err := ioutil.TempFile(os.TempDir(), "config-") + Expect(err).NotTo(HaveOccurred()) + + Expect(json.NewEncoder(file).Encode(cfg)).To(Succeed()) + + loadedConfig, err := config.LoadConfig(file.Name()) + Expect(err).NotTo(HaveOccurred()) + Expect(loadedConfig.VxlanInterfaceName).To(Equal("something")) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cmd/silk-cni/main.go b/src/code.cloudfoundry.org/silk/cmd/silk-cni/main.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cmd/silk-cni/main.go rename to src/code.cloudfoundry.org/silk/cmd/silk-cni/main.go diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cmd/silk-controller/main.go b/src/code.cloudfoundry.org/silk/cmd/silk-controller/main.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cmd/silk-controller/main.go rename to src/code.cloudfoundry.org/silk/cmd/silk-controller/main.go diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cmd/silk-daemon/main.go b/src/code.cloudfoundry.org/silk/cmd/silk-daemon/main.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cmd/silk-daemon/main.go rename to src/code.cloudfoundry.org/silk/cmd/silk-daemon/main.go diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cmd/silk-teardown/main.go b/src/code.cloudfoundry.org/silk/cmd/silk-teardown/main.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cmd/silk-teardown/main.go rename to src/code.cloudfoundry.org/silk/cmd/silk-teardown/main.go diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/adapter/ip_adapter.go b/src/code.cloudfoundry.org/silk/cni/adapter/ip_adapter.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/adapter/ip_adapter.go rename to src/code.cloudfoundry.org/silk/cni/adapter/ip_adapter.go diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/adapter/namespace_adapter.go b/src/code.cloudfoundry.org/silk/cni/adapter/namespace_adapter.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/adapter/namespace_adapter.go rename to src/code.cloudfoundry.org/silk/cni/adapter/namespace_adapter.go diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/adapter/sysctl_adapter.go b/src/code.cloudfoundry.org/silk/cni/adapter/sysctl_adapter.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/adapter/sysctl_adapter.go rename to src/code.cloudfoundry.org/silk/cni/adapter/sysctl_adapter.go diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/config/config.go b/src/code.cloudfoundry.org/silk/cni/config/config.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/config/config.go rename to src/code.cloudfoundry.org/silk/cni/config/config.go diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/config/config_creator.go b/src/code.cloudfoundry.org/silk/cni/config/config_creator.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/config/config_creator.go rename to src/code.cloudfoundry.org/silk/cni/config/config_creator.go diff --git a/src/code.cloudfoundry.org/silk/cni/config/config_creator_test.go b/src/code.cloudfoundry.org/silk/cni/config/config_creator_test.go new file mode 100644 index 00000000..902f23f7 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/config/config_creator_test.go @@ -0,0 +1,185 @@ +package config_test + +import ( + "errors" + "net" + + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/cni/config" + "code.cloudfoundry.org/silk/cni/config/fakes" + "github.com/containernetworking/cni/pkg/skel" + "github.com/containernetworking/cni/pkg/types" + current "github.com/containernetworking/cni/pkg/types/100" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ConfigCreator", func() { + Describe("Create", func() { + var ( + hostNS *fakes.NetNS + containerNS *fakes.NetNS + hostMAC net.HardwareAddr + containerMAC net.HardwareAddr + addCmdArgs *skel.CmdArgs + ipamResult *current.Result + fakeNamespaceAdapter *fakes.NamespaceAdapter + fakeHardwareAddressGenerator *fakes.HardwareAddressGenerator + fakeDeviceNameGenerator *fakes.DeviceNameGenerator + fakelogger *lagertest.TestLogger + configCreator *config.ConfigCreator + ) + BeforeEach(func() { + hostNS = &fakes.NetNS{} + containerNS = &fakes.NetNS{} + addCmdArgs = &skel.CmdArgs{ + Netns: "/some/container/namespace", + IfName: "eth0", + } + ipamResult = ¤t.Result{ + IPs: []*current.IPConfig{ + ¤t.IPConfig{ + Address: net.IPNet{ + IP: []byte{123, 124, 125, 126}, + Mask: []byte{255, 255, 255, 255}, + }, + Gateway: net.IP{10, 255, 0, 1}, + }, + }, + Routes: nil, + } + fakelogger = lagertest.NewTestLogger("test") + fakeNamespaceAdapter = &fakes.NamespaceAdapter{} + fakeHardwareAddressGenerator = &fakes.HardwareAddressGenerator{} + fakeDeviceNameGenerator = &fakes.DeviceNameGenerator{} + fakeNamespaceAdapter.GetNSReturns(containerNS, nil) + containerMAC, _ = net.ParseMAC("ee:ee:12:34:56:78") + hostMAC, _ = net.ParseMAC("aa:aa:12:34:56:78") + fakeHardwareAddressGenerator.GenerateForContainerReturns(containerMAC, nil) + fakeHardwareAddressGenerator.GenerateForHostReturns(hostMAC, nil) + fakeDeviceNameGenerator.GenerateForHostReturns("s-010255030004", nil) + fakeDeviceNameGenerator.GenerateTemporaryForContainerReturns("c-010255030004", nil) + containerNS.PathReturns("/some/container/namespace") + configCreator = &config.ConfigCreator{ + HardwareAddressGenerator: fakeHardwareAddressGenerator, + DeviceNameGenerator: fakeDeviceNameGenerator, + NamespaceAdapter: fakeNamespaceAdapter, + Logger: fakelogger, + } + }) + + It("creates a config with the desired container device metadata", func() { + conf, err := configCreator.Create(hostNS, addCmdArgs, ipamResult, 1450) + Expect(err).NotTo(HaveOccurred()) + + Expect(conf.Container.DeviceName).To(Equal("eth0")) + Expect(conf.Container.TemporaryDeviceName).To(Equal("c-010255030004")) + Expect(conf.Container.Namespace).To(Equal(containerNS)) + Expect(conf.Container.Address.IP).To(Equal(ipamResult.IPs[0].Address.IP)) + Expect(conf.Container.Address.Hardware).To(Equal(containerMAC)) + By("Adding a route with 169.254.0.1 as the gateway", func() { + Expect(conf.Container.Routes).To(ConsistOf([]*types.Route{ + &types.Route{ + Dst: net.IPNet{ + IP: net.IPv4zero, + Mask: net.CIDRMask(0, 32), + }, + GW: net.IP{169, 254, 0, 1}, + }, + })) + Expect(conf.Container.MTU).To(Equal(1450)) + }) + }) + + It("creates a config with the desired host device metadata", func() { + conf, err := configCreator.Create(hostNS, addCmdArgs, ipamResult, 1450) + Expect(err).NotTo(HaveOccurred()) + + Expect(conf.Host.DeviceName).To(Equal("s-010255030004")) + Expect(conf.Host.Namespace).To(Equal(hostNS)) + Expect(conf.Host.Address.IP).To(Equal(net.IP{169, 254, 0, 1})) + Expect(conf.Host.Address.Hardware).To(Equal(hostMAC)) + }) + + Context("when the args interface name is blank", func() { + BeforeEach(func() { + addCmdArgs.IfName = "" + }) + It("returns an error", func() { + _, err := configCreator.Create(hostNS, addCmdArgs, ipamResult, 1450) + Expect(err).To(MatchError("IfName cannot be empty")) + }) + }) + + Context("when the args interface name is longer than 15 characters", func() { + BeforeEach(func() { + addCmdArgs.IfName = "1234567890123456" + }) + It("returns an error", func() { + _, err := configCreator.Create(hostNS, addCmdArgs, ipamResult, 1450) + Expect(err).To(MatchError("IfName cannot be longer than 15 characters")) + }) + }) + + Context("when the container namespace cannot be found", func() { + BeforeEach(func() { + fakeNamespaceAdapter.GetNSReturns(nil, errors.New("banana")) + }) + It("returns an error", func() { + _, err := configCreator.Create(hostNS, addCmdArgs, ipamResult, 1450) + Expect(err).To(MatchError("getting container namespace: banana")) + }) + }) + + Context("when the hardware address generator fails for the container hw address", func() { + BeforeEach(func() { + fakeHardwareAddressGenerator.GenerateForContainerReturns(nil, errors.New("potato")) + }) + It("wraps and returns the error", func() { + _, err := configCreator.Create(hostNS, addCmdArgs, ipamResult, 1450) + Expect(err).To(MatchError("generating container veth hardware address: potato")) + }) + }) + + Context("when the hardware address generator fails for the host hw address", func() { + BeforeEach(func() { + fakeHardwareAddressGenerator.GenerateForHostReturns(nil, errors.New("potato")) + }) + It("wraps and returns the error", func() { + _, err := configCreator.Create(hostNS, addCmdArgs, ipamResult, 1450) + Expect(err).To(MatchError("generating host veth hardware address: potato")) + }) + }) + + Context("when the device name generator fails for the host device", func() { + BeforeEach(func() { + fakeDeviceNameGenerator.GenerateForHostReturns("", errors.New("potato")) + }) + It("wraps and returns the error", func() { + _, err := configCreator.Create(hostNS, addCmdArgs, ipamResult, 1450) + Expect(err).To(MatchError("generating host device name: potato")) + }) + }) + + Context("when the device name generator fails for the container's temporary name", func() { + BeforeEach(func() { + fakeDeviceNameGenerator.GenerateTemporaryForContainerReturns("", errors.New("potato")) + }) + It("wraps and returns the error", func() { + _, err := configCreator.Create(hostNS, addCmdArgs, ipamResult, 1450) + Expect(err).To(MatchError("generating temporary container device name: potato")) + }) + }) + + Context("when the IPAM result has no IP addresses", func() { + BeforeEach(func() { + ipamResult.IPs = []*current.IPConfig{} + }) + It("returns an error", func() { + _, err := configCreator.Create(hostNS, addCmdArgs, ipamResult, 1450) + Expect(err).To(MatchError("no IP address in IPAM result")) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/cni/config/config_suite_test.go b/src/code.cloudfoundry.org/silk/cni/config/config_suite_test.go new file mode 100644 index 00000000..5d33301d --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/config/config_suite_test.go @@ -0,0 +1,13 @@ +package config_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "CNI Config Suite") +} diff --git a/src/code.cloudfoundry.org/silk/cni/config/config_test.go b/src/code.cloudfoundry.org/silk/cni/config/config_test.go new file mode 100644 index 00000000..4a91dfd2 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/config/config_test.go @@ -0,0 +1,69 @@ +package config_test + +import ( + "net" + + "code.cloudfoundry.org/silk/cni/config" + "code.cloudfoundry.org/silk/cni/lib/fakes" + "github.com/containernetworking/cni/pkg/types" + current "github.com/containernetworking/cni/pkg/types/100" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Config", func() { + var cfg *config.Config + + BeforeEach(func() { + cfg = &config.Config{} + cfg.Container.DeviceName = "container-device-name" + fakeNamespace := &fakes.NetNS{} + fakeNamespace.PathReturns("/some/namespace") + cfg.Container.Namespace = fakeNamespace + cfg.Container.Address.IP = net.IP{10, 255, 30, 5} + cfg.Container.Address.Hardware = net.HardwareAddr{0x01, 0x02, 0x03, 0x0A, 0xBC, 0xDE} + cfg.Container.MTU = 1234 + cfg.Host.DeviceName = "host-device-name" + cfg.Host.Address.IP = net.IP{169, 254, 0, 1} + cfg.Host.Address.Hardware = net.HardwareAddr{0xdd, 0xdd, 0x03, 0x0A, 0xBC, 0xDE} + + cfg.Container.Routes = []*types.Route{ + &types.Route{ + Dst: net.IPNet{ + IP: net.IP{1, 1, 0, 0}, + Mask: []byte{255, 255, 255, 255}, + }, + GW: net.IP{1, 1, 0, 0}, + }, + } + }) + + AfterEach(func() { + cfg.Container.Namespace.Close() + }) + + Describe("AsCNIResult", func() { + It("returns a CNI v0.3.0 result that represents the config", func() { + result := cfg.AsCNIResult() + Expect(result.Interfaces).To(HaveLen(2)) + Expect(result.Interfaces[0]).To(Equal(¤t.Interface{ + Name: "host-device-name", + Mac: "dd:dd:03:0a:bc:de", + Sandbox: "", + })) + Expect(result.Interfaces[1]).To(Equal(¤t.Interface{ + Name: "container-device-name", + Mac: "01:02:03:0a:bc:de", + Sandbox: cfg.Container.Namespace.Path(), + })) + + Expect(result.IPs).To(HaveLen(1)) + index := result.IPs[0].Interface + Expect(result.Interfaces[*index].Name).To(Equal("container-device-name")) + Expect(result.IPs[0].Address.String()).To(Equal("10.255.30.5/32")) + Expect(result.IPs[0].Gateway.String()).To(Equal("169.254.0.1")) + + Expect(result.Routes).To(ConsistOf(cfg.Container.Routes)) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/config/device_name_generator.go b/src/code.cloudfoundry.org/silk/cni/config/device_name_generator.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/config/device_name_generator.go rename to src/code.cloudfoundry.org/silk/cni/config/device_name_generator.go diff --git a/src/code.cloudfoundry.org/silk/cni/config/device_name_generator_test.go b/src/code.cloudfoundry.org/silk/cni/config/device_name_generator_test.go new file mode 100644 index 00000000..3b451fe5 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/config/device_name_generator_test.go @@ -0,0 +1,62 @@ +package config_test + +import ( + "net" + + "code.cloudfoundry.org/silk/cni/config" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("DeviceNameGenerator", func() { + Describe("GenerateForHost", func() { + It("generates a valid Linux network device name from the given IPv4 address", func() { + g := config.DeviceNameGenerator{} + name, err := g.GenerateForHost(net.IP{10, 255, 30, 5}) + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(Equal("s-010255030005")) + }) + + Context("when given an IPv6 address", func() { + It("returns a meaningful error", func() { + g := config.DeviceNameGenerator{} + _, err := g.GenerateForHost(net.IPv6linklocalallnodes) + Expect(err).To(MatchError("generating device name: expecting valid IPv4 address")) + }) + }) + }) + + Describe("GenerateTemporaryForContainer", func() { + It("generates a device name that is distinct from the host device name", func() { + g := config.DeviceNameGenerator{} + name, err := g.GenerateTemporaryForContainer(net.IP{10, 255, 30, 5}) + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(Equal("c-010255030005")) + }) + + Context("when given an IPv6 address", func() { + It("returns a meaningful error", func() { + g := config.DeviceNameGenerator{} + _, err := g.GenerateTemporaryForContainer(net.IPv6linklocalallnodes) + Expect(err).To(MatchError("generating device name: expecting valid IPv4 address")) + }) + }) + }) + + Describe("GenerateForHostIFB", func() { + It("generates a device name that is distinct from the host device name", func() { + g := config.DeviceNameGenerator{} + name, err := g.GenerateForHostIFB(net.IP{10, 255, 30, 5}) + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(Equal("i-010255030005")) + }) + + Context("when given an IPv6 address", func() { + It("returns a meaningful error", func() { + g := config.DeviceNameGenerator{} + _, err := g.GenerateForHostIFB(net.IPv6linklocalallnodes) + Expect(err).To(MatchError("generating device name: expecting valid IPv4 address")) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/cni/config/fakes/deviceNameGenerator.go b/src/code.cloudfoundry.org/silk/cni/config/fakes/deviceNameGenerator.go new file mode 100644 index 00000000..2c570122 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/config/fakes/deviceNameGenerator.go @@ -0,0 +1,166 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "net" + "sync" +) + +type DeviceNameGenerator struct { + GenerateForHostStub func(containerIP net.IP) (string, error) + generateForHostMutex sync.RWMutex + generateForHostArgsForCall []struct { + containerIP net.IP + } + generateForHostReturns struct { + result1 string + result2 error + } + generateForHostReturnsOnCall map[int]struct { + result1 string + result2 error + } + GenerateTemporaryForContainerStub func(containerIP net.IP) (string, error) + generateTemporaryForContainerMutex sync.RWMutex + generateTemporaryForContainerArgsForCall []struct { + containerIP net.IP + } + generateTemporaryForContainerReturns struct { + result1 string + result2 error + } + generateTemporaryForContainerReturnsOnCall map[int]struct { + result1 string + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *DeviceNameGenerator) GenerateForHost(containerIP net.IP) (string, error) { + fake.generateForHostMutex.Lock() + ret, specificReturn := fake.generateForHostReturnsOnCall[len(fake.generateForHostArgsForCall)] + fake.generateForHostArgsForCall = append(fake.generateForHostArgsForCall, struct { + containerIP net.IP + }{containerIP}) + fake.recordInvocation("GenerateForHost", []interface{}{containerIP}) + fake.generateForHostMutex.Unlock() + if fake.GenerateForHostStub != nil { + return fake.GenerateForHostStub(containerIP) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.generateForHostReturns.result1, fake.generateForHostReturns.result2 +} + +func (fake *DeviceNameGenerator) GenerateForHostCallCount() int { + fake.generateForHostMutex.RLock() + defer fake.generateForHostMutex.RUnlock() + return len(fake.generateForHostArgsForCall) +} + +func (fake *DeviceNameGenerator) GenerateForHostArgsForCall(i int) net.IP { + fake.generateForHostMutex.RLock() + defer fake.generateForHostMutex.RUnlock() + return fake.generateForHostArgsForCall[i].containerIP +} + +func (fake *DeviceNameGenerator) GenerateForHostReturns(result1 string, result2 error) { + fake.GenerateForHostStub = nil + fake.generateForHostReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *DeviceNameGenerator) GenerateForHostReturnsOnCall(i int, result1 string, result2 error) { + fake.GenerateForHostStub = nil + if fake.generateForHostReturnsOnCall == nil { + fake.generateForHostReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.generateForHostReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *DeviceNameGenerator) GenerateTemporaryForContainer(containerIP net.IP) (string, error) { + fake.generateTemporaryForContainerMutex.Lock() + ret, specificReturn := fake.generateTemporaryForContainerReturnsOnCall[len(fake.generateTemporaryForContainerArgsForCall)] + fake.generateTemporaryForContainerArgsForCall = append(fake.generateTemporaryForContainerArgsForCall, struct { + containerIP net.IP + }{containerIP}) + fake.recordInvocation("GenerateTemporaryForContainer", []interface{}{containerIP}) + fake.generateTemporaryForContainerMutex.Unlock() + if fake.GenerateTemporaryForContainerStub != nil { + return fake.GenerateTemporaryForContainerStub(containerIP) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.generateTemporaryForContainerReturns.result1, fake.generateTemporaryForContainerReturns.result2 +} + +func (fake *DeviceNameGenerator) GenerateTemporaryForContainerCallCount() int { + fake.generateTemporaryForContainerMutex.RLock() + defer fake.generateTemporaryForContainerMutex.RUnlock() + return len(fake.generateTemporaryForContainerArgsForCall) +} + +func (fake *DeviceNameGenerator) GenerateTemporaryForContainerArgsForCall(i int) net.IP { + fake.generateTemporaryForContainerMutex.RLock() + defer fake.generateTemporaryForContainerMutex.RUnlock() + return fake.generateTemporaryForContainerArgsForCall[i].containerIP +} + +func (fake *DeviceNameGenerator) GenerateTemporaryForContainerReturns(result1 string, result2 error) { + fake.GenerateTemporaryForContainerStub = nil + fake.generateTemporaryForContainerReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *DeviceNameGenerator) GenerateTemporaryForContainerReturnsOnCall(i int, result1 string, result2 error) { + fake.GenerateTemporaryForContainerStub = nil + if fake.generateTemporaryForContainerReturnsOnCall == nil { + fake.generateTemporaryForContainerReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.generateTemporaryForContainerReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *DeviceNameGenerator) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.generateForHostMutex.RLock() + defer fake.generateForHostMutex.RUnlock() + fake.generateTemporaryForContainerMutex.RLock() + defer fake.generateTemporaryForContainerMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *DeviceNameGenerator) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/cni/config/fakes/hardwareAddressGenerator.go b/src/code.cloudfoundry.org/silk/cni/config/fakes/hardwareAddressGenerator.go new file mode 100644 index 00000000..73802bf2 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/config/fakes/hardwareAddressGenerator.go @@ -0,0 +1,166 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "net" + "sync" +) + +type HardwareAddressGenerator struct { + GenerateForContainerStub func(containerIP net.IP) (net.HardwareAddr, error) + generateForContainerMutex sync.RWMutex + generateForContainerArgsForCall []struct { + containerIP net.IP + } + generateForContainerReturns struct { + result1 net.HardwareAddr + result2 error + } + generateForContainerReturnsOnCall map[int]struct { + result1 net.HardwareAddr + result2 error + } + GenerateForHostStub func(containerIP net.IP) (net.HardwareAddr, error) + generateForHostMutex sync.RWMutex + generateForHostArgsForCall []struct { + containerIP net.IP + } + generateForHostReturns struct { + result1 net.HardwareAddr + result2 error + } + generateForHostReturnsOnCall map[int]struct { + result1 net.HardwareAddr + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *HardwareAddressGenerator) GenerateForContainer(containerIP net.IP) (net.HardwareAddr, error) { + fake.generateForContainerMutex.Lock() + ret, specificReturn := fake.generateForContainerReturnsOnCall[len(fake.generateForContainerArgsForCall)] + fake.generateForContainerArgsForCall = append(fake.generateForContainerArgsForCall, struct { + containerIP net.IP + }{containerIP}) + fake.recordInvocation("GenerateForContainer", []interface{}{containerIP}) + fake.generateForContainerMutex.Unlock() + if fake.GenerateForContainerStub != nil { + return fake.GenerateForContainerStub(containerIP) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.generateForContainerReturns.result1, fake.generateForContainerReturns.result2 +} + +func (fake *HardwareAddressGenerator) GenerateForContainerCallCount() int { + fake.generateForContainerMutex.RLock() + defer fake.generateForContainerMutex.RUnlock() + return len(fake.generateForContainerArgsForCall) +} + +func (fake *HardwareAddressGenerator) GenerateForContainerArgsForCall(i int) net.IP { + fake.generateForContainerMutex.RLock() + defer fake.generateForContainerMutex.RUnlock() + return fake.generateForContainerArgsForCall[i].containerIP +} + +func (fake *HardwareAddressGenerator) GenerateForContainerReturns(result1 net.HardwareAddr, result2 error) { + fake.GenerateForContainerStub = nil + fake.generateForContainerReturns = struct { + result1 net.HardwareAddr + result2 error + }{result1, result2} +} + +func (fake *HardwareAddressGenerator) GenerateForContainerReturnsOnCall(i int, result1 net.HardwareAddr, result2 error) { + fake.GenerateForContainerStub = nil + if fake.generateForContainerReturnsOnCall == nil { + fake.generateForContainerReturnsOnCall = make(map[int]struct { + result1 net.HardwareAddr + result2 error + }) + } + fake.generateForContainerReturnsOnCall[i] = struct { + result1 net.HardwareAddr + result2 error + }{result1, result2} +} + +func (fake *HardwareAddressGenerator) GenerateForHost(containerIP net.IP) (net.HardwareAddr, error) { + fake.generateForHostMutex.Lock() + ret, specificReturn := fake.generateForHostReturnsOnCall[len(fake.generateForHostArgsForCall)] + fake.generateForHostArgsForCall = append(fake.generateForHostArgsForCall, struct { + containerIP net.IP + }{containerIP}) + fake.recordInvocation("GenerateForHost", []interface{}{containerIP}) + fake.generateForHostMutex.Unlock() + if fake.GenerateForHostStub != nil { + return fake.GenerateForHostStub(containerIP) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.generateForHostReturns.result1, fake.generateForHostReturns.result2 +} + +func (fake *HardwareAddressGenerator) GenerateForHostCallCount() int { + fake.generateForHostMutex.RLock() + defer fake.generateForHostMutex.RUnlock() + return len(fake.generateForHostArgsForCall) +} + +func (fake *HardwareAddressGenerator) GenerateForHostArgsForCall(i int) net.IP { + fake.generateForHostMutex.RLock() + defer fake.generateForHostMutex.RUnlock() + return fake.generateForHostArgsForCall[i].containerIP +} + +func (fake *HardwareAddressGenerator) GenerateForHostReturns(result1 net.HardwareAddr, result2 error) { + fake.GenerateForHostStub = nil + fake.generateForHostReturns = struct { + result1 net.HardwareAddr + result2 error + }{result1, result2} +} + +func (fake *HardwareAddressGenerator) GenerateForHostReturnsOnCall(i int, result1 net.HardwareAddr, result2 error) { + fake.GenerateForHostStub = nil + if fake.generateForHostReturnsOnCall == nil { + fake.generateForHostReturnsOnCall = make(map[int]struct { + result1 net.HardwareAddr + result2 error + }) + } + fake.generateForHostReturnsOnCall[i] = struct { + result1 net.HardwareAddr + result2 error + }{result1, result2} +} + +func (fake *HardwareAddressGenerator) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.generateForContainerMutex.RLock() + defer fake.generateForContainerMutex.RUnlock() + fake.generateForHostMutex.RLock() + defer fake.generateForHostMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *HardwareAddressGenerator) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/cni/config/fakes/namespaceAdapter.go b/src/code.cloudfoundry.org/silk/cni/config/fakes/namespaceAdapter.go new file mode 100644 index 00000000..ea7d7ec4 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/config/fakes/namespaceAdapter.go @@ -0,0 +1,157 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + + "github.com/containernetworking/plugins/pkg/ns" +) + +type NamespaceAdapter struct { + GetNSStub func(string) (ns.NetNS, error) + getNSMutex sync.RWMutex + getNSArgsForCall []struct { + arg1 string + } + getNSReturns struct { + result1 ns.NetNS + result2 error + } + getNSReturnsOnCall map[int]struct { + result1 ns.NetNS + result2 error + } + GetCurrentNSStub func() (ns.NetNS, error) + getCurrentNSMutex sync.RWMutex + getCurrentNSArgsForCall []struct{} + getCurrentNSReturns struct { + result1 ns.NetNS + result2 error + } + getCurrentNSReturnsOnCall map[int]struct { + result1 ns.NetNS + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *NamespaceAdapter) GetNS(arg1 string) (ns.NetNS, error) { + fake.getNSMutex.Lock() + ret, specificReturn := fake.getNSReturnsOnCall[len(fake.getNSArgsForCall)] + fake.getNSArgsForCall = append(fake.getNSArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("GetNS", []interface{}{arg1}) + fake.getNSMutex.Unlock() + if fake.GetNSStub != nil { + return fake.GetNSStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.getNSReturns.result1, fake.getNSReturns.result2 +} + +func (fake *NamespaceAdapter) GetNSCallCount() int { + fake.getNSMutex.RLock() + defer fake.getNSMutex.RUnlock() + return len(fake.getNSArgsForCall) +} + +func (fake *NamespaceAdapter) GetNSArgsForCall(i int) string { + fake.getNSMutex.RLock() + defer fake.getNSMutex.RUnlock() + return fake.getNSArgsForCall[i].arg1 +} + +func (fake *NamespaceAdapter) GetNSReturns(result1 ns.NetNS, result2 error) { + fake.GetNSStub = nil + fake.getNSReturns = struct { + result1 ns.NetNS + result2 error + }{result1, result2} +} + +func (fake *NamespaceAdapter) GetNSReturnsOnCall(i int, result1 ns.NetNS, result2 error) { + fake.GetNSStub = nil + if fake.getNSReturnsOnCall == nil { + fake.getNSReturnsOnCall = make(map[int]struct { + result1 ns.NetNS + result2 error + }) + } + fake.getNSReturnsOnCall[i] = struct { + result1 ns.NetNS + result2 error + }{result1, result2} +} + +func (fake *NamespaceAdapter) GetCurrentNS() (ns.NetNS, error) { + fake.getCurrentNSMutex.Lock() + ret, specificReturn := fake.getCurrentNSReturnsOnCall[len(fake.getCurrentNSArgsForCall)] + fake.getCurrentNSArgsForCall = append(fake.getCurrentNSArgsForCall, struct{}{}) + fake.recordInvocation("GetCurrentNS", []interface{}{}) + fake.getCurrentNSMutex.Unlock() + if fake.GetCurrentNSStub != nil { + return fake.GetCurrentNSStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.getCurrentNSReturns.result1, fake.getCurrentNSReturns.result2 +} + +func (fake *NamespaceAdapter) GetCurrentNSCallCount() int { + fake.getCurrentNSMutex.RLock() + defer fake.getCurrentNSMutex.RUnlock() + return len(fake.getCurrentNSArgsForCall) +} + +func (fake *NamespaceAdapter) GetCurrentNSReturns(result1 ns.NetNS, result2 error) { + fake.GetCurrentNSStub = nil + fake.getCurrentNSReturns = struct { + result1 ns.NetNS + result2 error + }{result1, result2} +} + +func (fake *NamespaceAdapter) GetCurrentNSReturnsOnCall(i int, result1 ns.NetNS, result2 error) { + fake.GetCurrentNSStub = nil + if fake.getCurrentNSReturnsOnCall == nil { + fake.getCurrentNSReturnsOnCall = make(map[int]struct { + result1 ns.NetNS + result2 error + }) + } + fake.getCurrentNSReturnsOnCall[i] = struct { + result1 ns.NetNS + result2 error + }{result1, result2} +} + +func (fake *NamespaceAdapter) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getNSMutex.RLock() + defer fake.getNSMutex.RUnlock() + fake.getCurrentNSMutex.RLock() + defer fake.getCurrentNSMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *NamespaceAdapter) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/cni/config/fakes/netNS.go b/src/code.cloudfoundry.org/silk/cni/config/fakes/netNS.go new file mode 100644 index 00000000..813e554a --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/config/fakes/netNS.go @@ -0,0 +1,300 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + + "github.com/containernetworking/plugins/pkg/ns" +) + +type NetNS struct { + DoStub func(toRun func(ns.NetNS) error) error + doMutex sync.RWMutex + doArgsForCall []struct { + toRun func(ns.NetNS) error + } + doReturns struct { + result1 error + } + doReturnsOnCall map[int]struct { + result1 error + } + SetStub func() error + setMutex sync.RWMutex + setArgsForCall []struct{} + setReturns struct { + result1 error + } + setReturnsOnCall map[int]struct { + result1 error + } + PathStub func() string + pathMutex sync.RWMutex + pathArgsForCall []struct{} + pathReturns struct { + result1 string + } + pathReturnsOnCall map[int]struct { + result1 string + } + FdStub func() uintptr + fdMutex sync.RWMutex + fdArgsForCall []struct{} + fdReturns struct { + result1 uintptr + } + fdReturnsOnCall map[int]struct { + result1 uintptr + } + CloseStub func() error + closeMutex sync.RWMutex + closeArgsForCall []struct{} + closeReturns struct { + result1 error + } + closeReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *NetNS) Do(toRun func(ns.NetNS) error) error { + fake.doMutex.Lock() + ret, specificReturn := fake.doReturnsOnCall[len(fake.doArgsForCall)] + fake.doArgsForCall = append(fake.doArgsForCall, struct { + toRun func(ns.NetNS) error + }{toRun}) + fake.recordInvocation("Do", []interface{}{toRun}) + fake.doMutex.Unlock() + if fake.DoStub != nil { + return fake.DoStub(toRun) + } + if specificReturn { + return ret.result1 + } + return fake.doReturns.result1 +} + +func (fake *NetNS) DoCallCount() int { + fake.doMutex.RLock() + defer fake.doMutex.RUnlock() + return len(fake.doArgsForCall) +} + +func (fake *NetNS) DoArgsForCall(i int) func(ns.NetNS) error { + fake.doMutex.RLock() + defer fake.doMutex.RUnlock() + return fake.doArgsForCall[i].toRun +} + +func (fake *NetNS) DoReturns(result1 error) { + fake.DoStub = nil + fake.doReturns = struct { + result1 error + }{result1} +} + +func (fake *NetNS) DoReturnsOnCall(i int, result1 error) { + fake.DoStub = nil + if fake.doReturnsOnCall == nil { + fake.doReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.doReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetNS) Set() error { + fake.setMutex.Lock() + ret, specificReturn := fake.setReturnsOnCall[len(fake.setArgsForCall)] + fake.setArgsForCall = append(fake.setArgsForCall, struct{}{}) + fake.recordInvocation("Set", []interface{}{}) + fake.setMutex.Unlock() + if fake.SetStub != nil { + return fake.SetStub() + } + if specificReturn { + return ret.result1 + } + return fake.setReturns.result1 +} + +func (fake *NetNS) SetCallCount() int { + fake.setMutex.RLock() + defer fake.setMutex.RUnlock() + return len(fake.setArgsForCall) +} + +func (fake *NetNS) SetReturns(result1 error) { + fake.SetStub = nil + fake.setReturns = struct { + result1 error + }{result1} +} + +func (fake *NetNS) SetReturnsOnCall(i int, result1 error) { + fake.SetStub = nil + if fake.setReturnsOnCall == nil { + fake.setReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.setReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetNS) Path() string { + fake.pathMutex.Lock() + ret, specificReturn := fake.pathReturnsOnCall[len(fake.pathArgsForCall)] + fake.pathArgsForCall = append(fake.pathArgsForCall, struct{}{}) + fake.recordInvocation("Path", []interface{}{}) + fake.pathMutex.Unlock() + if fake.PathStub != nil { + return fake.PathStub() + } + if specificReturn { + return ret.result1 + } + return fake.pathReturns.result1 +} + +func (fake *NetNS) PathCallCount() int { + fake.pathMutex.RLock() + defer fake.pathMutex.RUnlock() + return len(fake.pathArgsForCall) +} + +func (fake *NetNS) PathReturns(result1 string) { + fake.PathStub = nil + fake.pathReturns = struct { + result1 string + }{result1} +} + +func (fake *NetNS) PathReturnsOnCall(i int, result1 string) { + fake.PathStub = nil + if fake.pathReturnsOnCall == nil { + fake.pathReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.pathReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *NetNS) Fd() uintptr { + fake.fdMutex.Lock() + ret, specificReturn := fake.fdReturnsOnCall[len(fake.fdArgsForCall)] + fake.fdArgsForCall = append(fake.fdArgsForCall, struct{}{}) + fake.recordInvocation("Fd", []interface{}{}) + fake.fdMutex.Unlock() + if fake.FdStub != nil { + return fake.FdStub() + } + if specificReturn { + return ret.result1 + } + return fake.fdReturns.result1 +} + +func (fake *NetNS) FdCallCount() int { + fake.fdMutex.RLock() + defer fake.fdMutex.RUnlock() + return len(fake.fdArgsForCall) +} + +func (fake *NetNS) FdReturns(result1 uintptr) { + fake.FdStub = nil + fake.fdReturns = struct { + result1 uintptr + }{result1} +} + +func (fake *NetNS) FdReturnsOnCall(i int, result1 uintptr) { + fake.FdStub = nil + if fake.fdReturnsOnCall == nil { + fake.fdReturnsOnCall = make(map[int]struct { + result1 uintptr + }) + } + fake.fdReturnsOnCall[i] = struct { + result1 uintptr + }{result1} +} + +func (fake *NetNS) Close() error { + fake.closeMutex.Lock() + ret, specificReturn := fake.closeReturnsOnCall[len(fake.closeArgsForCall)] + fake.closeArgsForCall = append(fake.closeArgsForCall, struct{}{}) + fake.recordInvocation("Close", []interface{}{}) + fake.closeMutex.Unlock() + if fake.CloseStub != nil { + return fake.CloseStub() + } + if specificReturn { + return ret.result1 + } + return fake.closeReturns.result1 +} + +func (fake *NetNS) CloseCallCount() int { + fake.closeMutex.RLock() + defer fake.closeMutex.RUnlock() + return len(fake.closeArgsForCall) +} + +func (fake *NetNS) CloseReturns(result1 error) { + fake.CloseStub = nil + fake.closeReturns = struct { + result1 error + }{result1} +} + +func (fake *NetNS) CloseReturnsOnCall(i int, result1 error) { + fake.CloseStub = nil + if fake.closeReturnsOnCall == nil { + fake.closeReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.closeReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetNS) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.doMutex.RLock() + defer fake.doMutex.RUnlock() + fake.setMutex.RLock() + defer fake.setMutex.RUnlock() + fake.pathMutex.RLock() + defer fake.pathMutex.RUnlock() + fake.fdMutex.RLock() + defer fake.fdMutex.RUnlock() + fake.closeMutex.RLock() + defer fake.closeMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *NetNS) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/config/hardware_address_generator.go b/src/code.cloudfoundry.org/silk/cni/config/hardware_address_generator.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/config/hardware_address_generator.go rename to src/code.cloudfoundry.org/silk/cni/config/hardware_address_generator.go diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/config/ipam.go b/src/code.cloudfoundry.org/silk/cni/config/ipam.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/config/ipam.go rename to src/code.cloudfoundry.org/silk/cni/config/ipam.go diff --git a/src/code.cloudfoundry.org/silk/cni/config/ipam_test.go b/src/code.cloudfoundry.org/silk/cni/config/ipam_test.go new file mode 100644 index 00000000..e2ea78fc --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/config/ipam_test.go @@ -0,0 +1,44 @@ +package config_test + +import ( + "code.cloudfoundry.org/silk/cni/config" + "github.com/containernetworking/cni/pkg/types" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Ipam config generation", func() { + It("returns IPAM config object", func() { + + generator := config.IPAMConfigGenerator{} + ipamConfig, err := generator.GenerateConfig("10.255.30.0/24", "some-network-name", "/some/data/dir") + Expect(err).NotTo(HaveOccurred()) + + subnetAsIPNet, err := types.ParseCIDR("10.255.30.0/24") + Expect(err).NotTo(HaveOccurred()) + + Expect(ipamConfig).To(Equal( + &config.HostLocalIPAM{ + CNIVersion: "1.0.0", + Name: "some-network-name", + IPAM: config.IPAMConfig{ + Type: "host-local", + Ranges: []config.RangeSet{ + []config.Range{ + { + Subnet: types.IPNet(*subnetAsIPNet), + }, + }}, + Routes: []*types.Route{}, + DataDir: "/some/data/dir/ipam", + }, + })) + }) + Context("when the subnet is invalid", func() { + It("returns an error", func() { + generator := config.IPAMConfigGenerator{} + _, err := generator.GenerateConfig("10.255.30.0/33", "some-network-name", "/some/data/dir") + Expect(err).To(MatchError("invalid subnet: invalid CIDR address: 10.255.30.0/33")) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/cni/integration/error_cases_test.go b/src/code.cloudfoundry.org/silk/cni/integration/error_cases_test.go new file mode 100644 index 00000000..2e911763 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/integration/error_cases_test.go @@ -0,0 +1,216 @@ +package integration_test + +import ( + "fmt" + "net/http" + + "encoding/json" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gexec" +) + +type cniDatabaseError struct { + Code int `json:"code"` + Msg string `json:"msg"` + Details string `json:"details"` +} + +var _ = Describe("errors", func() { + Describe("errors on ADD", func() { + Context("when the subnet file is missing", func() { + BeforeEach(func() { + cniStdin = cniConfigWithSubnetEnv(dataDir, datastorePath, "/path/does/not/exist") + }) + + It("exits with nonzero status and prints a CNI error result as JSON to stdout", func() { + session := startCommandInHost("ADD", cniStdin) + Eventually(session, cmdTimeout).Should(gexec.Exit(1)) + + Expect(session.Out.Contents()).To(MatchJSON(`{ + "code": 100, + "msg": "discover network info", + "details": "get netinfo: open /path/does/not/exist: no such file or directory" + }`)) + }) + }) + + Context("when the subnet file is corrupt", func() { + BeforeEach(func() { + subnetEnvFile = writeSubnetEnvFile("bad-subnet", fullNetwork.String()) + cniStdin = cniConfigWithSubnetEnv(dataDir, datastorePath, subnetEnvFile) + }) + + It("exits with nonzero status and prints a CNI error result as JSON to stdout", func() { + session := startCommandInHost("ADD", cniStdin) + Eventually(session, cmdTimeout).Should(gexec.Exit(1)) + + Expect(session.Out.Contents()).To(MatchJSON(`{ + "code": 100, + "msg": "discover network info", + "details": "get netinfo: unable to parse flannel subnet file" + }`)) + }) + }) + + Context("when the MTU is less than 0", func() { + BeforeEach(func() { + fakeServer = startFakeDaemonInHost(daemonPort, http.StatusOK, `{"overlay_subnet": "10.255.30.0/24", "mtu": 1472}`) + cniStdin = fmt.Sprintf(`{ + "cniVersion": "1.0.0", + "name": "my-silk-network", + "type": "silk", + "mtu": -123, + "dataDir": "%s", + "daemonPort": %d, + "datastore": "%s"}`, dataDir, daemonPort, datastorePath) + }) + It("exits with nonzero status and prints a CNI error result as JSON to stdout", func() { + session := startCommandInHost("ADD", cniStdin) + Eventually(session, cmdTimeout).Should(gexec.Exit(1)) + + Expect(session.Out.Contents()).To(MatchJSON(`{ + "code": 100, + "msg": "discover network info", + "details": "invalid config: MTU: less than min" + }`)) + }) + }) + + Context("when the daemon url fails to return a response", func() { + BeforeEach(func() { + if fakeServer != nil { + fakeServer.Interrupt() + Eventually(fakeServer, "5s").Should(gexec.Exit()) + } + cniStdin = cniConfig(dataDir, datastorePath, daemonPort) + }) + + It("exits with nonzero status and prints a CNI error result as JSON to stdout", func() { + session := startCommandInHost("ADD", cniStdin) + Eventually(session, cmdTimeout).Should(gexec.Exit(1)) + + cniError := &cniDatabaseError{} + err := json.Unmarshal(session.Out.Contents(), cniError) + Expect(err).ToNot(HaveOccurred()) + + Expect(cniError.Code).To(Equal(100)) + Expect(cniError.Msg).To(Equal("discover network info")) + expectedCNIDetails := fmt.Sprintf(`get netinfo: json client do: http client do: Get "http://127.0.0.1:%[1]d/": dial tcp 127.0.0.1:%[1]d: .* connection refused`, daemonPort) + Expect(cniError.Details).To(MatchRegexp(expectedCNIDetails)) + }) + }) + + Context("when the daemon network info cannot be unmarshaled", func() { + BeforeEach(func() { + fakeServer = startFakeDaemonInHost(daemonPort, http.StatusOK, `bad response`) + cniStdin = cniConfig(dataDir, datastorePath, daemonPort) + }) + + It("exits with nonzero status and prints a CNI error result as JSON to stdout", func() { + session := startCommandInHost("ADD", cniStdin) + Eventually(session, cmdTimeout).Should(gexec.Exit(1)) + + Expect(session.Out.Contents()).To(MatchJSON(`{ + "code": 100, + "msg": "discover network info", + "details": "get netinfo: json client do: json unmarshal: invalid character 'b' looking for beginning of value" + }`)) + }) + }) + + Context("when the ipam plugin errors on add", func() { + BeforeEach(func() { + fakeServer = startFakeDaemonInHost(daemonPort, http.StatusOK, `{"overlay_subnet": "10.255.30.0/33", "mtu": 1350}`) + cniStdin = cniConfig(dataDir, datastorePath, daemonPort) + }) + It("exits with nonzero status and prints a CNI error result as JSON to stdout", func() { + session := startCommandInHost("ADD", cniStdin) + Eventually(session, cmdTimeout).Should(gexec.Exit(1)) + + Expect(session.Out.Contents()).To(MatchJSON(`{ + "code": 100, + "msg": "generate ipam config", + "details": "invalid subnet: invalid CIDR address: 10.255.30.0/33" + }`)) + }) + }) + + Context("when the veth manager fails to create a veth pair", func() { + It("exits with nonzero status and prints a CNI error", func() { + cniEnv["CNI_IFNAME"] = "some-bad-eth-name" + cniStdin = cniConfig(dataDir, datastorePath, daemonPort) + session := startCommandInHost("ADD", cniStdin) + Eventually(session, cmdTimeout).Should(gexec.Exit(1)) + + Expect(session.Out.Contents()).To(MatchJSON(`{ + "code": 4, + "msg": "interface name is too long", + "details": "interface name should be less than 16 characters" + }`)) + }) + }) + + Context("when the datastore is not specified", func() { + It("fails with nonzero status and prints a CNI error", func() { + cniStdin = fmt.Sprintf(`{ + "cniVersion": "1.0.0", + "name": "my-silk-network", + "type": "silk", + "dataDir": "%s", + "daemonPort": %d, + "datastore": "" + }`, dataDir, daemonPort) + session := startCommandInHost("ADD", cniStdin) + Eventually(session, cmdTimeout).Should(gexec.Exit(1)) + + Expect(session.Out.Contents()).To(MatchJSON(`{ + "code": 100, + "msg": "write container metadata", + "details": "open lock: open : no such file or directory" + }`)) + }) + }) + }) + + Describe("errors on DEL", func() { + Context("when the network namespace doesn't exist", func() { + BeforeEach(func() { + cniEnv["CNI_NETNS"] = "/tmp/not/there" + }) + It("exits with zero status but logs the error", func() { + session := startCommandInHost("DEL", cniStdin) + Eventually(session, cmdTimeout).Should(gexec.Exit(0)) + + Expect(session.Err).To(gbytes.Say(`open-netns.*/tmp/not/there.*no such file or directory`)) + }) + }) + + Context("when the interface isn't present inside the container", func() { + It("exits with zero status, but logs the error", func() { + cniEnv["CNI_IFNAME"] = "not-there" + session := startCommandInHost("DEL", cniStdin) + Eventually(session, cmdTimeout).Should(gexec.Exit(0)) + Expect(string(session.Err.Contents())).To(ContainSubstring(`"deviceName":"not-there","message":"Link not found"`)) + }) + }) + + Context("when the datastore is not specified", func() { + It("prints a CNI error", func() { + cniStdin = fmt.Sprintf(`{ + "cniVersion": "1.0.0", + "name": "my-silk-network", + "type": "silk", + "dataDir": "%s", + "daemonPort": %d, + "datastore": "" + }`, dataDir, daemonPort) + session := startCommandInHost("DEL", cniStdin) + Eventually(session, cmdTimeout).Should(gexec.Exit(0)) + Expect(string(session.Err.Contents())).To(MatchRegexp(`delete-from-container-metadata.*"open lock: open : no such file or directory"`)) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/cni/integration/fake_daemon/main.go b/src/code.cloudfoundry.org/silk/cni/integration/fake_daemon/main.go new file mode 100644 index 00000000..2505975f --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/integration/fake_daemon/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "strconv" + + "github.com/tedsuo/ifrit" + "github.com/tedsuo/ifrit/grouper" + "github.com/tedsuo/ifrit/http_server" + "github.com/tedsuo/ifrit/sigmon" +) + +func main() { + if len(os.Args) != 4 { + log.Fatalf("expected 3 args\nport, statusCode, response") + } + port := os.Args[1] + statusCode := os.Args[2] + response := os.Args[3] + code, err := strconv.Atoi(statusCode) + if err != nil { + log.Fatalf("status code must be an int") + } + + server := http_server.New( + fmt.Sprintf("127.0.0.1:%s", port), + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(code) + w.Write([]byte(response)) + }), + ) + + members := grouper.Members{{Name: "server", Runner: server}} + group := grouper.NewOrdered(os.Interrupt, members) + monitor := ifrit.Invoke(sigmon.New(group)) + + err = <-monitor.Wait() + if err != nil { + log.Fatal(err) + } +} diff --git a/src/code.cloudfoundry.org/silk/cni/integration/integration_suite_test.go b/src/code.cloudfoundry.org/silk/cni/integration/integration_suite_test.go new file mode 100644 index 00000000..6fb71c6c --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/integration/integration_suite_test.go @@ -0,0 +1,65 @@ +package integration_test + +import ( + "encoding/json" + "fmt" + "math/rand" + "path" + + "github.com/containernetworking/plugins/pkg/ns" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" + + "testing" +) + +func TestIntegration(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "CNI Integration Suite") +} + +var ( + paths testPaths + hostNS ns.NetNS +) + +type testPaths struct { + PathToPlugin string + CNIPath string + PathToFakeDaemon string +} + +var _ = SynchronizedBeforeSuite(func() []byte { + var err error + + hostNS, err = ns.GetCurrentNS() + Expect(err).NotTo(HaveOccurred()) + + pathToSilkCNI, err := gexec.Build("code.cloudfoundry.org/silk/cmd/silk-cni", `-ldflags=-extldflags=-Wl,--allow-multiple-definition -X main.LoggingDevice=stderr`, "-race", "-buildvcs=false") + Expect(err).NotTo(HaveOccurred()) + + pathToIPAM, err := gexec.Build("github.com/containernetworking/plugins/plugins/ipam/host-local", "-race", "-buildvcs=false") + Expect(err).NotTo(HaveOccurred()) + + pathToFakeDaemon, err := gexec.Build("code.cloudfoundry.org/silk/cni/integration/fake_daemon", "-race", "-buildvcs=false") + Expect(err).NotTo(HaveOccurred()) + + paths = testPaths{ + PathToPlugin: pathToSilkCNI, + CNIPath: fmt.Sprintf("%s:%s", path.Dir(pathToSilkCNI), path.Dir(pathToIPAM)), + PathToFakeDaemon: pathToFakeDaemon, + } + + bytes, err := json.Marshal(paths) + Expect(err).NotTo(HaveOccurred()) + return bytes +}, func(data []byte) { + Expect(json.Unmarshal(data, &paths)).To(Succeed()) + + rand.Seed(GinkgoRandomSeed() + int64(GinkgoParallelProcess())) +}) + +var _ = SynchronizedAfterSuite(func() {}, func() { + gexec.CleanupBuildArtifacts() +}) diff --git a/src/code.cloudfoundry.org/silk/cni/integration/integration_test.go b/src/code.cloudfoundry.org/silk/cni/integration/integration_test.go new file mode 100644 index 00000000..1b1c08c7 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/integration/integration_test.go @@ -0,0 +1,832 @@ +package integration_test + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "math" + "math/rand" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "time" + + current "github.com/containernetworking/cni/pkg/types/100" + "github.com/containernetworking/plugins/pkg/ns" + "github.com/containernetworking/plugins/pkg/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" + "github.com/vishvananda/netlink" +) + +const cmdTimeout = 15 * time.Second + +var ( + fakeServer *gexec.Session + + cniEnv map[string]string + containerNS ns.NetNS + fakeHostNS ns.NetNS + cniStdin string + dataDir string + flannelSubnet *net.IPNet + fullNetwork *net.IPNet + subnetEnvFile string + datastorePath string + fakeHostNSName string + containerNSName string + containerID string + daemonPort int +) + +var _ = BeforeEach(func() { + By("setting up namespaces for the 'host' and 'container'") + containerNSName = fmt.Sprintf("container-%03d", GinkgoParallelProcess()) + mustSucceed("ip", "netns", "add", containerNSName) + var err error + containerNS, err = ns.GetNS(fmt.Sprintf("/var/run/netns/%s", containerNSName)) + Expect(err).NotTo(HaveOccurred()) + + fakeHostNSName = fmt.Sprintf("host-%03d", GinkgoParallelProcess()) + mustSucceed("ip", "netns", "add", fakeHostNSName) + fakeHostNS, err = ns.GetNS(fmt.Sprintf("/var/run/netns/%s", fakeHostNSName)) + Expect(err).NotTo(HaveOccurred()) + + containerID = fmt.Sprintf("test-%03d-%x", GinkgoParallelProcess(), rand.Int31()) + + By("setting up CNI config") + cniEnv = map[string]string{ + "CNI_IFNAME": "eth0", + "CNI_CONTAINERID": containerID, + "CNI_PATH": paths.CNIPath, + } + cniEnv["CNI_NETNS"] = containerNS.Path() + + dataDir, err = ioutil.TempDir("", "cni-data-dir-") + Expect(err).NotTo(HaveOccurred()) + + flannelSubnetBaseIP, flannelSubnetCIDR, _ := net.ParseCIDR("10.255.30.0/24") + _, fullNetwork, _ = net.ParseCIDR("10.255.0.0/16") + flannelSubnet = &net.IPNet{ + IP: flannelSubnetBaseIP, + Mask: flannelSubnetCIDR.Mask, + } + + daemonPort = 40000 + GinkgoParallelProcess() + fakeServer = startFakeDaemonInHost(daemonPort, http.StatusOK, `{"overlay_subnet": "10.255.30.0/24", "mtu": 1472}`) + + cniStdin = cniConfig(dataDir, datastorePath, daemonPort) + + datastoreDir, err := ioutil.TempDir("", "metadata-dir-") + Expect(err).NotTo(HaveOccurred()) + datastorePath = filepath.Join(datastoreDir, "container-metadata.json") +}) + +var _ = AfterEach(func() { + if fakeServer != nil { + fakeServer.Interrupt() + Eventually(fakeServer, "5s").Should(gexec.Exit()) + } + + containerNS.Close() + fakeHostNS.Close() + mustSucceed("ip", "netns", "del", fakeHostNSName) + mustSucceed("ip", "netns", "del", containerNSName) + Expect(os.RemoveAll(subnetEnvFile)).To(Succeed()) + Expect(os.RemoveAll(dataDir)).To(Succeed()) + Expect(os.RemoveAll(datastorePath)).To(Succeed()) +}) + +var _ = Describe("Silk CNI Integration", func() { + Describe("veth devices", func() { + BeforeEach(func() { + cniStdin = cniConfig(dataDir, datastorePath, daemonPort) + }) + + It("returns the expected CNI result", func() { + By("calling ADD") + sess := startCommandInHost("ADD", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + result := cniResultForCurrentVersion(sess.Out.Contents()) + + inHost := ifacesWithNS(result.Interfaces, "") + + expectedCNIStdout := fmt.Sprintf(` + { + "cniVersion": "1.0.0", + "interfaces": [ + { + "name": "%s", + "mac": "aa:aa:0a:ff:1e:02" + }, + { + "name": "eth0", + "mac": "ee:ee:0a:ff:1e:02", + "sandbox": "%s" + } + ], + "ips": [ + { + "address": "10.255.30.2/32", + "gateway": "169.254.0.1", + "interface": 1 + } + ], + "routes": [{"dst": "0.0.0.0/0", "gw": "169.254.0.1"}], + "dns": {} + } + `, inHost[0].Name, containerNS.Path()) + + Expect(sess.Out.Contents()).To(MatchJSON(expectedCNIStdout)) + }) + + It("creates and destroys a veth pair", func() { + sess := startCommandInHost("ADD", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + + result := cniResultForCurrentVersion(sess.Out.Contents()) + Expect(result.Interfaces).To(HaveLen(2)) + + inHost := ifacesWithNS(result.Interfaces, "") + inContainer := ifacesWithNS(result.Interfaces, containerNS.Path()) + + Expect(inHost).To(HaveLen(1)) + Expect(inContainer).To(HaveLen(1)) + + By("checking the link was created in the host") + mustSucceedInFakeHost("ip", "link", "list", "dev", inHost[0].Name) + + By("checking the link was created in the container") + mustSucceedInContainer("ip", "link", "list", "dev", inContainer[0].Name) + + sess = startCommandInHost("DEL", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + + By("checking the link was deleted from the host") + mustFailInHost("does not exist", "ip", "link", "list", "dev", inHost[0].Name) + + By("checking the link was deleted from the host") + mustFailInContainer("does not exist", "ip", "link", "list", "dev", inContainer[0].Name) + }) + + It("can be deleted multiple times without an error status", func() { + sess := startCommandInHost("ADD", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + + sess = startCommandInHost("DEL", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + + sess = startCommandInHost("DEL", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + }) + + It("can be deleted when silk daemon is not running", func() { + sess := startCommandInHost("ADD", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + + Expect(filepath.Join(dataDir, "ipam/my-silk-network/10.255.30.2")).To(BeAnExistingFile()) + fakeServer.Interrupt() + Eventually(fakeServer, "5s").Should(gexec.Exit()) + + sess = startCommandInHost("DEL", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + + By("checking that the ip reserved is freed") + Expect(filepath.Join(dataDir, "ipam/my-silk-network/10.255.30.2")).NotTo(BeAnExistingFile()) + }) + + hostLinkFromResult := func(cniResult []byte) netlink.Link { + result := cniResultForCurrentVersion(cniResult) + Expect(result.Interfaces).To(HaveLen(2)) + inHost := ifacesWithNS(result.Interfaces, "") + link, err := netlink.LinkByName(inHost[0].Name) + Expect(err).NotTo(HaveOccurred()) + return link + } + + It("sets up the IP address and MAC address", func() { + By("calling ADD") + sess := startCommandInHost("ADD", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + + By("checking the host side") + err := fakeHostNS.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + + hostLink := hostLinkFromResult(sess.Out.Contents()) + + hostAddrs, err := netlink.AddrList(hostLink, netlink.FAMILY_ALL) + Expect(err).NotTo(HaveOccurred()) + + Expect(hostAddrs).To(HaveLen(1)) + Expect(hostAddrs[0].IPNet.String()).To(Equal("169.254.0.1/32")) + Expect(hostAddrs[0].Scope).To(Equal(int(netlink.SCOPE_LINK))) + Expect(hostAddrs[0].Peer.String()).To(Equal("10.255.30.2/32")) + Expect(hostLink.Attrs().HardwareAddr.String()).To(Equal("aa:aa:0a:ff:1e:02")) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + By("checking the container side") + err = containerNS.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName("eth0") + Expect(err).NotTo(HaveOccurred()) + + Expect(link.Attrs().Name).To(Equal("eth0")) + + containerAddrs, err := netlink.AddrList(link, netlink.FAMILY_ALL) + + Expect(err).NotTo(HaveOccurred()) + Expect(containerAddrs).To(HaveLen(1)) + Expect(containerAddrs[0].IPNet.String()).To(Equal("10.255.30.2/32")) + Expect(containerAddrs[0].Scope).To(Equal(int(netlink.SCOPE_LINK))) + Expect(containerAddrs[0].Peer.String()).To(Equal("169.254.0.1/32")) + Expect(link.Attrs().HardwareAddr.String()).To(Equal("ee:ee:0a:ff:1e:02")) + return nil + }) + + Expect(err).NotTo(HaveOccurred()) + }) + + It("enables connectivity between the host and container", func() { + cniStdin = cniConfig(dataDir, datastorePath, daemonPort) + + sess := startCommandInHost("ADD", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + + By("enabling connectivity from the host to the container") + // This does *not* fail as expected on Docker, but + // does properly fail in Concourse (Garden). + // see: https://github.com/docker/for-mac/issues/57 + mustSucceedInFakeHost("ping", "-c", "1", "10.255.30.2") + + By("enabling connectivity from the container to the host") + mustSucceedInContainer("ping", "-c", "1", "169.254.0.1") + }) + + It("turns off ARP for veth devices", func() { + cniStdin = cniConfig(dataDir, datastorePath, daemonPort) + + sess := startCommandInHost("ADD", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + + By("checking the host side") + err := fakeHostNS.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + + hostLink := hostLinkFromResult(sess.Out.Contents()) + Expect(hostLink.Attrs().RawFlags & syscall.IFF_NOARP).To(Equal(uint32(syscall.IFF_NOARP))) + + neighs, err := netlink.NeighList(hostLink.Attrs().Index, netlink.FAMILY_V4) + Expect(err).NotTo(HaveOccurred()) + + Expect(neighs).To(HaveLen(1)) + Expect(neighs[0].IP.String()).To(Equal("10.255.30.2")) + Expect(neighs[0].HardwareAddr.String()).To(Equal("ee:ee:0a:ff:1e:02")) + Expect(neighs[0].State).To(Equal(netlink.NUD_PERMANENT)) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + By("checking the container side") + err = containerNS.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + + containerLink, err := netlink.LinkByName("eth0") + Expect(err).NotTo(HaveOccurred()) + Expect(containerLink.Attrs().RawFlags & syscall.IFF_NOARP).To(Equal(uint32(syscall.IFF_NOARP))) + + neighs, err := netlink.NeighList(containerLink.Attrs().Index, netlink.FAMILY_V4) + Expect(err).NotTo(HaveOccurred()) + + Expect(neighs).To(HaveLen(1)) + Expect(neighs[0].IP.String()).To(Equal("169.254.0.1")) + Expect(neighs[0].HardwareAddr.String()).To(Equal("aa:aa:0a:ff:1e:02")) + Expect(neighs[0].State).To(Equal(netlink.NUD_PERMANENT)) + + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + }) + + It("adds routes to the container", func() { + cniStdin = cniConfig(dataDir, datastorePath, daemonPort) + + sess := startCommandInHost("ADD", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + + By("checking the routes are present inside the container") + err := containerNS.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName("eth0") + Expect(err).NotTo(HaveOccurred()) + + routes, err := netlink.RouteList(link, netlink.FAMILY_V4) + Expect(err).NotTo(HaveOccurred()) + + Expect(routes).To(HaveLen(2)) + + // the route returned by the IPAM result + Expect(routes[0].Dst).To(BeNil()) // same as 0.0.0.0/0 + Expect(routes[0].Gw.String()).To(Equal("169.254.0.1")) + + // the route created when the address is assigned + Expect(routes[1].Dst.String()).To(Equal("169.254.0.1/32")) + Expect(routes[1].Gw).To(BeNil()) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("allows the container to reach IP addresses on the host namespace", func() { + cniStdin = cniConfig(dataDir, datastorePath, daemonPort) + + sess := startCommandInHost("ADD", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + + const ipOnTheHost = "169.254.50.50" + + By("creating a endpoint on the host") + mustSucceedInFakeHost("ip", "addr", "add", ipOnTheHost, "dev", "lo") + + By("checking that the container can reach that endpoint") + mustSucceedInContainer("ping", "-c", "1", ipOnTheHost) + }) + + It("allows the container to reach IP addresses on the internet", func() { + // NOTE: unlike all other tests in this suite + // this one uses the REAL host namespace in order to + // test proper packet forwarding to the internet + // Because it messes with the REAL host namespace, it cannot safely run + // concurrently with any other test that also touches the REAL host namespace + // Avoid writing such tests if you can. + By("starting the fake daemon") + fakeServer = startFakeDaemonInRealHostNamespace(daemonPort, http.StatusOK, `{"overlay_subnet": "10.255.30.0/24", "mtu": 1350}`) + + By("calling CNI with ADD") + cniStdin = cniConfig(dataDir, datastorePath, daemonPort) + sess := startCommandInRealHostNamespace("ADD", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + + By("discovering the container IP") + var cniResult current.Result + Expect(json.Unmarshal(sess.Out.Contents(), &cniResult)).To(Succeed()) + sourceIP := fmt.Sprintf("%s/32", cniResult.IPs[0].Address.IP.String()) + + By("installing the requisite iptables rule") + iptablesRule := func(action string) []string { + return []string{"-t", "nat", action, "POSTROUTING", "-s", sourceIP, "!", "-d", "10.255.0.0/16", "-j", "MASQUERADE"} + } + mustSucceed("iptables", iptablesRule("-A")...) + + By("attempting to reach the internet from the container") + mustSucceedInContainer("curl", "-f", "example.com") + + By("removing the iptables rule from the host") + mustSucceed("iptables", iptablesRule("-D")...) + + By("calling CNI with DEL to clean up") + sess = startCommandInRealHostNamespace("DEL", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + }) + + Context("when MTU is not specified on the input", func() { + It("sets the MTU based on the daemon network info", func() { + By("calling ADD") + sess := startCommandInHost("ADD", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + + By("checking the host side") + err := fakeHostNS.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + + hostLink := hostLinkFromResult(sess.Out.Contents()) + Expect(hostLink.Attrs().MTU).To(Equal(1472)) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + By("checking the container side") + err = containerNS.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName("eth0") + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().MTU).To(Equal(1472)) + return nil + }) + + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("when MTU is specified on the input", func() { + It("sets the MTU based on the input", func() { + By("calling ADD") + cniStdin = fmt.Sprintf(`{ + "cniVersion": "1.0.0", + "name": "my-silk-network", + "type": "silk", + "mtu": 1350, + "dataDir": "%s", + "daemonPort": %d, + "datastore": "%s" + }`, dataDir, daemonPort, datastorePath) + sess := startCommandInHost("ADD", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + + By("checking the host side") + err := fakeHostNS.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + + hostLink := hostLinkFromResult(sess.Out.Contents()) + Expect(hostLink.Attrs().MTU).To(Equal(1350)) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + By("checking the container side") + err = containerNS.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName("eth0") + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().MTU).To(Equal(1350)) + return nil + }) + + Expect(err).NotTo(HaveOccurred()) + }) + }) + + }) + + Describe("CNI version support", func() { + It("only claims to support CNI spec version 0.3.1", func() { + sess := startCommandInHost("VERSION", "{}") + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + Expect(sess.Out.Contents()).To(MatchJSON(`{ + "cniVersion": "1.0.0", + "supportedVersions": [ "1.0.0" ] + }`)) + }) + }) + + Describe("Lifecycle", func() { + BeforeEach(func() { + cniStdin = cniConfig(dataDir, datastorePath, daemonPort) + }) + It("allocates and frees ips", func() { + By("calling ADD") + sess := startCommandInHost("ADD", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + + result := cniResultForCurrentVersion(sess.Out.Contents()) + + Expect(result.IPs).To(HaveLen(1)) + Expect(result.IPs).To(HaveLen(1)) + Expect(*result.IPs[0].Interface).To(Equal(1)) + Expect(result.IPs[0].Address.String()).To(Equal("10.255.30.2/32")) + Expect(result.IPs[0].Gateway.String()).To(Equal("169.254.0.1")) + + By("checking that the ip is reserved for the correct container id") + bytes, err := ioutil.ReadFile(filepath.Join(dataDir, "ipam/my-silk-network/10.255.30.2")) + Expect(err).NotTo(HaveOccurred()) + Expect(string(bytes)).To(Equal(fmt.Sprintf("%s\r\neth0", containerID))) + + By("calling DEL") + sess = startCommandInHost("DEL", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + Expect(sess.Out.Contents()).To(BeEmpty()) + + By("checking that the ip reserved is freed") + Expect(filepath.Join(dataDir, "ipam/my-silk-network/10.255.30.2")).NotTo(BeAnExistingFile()) + }) + + It("writes and deletes container metadata", func() { + By("calling ADD") + sess := startCommandInHost("ADD", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + + By("checking that the container metadata is written") + containerMetadata, err := ioutil.ReadFile(datastorePath) + Expect(err).NotTo(HaveOccurred()) + + Expect(string(containerMetadata)).To(MatchJSON(fmt.Sprintf(`{ + "%s": { + "handle":"%s", + "ip":"10.255.30.2", + "metadata":null + } + }`, containerNSName, containerNSName))) + + By("calling DEL") + sess = startCommandInHost("DEL", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + Expect(sess.Out.Contents()).To(BeEmpty()) + + By("checking that the container metadata is deleted") + containerMetadata, err = ioutil.ReadFile(datastorePath) + Expect(err).NotTo(HaveOccurred()) + + Expect(string(containerMetadata)).NotTo(ContainSubstring("169.254.0.1")) + }) + }) + + Describe("Reserve all IPs", func() { + var ( + containerNSList []ns.NetNS + numIPAllocations int + ) + BeforeEach(func() { + cniStdin = cniConfig(dataDir, datastorePath, daemonPort) + prefixSize := 29 + fakeServer = startFakeDaemonInHost(daemonPort, http.StatusOK, fmt.Sprintf(`{"overlay_subnet": "10.255.30.0/%d", "mtu": 1350}`, prefixSize)) + numIPAllocations = int(math.Pow(2, float64(32-prefixSize)) - 2) + + for i := 0; i < numIPAllocations; i++ { + containerNS, err := testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + containerNSList = append(containerNSList, containerNS) + } + }) + AfterEach(func() { + for _, containerNS := range containerNSList { + containerNS.Close() + } + }) + + It("fails to allocate an IP if none is available", func() { + By("exhausting all ips") + for i := 0; i < numIPAllocations-1; i++ { + cniEnv["CNI_NETNS"] = containerNSList[i].Path() + cniEnv["CNI_CONTAINERID"] = fmt.Sprintf("test-%03d-%x", GinkgoParallelProcess(), rand.Int31()) + sess := startCommandInHost("ADD", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + + result := cniResultForCurrentVersion(sess.Out.Contents()) + + Expect(result.IPs).To(HaveLen(1)) + Expect(*result.IPs[0].Interface).To(Equal(1)) + Expect(result.IPs[0].Address.String()).To(Equal(fmt.Sprintf("10.255.30.%d/32", i+2))) + Expect(result.IPs[0].Gateway.String()).To(Equal("169.254.0.1")) + } + + cniEnv["CNI_NETNS"] = containerNSList[numIPAllocations-1].Path() + cniEnv["CNI_CONTAINERID"] = fmt.Sprintf("test-%03d-%x", GinkgoParallelProcess(), rand.Int31()) + sess := startCommandInHost("ADD", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(1)) + Expect(sess.Out.Contents()).To(MatchJSON(`{ + "code": 100, + "msg": "run ipam plugin", + "details": "failed to allocate for range 0: no IP addresses available in range set: 10.255.30.1-10.255.30.6" + }`)) + }) + }) + + Describe("when configured to use the subnet.env file", func() { + BeforeEach(func() { + subnetFile := writeSubnetEnvFile(flannelSubnet.String(), fullNetwork.String()) + cniStdin = cniConfigWithSubnetEnv(dataDir, datastorePath, subnetFile) + }) + + It("returns the expected CNI result", func() { + By("calling ADD") + sess := startCommandInHost("ADD", cniStdin) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + result := cniResultForCurrentVersion(sess.Out.Contents()) + + inHost := ifacesWithNS(result.Interfaces, "") + + expectedCNIStdout := fmt.Sprintf(` + { + "cniVersion": "1.0.0", + "interfaces": [ + { + "name": "%s", + "mac": "aa:aa:0a:ff:1e:02" + }, + { + "name": "eth0", + "mac": "ee:ee:0a:ff:1e:02", + "sandbox": "%s" + } + ], + "ips": [ + { + "address": "10.255.30.2/32", + "gateway": "169.254.0.1", + "interface": 1 + } + ], + "routes": [{"dst": "0.0.0.0/0", "gw": "169.254.0.1"}], + "dns": {} + } + `, inHost[0].Name, containerNS.Path()) + + Expect(sess.Out.Contents()).To(MatchJSON(expectedCNIStdout)) + }) + }) +}) + +func writeSubnetEnvFile(subnet, fullNetwork string) string { + tempFile, err := ioutil.TempFile("", "subnet.env") + defer tempFile.Close() + Expect(err).NotTo(HaveOccurred()) + _, err = fmt.Fprintf(tempFile, ` +FLANNEL_SUBNET=%s +FLANNEL_NETWORK=%s +FLANNEL_MTU=1472 +FLANNEL_IPMASQ=false # we'll ignore this field +`, subnet, fullNetwork) + Expect(err).NotTo(HaveOccurred()) + return tempFile.Name() +} + +func cniConfigWithExtras(dataDir, datastore string, daemonPort int, extras map[string]interface{}) string { + conf := map[string]interface{}{ + "cniVersion": "1.0.0", + "name": "my-silk-network", + "type": "silk", + "dataDir": dataDir, + "daemonPort": daemonPort, + "datastore": datastore, + } + for k, v := range extras { + conf[k] = v + } + confBytes, _ := json.Marshal(conf) + return string(confBytes) +} + +func cniConfig(dataDir, datastore string, daemonPort int) string { + return cniConfigWithExtras(dataDir, datastore, daemonPort, nil) +} + +func cniConfigWithSubnetEnv(dataDir, datastore, subnetFile string) string { + return fmt.Sprintf(`{ + "cniVersion": "1.0.0", + "name": "my-silk-network", + "type": "silk", + "dataDir": "%s", + "subnetFile": "%s", + "datastore": "%s" +}`, dataDir, subnetFile, datastore) +} + +func startCommandInHost(cniCommand, cniStdin string) *gexec.Session { + cmd := exec.Command("ip", "netns", "exec", fakeHostNSName, paths.PathToPlugin) + cmd.Stdin = strings.NewReader(cniStdin) + // Set command env + cniEnv["CNI_COMMAND"] = cniCommand + for k, v := range cniEnv { + cmd.Env = append(cmd.Env, k+"="+v) + } + + // Run command + sess, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + return sess +} + +func startFakeDaemonInHost(port, statusCode int, response string) *gexec.Session { + if fakeServer != nil { + fakeServer.Interrupt() + Eventually(fakeServer, "5s").Should(gexec.Exit()) + } + + cmd := exec.Command("ip", "netns", "exec", fakeHostNSName, "ip", "link", "set", "lo", "up") + sess, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + Eventually(sess).Should(gexec.Exit(0)) + + cmd = exec.Command("ip", "netns", "exec", fakeHostNSName, paths.PathToFakeDaemon, fmt.Sprintf("%d", port), fmt.Sprintf("%d", statusCode), response) + sess, err = gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + + waitUntilUp := func() error { + cmd = exec.Command("ip", "netns", "exec", fakeHostNSName, "curl", fmt.Sprintf("http://127.0.0.1:%d", port)) + return cmd.Run() + } + + Eventually(waitUntilUp, "5s").Should(Succeed()) + + return sess +} + +func startFakeDaemonInRealHostNamespace(port, statusCode int, response string) *gexec.Session { + if fakeServer != nil { + fakeServer.Interrupt() + Eventually(fakeServer, "5s").Should(gexec.Exit()) + } + + cmd := exec.Command(paths.PathToFakeDaemon, fmt.Sprintf("%d", port), fmt.Sprintf("%d", statusCode), response) + sess, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + + waitUntilUp := func() error { + cmd = exec.Command("curl", fmt.Sprintf("http://127.0.0.1:%d", port)) + return cmd.Run() + } + + Eventually(waitUntilUp, "5s").Should(Succeed()) + + return sess +} + +func startCommandInRealHostNamespace(cniCommand, cniStdin string) *gexec.Session { + cmd := exec.Command(paths.PathToPlugin) + cmd.Stdin = strings.NewReader(cniStdin) + // Set command env + cniEnv["CNI_COMMAND"] = cniCommand + for k, v := range cniEnv { + cmd.Env = append(cmd.Env, k+"="+v) + } + + // Run command + sess, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + return sess +} + +func ifacesWithNS(result []*current.Interface, nsPath string) []*current.Interface { + ret := []*current.Interface{} + for _, iface := range result { + if iface.Sandbox == nsPath { + ret = append(ret, iface) + } + } + return ret +} + +func cniResultForCurrentVersion(output []byte) *current.Result { + resultInterface, err := current.NewResult(output) + Expect(err).NotTo(HaveOccurred()) + result, err := current.NewResultFromResult(resultInterface) + Expect(err).NotTo(HaveOccurred()) + + return result +} + +func mustStart(binary string, args ...string) *gexec.Session { + cmd := exec.Command(binary, args...) + sess, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + return sess +} + +func mustSucceed(binary string, args ...string) string { + sess := mustStart(binary, args...) + Eventually(sess, cmdTimeout).Should(gexec.Exit(0)) + return string(sess.Out.Contents()) +} + +func mustFailWith(expectedErrorSubstring string, binary string, args ...string) { + cmd := exec.Command(binary, args...) + allOutput, err := cmd.CombinedOutput() + Expect(err).To(HaveOccurred()) + Expect(allOutput).To(ContainSubstring(expectedErrorSubstring)) +} + +func mustSucceedInContainer(binary string, args ...string) string { + cmdArgs := []string{"netns", "exec", containerNSName, binary} + cmdArgs = append(cmdArgs, args...) + return mustSucceed("ip", cmdArgs...) +} + +func mustStartInFakeHost(binary string, args ...string) *gexec.Session { + cmdArgs := []string{"netns", "exec", fakeHostNSName, binary} + cmdArgs = append(cmdArgs, args...) + return mustStart("ip", cmdArgs...) +} + +func mustSucceedInFakeHost(binary string, args ...string) string { + cmdArgs := []string{"netns", "exec", fakeHostNSName, binary} + cmdArgs = append(cmdArgs, args...) + return mustSucceed("ip", cmdArgs...) +} + +func mustFailInContainer(expectedErrorSubstring string, binary string, args ...string) { + cmdArgs := []string{"netns", "exec", containerNSName, binary} + cmdArgs = append(cmdArgs, args...) + mustFailWith(expectedErrorSubstring, "ip", cmdArgs...) +} + +func mustFailInHost(expectedErrorSubstring string, binary string, args ...string) { + cmdArgs := []string{"netns", "exec", fakeHostNSName, binary} + cmdArgs = append(cmdArgs, args...) + mustFailWith(expectedErrorSubstring, "ip", cmdArgs...) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/lib/common.go b/src/code.cloudfoundry.org/silk/cni/lib/common.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/lib/common.go rename to src/code.cloudfoundry.org/silk/cni/lib/common.go diff --git a/src/code.cloudfoundry.org/silk/cni/lib/common_test.go b/src/code.cloudfoundry.org/silk/cni/lib/common_test.go new file mode 100644 index 00000000..aa625c31 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/lib/common_test.go @@ -0,0 +1,222 @@ +package lib_test + +import ( + "errors" + "net" + + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/cni/config" + "code.cloudfoundry.org/silk/cni/lib" + "code.cloudfoundry.org/silk/cni/lib/fakes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/vishvananda/netlink" +) + +var _ = Describe("Common", func() { + Describe("BasicSetup", func() { + var ( + fakeNetlinkAdapter *fakes.NetlinkAdapter + fakeLinkOperations *fakes.LinkOperations + fakeLink netlink.Link + deviceName string + local config.DualAddress + peer config.DualAddress + common *lib.Common + fakelogger *lagertest.TestLogger + ) + BeforeEach(func() { + localMAC, err := net.ParseMAC("aa:aa:12:34:56:78") + Expect(err).NotTo(HaveOccurred()) + peerMAC, err := net.ParseMAC("ee:ee:12:34:56:78") + Expect(err).NotTo(HaveOccurred()) + local = config.DualAddress{ + IP: net.IP{10, 255, 30, 4}, + Hardware: localMAC, + } + peer = config.DualAddress{ + IP: net.IP{169, 254, 0, 1}, + Hardware: peerMAC, + } + + fakelogger = lagertest.NewTestLogger("test") + fakeNetlinkAdapter = &fakes.NetlinkAdapter{} + fakeLinkOperations = &fakes.LinkOperations{} + fakeLink = &netlink.Bridge{ + LinkAttrs: netlink.LinkAttrs{ + Name: "my-fake-bridge", + HardwareAddr: local.Hardware, + }, + } + fakeNetlinkAdapter.LinkByNameReturns(fakeLink, nil) + + common = &lib.Common{ + NetlinkAdapter: fakeNetlinkAdapter, + LinkOperations: fakeLinkOperations, + Logger: fakelogger, + } + deviceName = "myDeviceName" + }) + + It("sets up a veth device", func() { + err := common.BasicSetup(deviceName, local, peer) + Expect(err).NotTo(HaveOccurred()) + Expect(fakeNetlinkAdapter.LinkByNameCallCount()).To(Equal(2)) + Expect(fakeNetlinkAdapter.LinkByNameArgsForCall(0)).To(Equal("myDeviceName")) + + Expect(fakeNetlinkAdapter.LinkSetHardwareAddrCallCount()).To(Equal(0)) + + Expect(fakeLinkOperations.DisableIPv6CallCount()).To(Equal(1)) + Expect(fakeLinkOperations.DisableIPv6ArgsForCall(0)).To(Equal("myDeviceName")) + + Expect(fakeLinkOperations.StaticNeighborNoARPCallCount()).To(Equal(1)) + link, peerIP, peerHardwareAddr := fakeLinkOperations.StaticNeighborNoARPArgsForCall(0) + Expect(link).To(Equal(fakeLink)) + Expect(peerIP).To(Equal(peer.IP)) + Expect(peerHardwareAddr).To(Equal(peer.Hardware)) + + Expect(fakeLinkOperations.SetPointToPointAddressCallCount()).To(Equal(1)) + link, localIP, peerIP := fakeLinkOperations.SetPointToPointAddressArgsForCall(0) + Expect(link).To(Equal(fakeLink)) + Expect(localIP).To(Equal(local.IP)) + Expect(peerIP).To(Equal(peer.IP)) + + Expect(fakeLinkOperations.EnableReversePathFilteringCallCount()).To(Equal(1)) + Expect(fakeLinkOperations.EnableReversePathFilteringArgsForCall(0)).To(Equal("myDeviceName")) + + Expect(fakeNetlinkAdapter.LinkSetUpCallCount()).To(Equal(1)) + Expect(fakeNetlinkAdapter.LinkSetUpArgsForCall(0)).To(Equal(fakeLink)) + }) + + Context("when the link cannot be found", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkByNameReturns(nil, errors.New("strawberry")) + }) + It("wraps and returns the error", func() { + err := common.BasicSetup(deviceName, local, peer) + Expect(err).To(Equal(errors.New("failed to find link \"myDeviceName\": strawberry"))) + + }) + }) + + Context("when the hardware address is set to the wrong thing", func() { + var fakeLinkWithBadHardwareAddr *netlink.Bridge + var fakeLinkWithCorrectLocalHardwareAddr *netlink.Bridge + var randomHardwareAddr net.HardwareAddr + + BeforeEach(func() { + + var err error + randomHardwareAddr, err = net.ParseMAC("ff:ff:ff:ff:ff:ff") + Expect(err).NotTo(HaveOccurred()) + + fakeLinkWithBadHardwareAddr = &netlink.Bridge{ + LinkAttrs: netlink.LinkAttrs{ + Name: "my-fake-bridge", + HardwareAddr: randomHardwareAddr, + }, + } + + fakeLinkWithCorrectLocalHardwareAddr = &netlink.Bridge{ + LinkAttrs: netlink.LinkAttrs{ + Name: "my-fake-bridge", + HardwareAddr: local.Hardware, + }, + } + fakeNetlinkAdapter.LinkByNameReturns(fakeLinkWithBadHardwareAddr, nil) + }) + Context("when setting the hardware address fails", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkSetHardwareAddrReturns(errors.New("apple")) + }) + It("wraps and returns the error", func() { + err := common.BasicSetup(deviceName, local, peer) + Expect(err).To(Equal(errors.New("setting hardware address: apple"))) + }) + }) + + Context("but eventually is set to the right thing", func() { + BeforeEach(func() { + // The 0th time this function is for getting the link so we + // can check the hardware addr is accurate, and if not, change the hardware addr. + // The 1st time is for checking to make sure the hardware addr is set correctly after we retried + // The 2nd time checks again, validating that the 1st sethardwareaddr call worked + // The 3rd time is ensuring our loop re-tries until a success is found. + fakeNetlinkAdapter.LinkByNameReturnsOnCall(3, fakeLinkWithCorrectLocalHardwareAddr, nil) + }) + + It("retries and eventually sets the right address", func() { + err := common.BasicSetup(deviceName, local, peer) + Expect(err).NotTo(HaveOccurred()) + Expect(fakeNetlinkAdapter.LinkSetHardwareAddrCallCount()).To(Equal(2)) + link, hwAddr := fakeNetlinkAdapter.LinkSetHardwareAddrArgsForCall(1) + Expect(link).To(Equal(fakeLinkWithBadHardwareAddr)) // ensure we passed in the link we want to update + Expect(hwAddr).To(Equal(local.Hardware)) + }) + }) + + Context("and it is never set to the right thing", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkByNameReturns(fakeLinkWithBadHardwareAddr, nil) + }) + + It("runs out of retries and wraps and returns an error", func() { + err := common.BasicSetup(deviceName, local, peer) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to set hardware addr")) + }) + }) + }) + + Context("when disabling IPv6 fails", func() { + BeforeEach(func() { + fakeLinkOperations.DisableIPv6Returns(errors.New("kiwi")) + }) + It("ignores the error", func() { + err := common.BasicSetup(deviceName, local, peer) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("when replacing ARP with permanent neighbor rule fails", func() { + BeforeEach(func() { + fakeLinkOperations.StaticNeighborNoARPReturns(errors.New("raspberry")) + }) + It("wraps and returns the error", func() { + err := common.BasicSetup(deviceName, local, peer) + Expect(err).To(Equal(errors.New("replace ARP with permanent neighbor rule: raspberry"))) + }) + }) + + Context("when setting the point to point address fails", func() { + BeforeEach(func() { + fakeLinkOperations.SetPointToPointAddressReturns(errors.New("dragonfruit")) + }) + It("wraps and returns the error", func() { + err := common.BasicSetup(deviceName, local, peer) + Expect(err).To(Equal(errors.New("setting point to point address: dragonfruit"))) + }) + }) + + Context("when enabling reverse path filtering fails", func() { + BeforeEach(func() { + fakeLinkOperations.EnableReversePathFilteringReturns(errors.New("pomegranate")) + }) + It("wraps and returns the error", func() { + err := common.BasicSetup(deviceName, local, peer) + Expect(err).To(Equal(errors.New("enable reverse path filtering: pomegranate"))) + }) + }) + + Context("when setting link up fails", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkSetUpReturns(errors.New("cantaloupe")) + }) + It("wraps and returns the error", func() { + err := common.BasicSetup(deviceName, local, peer) + Expect(err).To(Equal(errors.New("setting link myDeviceName up: cantaloupe"))) + }) + }) + + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/lib/container_setup.go b/src/code.cloudfoundry.org/silk/cni/lib/container_setup.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/lib/container_setup.go rename to src/code.cloudfoundry.org/silk/cni/lib/container_setup.go diff --git a/src/code.cloudfoundry.org/silk/cni/lib/container_setup_test.go b/src/code.cloudfoundry.org/silk/cni/lib/container_setup_test.go new file mode 100644 index 00000000..85320c4b --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/lib/container_setup_test.go @@ -0,0 +1,148 @@ +package lib_test + +import ( + "errors" + "net" + + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/cni/config" + "code.cloudfoundry.org/silk/cni/lib" + "code.cloudfoundry.org/silk/cni/lib/fakes" + "github.com/containernetworking/cni/pkg/types" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Container Setup", func() { + + var ( + containerNS *fakes.NetNS + cfg *config.Config + fakeLinkOperations *fakes.LinkOperations + fakeCommon *fakes.Common + containerSetup *lib.Container + containerAddr config.DualAddress + hostAddr config.DualAddress + fakelogger *lagertest.TestLogger + ) + + BeforeEach(func() { + fakeLinkOperations = &fakes.LinkOperations{} + fakeCommon = &fakes.Common{} + fakelogger = lagertest.NewTestLogger("test") + containerNS = &fakes.NetNS{} + containerNS.DoStub = lib.NetNsDoStub + + containerAddr = config.DualAddress{IP: net.IP{10, 255, 30, 4}} + hostAddr = config.DualAddress{IP: net.IP{169, 254, 0, 1}} + + cfg = &config.Config{} + cfg.Container.DeviceName = "eth0" + cfg.Container.Namespace = containerNS + cfg.Container.TemporaryDeviceName = "someTemporaryDeviceName" + cfg.Container.Address = containerAddr + cfg.Host.Address = hostAddr + cfg.Container.Routes = []*types.Route{ + &types.Route{ + Dst: net.IPNet{ + IP: []byte{50, 51, 52, 53}, + Mask: []byte{255, 255, 255, 255}, + }, + }, + &types.Route{ + Dst: net.IPNet{ + IP: []byte{150, 151, 152, 153}, + Mask: []byte{255, 255, 255, 255}, + }, + GW: net.IP{10, 150, 25, 2}, + }, + &types.Route{ + Dst: net.IPNet{ + IP: []byte{250, 251, 252, 0}, + Mask: []byte{255, 255, 255, 0}, + }, + GW: net.IP{10, 250, 25, 2}, + }, + } + + containerSetup = &lib.Container{ + Common: fakeCommon, + LinkOperations: fakeLinkOperations, + Logger: fakelogger, + } + }) + + Describe("Setup", func() { + It("renames the device and calls basic setup in the container namespace", func() { + err := containerSetup.Setup(cfg) + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeLinkOperations.RenameLinkCallCount()).To(Equal(1)) + oldName, newName := fakeLinkOperations.RenameLinkArgsForCall(0) + Expect(oldName).To(Equal("someTemporaryDeviceName")) + Expect(newName).To(Equal("eth0")) + + Expect(fakeCommon.BasicSetupCallCount()).To(Equal(1)) + device, local, peer := fakeCommon.BasicSetupArgsForCall(0) + Expect(device).To(Equal("eth0")) + Expect(local).To(Equal(containerAddr)) + Expect(peer).To(Equal(hostAddr)) + + By("Adding all the routes") + Expect(fakeLinkOperations.RouteAddAllCallCount()).To(Equal(1)) + routes, srcIP := fakeLinkOperations.RouteAddAllArgsForCall(0) + Expect(routes).To(Equal(cfg.Container.Routes)) + Expect(srcIP).To(Equal(cfg.Container.Address.IP)) + }) + + Context("when renaming the link fails", func() { + BeforeEach(func() { + fakeLinkOperations.RenameLinkReturns(errors.New("asparagus")) + }) + It("returns a meaningful error", func() { + err := containerSetup.Setup(cfg) + Expect(err).To(MatchError("renaming link in container: asparagus")) + }) + }) + + Context("when the basic device setup fails", func() { + BeforeEach(func() { + fakeCommon.BasicSetupReturns(errors.New("lettuce")) + }) + It("returns a meaningful error", func() { + err := containerSetup.Setup(cfg) + Expect(err).To(MatchError("setting up device in container: lettuce")) + }) + }) + + Context("when adding the routes fails", func() { + BeforeEach(func() { + fakeLinkOperations.RouteAddAllReturns(errors.New("lettuce")) + }) + It("returns a meaningful error", func() { + err := containerSetup.Setup(cfg) + Expect(err).To(MatchError("adding route in container: lettuce")) + }) + }) + }) + + Describe("Teardown", func() { + It("deletes the link in the specified namespace and device name", func() { + err := containerSetup.Teardown(containerNS, "eth0") + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeLinkOperations.DeleteLinkByNameCallCount()).To(Equal(1)) + Expect(fakeLinkOperations.DeleteLinkByNameArgsForCall(0)).To(Equal("eth0")) + }) + + Context("when deleting the link fails", func() { + BeforeEach(func() { + fakeLinkOperations.DeleteLinkByNameReturns(errors.New("eggplant")) + }) + It("returns a meaningul error", func() { + err := containerSetup.Teardown(containerNS, "eth0") + Expect(err).To(MatchError("deleting link: eggplant")) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/cni/lib/fakes/common.go b/src/code.cloudfoundry.org/silk/cni/lib/fakes/common.go new file mode 100644 index 00000000..909b3046 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/lib/fakes/common.go @@ -0,0 +1,100 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + + "code.cloudfoundry.org/silk/cni/config" +) + +type Common struct { + BasicSetupStub func(deviceName string, local, peer config.DualAddress) error + basicSetupMutex sync.RWMutex + basicSetupArgsForCall []struct { + deviceName string + local config.DualAddress + peer config.DualAddress + } + basicSetupReturns struct { + result1 error + } + basicSetupReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *Common) BasicSetup(deviceName string, local config.DualAddress, peer config.DualAddress) error { + fake.basicSetupMutex.Lock() + ret, specificReturn := fake.basicSetupReturnsOnCall[len(fake.basicSetupArgsForCall)] + fake.basicSetupArgsForCall = append(fake.basicSetupArgsForCall, struct { + deviceName string + local config.DualAddress + peer config.DualAddress + }{deviceName, local, peer}) + fake.recordInvocation("BasicSetup", []interface{}{deviceName, local, peer}) + fake.basicSetupMutex.Unlock() + if fake.BasicSetupStub != nil { + return fake.BasicSetupStub(deviceName, local, peer) + } + if specificReturn { + return ret.result1 + } + return fake.basicSetupReturns.result1 +} + +func (fake *Common) BasicSetupCallCount() int { + fake.basicSetupMutex.RLock() + defer fake.basicSetupMutex.RUnlock() + return len(fake.basicSetupArgsForCall) +} + +func (fake *Common) BasicSetupArgsForCall(i int) (string, config.DualAddress, config.DualAddress) { + fake.basicSetupMutex.RLock() + defer fake.basicSetupMutex.RUnlock() + return fake.basicSetupArgsForCall[i].deviceName, fake.basicSetupArgsForCall[i].local, fake.basicSetupArgsForCall[i].peer +} + +func (fake *Common) BasicSetupReturns(result1 error) { + fake.BasicSetupStub = nil + fake.basicSetupReturns = struct { + result1 error + }{result1} +} + +func (fake *Common) BasicSetupReturnsOnCall(i int, result1 error) { + fake.BasicSetupStub = nil + if fake.basicSetupReturnsOnCall == nil { + fake.basicSetupReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.basicSetupReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *Common) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.basicSetupMutex.RLock() + defer fake.basicSetupMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *Common) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/cni/lib/fakes/deviceNameGenerator.go b/src/code.cloudfoundry.org/silk/cni/lib/fakes/deviceNameGenerator.go new file mode 100644 index 00000000..696b6dea --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/lib/fakes/deviceNameGenerator.go @@ -0,0 +1,100 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "net" + "sync" +) + +type DeviceNameGenerator struct { + GenerateForHostIFBStub func(containerIP net.IP) (string, error) + generateForHostIFBMutex sync.RWMutex + generateForHostIFBArgsForCall []struct { + containerIP net.IP + } + generateForHostIFBReturns struct { + result1 string + result2 error + } + generateForHostIFBReturnsOnCall map[int]struct { + result1 string + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *DeviceNameGenerator) GenerateForHostIFB(containerIP net.IP) (string, error) { + fake.generateForHostIFBMutex.Lock() + ret, specificReturn := fake.generateForHostIFBReturnsOnCall[len(fake.generateForHostIFBArgsForCall)] + fake.generateForHostIFBArgsForCall = append(fake.generateForHostIFBArgsForCall, struct { + containerIP net.IP + }{containerIP}) + fake.recordInvocation("GenerateForHostIFB", []interface{}{containerIP}) + fake.generateForHostIFBMutex.Unlock() + if fake.GenerateForHostIFBStub != nil { + return fake.GenerateForHostIFBStub(containerIP) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.generateForHostIFBReturns.result1, fake.generateForHostIFBReturns.result2 +} + +func (fake *DeviceNameGenerator) GenerateForHostIFBCallCount() int { + fake.generateForHostIFBMutex.RLock() + defer fake.generateForHostIFBMutex.RUnlock() + return len(fake.generateForHostIFBArgsForCall) +} + +func (fake *DeviceNameGenerator) GenerateForHostIFBArgsForCall(i int) net.IP { + fake.generateForHostIFBMutex.RLock() + defer fake.generateForHostIFBMutex.RUnlock() + return fake.generateForHostIFBArgsForCall[i].containerIP +} + +func (fake *DeviceNameGenerator) GenerateForHostIFBReturns(result1 string, result2 error) { + fake.GenerateForHostIFBStub = nil + fake.generateForHostIFBReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *DeviceNameGenerator) GenerateForHostIFBReturnsOnCall(i int, result1 string, result2 error) { + fake.GenerateForHostIFBStub = nil + if fake.generateForHostIFBReturnsOnCall == nil { + fake.generateForHostIFBReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.generateForHostIFBReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *DeviceNameGenerator) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.generateForHostIFBMutex.RLock() + defer fake.generateForHostIFBMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *DeviceNameGenerator) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/cni/lib/fakes/linkOperations.go b/src/code.cloudfoundry.org/silk/cni/lib/fakes/linkOperations.go new file mode 100644 index 00000000..d4cb3a1b --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/lib/fakes/linkOperations.go @@ -0,0 +1,532 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "net" + "sync" + + "github.com/containernetworking/cni/pkg/types" + "github.com/vishvananda/netlink" +) + +type LinkOperations struct { + DisableIPv6Stub func(deviceName string) error + disableIPv6Mutex sync.RWMutex + disableIPv6ArgsForCall []struct { + deviceName string + } + disableIPv6Returns struct { + result1 error + } + disableIPv6ReturnsOnCall map[int]struct { + result1 error + } + StaticNeighborNoARPStub func(link netlink.Link, dstIP net.IP, mac net.HardwareAddr) error + staticNeighborNoARPMutex sync.RWMutex + staticNeighborNoARPArgsForCall []struct { + link netlink.Link + dstIP net.IP + mac net.HardwareAddr + } + staticNeighborNoARPReturns struct { + result1 error + } + staticNeighborNoARPReturnsOnCall map[int]struct { + result1 error + } + SetPointToPointAddressStub func(link netlink.Link, localIPAddr, peerIPAddr net.IP) error + setPointToPointAddressMutex sync.RWMutex + setPointToPointAddressArgsForCall []struct { + link netlink.Link + localIPAddr net.IP + peerIPAddr net.IP + } + setPointToPointAddressReturns struct { + result1 error + } + setPointToPointAddressReturnsOnCall map[int]struct { + result1 error + } + RenameLinkStub func(oldName, newName string) error + renameLinkMutex sync.RWMutex + renameLinkArgsForCall []struct { + oldName string + newName string + } + renameLinkReturns struct { + result1 error + } + renameLinkReturnsOnCall map[int]struct { + result1 error + } + DeleteLinkByNameStub func(deviceName string) error + deleteLinkByNameMutex sync.RWMutex + deleteLinkByNameArgsForCall []struct { + deviceName string + } + deleteLinkByNameReturns struct { + result1 error + } + deleteLinkByNameReturnsOnCall map[int]struct { + result1 error + } + RouteAddAllStub func(route []*types.Route, sourceIP net.IP) error + routeAddAllMutex sync.RWMutex + routeAddAllArgsForCall []struct { + route []*types.Route + sourceIP net.IP + } + routeAddAllReturns struct { + result1 error + } + routeAddAllReturnsOnCall map[int]struct { + result1 error + } + EnableIPv4ForwardingStub func() error + enableIPv4ForwardingMutex sync.RWMutex + enableIPv4ForwardingArgsForCall []struct{} + enableIPv4ForwardingReturns struct { + result1 error + } + enableIPv4ForwardingReturnsOnCall map[int]struct { + result1 error + } + EnableReversePathFilteringStub func(deviceName string) error + enableReversePathFilteringMutex sync.RWMutex + enableReversePathFilteringArgsForCall []struct { + deviceName string + } + enableReversePathFilteringReturns struct { + result1 error + } + enableReversePathFilteringReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *LinkOperations) DisableIPv6(deviceName string) error { + fake.disableIPv6Mutex.Lock() + ret, specificReturn := fake.disableIPv6ReturnsOnCall[len(fake.disableIPv6ArgsForCall)] + fake.disableIPv6ArgsForCall = append(fake.disableIPv6ArgsForCall, struct { + deviceName string + }{deviceName}) + fake.recordInvocation("DisableIPv6", []interface{}{deviceName}) + fake.disableIPv6Mutex.Unlock() + if fake.DisableIPv6Stub != nil { + return fake.DisableIPv6Stub(deviceName) + } + if specificReturn { + return ret.result1 + } + return fake.disableIPv6Returns.result1 +} + +func (fake *LinkOperations) DisableIPv6CallCount() int { + fake.disableIPv6Mutex.RLock() + defer fake.disableIPv6Mutex.RUnlock() + return len(fake.disableIPv6ArgsForCall) +} + +func (fake *LinkOperations) DisableIPv6ArgsForCall(i int) string { + fake.disableIPv6Mutex.RLock() + defer fake.disableIPv6Mutex.RUnlock() + return fake.disableIPv6ArgsForCall[i].deviceName +} + +func (fake *LinkOperations) DisableIPv6Returns(result1 error) { + fake.DisableIPv6Stub = nil + fake.disableIPv6Returns = struct { + result1 error + }{result1} +} + +func (fake *LinkOperations) DisableIPv6ReturnsOnCall(i int, result1 error) { + fake.DisableIPv6Stub = nil + if fake.disableIPv6ReturnsOnCall == nil { + fake.disableIPv6ReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.disableIPv6ReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *LinkOperations) StaticNeighborNoARP(link netlink.Link, dstIP net.IP, mac net.HardwareAddr) error { + fake.staticNeighborNoARPMutex.Lock() + ret, specificReturn := fake.staticNeighborNoARPReturnsOnCall[len(fake.staticNeighborNoARPArgsForCall)] + fake.staticNeighborNoARPArgsForCall = append(fake.staticNeighborNoARPArgsForCall, struct { + link netlink.Link + dstIP net.IP + mac net.HardwareAddr + }{link, dstIP, mac}) + fake.recordInvocation("StaticNeighborNoARP", []interface{}{link, dstIP, mac}) + fake.staticNeighborNoARPMutex.Unlock() + if fake.StaticNeighborNoARPStub != nil { + return fake.StaticNeighborNoARPStub(link, dstIP, mac) + } + if specificReturn { + return ret.result1 + } + return fake.staticNeighborNoARPReturns.result1 +} + +func (fake *LinkOperations) StaticNeighborNoARPCallCount() int { + fake.staticNeighborNoARPMutex.RLock() + defer fake.staticNeighborNoARPMutex.RUnlock() + return len(fake.staticNeighborNoARPArgsForCall) +} + +func (fake *LinkOperations) StaticNeighborNoARPArgsForCall(i int) (netlink.Link, net.IP, net.HardwareAddr) { + fake.staticNeighborNoARPMutex.RLock() + defer fake.staticNeighborNoARPMutex.RUnlock() + return fake.staticNeighborNoARPArgsForCall[i].link, fake.staticNeighborNoARPArgsForCall[i].dstIP, fake.staticNeighborNoARPArgsForCall[i].mac +} + +func (fake *LinkOperations) StaticNeighborNoARPReturns(result1 error) { + fake.StaticNeighborNoARPStub = nil + fake.staticNeighborNoARPReturns = struct { + result1 error + }{result1} +} + +func (fake *LinkOperations) StaticNeighborNoARPReturnsOnCall(i int, result1 error) { + fake.StaticNeighborNoARPStub = nil + if fake.staticNeighborNoARPReturnsOnCall == nil { + fake.staticNeighborNoARPReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.staticNeighborNoARPReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *LinkOperations) SetPointToPointAddress(link netlink.Link, localIPAddr net.IP, peerIPAddr net.IP) error { + fake.setPointToPointAddressMutex.Lock() + ret, specificReturn := fake.setPointToPointAddressReturnsOnCall[len(fake.setPointToPointAddressArgsForCall)] + fake.setPointToPointAddressArgsForCall = append(fake.setPointToPointAddressArgsForCall, struct { + link netlink.Link + localIPAddr net.IP + peerIPAddr net.IP + }{link, localIPAddr, peerIPAddr}) + fake.recordInvocation("SetPointToPointAddress", []interface{}{link, localIPAddr, peerIPAddr}) + fake.setPointToPointAddressMutex.Unlock() + if fake.SetPointToPointAddressStub != nil { + return fake.SetPointToPointAddressStub(link, localIPAddr, peerIPAddr) + } + if specificReturn { + return ret.result1 + } + return fake.setPointToPointAddressReturns.result1 +} + +func (fake *LinkOperations) SetPointToPointAddressCallCount() int { + fake.setPointToPointAddressMutex.RLock() + defer fake.setPointToPointAddressMutex.RUnlock() + return len(fake.setPointToPointAddressArgsForCall) +} + +func (fake *LinkOperations) SetPointToPointAddressArgsForCall(i int) (netlink.Link, net.IP, net.IP) { + fake.setPointToPointAddressMutex.RLock() + defer fake.setPointToPointAddressMutex.RUnlock() + return fake.setPointToPointAddressArgsForCall[i].link, fake.setPointToPointAddressArgsForCall[i].localIPAddr, fake.setPointToPointAddressArgsForCall[i].peerIPAddr +} + +func (fake *LinkOperations) SetPointToPointAddressReturns(result1 error) { + fake.SetPointToPointAddressStub = nil + fake.setPointToPointAddressReturns = struct { + result1 error + }{result1} +} + +func (fake *LinkOperations) SetPointToPointAddressReturnsOnCall(i int, result1 error) { + fake.SetPointToPointAddressStub = nil + if fake.setPointToPointAddressReturnsOnCall == nil { + fake.setPointToPointAddressReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.setPointToPointAddressReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *LinkOperations) RenameLink(oldName string, newName string) error { + fake.renameLinkMutex.Lock() + ret, specificReturn := fake.renameLinkReturnsOnCall[len(fake.renameLinkArgsForCall)] + fake.renameLinkArgsForCall = append(fake.renameLinkArgsForCall, struct { + oldName string + newName string + }{oldName, newName}) + fake.recordInvocation("RenameLink", []interface{}{oldName, newName}) + fake.renameLinkMutex.Unlock() + if fake.RenameLinkStub != nil { + return fake.RenameLinkStub(oldName, newName) + } + if specificReturn { + return ret.result1 + } + return fake.renameLinkReturns.result1 +} + +func (fake *LinkOperations) RenameLinkCallCount() int { + fake.renameLinkMutex.RLock() + defer fake.renameLinkMutex.RUnlock() + return len(fake.renameLinkArgsForCall) +} + +func (fake *LinkOperations) RenameLinkArgsForCall(i int) (string, string) { + fake.renameLinkMutex.RLock() + defer fake.renameLinkMutex.RUnlock() + return fake.renameLinkArgsForCall[i].oldName, fake.renameLinkArgsForCall[i].newName +} + +func (fake *LinkOperations) RenameLinkReturns(result1 error) { + fake.RenameLinkStub = nil + fake.renameLinkReturns = struct { + result1 error + }{result1} +} + +func (fake *LinkOperations) RenameLinkReturnsOnCall(i int, result1 error) { + fake.RenameLinkStub = nil + if fake.renameLinkReturnsOnCall == nil { + fake.renameLinkReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.renameLinkReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *LinkOperations) DeleteLinkByName(deviceName string) error { + fake.deleteLinkByNameMutex.Lock() + ret, specificReturn := fake.deleteLinkByNameReturnsOnCall[len(fake.deleteLinkByNameArgsForCall)] + fake.deleteLinkByNameArgsForCall = append(fake.deleteLinkByNameArgsForCall, struct { + deviceName string + }{deviceName}) + fake.recordInvocation("DeleteLinkByName", []interface{}{deviceName}) + fake.deleteLinkByNameMutex.Unlock() + if fake.DeleteLinkByNameStub != nil { + return fake.DeleteLinkByNameStub(deviceName) + } + if specificReturn { + return ret.result1 + } + return fake.deleteLinkByNameReturns.result1 +} + +func (fake *LinkOperations) DeleteLinkByNameCallCount() int { + fake.deleteLinkByNameMutex.RLock() + defer fake.deleteLinkByNameMutex.RUnlock() + return len(fake.deleteLinkByNameArgsForCall) +} + +func (fake *LinkOperations) DeleteLinkByNameArgsForCall(i int) string { + fake.deleteLinkByNameMutex.RLock() + defer fake.deleteLinkByNameMutex.RUnlock() + return fake.deleteLinkByNameArgsForCall[i].deviceName +} + +func (fake *LinkOperations) DeleteLinkByNameReturns(result1 error) { + fake.DeleteLinkByNameStub = nil + fake.deleteLinkByNameReturns = struct { + result1 error + }{result1} +} + +func (fake *LinkOperations) DeleteLinkByNameReturnsOnCall(i int, result1 error) { + fake.DeleteLinkByNameStub = nil + if fake.deleteLinkByNameReturnsOnCall == nil { + fake.deleteLinkByNameReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteLinkByNameReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *LinkOperations) RouteAddAll(route []*types.Route, sourceIP net.IP) error { + var routeCopy []*types.Route + if route != nil { + routeCopy = make([]*types.Route, len(route)) + copy(routeCopy, route) + } + fake.routeAddAllMutex.Lock() + ret, specificReturn := fake.routeAddAllReturnsOnCall[len(fake.routeAddAllArgsForCall)] + fake.routeAddAllArgsForCall = append(fake.routeAddAllArgsForCall, struct { + route []*types.Route + sourceIP net.IP + }{routeCopy, sourceIP}) + fake.recordInvocation("RouteAddAll", []interface{}{routeCopy, sourceIP}) + fake.routeAddAllMutex.Unlock() + if fake.RouteAddAllStub != nil { + return fake.RouteAddAllStub(route, sourceIP) + } + if specificReturn { + return ret.result1 + } + return fake.routeAddAllReturns.result1 +} + +func (fake *LinkOperations) RouteAddAllCallCount() int { + fake.routeAddAllMutex.RLock() + defer fake.routeAddAllMutex.RUnlock() + return len(fake.routeAddAllArgsForCall) +} + +func (fake *LinkOperations) RouteAddAllArgsForCall(i int) ([]*types.Route, net.IP) { + fake.routeAddAllMutex.RLock() + defer fake.routeAddAllMutex.RUnlock() + return fake.routeAddAllArgsForCall[i].route, fake.routeAddAllArgsForCall[i].sourceIP +} + +func (fake *LinkOperations) RouteAddAllReturns(result1 error) { + fake.RouteAddAllStub = nil + fake.routeAddAllReturns = struct { + result1 error + }{result1} +} + +func (fake *LinkOperations) RouteAddAllReturnsOnCall(i int, result1 error) { + fake.RouteAddAllStub = nil + if fake.routeAddAllReturnsOnCall == nil { + fake.routeAddAllReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.routeAddAllReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *LinkOperations) EnableIPv4Forwarding() error { + fake.enableIPv4ForwardingMutex.Lock() + ret, specificReturn := fake.enableIPv4ForwardingReturnsOnCall[len(fake.enableIPv4ForwardingArgsForCall)] + fake.enableIPv4ForwardingArgsForCall = append(fake.enableIPv4ForwardingArgsForCall, struct{}{}) + fake.recordInvocation("EnableIPv4Forwarding", []interface{}{}) + fake.enableIPv4ForwardingMutex.Unlock() + if fake.EnableIPv4ForwardingStub != nil { + return fake.EnableIPv4ForwardingStub() + } + if specificReturn { + return ret.result1 + } + return fake.enableIPv4ForwardingReturns.result1 +} + +func (fake *LinkOperations) EnableIPv4ForwardingCallCount() int { + fake.enableIPv4ForwardingMutex.RLock() + defer fake.enableIPv4ForwardingMutex.RUnlock() + return len(fake.enableIPv4ForwardingArgsForCall) +} + +func (fake *LinkOperations) EnableIPv4ForwardingReturns(result1 error) { + fake.EnableIPv4ForwardingStub = nil + fake.enableIPv4ForwardingReturns = struct { + result1 error + }{result1} +} + +func (fake *LinkOperations) EnableIPv4ForwardingReturnsOnCall(i int, result1 error) { + fake.EnableIPv4ForwardingStub = nil + if fake.enableIPv4ForwardingReturnsOnCall == nil { + fake.enableIPv4ForwardingReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.enableIPv4ForwardingReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *LinkOperations) EnableReversePathFiltering(deviceName string) error { + fake.enableReversePathFilteringMutex.Lock() + ret, specificReturn := fake.enableReversePathFilteringReturnsOnCall[len(fake.enableReversePathFilteringArgsForCall)] + fake.enableReversePathFilteringArgsForCall = append(fake.enableReversePathFilteringArgsForCall, struct { + deviceName string + }{deviceName}) + fake.recordInvocation("EnableReversePathFiltering", []interface{}{deviceName}) + fake.enableReversePathFilteringMutex.Unlock() + if fake.EnableReversePathFilteringStub != nil { + return fake.EnableReversePathFilteringStub(deviceName) + } + if specificReturn { + return ret.result1 + } + return fake.enableReversePathFilteringReturns.result1 +} + +func (fake *LinkOperations) EnableReversePathFilteringCallCount() int { + fake.enableReversePathFilteringMutex.RLock() + defer fake.enableReversePathFilteringMutex.RUnlock() + return len(fake.enableReversePathFilteringArgsForCall) +} + +func (fake *LinkOperations) EnableReversePathFilteringArgsForCall(i int) string { + fake.enableReversePathFilteringMutex.RLock() + defer fake.enableReversePathFilteringMutex.RUnlock() + return fake.enableReversePathFilteringArgsForCall[i].deviceName +} + +func (fake *LinkOperations) EnableReversePathFilteringReturns(result1 error) { + fake.EnableReversePathFilteringStub = nil + fake.enableReversePathFilteringReturns = struct { + result1 error + }{result1} +} + +func (fake *LinkOperations) EnableReversePathFilteringReturnsOnCall(i int, result1 error) { + fake.EnableReversePathFilteringStub = nil + if fake.enableReversePathFilteringReturnsOnCall == nil { + fake.enableReversePathFilteringReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.enableReversePathFilteringReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *LinkOperations) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.disableIPv6Mutex.RLock() + defer fake.disableIPv6Mutex.RUnlock() + fake.staticNeighborNoARPMutex.RLock() + defer fake.staticNeighborNoARPMutex.RUnlock() + fake.setPointToPointAddressMutex.RLock() + defer fake.setPointToPointAddressMutex.RUnlock() + fake.renameLinkMutex.RLock() + defer fake.renameLinkMutex.RUnlock() + fake.deleteLinkByNameMutex.RLock() + defer fake.deleteLinkByNameMutex.RUnlock() + fake.routeAddAllMutex.RLock() + defer fake.routeAddAllMutex.RUnlock() + fake.enableIPv4ForwardingMutex.RLock() + defer fake.enableIPv4ForwardingMutex.RUnlock() + fake.enableReversePathFilteringMutex.RLock() + defer fake.enableReversePathFilteringMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *LinkOperations) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/cni/lib/fakes/namespaceAdapter.go b/src/code.cloudfoundry.org/silk/cni/lib/fakes/namespaceAdapter.go new file mode 100644 index 00000000..ea7d7ec4 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/lib/fakes/namespaceAdapter.go @@ -0,0 +1,157 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + + "github.com/containernetworking/plugins/pkg/ns" +) + +type NamespaceAdapter struct { + GetNSStub func(string) (ns.NetNS, error) + getNSMutex sync.RWMutex + getNSArgsForCall []struct { + arg1 string + } + getNSReturns struct { + result1 ns.NetNS + result2 error + } + getNSReturnsOnCall map[int]struct { + result1 ns.NetNS + result2 error + } + GetCurrentNSStub func() (ns.NetNS, error) + getCurrentNSMutex sync.RWMutex + getCurrentNSArgsForCall []struct{} + getCurrentNSReturns struct { + result1 ns.NetNS + result2 error + } + getCurrentNSReturnsOnCall map[int]struct { + result1 ns.NetNS + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *NamespaceAdapter) GetNS(arg1 string) (ns.NetNS, error) { + fake.getNSMutex.Lock() + ret, specificReturn := fake.getNSReturnsOnCall[len(fake.getNSArgsForCall)] + fake.getNSArgsForCall = append(fake.getNSArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("GetNS", []interface{}{arg1}) + fake.getNSMutex.Unlock() + if fake.GetNSStub != nil { + return fake.GetNSStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.getNSReturns.result1, fake.getNSReturns.result2 +} + +func (fake *NamespaceAdapter) GetNSCallCount() int { + fake.getNSMutex.RLock() + defer fake.getNSMutex.RUnlock() + return len(fake.getNSArgsForCall) +} + +func (fake *NamespaceAdapter) GetNSArgsForCall(i int) string { + fake.getNSMutex.RLock() + defer fake.getNSMutex.RUnlock() + return fake.getNSArgsForCall[i].arg1 +} + +func (fake *NamespaceAdapter) GetNSReturns(result1 ns.NetNS, result2 error) { + fake.GetNSStub = nil + fake.getNSReturns = struct { + result1 ns.NetNS + result2 error + }{result1, result2} +} + +func (fake *NamespaceAdapter) GetNSReturnsOnCall(i int, result1 ns.NetNS, result2 error) { + fake.GetNSStub = nil + if fake.getNSReturnsOnCall == nil { + fake.getNSReturnsOnCall = make(map[int]struct { + result1 ns.NetNS + result2 error + }) + } + fake.getNSReturnsOnCall[i] = struct { + result1 ns.NetNS + result2 error + }{result1, result2} +} + +func (fake *NamespaceAdapter) GetCurrentNS() (ns.NetNS, error) { + fake.getCurrentNSMutex.Lock() + ret, specificReturn := fake.getCurrentNSReturnsOnCall[len(fake.getCurrentNSArgsForCall)] + fake.getCurrentNSArgsForCall = append(fake.getCurrentNSArgsForCall, struct{}{}) + fake.recordInvocation("GetCurrentNS", []interface{}{}) + fake.getCurrentNSMutex.Unlock() + if fake.GetCurrentNSStub != nil { + return fake.GetCurrentNSStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.getCurrentNSReturns.result1, fake.getCurrentNSReturns.result2 +} + +func (fake *NamespaceAdapter) GetCurrentNSCallCount() int { + fake.getCurrentNSMutex.RLock() + defer fake.getCurrentNSMutex.RUnlock() + return len(fake.getCurrentNSArgsForCall) +} + +func (fake *NamespaceAdapter) GetCurrentNSReturns(result1 ns.NetNS, result2 error) { + fake.GetCurrentNSStub = nil + fake.getCurrentNSReturns = struct { + result1 ns.NetNS + result2 error + }{result1, result2} +} + +func (fake *NamespaceAdapter) GetCurrentNSReturnsOnCall(i int, result1 ns.NetNS, result2 error) { + fake.GetCurrentNSStub = nil + if fake.getCurrentNSReturnsOnCall == nil { + fake.getCurrentNSReturnsOnCall = make(map[int]struct { + result1 ns.NetNS + result2 error + }) + } + fake.getCurrentNSReturnsOnCall[i] = struct { + result1 ns.NetNS + result2 error + }{result1, result2} +} + +func (fake *NamespaceAdapter) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getNSMutex.RLock() + defer fake.getNSMutex.RUnlock() + fake.getCurrentNSMutex.RLock() + defer fake.getCurrentNSMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *NamespaceAdapter) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/cni/lib/fakes/netNS.go b/src/code.cloudfoundry.org/silk/cni/lib/fakes/netNS.go new file mode 100644 index 00000000..813e554a --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/lib/fakes/netNS.go @@ -0,0 +1,300 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + + "github.com/containernetworking/plugins/pkg/ns" +) + +type NetNS struct { + DoStub func(toRun func(ns.NetNS) error) error + doMutex sync.RWMutex + doArgsForCall []struct { + toRun func(ns.NetNS) error + } + doReturns struct { + result1 error + } + doReturnsOnCall map[int]struct { + result1 error + } + SetStub func() error + setMutex sync.RWMutex + setArgsForCall []struct{} + setReturns struct { + result1 error + } + setReturnsOnCall map[int]struct { + result1 error + } + PathStub func() string + pathMutex sync.RWMutex + pathArgsForCall []struct{} + pathReturns struct { + result1 string + } + pathReturnsOnCall map[int]struct { + result1 string + } + FdStub func() uintptr + fdMutex sync.RWMutex + fdArgsForCall []struct{} + fdReturns struct { + result1 uintptr + } + fdReturnsOnCall map[int]struct { + result1 uintptr + } + CloseStub func() error + closeMutex sync.RWMutex + closeArgsForCall []struct{} + closeReturns struct { + result1 error + } + closeReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *NetNS) Do(toRun func(ns.NetNS) error) error { + fake.doMutex.Lock() + ret, specificReturn := fake.doReturnsOnCall[len(fake.doArgsForCall)] + fake.doArgsForCall = append(fake.doArgsForCall, struct { + toRun func(ns.NetNS) error + }{toRun}) + fake.recordInvocation("Do", []interface{}{toRun}) + fake.doMutex.Unlock() + if fake.DoStub != nil { + return fake.DoStub(toRun) + } + if specificReturn { + return ret.result1 + } + return fake.doReturns.result1 +} + +func (fake *NetNS) DoCallCount() int { + fake.doMutex.RLock() + defer fake.doMutex.RUnlock() + return len(fake.doArgsForCall) +} + +func (fake *NetNS) DoArgsForCall(i int) func(ns.NetNS) error { + fake.doMutex.RLock() + defer fake.doMutex.RUnlock() + return fake.doArgsForCall[i].toRun +} + +func (fake *NetNS) DoReturns(result1 error) { + fake.DoStub = nil + fake.doReturns = struct { + result1 error + }{result1} +} + +func (fake *NetNS) DoReturnsOnCall(i int, result1 error) { + fake.DoStub = nil + if fake.doReturnsOnCall == nil { + fake.doReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.doReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetNS) Set() error { + fake.setMutex.Lock() + ret, specificReturn := fake.setReturnsOnCall[len(fake.setArgsForCall)] + fake.setArgsForCall = append(fake.setArgsForCall, struct{}{}) + fake.recordInvocation("Set", []interface{}{}) + fake.setMutex.Unlock() + if fake.SetStub != nil { + return fake.SetStub() + } + if specificReturn { + return ret.result1 + } + return fake.setReturns.result1 +} + +func (fake *NetNS) SetCallCount() int { + fake.setMutex.RLock() + defer fake.setMutex.RUnlock() + return len(fake.setArgsForCall) +} + +func (fake *NetNS) SetReturns(result1 error) { + fake.SetStub = nil + fake.setReturns = struct { + result1 error + }{result1} +} + +func (fake *NetNS) SetReturnsOnCall(i int, result1 error) { + fake.SetStub = nil + if fake.setReturnsOnCall == nil { + fake.setReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.setReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetNS) Path() string { + fake.pathMutex.Lock() + ret, specificReturn := fake.pathReturnsOnCall[len(fake.pathArgsForCall)] + fake.pathArgsForCall = append(fake.pathArgsForCall, struct{}{}) + fake.recordInvocation("Path", []interface{}{}) + fake.pathMutex.Unlock() + if fake.PathStub != nil { + return fake.PathStub() + } + if specificReturn { + return ret.result1 + } + return fake.pathReturns.result1 +} + +func (fake *NetNS) PathCallCount() int { + fake.pathMutex.RLock() + defer fake.pathMutex.RUnlock() + return len(fake.pathArgsForCall) +} + +func (fake *NetNS) PathReturns(result1 string) { + fake.PathStub = nil + fake.pathReturns = struct { + result1 string + }{result1} +} + +func (fake *NetNS) PathReturnsOnCall(i int, result1 string) { + fake.PathStub = nil + if fake.pathReturnsOnCall == nil { + fake.pathReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.pathReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *NetNS) Fd() uintptr { + fake.fdMutex.Lock() + ret, specificReturn := fake.fdReturnsOnCall[len(fake.fdArgsForCall)] + fake.fdArgsForCall = append(fake.fdArgsForCall, struct{}{}) + fake.recordInvocation("Fd", []interface{}{}) + fake.fdMutex.Unlock() + if fake.FdStub != nil { + return fake.FdStub() + } + if specificReturn { + return ret.result1 + } + return fake.fdReturns.result1 +} + +func (fake *NetNS) FdCallCount() int { + fake.fdMutex.RLock() + defer fake.fdMutex.RUnlock() + return len(fake.fdArgsForCall) +} + +func (fake *NetNS) FdReturns(result1 uintptr) { + fake.FdStub = nil + fake.fdReturns = struct { + result1 uintptr + }{result1} +} + +func (fake *NetNS) FdReturnsOnCall(i int, result1 uintptr) { + fake.FdStub = nil + if fake.fdReturnsOnCall == nil { + fake.fdReturnsOnCall = make(map[int]struct { + result1 uintptr + }) + } + fake.fdReturnsOnCall[i] = struct { + result1 uintptr + }{result1} +} + +func (fake *NetNS) Close() error { + fake.closeMutex.Lock() + ret, specificReturn := fake.closeReturnsOnCall[len(fake.closeArgsForCall)] + fake.closeArgsForCall = append(fake.closeArgsForCall, struct{}{}) + fake.recordInvocation("Close", []interface{}{}) + fake.closeMutex.Unlock() + if fake.CloseStub != nil { + return fake.CloseStub() + } + if specificReturn { + return ret.result1 + } + return fake.closeReturns.result1 +} + +func (fake *NetNS) CloseCallCount() int { + fake.closeMutex.RLock() + defer fake.closeMutex.RUnlock() + return len(fake.closeArgsForCall) +} + +func (fake *NetNS) CloseReturns(result1 error) { + fake.CloseStub = nil + fake.closeReturns = struct { + result1 error + }{result1} +} + +func (fake *NetNS) CloseReturnsOnCall(i int, result1 error) { + fake.CloseStub = nil + if fake.closeReturnsOnCall == nil { + fake.closeReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.closeReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetNS) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.doMutex.RLock() + defer fake.doMutex.RUnlock() + fake.setMutex.RLock() + defer fake.setMutex.RUnlock() + fake.pathMutex.RLock() + defer fake.pathMutex.RUnlock() + fake.fdMutex.RLock() + defer fake.fdMutex.RUnlock() + fake.closeMutex.RLock() + defer fake.closeMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *NetNS) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/cni/lib/fakes/netlinkAdapter.go b/src/code.cloudfoundry.org/silk/cni/lib/fakes/netlinkAdapter.go new file mode 100644 index 00000000..c3f62d9d --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/lib/fakes/netlinkAdapter.go @@ -0,0 +1,1031 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "net" + "sync" + + "github.com/vishvananda/netlink" +) + +type NetlinkAdapter struct { + LinkByNameStub func(string) (netlink.Link, error) + linkByNameMutex sync.RWMutex + linkByNameArgsForCall []struct { + arg1 string + } + linkByNameReturns struct { + result1 netlink.Link + result2 error + } + linkByNameReturnsOnCall map[int]struct { + result1 netlink.Link + result2 error + } + ParseAddrStub func(string) (*netlink.Addr, error) + parseAddrMutex sync.RWMutex + parseAddrArgsForCall []struct { + arg1 string + } + parseAddrReturns struct { + result1 *netlink.Addr + result2 error + } + parseAddrReturnsOnCall map[int]struct { + result1 *netlink.Addr + result2 error + } + AddrAddScopeLinkStub func(netlink.Link, *netlink.Addr) error + addrAddScopeLinkMutex sync.RWMutex + addrAddScopeLinkArgsForCall []struct { + arg1 netlink.Link + arg2 *netlink.Addr + } + addrAddScopeLinkReturns struct { + result1 error + } + addrAddScopeLinkReturnsOnCall map[int]struct { + result1 error + } + LinkSetHardwareAddrStub func(netlink.Link, net.HardwareAddr) error + linkSetHardwareAddrMutex sync.RWMutex + linkSetHardwareAddrArgsForCall []struct { + arg1 netlink.Link + arg2 net.HardwareAddr + } + linkSetHardwareAddrReturns struct { + result1 error + } + linkSetHardwareAddrReturnsOnCall map[int]struct { + result1 error + } + NeighAddPermanentIPv4Stub func(index int, destIP net.IP, hwAddr net.HardwareAddr) error + neighAddPermanentIPv4Mutex sync.RWMutex + neighAddPermanentIPv4ArgsForCall []struct { + index int + destIP net.IP + hwAddr net.HardwareAddr + } + neighAddPermanentIPv4Returns struct { + result1 error + } + neighAddPermanentIPv4ReturnsOnCall map[int]struct { + result1 error + } + LinkSetARPOffStub func(netlink.Link) error + linkSetARPOffMutex sync.RWMutex + linkSetARPOffArgsForCall []struct { + arg1 netlink.Link + } + linkSetARPOffReturns struct { + result1 error + } + linkSetARPOffReturnsOnCall map[int]struct { + result1 error + } + LinkSetNameStub func(netlink.Link, string) error + linkSetNameMutex sync.RWMutex + linkSetNameArgsForCall []struct { + arg1 netlink.Link + arg2 string + } + linkSetNameReturns struct { + result1 error + } + linkSetNameReturnsOnCall map[int]struct { + result1 error + } + LinkSetUpStub func(netlink.Link) error + linkSetUpMutex sync.RWMutex + linkSetUpArgsForCall []struct { + arg1 netlink.Link + } + linkSetUpReturns struct { + result1 error + } + linkSetUpReturnsOnCall map[int]struct { + result1 error + } + LinkDelStub func(netlink.Link) error + linkDelMutex sync.RWMutex + linkDelArgsForCall []struct { + arg1 netlink.Link + } + linkDelReturns struct { + result1 error + } + linkDelReturnsOnCall map[int]struct { + result1 error + } + LinkAddStub func(netlink.Link) error + linkAddMutex sync.RWMutex + linkAddArgsForCall []struct { + arg1 netlink.Link + } + linkAddReturns struct { + result1 error + } + linkAddReturnsOnCall map[int]struct { + result1 error + } + LinkSetNsFdStub func(netlink.Link, int) error + linkSetNsFdMutex sync.RWMutex + linkSetNsFdArgsForCall []struct { + arg1 netlink.Link + arg2 int + } + linkSetNsFdReturns struct { + result1 error + } + linkSetNsFdReturnsOnCall map[int]struct { + result1 error + } + RouteAddStub func(route *netlink.Route) error + routeAddMutex sync.RWMutex + routeAddArgsForCall []struct { + route *netlink.Route + } + routeAddReturns struct { + result1 error + } + routeAddReturnsOnCall map[int]struct { + result1 error + } + QdiscAddStub func(qdisc netlink.Qdisc) error + qdiscAddMutex sync.RWMutex + qdiscAddArgsForCall []struct { + qdisc netlink.Qdisc + } + qdiscAddReturns struct { + result1 error + } + qdiscAddReturnsOnCall map[int]struct { + result1 error + } + FilterAddStub func(netlink.Filter) error + filterAddMutex sync.RWMutex + filterAddArgsForCall []struct { + arg1 netlink.Filter + } + filterAddReturns struct { + result1 error + } + filterAddReturnsOnCall map[int]struct { + result1 error + } + AddrListStub func(link netlink.Link, family int) ([]netlink.Addr, error) + addrListMutex sync.RWMutex + addrListArgsForCall []struct { + link netlink.Link + family int + } + addrListReturns struct { + result1 []netlink.Addr + result2 error + } + addrListReturnsOnCall map[int]struct { + result1 []netlink.Addr + result2 error + } + TickInUsecStub func() float64 + tickInUsecMutex sync.RWMutex + tickInUsecArgsForCall []struct{} + tickInUsecReturns struct { + result1 float64 + } + tickInUsecReturnsOnCall map[int]struct { + result1 float64 + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *NetlinkAdapter) LinkByName(arg1 string) (netlink.Link, error) { + fake.linkByNameMutex.Lock() + ret, specificReturn := fake.linkByNameReturnsOnCall[len(fake.linkByNameArgsForCall)] + fake.linkByNameArgsForCall = append(fake.linkByNameArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("LinkByName", []interface{}{arg1}) + fake.linkByNameMutex.Unlock() + if fake.LinkByNameStub != nil { + return fake.LinkByNameStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.linkByNameReturns.result1, fake.linkByNameReturns.result2 +} + +func (fake *NetlinkAdapter) LinkByNameCallCount() int { + fake.linkByNameMutex.RLock() + defer fake.linkByNameMutex.RUnlock() + return len(fake.linkByNameArgsForCall) +} + +func (fake *NetlinkAdapter) LinkByNameArgsForCall(i int) string { + fake.linkByNameMutex.RLock() + defer fake.linkByNameMutex.RUnlock() + return fake.linkByNameArgsForCall[i].arg1 +} + +func (fake *NetlinkAdapter) LinkByNameReturns(result1 netlink.Link, result2 error) { + fake.LinkByNameStub = nil + fake.linkByNameReturns = struct { + result1 netlink.Link + result2 error + }{result1, result2} +} + +func (fake *NetlinkAdapter) LinkByNameReturnsOnCall(i int, result1 netlink.Link, result2 error) { + fake.LinkByNameStub = nil + if fake.linkByNameReturnsOnCall == nil { + fake.linkByNameReturnsOnCall = make(map[int]struct { + result1 netlink.Link + result2 error + }) + } + fake.linkByNameReturnsOnCall[i] = struct { + result1 netlink.Link + result2 error + }{result1, result2} +} + +func (fake *NetlinkAdapter) ParseAddr(arg1 string) (*netlink.Addr, error) { + fake.parseAddrMutex.Lock() + ret, specificReturn := fake.parseAddrReturnsOnCall[len(fake.parseAddrArgsForCall)] + fake.parseAddrArgsForCall = append(fake.parseAddrArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("ParseAddr", []interface{}{arg1}) + fake.parseAddrMutex.Unlock() + if fake.ParseAddrStub != nil { + return fake.ParseAddrStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.parseAddrReturns.result1, fake.parseAddrReturns.result2 +} + +func (fake *NetlinkAdapter) ParseAddrCallCount() int { + fake.parseAddrMutex.RLock() + defer fake.parseAddrMutex.RUnlock() + return len(fake.parseAddrArgsForCall) +} + +func (fake *NetlinkAdapter) ParseAddrArgsForCall(i int) string { + fake.parseAddrMutex.RLock() + defer fake.parseAddrMutex.RUnlock() + return fake.parseAddrArgsForCall[i].arg1 +} + +func (fake *NetlinkAdapter) ParseAddrReturns(result1 *netlink.Addr, result2 error) { + fake.ParseAddrStub = nil + fake.parseAddrReturns = struct { + result1 *netlink.Addr + result2 error + }{result1, result2} +} + +func (fake *NetlinkAdapter) ParseAddrReturnsOnCall(i int, result1 *netlink.Addr, result2 error) { + fake.ParseAddrStub = nil + if fake.parseAddrReturnsOnCall == nil { + fake.parseAddrReturnsOnCall = make(map[int]struct { + result1 *netlink.Addr + result2 error + }) + } + fake.parseAddrReturnsOnCall[i] = struct { + result1 *netlink.Addr + result2 error + }{result1, result2} +} + +func (fake *NetlinkAdapter) AddrAddScopeLink(arg1 netlink.Link, arg2 *netlink.Addr) error { + fake.addrAddScopeLinkMutex.Lock() + ret, specificReturn := fake.addrAddScopeLinkReturnsOnCall[len(fake.addrAddScopeLinkArgsForCall)] + fake.addrAddScopeLinkArgsForCall = append(fake.addrAddScopeLinkArgsForCall, struct { + arg1 netlink.Link + arg2 *netlink.Addr + }{arg1, arg2}) + fake.recordInvocation("AddrAddScopeLink", []interface{}{arg1, arg2}) + fake.addrAddScopeLinkMutex.Unlock() + if fake.AddrAddScopeLinkStub != nil { + return fake.AddrAddScopeLinkStub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fake.addrAddScopeLinkReturns.result1 +} + +func (fake *NetlinkAdapter) AddrAddScopeLinkCallCount() int { + fake.addrAddScopeLinkMutex.RLock() + defer fake.addrAddScopeLinkMutex.RUnlock() + return len(fake.addrAddScopeLinkArgsForCall) +} + +func (fake *NetlinkAdapter) AddrAddScopeLinkArgsForCall(i int) (netlink.Link, *netlink.Addr) { + fake.addrAddScopeLinkMutex.RLock() + defer fake.addrAddScopeLinkMutex.RUnlock() + return fake.addrAddScopeLinkArgsForCall[i].arg1, fake.addrAddScopeLinkArgsForCall[i].arg2 +} + +func (fake *NetlinkAdapter) AddrAddScopeLinkReturns(result1 error) { + fake.AddrAddScopeLinkStub = nil + fake.addrAddScopeLinkReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) AddrAddScopeLinkReturnsOnCall(i int, result1 error) { + fake.AddrAddScopeLinkStub = nil + if fake.addrAddScopeLinkReturnsOnCall == nil { + fake.addrAddScopeLinkReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.addrAddScopeLinkReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkSetHardwareAddr(arg1 netlink.Link, arg2 net.HardwareAddr) error { + fake.linkSetHardwareAddrMutex.Lock() + ret, specificReturn := fake.linkSetHardwareAddrReturnsOnCall[len(fake.linkSetHardwareAddrArgsForCall)] + fake.linkSetHardwareAddrArgsForCall = append(fake.linkSetHardwareAddrArgsForCall, struct { + arg1 netlink.Link + arg2 net.HardwareAddr + }{arg1, arg2}) + fake.recordInvocation("LinkSetHardwareAddr", []interface{}{arg1, arg2}) + fake.linkSetHardwareAddrMutex.Unlock() + if fake.LinkSetHardwareAddrStub != nil { + return fake.LinkSetHardwareAddrStub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fake.linkSetHardwareAddrReturns.result1 +} + +func (fake *NetlinkAdapter) LinkSetHardwareAddrCallCount() int { + fake.linkSetHardwareAddrMutex.RLock() + defer fake.linkSetHardwareAddrMutex.RUnlock() + return len(fake.linkSetHardwareAddrArgsForCall) +} + +func (fake *NetlinkAdapter) LinkSetHardwareAddrArgsForCall(i int) (netlink.Link, net.HardwareAddr) { + fake.linkSetHardwareAddrMutex.RLock() + defer fake.linkSetHardwareAddrMutex.RUnlock() + return fake.linkSetHardwareAddrArgsForCall[i].arg1, fake.linkSetHardwareAddrArgsForCall[i].arg2 +} + +func (fake *NetlinkAdapter) LinkSetHardwareAddrReturns(result1 error) { + fake.LinkSetHardwareAddrStub = nil + fake.linkSetHardwareAddrReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkSetHardwareAddrReturnsOnCall(i int, result1 error) { + fake.LinkSetHardwareAddrStub = nil + if fake.linkSetHardwareAddrReturnsOnCall == nil { + fake.linkSetHardwareAddrReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.linkSetHardwareAddrReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) NeighAddPermanentIPv4(index int, destIP net.IP, hwAddr net.HardwareAddr) error { + fake.neighAddPermanentIPv4Mutex.Lock() + ret, specificReturn := fake.neighAddPermanentIPv4ReturnsOnCall[len(fake.neighAddPermanentIPv4ArgsForCall)] + fake.neighAddPermanentIPv4ArgsForCall = append(fake.neighAddPermanentIPv4ArgsForCall, struct { + index int + destIP net.IP + hwAddr net.HardwareAddr + }{index, destIP, hwAddr}) + fake.recordInvocation("NeighAddPermanentIPv4", []interface{}{index, destIP, hwAddr}) + fake.neighAddPermanentIPv4Mutex.Unlock() + if fake.NeighAddPermanentIPv4Stub != nil { + return fake.NeighAddPermanentIPv4Stub(index, destIP, hwAddr) + } + if specificReturn { + return ret.result1 + } + return fake.neighAddPermanentIPv4Returns.result1 +} + +func (fake *NetlinkAdapter) NeighAddPermanentIPv4CallCount() int { + fake.neighAddPermanentIPv4Mutex.RLock() + defer fake.neighAddPermanentIPv4Mutex.RUnlock() + return len(fake.neighAddPermanentIPv4ArgsForCall) +} + +func (fake *NetlinkAdapter) NeighAddPermanentIPv4ArgsForCall(i int) (int, net.IP, net.HardwareAddr) { + fake.neighAddPermanentIPv4Mutex.RLock() + defer fake.neighAddPermanentIPv4Mutex.RUnlock() + return fake.neighAddPermanentIPv4ArgsForCall[i].index, fake.neighAddPermanentIPv4ArgsForCall[i].destIP, fake.neighAddPermanentIPv4ArgsForCall[i].hwAddr +} + +func (fake *NetlinkAdapter) NeighAddPermanentIPv4Returns(result1 error) { + fake.NeighAddPermanentIPv4Stub = nil + fake.neighAddPermanentIPv4Returns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) NeighAddPermanentIPv4ReturnsOnCall(i int, result1 error) { + fake.NeighAddPermanentIPv4Stub = nil + if fake.neighAddPermanentIPv4ReturnsOnCall == nil { + fake.neighAddPermanentIPv4ReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.neighAddPermanentIPv4ReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkSetARPOff(arg1 netlink.Link) error { + fake.linkSetARPOffMutex.Lock() + ret, specificReturn := fake.linkSetARPOffReturnsOnCall[len(fake.linkSetARPOffArgsForCall)] + fake.linkSetARPOffArgsForCall = append(fake.linkSetARPOffArgsForCall, struct { + arg1 netlink.Link + }{arg1}) + fake.recordInvocation("LinkSetARPOff", []interface{}{arg1}) + fake.linkSetARPOffMutex.Unlock() + if fake.LinkSetARPOffStub != nil { + return fake.LinkSetARPOffStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.linkSetARPOffReturns.result1 +} + +func (fake *NetlinkAdapter) LinkSetARPOffCallCount() int { + fake.linkSetARPOffMutex.RLock() + defer fake.linkSetARPOffMutex.RUnlock() + return len(fake.linkSetARPOffArgsForCall) +} + +func (fake *NetlinkAdapter) LinkSetARPOffArgsForCall(i int) netlink.Link { + fake.linkSetARPOffMutex.RLock() + defer fake.linkSetARPOffMutex.RUnlock() + return fake.linkSetARPOffArgsForCall[i].arg1 +} + +func (fake *NetlinkAdapter) LinkSetARPOffReturns(result1 error) { + fake.LinkSetARPOffStub = nil + fake.linkSetARPOffReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkSetARPOffReturnsOnCall(i int, result1 error) { + fake.LinkSetARPOffStub = nil + if fake.linkSetARPOffReturnsOnCall == nil { + fake.linkSetARPOffReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.linkSetARPOffReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkSetName(arg1 netlink.Link, arg2 string) error { + fake.linkSetNameMutex.Lock() + ret, specificReturn := fake.linkSetNameReturnsOnCall[len(fake.linkSetNameArgsForCall)] + fake.linkSetNameArgsForCall = append(fake.linkSetNameArgsForCall, struct { + arg1 netlink.Link + arg2 string + }{arg1, arg2}) + fake.recordInvocation("LinkSetName", []interface{}{arg1, arg2}) + fake.linkSetNameMutex.Unlock() + if fake.LinkSetNameStub != nil { + return fake.LinkSetNameStub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fake.linkSetNameReturns.result1 +} + +func (fake *NetlinkAdapter) LinkSetNameCallCount() int { + fake.linkSetNameMutex.RLock() + defer fake.linkSetNameMutex.RUnlock() + return len(fake.linkSetNameArgsForCall) +} + +func (fake *NetlinkAdapter) LinkSetNameArgsForCall(i int) (netlink.Link, string) { + fake.linkSetNameMutex.RLock() + defer fake.linkSetNameMutex.RUnlock() + return fake.linkSetNameArgsForCall[i].arg1, fake.linkSetNameArgsForCall[i].arg2 +} + +func (fake *NetlinkAdapter) LinkSetNameReturns(result1 error) { + fake.LinkSetNameStub = nil + fake.linkSetNameReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkSetNameReturnsOnCall(i int, result1 error) { + fake.LinkSetNameStub = nil + if fake.linkSetNameReturnsOnCall == nil { + fake.linkSetNameReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.linkSetNameReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkSetUp(arg1 netlink.Link) error { + fake.linkSetUpMutex.Lock() + ret, specificReturn := fake.linkSetUpReturnsOnCall[len(fake.linkSetUpArgsForCall)] + fake.linkSetUpArgsForCall = append(fake.linkSetUpArgsForCall, struct { + arg1 netlink.Link + }{arg1}) + fake.recordInvocation("LinkSetUp", []interface{}{arg1}) + fake.linkSetUpMutex.Unlock() + if fake.LinkSetUpStub != nil { + return fake.LinkSetUpStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.linkSetUpReturns.result1 +} + +func (fake *NetlinkAdapter) LinkSetUpCallCount() int { + fake.linkSetUpMutex.RLock() + defer fake.linkSetUpMutex.RUnlock() + return len(fake.linkSetUpArgsForCall) +} + +func (fake *NetlinkAdapter) LinkSetUpArgsForCall(i int) netlink.Link { + fake.linkSetUpMutex.RLock() + defer fake.linkSetUpMutex.RUnlock() + return fake.linkSetUpArgsForCall[i].arg1 +} + +func (fake *NetlinkAdapter) LinkSetUpReturns(result1 error) { + fake.LinkSetUpStub = nil + fake.linkSetUpReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkSetUpReturnsOnCall(i int, result1 error) { + fake.LinkSetUpStub = nil + if fake.linkSetUpReturnsOnCall == nil { + fake.linkSetUpReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.linkSetUpReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkDel(arg1 netlink.Link) error { + fake.linkDelMutex.Lock() + ret, specificReturn := fake.linkDelReturnsOnCall[len(fake.linkDelArgsForCall)] + fake.linkDelArgsForCall = append(fake.linkDelArgsForCall, struct { + arg1 netlink.Link + }{arg1}) + fake.recordInvocation("LinkDel", []interface{}{arg1}) + fake.linkDelMutex.Unlock() + if fake.LinkDelStub != nil { + return fake.LinkDelStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.linkDelReturns.result1 +} + +func (fake *NetlinkAdapter) LinkDelCallCount() int { + fake.linkDelMutex.RLock() + defer fake.linkDelMutex.RUnlock() + return len(fake.linkDelArgsForCall) +} + +func (fake *NetlinkAdapter) LinkDelArgsForCall(i int) netlink.Link { + fake.linkDelMutex.RLock() + defer fake.linkDelMutex.RUnlock() + return fake.linkDelArgsForCall[i].arg1 +} + +func (fake *NetlinkAdapter) LinkDelReturns(result1 error) { + fake.LinkDelStub = nil + fake.linkDelReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkDelReturnsOnCall(i int, result1 error) { + fake.LinkDelStub = nil + if fake.linkDelReturnsOnCall == nil { + fake.linkDelReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.linkDelReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkAdd(arg1 netlink.Link) error { + fake.linkAddMutex.Lock() + ret, specificReturn := fake.linkAddReturnsOnCall[len(fake.linkAddArgsForCall)] + fake.linkAddArgsForCall = append(fake.linkAddArgsForCall, struct { + arg1 netlink.Link + }{arg1}) + fake.recordInvocation("LinkAdd", []interface{}{arg1}) + fake.linkAddMutex.Unlock() + if fake.LinkAddStub != nil { + return fake.LinkAddStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.linkAddReturns.result1 +} + +func (fake *NetlinkAdapter) LinkAddCallCount() int { + fake.linkAddMutex.RLock() + defer fake.linkAddMutex.RUnlock() + return len(fake.linkAddArgsForCall) +} + +func (fake *NetlinkAdapter) LinkAddArgsForCall(i int) netlink.Link { + fake.linkAddMutex.RLock() + defer fake.linkAddMutex.RUnlock() + return fake.linkAddArgsForCall[i].arg1 +} + +func (fake *NetlinkAdapter) LinkAddReturns(result1 error) { + fake.LinkAddStub = nil + fake.linkAddReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkAddReturnsOnCall(i int, result1 error) { + fake.LinkAddStub = nil + if fake.linkAddReturnsOnCall == nil { + fake.linkAddReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.linkAddReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkSetNsFd(arg1 netlink.Link, arg2 int) error { + fake.linkSetNsFdMutex.Lock() + ret, specificReturn := fake.linkSetNsFdReturnsOnCall[len(fake.linkSetNsFdArgsForCall)] + fake.linkSetNsFdArgsForCall = append(fake.linkSetNsFdArgsForCall, struct { + arg1 netlink.Link + arg2 int + }{arg1, arg2}) + fake.recordInvocation("LinkSetNsFd", []interface{}{arg1, arg2}) + fake.linkSetNsFdMutex.Unlock() + if fake.LinkSetNsFdStub != nil { + return fake.LinkSetNsFdStub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fake.linkSetNsFdReturns.result1 +} + +func (fake *NetlinkAdapter) LinkSetNsFdCallCount() int { + fake.linkSetNsFdMutex.RLock() + defer fake.linkSetNsFdMutex.RUnlock() + return len(fake.linkSetNsFdArgsForCall) +} + +func (fake *NetlinkAdapter) LinkSetNsFdArgsForCall(i int) (netlink.Link, int) { + fake.linkSetNsFdMutex.RLock() + defer fake.linkSetNsFdMutex.RUnlock() + return fake.linkSetNsFdArgsForCall[i].arg1, fake.linkSetNsFdArgsForCall[i].arg2 +} + +func (fake *NetlinkAdapter) LinkSetNsFdReturns(result1 error) { + fake.LinkSetNsFdStub = nil + fake.linkSetNsFdReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkSetNsFdReturnsOnCall(i int, result1 error) { + fake.LinkSetNsFdStub = nil + if fake.linkSetNsFdReturnsOnCall == nil { + fake.linkSetNsFdReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.linkSetNsFdReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) RouteAdd(route *netlink.Route) error { + fake.routeAddMutex.Lock() + ret, specificReturn := fake.routeAddReturnsOnCall[len(fake.routeAddArgsForCall)] + fake.routeAddArgsForCall = append(fake.routeAddArgsForCall, struct { + route *netlink.Route + }{route}) + fake.recordInvocation("RouteAdd", []interface{}{route}) + fake.routeAddMutex.Unlock() + if fake.RouteAddStub != nil { + return fake.RouteAddStub(route) + } + if specificReturn { + return ret.result1 + } + return fake.routeAddReturns.result1 +} + +func (fake *NetlinkAdapter) RouteAddCallCount() int { + fake.routeAddMutex.RLock() + defer fake.routeAddMutex.RUnlock() + return len(fake.routeAddArgsForCall) +} + +func (fake *NetlinkAdapter) RouteAddArgsForCall(i int) *netlink.Route { + fake.routeAddMutex.RLock() + defer fake.routeAddMutex.RUnlock() + return fake.routeAddArgsForCall[i].route +} + +func (fake *NetlinkAdapter) RouteAddReturns(result1 error) { + fake.RouteAddStub = nil + fake.routeAddReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) RouteAddReturnsOnCall(i int, result1 error) { + fake.RouteAddStub = nil + if fake.routeAddReturnsOnCall == nil { + fake.routeAddReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.routeAddReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) QdiscAdd(qdisc netlink.Qdisc) error { + fake.qdiscAddMutex.Lock() + ret, specificReturn := fake.qdiscAddReturnsOnCall[len(fake.qdiscAddArgsForCall)] + fake.qdiscAddArgsForCall = append(fake.qdiscAddArgsForCall, struct { + qdisc netlink.Qdisc + }{qdisc}) + fake.recordInvocation("QdiscAdd", []interface{}{qdisc}) + fake.qdiscAddMutex.Unlock() + if fake.QdiscAddStub != nil { + return fake.QdiscAddStub(qdisc) + } + if specificReturn { + return ret.result1 + } + return fake.qdiscAddReturns.result1 +} + +func (fake *NetlinkAdapter) QdiscAddCallCount() int { + fake.qdiscAddMutex.RLock() + defer fake.qdiscAddMutex.RUnlock() + return len(fake.qdiscAddArgsForCall) +} + +func (fake *NetlinkAdapter) QdiscAddArgsForCall(i int) netlink.Qdisc { + fake.qdiscAddMutex.RLock() + defer fake.qdiscAddMutex.RUnlock() + return fake.qdiscAddArgsForCall[i].qdisc +} + +func (fake *NetlinkAdapter) QdiscAddReturns(result1 error) { + fake.QdiscAddStub = nil + fake.qdiscAddReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) QdiscAddReturnsOnCall(i int, result1 error) { + fake.QdiscAddStub = nil + if fake.qdiscAddReturnsOnCall == nil { + fake.qdiscAddReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.qdiscAddReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) FilterAdd(arg1 netlink.Filter) error { + fake.filterAddMutex.Lock() + ret, specificReturn := fake.filterAddReturnsOnCall[len(fake.filterAddArgsForCall)] + fake.filterAddArgsForCall = append(fake.filterAddArgsForCall, struct { + arg1 netlink.Filter + }{arg1}) + fake.recordInvocation("FilterAdd", []interface{}{arg1}) + fake.filterAddMutex.Unlock() + if fake.FilterAddStub != nil { + return fake.FilterAddStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.filterAddReturns.result1 +} + +func (fake *NetlinkAdapter) FilterAddCallCount() int { + fake.filterAddMutex.RLock() + defer fake.filterAddMutex.RUnlock() + return len(fake.filterAddArgsForCall) +} + +func (fake *NetlinkAdapter) FilterAddArgsForCall(i int) netlink.Filter { + fake.filterAddMutex.RLock() + defer fake.filterAddMutex.RUnlock() + return fake.filterAddArgsForCall[i].arg1 +} + +func (fake *NetlinkAdapter) FilterAddReturns(result1 error) { + fake.FilterAddStub = nil + fake.filterAddReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) FilterAddReturnsOnCall(i int, result1 error) { + fake.FilterAddStub = nil + if fake.filterAddReturnsOnCall == nil { + fake.filterAddReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.filterAddReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) AddrList(link netlink.Link, family int) ([]netlink.Addr, error) { + fake.addrListMutex.Lock() + ret, specificReturn := fake.addrListReturnsOnCall[len(fake.addrListArgsForCall)] + fake.addrListArgsForCall = append(fake.addrListArgsForCall, struct { + link netlink.Link + family int + }{link, family}) + fake.recordInvocation("AddrList", []interface{}{link, family}) + fake.addrListMutex.Unlock() + if fake.AddrListStub != nil { + return fake.AddrListStub(link, family) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.addrListReturns.result1, fake.addrListReturns.result2 +} + +func (fake *NetlinkAdapter) AddrListCallCount() int { + fake.addrListMutex.RLock() + defer fake.addrListMutex.RUnlock() + return len(fake.addrListArgsForCall) +} + +func (fake *NetlinkAdapter) AddrListArgsForCall(i int) (netlink.Link, int) { + fake.addrListMutex.RLock() + defer fake.addrListMutex.RUnlock() + return fake.addrListArgsForCall[i].link, fake.addrListArgsForCall[i].family +} + +func (fake *NetlinkAdapter) AddrListReturns(result1 []netlink.Addr, result2 error) { + fake.AddrListStub = nil + fake.addrListReturns = struct { + result1 []netlink.Addr + result2 error + }{result1, result2} +} + +func (fake *NetlinkAdapter) AddrListReturnsOnCall(i int, result1 []netlink.Addr, result2 error) { + fake.AddrListStub = nil + if fake.addrListReturnsOnCall == nil { + fake.addrListReturnsOnCall = make(map[int]struct { + result1 []netlink.Addr + result2 error + }) + } + fake.addrListReturnsOnCall[i] = struct { + result1 []netlink.Addr + result2 error + }{result1, result2} +} + +func (fake *NetlinkAdapter) TickInUsec() float64 { + fake.tickInUsecMutex.Lock() + ret, specificReturn := fake.tickInUsecReturnsOnCall[len(fake.tickInUsecArgsForCall)] + fake.tickInUsecArgsForCall = append(fake.tickInUsecArgsForCall, struct{}{}) + fake.recordInvocation("TickInUsec", []interface{}{}) + fake.tickInUsecMutex.Unlock() + if fake.TickInUsecStub != nil { + return fake.TickInUsecStub() + } + if specificReturn { + return ret.result1 + } + return fake.tickInUsecReturns.result1 +} + +func (fake *NetlinkAdapter) TickInUsecCallCount() int { + fake.tickInUsecMutex.RLock() + defer fake.tickInUsecMutex.RUnlock() + return len(fake.tickInUsecArgsForCall) +} + +func (fake *NetlinkAdapter) TickInUsecReturns(result1 float64) { + fake.TickInUsecStub = nil + fake.tickInUsecReturns = struct { + result1 float64 + }{result1} +} + +func (fake *NetlinkAdapter) TickInUsecReturnsOnCall(i int, result1 float64) { + fake.TickInUsecStub = nil + if fake.tickInUsecReturnsOnCall == nil { + fake.tickInUsecReturnsOnCall = make(map[int]struct { + result1 float64 + }) + } + fake.tickInUsecReturnsOnCall[i] = struct { + result1 float64 + }{result1} +} + +func (fake *NetlinkAdapter) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.linkByNameMutex.RLock() + defer fake.linkByNameMutex.RUnlock() + fake.parseAddrMutex.RLock() + defer fake.parseAddrMutex.RUnlock() + fake.addrAddScopeLinkMutex.RLock() + defer fake.addrAddScopeLinkMutex.RUnlock() + fake.linkSetHardwareAddrMutex.RLock() + defer fake.linkSetHardwareAddrMutex.RUnlock() + fake.neighAddPermanentIPv4Mutex.RLock() + defer fake.neighAddPermanentIPv4Mutex.RUnlock() + fake.linkSetARPOffMutex.RLock() + defer fake.linkSetARPOffMutex.RUnlock() + fake.linkSetNameMutex.RLock() + defer fake.linkSetNameMutex.RUnlock() + fake.linkSetUpMutex.RLock() + defer fake.linkSetUpMutex.RUnlock() + fake.linkDelMutex.RLock() + defer fake.linkDelMutex.RUnlock() + fake.linkAddMutex.RLock() + defer fake.linkAddMutex.RUnlock() + fake.linkSetNsFdMutex.RLock() + defer fake.linkSetNsFdMutex.RUnlock() + fake.routeAddMutex.RLock() + defer fake.routeAddMutex.RUnlock() + fake.qdiscAddMutex.RLock() + defer fake.qdiscAddMutex.RUnlock() + fake.filterAddMutex.RLock() + defer fake.filterAddMutex.RUnlock() + fake.addrListMutex.RLock() + defer fake.addrListMutex.RUnlock() + fake.tickInUsecMutex.RLock() + defer fake.tickInUsecMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *NetlinkAdapter) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/cni/lib/fakes/sysctlAdapter.go b/src/code.cloudfoundry.org/silk/cni/lib/fakes/sysctlAdapter.go new file mode 100644 index 00000000..109bbec1 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/lib/fakes/sysctlAdapter.go @@ -0,0 +1,101 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" +) + +type SysctlAdapter struct { + SysctlStub func(name string, params ...string) (string, error) + sysctlMutex sync.RWMutex + sysctlArgsForCall []struct { + name string + params []string + } + sysctlReturns struct { + result1 string + result2 error + } + sysctlReturnsOnCall map[int]struct { + result1 string + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *SysctlAdapter) Sysctl(name string, params ...string) (string, error) { + fake.sysctlMutex.Lock() + ret, specificReturn := fake.sysctlReturnsOnCall[len(fake.sysctlArgsForCall)] + fake.sysctlArgsForCall = append(fake.sysctlArgsForCall, struct { + name string + params []string + }{name, params}) + fake.recordInvocation("Sysctl", []interface{}{name, params}) + fake.sysctlMutex.Unlock() + if fake.SysctlStub != nil { + return fake.SysctlStub(name, params...) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.sysctlReturns.result1, fake.sysctlReturns.result2 +} + +func (fake *SysctlAdapter) SysctlCallCount() int { + fake.sysctlMutex.RLock() + defer fake.sysctlMutex.RUnlock() + return len(fake.sysctlArgsForCall) +} + +func (fake *SysctlAdapter) SysctlArgsForCall(i int) (string, []string) { + fake.sysctlMutex.RLock() + defer fake.sysctlMutex.RUnlock() + return fake.sysctlArgsForCall[i].name, fake.sysctlArgsForCall[i].params +} + +func (fake *SysctlAdapter) SysctlReturns(result1 string, result2 error) { + fake.SysctlStub = nil + fake.sysctlReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *SysctlAdapter) SysctlReturnsOnCall(i int, result1 string, result2 error) { + fake.SysctlStub = nil + if fake.sysctlReturnsOnCall == nil { + fake.sysctlReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.sysctlReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *SysctlAdapter) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.sysctlMutex.RLock() + defer fake.sysctlMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *SysctlAdapter) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/lib/host_setup.go b/src/code.cloudfoundry.org/silk/cni/lib/host_setup.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/lib/host_setup.go rename to src/code.cloudfoundry.org/silk/cni/lib/host_setup.go diff --git a/src/code.cloudfoundry.org/silk/cni/lib/host_setup_test.go b/src/code.cloudfoundry.org/silk/cni/lib/host_setup_test.go new file mode 100644 index 00000000..f44541e0 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/lib/host_setup_test.go @@ -0,0 +1,91 @@ +package lib_test + +import ( + "errors" + "net" + + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/cni/config" + "code.cloudfoundry.org/silk/cni/lib" + "code.cloudfoundry.org/silk/cni/lib/fakes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Host Setup", func() { + + var ( + hostNS *fakes.NetNS + cfg *config.Config + fakeLinkOperations *fakes.LinkOperations + fakeCommon *fakes.Common + hostSetup *lib.Host + containerAddr config.DualAddress + hostAddr config.DualAddress + fakelogger *lagertest.TestLogger + ) + + BeforeEach(func() { + fakeLinkOperations = &fakes.LinkOperations{} + fakeCommon = &fakes.Common{} + hostNS = &fakes.NetNS{} + hostNS.DoStub = lib.NetNsDoStub + + fakelogger = lagertest.NewTestLogger("test") + + containerAddr = config.DualAddress{IP: net.IP{10, 255, 30, 4}} + hostAddr = config.DualAddress{IP: net.IP{169, 254, 0, 1}} + + cfg = &config.Config{} + cfg.Host.DeviceName = "someHostDeviceName" + cfg.Host.Namespace = hostNS + cfg.Container.Address = containerAddr + cfg.Host.Address = hostAddr + + hostSetup = &lib.Host{ + Common: fakeCommon, + LinkOperations: fakeLinkOperations, + Logger: fakelogger, + } + }) + + Describe("Setup", func() { + It("calls basic setup in the host namespace", func() { + err := hostSetup.Setup(cfg) + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeCommon.BasicSetupCallCount()).To(Equal(1)) + device, local, peer := fakeCommon.BasicSetupArgsForCall(0) + Expect(device).To(Equal("someHostDeviceName")) + Expect(local).To(Equal(hostAddr)) + Expect(peer).To(Equal(containerAddr)) + }) + + It("enables IPv4 forwarding on the host", func() { + err := hostSetup.Setup(cfg) + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeLinkOperations.EnableIPv4ForwardingCallCount()).To(Equal(1)) + }) + + Context("when the basic device setup fails", func() { + BeforeEach(func() { + fakeCommon.BasicSetupReturns(errors.New("beans")) + }) + It("returns a meaningful error", func() { + err := hostSetup.Setup(cfg) + Expect(err).To(MatchError("setting up device in host: beans")) + }) + }) + + Context("when enabling packet forwarding fails", func() { + BeforeEach(func() { + fakeLinkOperations.EnableIPv4ForwardingReturns(errors.New("beans")) + }) + It("returns a meaningful error", func() { + err := hostSetup.Setup(cfg) + Expect(err).To(MatchError("enabling packet forwarding on host: beans")) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/lib/interface.go b/src/code.cloudfoundry.org/silk/cni/lib/interface.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/lib/interface.go rename to src/code.cloudfoundry.org/silk/cni/lib/interface.go diff --git a/src/code.cloudfoundry.org/silk/cni/lib/lib_suite_test.go b/src/code.cloudfoundry.org/silk/cni/lib/lib_suite_test.go new file mode 100644 index 00000000..8034d451 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/lib/lib_suite_test.go @@ -0,0 +1,13 @@ +package lib_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" +) + +func TestLib(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "CNI Lib Suite") +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/lib/link_operations.go b/src/code.cloudfoundry.org/silk/cni/lib/link_operations.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/lib/link_operations.go rename to src/code.cloudfoundry.org/silk/cni/lib/link_operations.go diff --git a/src/code.cloudfoundry.org/silk/cni/lib/link_operations_test.go b/src/code.cloudfoundry.org/silk/cni/lib/link_operations_test.go new file mode 100644 index 00000000..8df03bf5 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/lib/link_operations_test.go @@ -0,0 +1,369 @@ +package lib_test + +import ( + "errors" + "net" + + "code.cloudfoundry.org/lager/v3/lagertest" + + "code.cloudfoundry.org/silk/cni/lib" + "code.cloudfoundry.org/silk/cni/lib/fakes" + "github.com/containernetworking/cni/pkg/types" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/vishvananda/netlink" +) + +var _ = Describe("Link Operations", func() { + + var ( + fakeSysctlAdapter *fakes.SysctlAdapter + fakeNetlinkAdapter *fakes.NetlinkAdapter + linkOperations *lib.LinkOperations + fakeLink netlink.Link + ipAddr net.IP + peerIP net.IP + hwAddr net.HardwareAddr + routes []*types.Route + logger *lagertest.TestLogger + ) + + BeforeEach(func() { + logger = lagertest.NewTestLogger("test") + fakeSysctlAdapter = &fakes.SysctlAdapter{} + fakeNetlinkAdapter = &fakes.NetlinkAdapter{} + linkOperations = &lib.LinkOperations{ + SysctlAdapter: fakeSysctlAdapter, + NetlinkAdapter: fakeNetlinkAdapter, + Logger: logger, + } + fakeLink = &netlink.Bridge{ + LinkAttrs: netlink.LinkAttrs{ + Name: "my-fake-bridge", + Index: 42, + }, + } + + var err error + ipAddr = net.IP{10, 255, 30, 4} + peerIP = net.IP{169, 254, 0, 1} + hwAddr, err = net.ParseMAC("aa:aa:12:34:56:78") + Expect(err).NotTo(HaveOccurred()) + + routes = []*types.Route{ + &types.Route{ + Dst: net.IPNet{ + IP: []byte{200, 201, 202, 203}, + Mask: []byte{255, 255, 255, 255}, + }, + GW: net.IP{10, 255, 30, 2}, + }, + &types.Route{ + Dst: net.IPNet{ + IP: []byte{100, 101, 102, 103}, + Mask: []byte{255, 255, 255, 255}, + }, + GW: net.IP{10, 255, 30, 1}, + }, + &types.Route{ + Dst: net.IPNet{ + IP: []byte{0, 1, 2, 3}, + Mask: []byte{255, 255, 255, 255}, + }, + GW: net.IP{10, 255, 30, 0}, + }, + } + }) + + Describe("Disable IPv6", func() { + It("calls the sysctl adapter to disable IPv6", func() { + err := linkOperations.DisableIPv6("someDevice") + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeSysctlAdapter.SysctlCallCount()).To(Equal(1)) + name, params := fakeSysctlAdapter.SysctlArgsForCall(0) + Expect(name).To(Equal("net.ipv6.conf.someDevice.disable_ipv6")) + Expect(len(params)).To(Equal(1)) + Expect(params[0]).To(Equal("1")) + }) + + Context("when the sysctl command fails", func() { + BeforeEach(func() { + fakeSysctlAdapter.SysctlReturns("", errors.New("cuttlefish")) + }) + It("returns a meaningful error", func() { + err := linkOperations.DisableIPv6("someDevice") + Expect(err).To(MatchError("sysctl for someDevice: cuttlefish")) + }) + }) + }) + + Describe("EnableReversePathFiltering", func() { + It("calls the sysctl adapter to set rp_filtering to strict mode", func() { + err := linkOperations.EnableReversePathFiltering("someDevice") + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeSysctlAdapter.SysctlCallCount()).To(Equal(1)) + name, params := fakeSysctlAdapter.SysctlArgsForCall(0) + Expect(name).To(Equal("net.ipv4.conf.someDevice.rp_filter")) + Expect(len(params)).To(Equal(1)) + Expect(params[0]).To(Equal("1")) + }) + + Context("when the sysctl command fails", func() { + BeforeEach(func() { + fakeSysctlAdapter.SysctlReturns("", errors.New("cuttlefish")) + }) + It("returns a meaningful error", func() { + err := linkOperations.EnableReversePathFiltering("someDevice") + Expect(err).To(MatchError("sysctl for someDevice: cuttlefish")) + }) + }) + }) + + Describe("EnableIPv4Forwarding", func() { + It("calls the sysctl adapter to enable IPv4 forwarding", func() { + err := linkOperations.EnableIPv4Forwarding() + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeSysctlAdapter.SysctlCallCount()).To(Equal(1)) + name, params := fakeSysctlAdapter.SysctlArgsForCall(0) + Expect(name).To(Equal("net.ipv4.ip_forward")) + Expect(len(params)).To(Equal(1)) + Expect(params[0]).To(Equal("1")) + }) + + Context("when the sysctl command fails", func() { + BeforeEach(func() { + fakeSysctlAdapter.SysctlReturns("", errors.New("cuttlefish")) + }) + It("returns a meaningful error", func() { + err := linkOperations.EnableIPv4Forwarding() + Expect(err).To(MatchError("enabling IPv4 forwarding: cuttlefish")) + }) + }) + }) + + Describe("StaticNeighborNoARP", func() { + It("calls the netlink adapter to disable ARP", func() { + err := linkOperations.StaticNeighborNoARP(fakeLink, ipAddr, hwAddr) + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeNetlinkAdapter.LinkSetARPOffCallCount()).To(Equal(1)) + Expect(fakeNetlinkAdapter.LinkSetARPOffArgsForCall(0)).To(Equal(fakeLink)) + }) + + It("calls the netlink adapter to install a permanent neighbor rule", func() { + err := linkOperations.StaticNeighborNoARP(fakeLink, ipAddr, hwAddr) + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeNetlinkAdapter.NeighAddPermanentIPv4CallCount()).To(Equal(1)) + index, destIP, destHardwareAddr := fakeNetlinkAdapter.NeighAddPermanentIPv4ArgsForCall(0) + Expect(index).To(Equal(42)) + Expect(destIP).To(Equal(ipAddr)) + Expect(destHardwareAddr).To(Equal(hwAddr)) + }) + + Context("when disabling ARP fails", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkSetARPOffReturns(errors.New("shrimp")) + }) + It("returns a meaningul error", func() { + err := linkOperations.StaticNeighborNoARP(fakeLink, ipAddr, hwAddr) + Expect(err).To(MatchError("set ARP off: shrimp")) + }) + }) + + Context("when installing the neighbor rule fails", func() { + BeforeEach(func() { + fakeNetlinkAdapter.NeighAddPermanentIPv4Returns(errors.New("crab")) + }) + It("returns a meaningul error", func() { + err := linkOperations.StaticNeighborNoARP(fakeLink, ipAddr, hwAddr) + Expect(err).To(MatchError("neigh add: crab")) + }) + }) + }) + + Describe("SetPointToPointAddress", func() { + var ( + parsedAddr *netlink.Addr + ptpAddr *netlink.Addr + ) + BeforeEach(func() { + parsedAddr = &netlink.Addr{} + ptpAddr = &netlink.Addr{Peer: &net.IPNet{ + IP: peerIP, + Mask: []byte{255, 255, 255, 255}, + }} + fakeNetlinkAdapter.ParseAddrReturns(parsedAddr, nil) + }) + It("sets the peer IP address on the link", func() { + err := linkOperations.SetPointToPointAddress(fakeLink, ipAddr, peerIP) + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeNetlinkAdapter.ParseAddrCallCount()).To(Equal(1)) + Expect(fakeNetlinkAdapter.ParseAddrArgsForCall(0)).To(Equal("10.255.30.4/32")) + + Expect(fakeNetlinkAdapter.AddrAddScopeLinkCallCount()).To(Equal(1)) + link, addr := fakeNetlinkAdapter.AddrAddScopeLinkArgsForCall(0) + Expect(link).To(Equal(fakeLink)) + Expect(addr).To(Equal(ptpAddr)) + }) + + Context("when parsing the IP address fails", func() { + BeforeEach(func() { + fakeNetlinkAdapter.ParseAddrReturns(nil, errors.New("lobster")) + }) + It("returns a meaningul error", func() { + err := linkOperations.SetPointToPointAddress(fakeLink, ipAddr, peerIP) + Expect(err).To(MatchError("parsing address 10.255.30.4/32: lobster")) + }) + }) + + Context("when setting the point to point address fails", func() { + BeforeEach(func() { + fakeNetlinkAdapter.AddrAddScopeLinkReturns(errors.New("oyster")) + }) + It("returns a meaningul error", func() { + err := linkOperations.SetPointToPointAddress(fakeLink, ipAddr, peerIP) + Expect(err).To(MatchError("adding IP address 10.255.30.4/32: oyster")) + }) + }) + }) + + Describe("RenameLink", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkByNameReturns(fakeLink, nil) + }) + It("finds the link with the old name and renames it to the new name", func() { + err := linkOperations.RenameLink("old", "new") + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeNetlinkAdapter.LinkByNameCallCount()).To(Equal(1)) + Expect(fakeNetlinkAdapter.LinkByNameArgsForCall(0)).To(Equal("old")) + + Expect(fakeNetlinkAdapter.LinkSetNameCallCount()).To(Equal(1)) + link, new := fakeNetlinkAdapter.LinkSetNameArgsForCall(0) + Expect(link).To(Equal(fakeLink)) + Expect(new).To(Equal("new")) + }) + + Context("when finding the link fails", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkByNameReturns(nil, errors.New("uni")) + }) + It("returns a meaningful error", func() { + err := linkOperations.RenameLink("old", "new") + Expect(err).To(MatchError("failed to find link \"old\": uni")) + }) + }) + + Context("when setting the link name fails", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkSetNameReturns(errors.New("starfish")) + }) + It("returns a meaningful error", func() { + err := linkOperations.RenameLink("old", "new") + Expect(err).To(MatchError("set link name: starfish")) + }) + }) + }) + + Describe("DeleteLinkByName", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkByNameReturns(fakeLink, nil) + }) + It("finds the link by name and deletes it", func() { + err := linkOperations.DeleteLinkByName("someName") + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeNetlinkAdapter.LinkByNameCallCount()).To(Equal(1)) + Expect(fakeNetlinkAdapter.LinkByNameArgsForCall(0)).To(Equal("someName")) + + Expect(fakeNetlinkAdapter.LinkDelCallCount()).To(Equal(1)) + Expect(fakeNetlinkAdapter.LinkDelArgsForCall(0)).To(Equal(fakeLink)) + }) + + Context("when finding the link fails", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkByNameReturns(nil, errors.New("some error returned by LinkByName")) + }) + It("swallows the error and return success", func() { + err := linkOperations.DeleteLinkByName("someName") + Expect(err).NotTo(HaveOccurred()) + }) + + It("logs an informational message with the failure", func() { + linkOperations.DeleteLinkByName("someName") + + Expect(logger.Logs()).To(HaveLen(1)) + Expect(logger.Logs()[0].Data).To(HaveKeyWithValue("deviceName", "someName")) + Expect(logger.Logs()[0].Data).To(HaveKeyWithValue("message", "some error returned by LinkByName")) + }) + }) + + Context("when deleting the link fails", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkDelReturns(errors.New("starfish")) + }) + It("returns the error", func() { + err := linkOperations.DeleteLinkByName("someName") + Expect(err).To(MatchError("starfish")) + }) + }) + }) + + Describe("RouteAddAll", func() { + BeforeEach(func() { + fakeNetlinkAdapter.RouteAddReturns(nil) + }) + It("adds all routes", func() { + err := linkOperations.RouteAddAll(routes, ipAddr) + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeNetlinkAdapter.RouteAddCallCount()).To(Equal(3)) + Expect(fakeNetlinkAdapter.RouteAddArgsForCall(0)).To(Equal(&netlink.Route{ + Src: ipAddr, + Dst: &net.IPNet{ + IP: []byte{200, 201, 202, 203}, + Mask: []byte{255, 255, 255, 255}, + }, + Gw: net.IP{10, 255, 30, 2}, + })) + Expect(fakeNetlinkAdapter.RouteAddArgsForCall(1)).To(Equal(&netlink.Route{ + Src: ipAddr, + Dst: &net.IPNet{ + IP: []byte{100, 101, 102, 103}, + Mask: []byte{255, 255, 255, 255}, + }, + Gw: net.IP{10, 255, 30, 1}, + })) + Expect(fakeNetlinkAdapter.RouteAddArgsForCall(2)).To(Equal(&netlink.Route{ + Src: ipAddr, + Dst: &net.IPNet{ + IP: []byte{0, 1, 2, 3}, + Mask: []byte{255, 255, 255, 255}, + }, + Gw: net.IP{10, 255, 30, 0}, + })) + }) + + Context("when adding one of the routes fails", func() { + BeforeEach(func() { + fakeNetlinkAdapter.RouteAddStub = func(route *netlink.Route) error { + if route.Gw.String() == "10.255.30.1" { + return errors.New("pickle") + } + return nil + } + }) + It("returns a meaningful error", func() { + err := linkOperations.RouteAddAll(routes, ipAddr) + Expect(err).To(MatchError("adding route: pickle")) + + Expect(fakeNetlinkAdapter.RouteAddCallCount()).To(Equal(2)) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/lib/pair_creator.go b/src/code.cloudfoundry.org/silk/cni/lib/pair_creator.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/lib/pair_creator.go rename to src/code.cloudfoundry.org/silk/cni/lib/pair_creator.go diff --git a/src/code.cloudfoundry.org/silk/cni/lib/pair_creator_test.go b/src/code.cloudfoundry.org/silk/cni/lib/pair_creator_test.go new file mode 100644 index 00000000..99159183 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/lib/pair_creator_test.go @@ -0,0 +1,138 @@ +package lib_test + +import ( + "errors" + "net" + + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/cni/config" + "code.cloudfoundry.org/silk/cni/lib" + "code.cloudfoundry.org/silk/cni/lib/fakes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/vishvananda/netlink" +) + +var _ = Describe("VethPairCreator", func() { + Describe("Create", func() { + + var ( + containerNS *fakes.NetNS + hostNS *fakes.NetNS + cfg *config.Config + creator *lib.VethPairCreator + fakeNetlinkAdapter *fakes.NetlinkAdapter + fakelogger *lagertest.TestLogger + err error + hostAddr, containerAddr net.HardwareAddr + ) + + BeforeEach(func() { + containerNS = &fakes.NetNS{} + containerNS.FdReturns(42) + hostNS = &fakes.NetNS{} + hostNS.DoStub = lib.NetNsDoStub + + hostAddr, err = net.ParseMAC("aa:aa:0a:ff:ad:39") + Expect(err).NotTo(HaveOccurred()) + containerAddr, err = net.ParseMAC("ee:ee:0a:ff:ad:39") + Expect(err).NotTo(HaveOccurred()) + + cfg = &config.Config{} + cfg.Container.TemporaryDeviceName = "myTemporaryDeviceName" + cfg.Container.Namespace = containerNS + cfg.Container.MTU = 1234 + cfg.Host.DeviceName = "myDeviceName" + cfg.Host.Namespace = hostNS + cfg.Host.Address.Hardware = hostAddr + cfg.Container.Address.Hardware = containerAddr + + fakelogger = lagertest.NewTestLogger("test") + fakeNetlinkAdapter = &fakes.NetlinkAdapter{} + fakeNetlinkAdapter.LinkByNameReturns(&netlink.Bridge{ + LinkAttrs: netlink.LinkAttrs{ + Name: "my-fake-bridge", + }, + }, nil) + creator = &lib.VethPairCreator{ + NetlinkAdapter: fakeNetlinkAdapter, + Logger: fakelogger, + } + }) + + AfterEach(func() { + containerNS.Close() // don't bother checking errors here + hostNS.Close() + }) + + It("creates a correctly-named veth device in the host namespace with the correct MTU and HW addr", func() { + Expect(creator.Create(cfg)).To(Succeed()) + + By("requesting to create a container veth device") + Expect(fakeNetlinkAdapter.LinkAddCallCount()).To(Equal(1)) + Expect(fakeNetlinkAdapter.LinkAddArgsForCall(0)).To(Equal(&netlink.Veth{ + LinkAttrs: netlink.LinkAttrs{ + Name: "myDeviceName", + Flags: net.FlagUp, + MTU: 1234, + HardwareAddr: hostAddr, + }, + PeerName: "myTemporaryDeviceName", + PeerHardwareAddr: containerAddr, + })) + + By("getting the newly-created container veth device") + Expect(fakeNetlinkAdapter.LinkByNameCallCount()).To(Equal(1)) + Expect(fakeNetlinkAdapter.LinkByNameArgsForCall(0)).To(Equal("myTemporaryDeviceName")) + + By("putting the container veth device into the container namespace") + Expect(fakeNetlinkAdapter.LinkSetNsFdCallCount()).To(Equal(1)) + link, fd := fakeNetlinkAdapter.LinkSetNsFdArgsForCall(0) + Expect(link).To(Equal(&netlink.Bridge{ + LinkAttrs: netlink.LinkAttrs{ + Name: "my-fake-bridge", + }, + })) + Expect(fd).To(Equal(42)) + }) + + Context("when adding the link fails", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkAddReturns(errors.New("banana")) + creator = &lib.VethPairCreator{ + NetlinkAdapter: fakeNetlinkAdapter, + Logger: fakelogger, + } + }) + + It("wraps and returns the error", func() { + err := creator.Create(cfg) + Expect(err).To(MatchError("creating veth pair: banana")) + }) + }) + + Context("when getting the container veth device fails", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkByNameReturns(nil, errors.New("banana")) + }) + + It("wraps and returns the error", func() { + err := creator.Create(cfg) + Expect(err).To(MatchError( + errors.New("failed to find newly-created veth device \"myTemporaryDeviceName\": banana"), + )) + }) + }) + + Context("when moving the container-side veth into the container fails", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkSetNsFdReturns(errors.New("kiwi")) + }) + + It("wraps and returns the error", func() { + err := creator.Create(cfg) + Expect(err).To(MatchError("failed to move veth to container namespace: kiwi")) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/netinfo/daemon.go b/src/code.cloudfoundry.org/silk/cni/netinfo/daemon.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/netinfo/daemon.go rename to src/code.cloudfoundry.org/silk/cni/netinfo/daemon.go diff --git a/src/code.cloudfoundry.org/silk/cni/netinfo/daemon_test.go b/src/code.cloudfoundry.org/silk/cni/netinfo/daemon_test.go new file mode 100644 index 00000000..8850bd11 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/netinfo/daemon_test.go @@ -0,0 +1,56 @@ +package netinfo_test + +import ( + "errors" + + helperfakes "code.cloudfoundry.org/cf-networking-helpers/fakes" + "code.cloudfoundry.org/silk/cni/netinfo" + "code.cloudfoundry.org/silk/daemon" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Daemon Netinfo", func() { + var ( + daemonNetInfo *netinfo.Daemon + fakeJsonClient *helperfakes.JSONClient + ) + BeforeEach(func() { + fakeJsonClient = &helperfakes.JSONClient{} + daemonNetInfo = &netinfo.Daemon{ + JSONClient: fakeJsonClient, + } + + fakeJsonClient.DoStub = func(method, route string, reqData, respData interface{}, token string) error { + netInfo := respData.(*daemon.NetworkInfo) + netInfo.MTU = 543 + netInfo.OverlaySubnet = "10.240.0.0/12" + return nil + } + }) + It("gets the daemon netinfo with the http client", func() { + info, err := daemonNetInfo.Get() + Expect(err).NotTo(HaveOccurred()) + Expect(info).To(Equal(daemon.NetworkInfo{ + MTU: 543, + OverlaySubnet: "10.240.0.0/12", + })) + + Expect(fakeJsonClient.DoCallCount()).To(Equal(1)) + method, route, reqData, respData, _ := fakeJsonClient.DoArgsForCall(0) + Expect(method).To(Equal("GET")) + Expect(route).To(Equal("/")) + Expect(reqData).To(BeNil()) + Expect(respData).To(BeAssignableToTypeOf(&daemon.NetworkInfo{})) + }) + Context("when the json client errors", func() { + BeforeEach(func() { + fakeJsonClient.DoReturns(errors.New("banana")) + }) + It("returns the error", func() { + _, err := daemonNetInfo.Get() + Expect(err).To(MatchError("json client do: banana")) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/netinfo/discover.go b/src/code.cloudfoundry.org/silk/cni/netinfo/discover.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/netinfo/discover.go rename to src/code.cloudfoundry.org/silk/cni/netinfo/discover.go diff --git a/src/code.cloudfoundry.org/silk/cni/netinfo/discover_test.go b/src/code.cloudfoundry.org/silk/cni/netinfo/discover_test.go new file mode 100644 index 00000000..5b9f9018 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/netinfo/discover_test.go @@ -0,0 +1,66 @@ +package netinfo_test + +import ( + "errors" + + "code.cloudfoundry.org/silk/cni/netinfo" + "code.cloudfoundry.org/silk/cni/netinfo/fakes" + "code.cloudfoundry.org/silk/daemon" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Discover", func() { + var ( + discoverer *netinfo.Discoverer + fakeNetInfo *fakes.NetInfo + ) + + BeforeEach(func() { + fakeNetInfo = &fakes.NetInfo{} + discoverer = &netinfo.Discoverer{ + NetInfo: fakeNetInfo, + } + + fakeNetInfo.GetReturns(daemon.NetworkInfo{ + OverlaySubnet: "1.2.3.4/23", + MTU: 4321, + }, nil) + }) + + Context("when it is called with zero MTU", func() { + It("gets the netinfo and sets the MTU based on the netinfo", func() { + info, err := discoverer.Discover(0) + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeNetInfo.GetCallCount()).To(Equal(1)) + Expect(info).To(Equal(daemon.NetworkInfo{ + OverlaySubnet: "1.2.3.4/23", + MTU: 4321, + })) + }) + }) + + Context("when it is called with non-zero MTU", func() { + It("overrides the MTU from the netinfo", func() { + info, err := discoverer.Discover(42) + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeNetInfo.GetCallCount()).To(Equal(1)) + Expect(info).To(Equal(daemon.NetworkInfo{ + OverlaySubnet: "1.2.3.4/23", + MTU: 42, + })) + }) + }) + + Context("when getting the netinfo fails", func() { + BeforeEach(func() { + fakeNetInfo.GetReturns(daemon.NetworkInfo{}, errors.New("banana")) + }) + It("returns an error", func() { + _, err := discoverer.Discover(42) + Expect(err).To(MatchError("get netinfo: banana")) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/cni/netinfo/fakes/netinfo.go b/src/code.cloudfoundry.org/silk/cni/netinfo/fakes/netinfo.go new file mode 100644 index 00000000..919628b9 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/netinfo/fakes/netinfo.go @@ -0,0 +1,87 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + "code.cloudfoundry.org/silk/daemon" +) + +type NetInfo struct { + GetStub func() (daemon.NetworkInfo, error) + getMutex sync.RWMutex + getArgsForCall []struct{} + getReturns struct { + result1 daemon.NetworkInfo + result2 error + } + getReturnsOnCall map[int]struct { + result1 daemon.NetworkInfo + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *NetInfo) Get() (daemon.NetworkInfo, error) { + fake.getMutex.Lock() + ret, specificReturn := fake.getReturnsOnCall[len(fake.getArgsForCall)] + fake.getArgsForCall = append(fake.getArgsForCall, struct{}{}) + fake.recordInvocation("Get", []interface{}{}) + fake.getMutex.Unlock() + if fake.GetStub != nil { + return fake.GetStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.getReturns.result1, fake.getReturns.result2 +} + +func (fake *NetInfo) GetCallCount() int { + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + return len(fake.getArgsForCall) +} + +func (fake *NetInfo) GetReturns(result1 daemon.NetworkInfo, result2 error) { + fake.GetStub = nil + fake.getReturns = struct { + result1 daemon.NetworkInfo + result2 error + }{result1, result2} +} + +func (fake *NetInfo) GetReturnsOnCall(i int, result1 daemon.NetworkInfo, result2 error) { + fake.GetStub = nil + if fake.getReturnsOnCall == nil { + fake.getReturnsOnCall = make(map[int]struct { + result1 daemon.NetworkInfo + result2 error + }) + } + fake.getReturnsOnCall[i] = struct { + result1 daemon.NetworkInfo + result2 error + }{result1, result2} +} + +func (fake *NetInfo) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + return fake.invocations +} + +func (fake *NetInfo) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/netinfo/flannel.go b/src/code.cloudfoundry.org/silk/cni/netinfo/flannel.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/cni/netinfo/flannel.go rename to src/code.cloudfoundry.org/silk/cni/netinfo/flannel.go diff --git a/src/code.cloudfoundry.org/silk/cni/netinfo/flannel_test.go b/src/code.cloudfoundry.org/silk/cni/netinfo/flannel_test.go new file mode 100644 index 00000000..277f1aad --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/netinfo/flannel_test.go @@ -0,0 +1,91 @@ +package netinfo_test + +import ( + "io/ioutil" + "os" + + "code.cloudfoundry.org/silk/cni/netinfo" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Flannel", func() { + var ( + filePath string + flannelNetInfo *netinfo.Flannel + ) + + BeforeEach(func() { + contents := `FLANNEL_NETWORK=10.240.0.0/12 +FLANNEL_SUBNET=10.255.19.1/24 +FLANNEL_MTU=1450 +FLANNEL_IPMASQ=false +` + tempFile, err := ioutil.TempFile("", "subnet.env") + Expect(err).NotTo(HaveOccurred()) + + _, err = tempFile.WriteString(contents) + Expect(err).NotTo(HaveOccurred()) + Expect(tempFile.Close()).To(Succeed()) + + filePath = tempFile.Name() + + flannelNetInfo = &netinfo.Flannel{ + SubnetFilePath: filePath, + } + }) + + AfterEach(func() { + Expect(os.RemoveAll(filePath)).To(Succeed()) + }) + + It("returns the subnet and mtu in the flannel subnet env", func() { + networkInfo, err := flannelNetInfo.Get() + Expect(err).NotTo(HaveOccurred()) + Expect(networkInfo.OverlaySubnet).To(Equal("10.255.19.1/24")) + Expect(networkInfo.MTU).To(Equal(1450)) + }) + + Context("when there is a problem opening the file", func() { + It("returns a helpful error", func() { + flannelNetInfo = &netinfo.Flannel{ + SubnetFilePath: "bad-path", + } + _, err := flannelNetInfo.Get() + Expect(err).To(MatchError("open bad-path: no such file or directory")) + }) + }) + + Context("when the file is malformed", func() { + It("returns a helpful error", func() { + Expect(ioutil.WriteFile(filePath, []byte("boo"), 0600)).To(Succeed()) + + _, err := flannelNetInfo.Get() + Expect(err).To(MatchError("unable to parse flannel subnet file")) + }) + }) + + Context("when the file doesn't have a valid subnet entry", func() { + It("returns a helpful error", func() { + Expect(ioutil.WriteFile(filePath, []byte(`FLANNEL_NETWORK=10.255.0.0/16 +FLANNEL_SUBNET=banana +FLANNEL_MTU=1450 +FLANNEL_IPMASQ=false +`), 0600)).To(Succeed()) + _, err := flannelNetInfo.Get() + Expect(err).To(MatchError("unable to parse flannel subnet file")) + }) + }) + + Context("when the file doesn't have a valid mtu entry", func() { + It("returns a helpful error", func() { + Expect(ioutil.WriteFile(filePath, []byte(`FLANNEL_NETWORK=10.255.0.0/16 +FLANNEL_SUBNET=10.255.19.1/24 +FLANNEL_MTU=banana +FLANNEL_IPMASQ=false +`), 0600)).To(Succeed()) + _, err := flannelNetInfo.Get() + Expect(err).To(MatchError("unable to parse MTU from subnet file")) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/cni/netinfo/netinfo_suite_test.go b/src/code.cloudfoundry.org/silk/cni/netinfo/netinfo_suite_test.go new file mode 100644 index 00000000..d891c897 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/cni/netinfo/netinfo_suite_test.go @@ -0,0 +1,13 @@ +package netinfo_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" +) + +func TestLegacyFlannel(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "CNI Netinfo Suite") +} diff --git a/src/code.cloudfoundry.org/silk/control-plane.png b/src/code.cloudfoundry.org/silk/control-plane.png new file mode 100644 index 0000000000000000000000000000000000000000..39e62eda29bb0940ba3a58d50d49cb60e37d3652 GIT binary patch literal 34142 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 NYO3i$^HlCW|37k~3eo@o literal 0 HcmV?d00001 diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/client.go b/src/code.cloudfoundry.org/silk/controller/client.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/client.go rename to src/code.cloudfoundry.org/silk/controller/client.go diff --git a/src/code.cloudfoundry.org/silk/controller/client_test.go b/src/code.cloudfoundry.org/silk/controller/client_test.go new file mode 100644 index 00000000..1c8a9e20 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/client_test.go @@ -0,0 +1,248 @@ +package controller_test + +import ( + "encoding/json" + "errors" + "net/http" + + "code.cloudfoundry.org/cf-networking-helpers/fakes" + "code.cloudfoundry.org/cf-networking-helpers/json_client" + "code.cloudfoundry.org/silk/controller" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Client", func() { + var ( + client *controller.Client + jsonClient *fakes.JSONClient + ) + + BeforeEach(func() { + jsonClient = &fakes.JSONClient{} + client = &controller.Client{ + JsonClient: jsonClient, + } + }) + + Describe("GetActiveLeases", func() { + BeforeEach(func() { + jsonClient.DoStub = func(method, route string, reqData, respData interface{}, token string) error { + respBytes := []byte(` + { + "leases": [ + { "underlay_ip": "10.0.3.1", "overlay_subnet": "10.255.90.0/24" }, + { "underlay_ip": "10.0.5.9", "overlay_subnet": "10.253.30.0/24" }, + { "underlay_ip": "10.0.0.8", "overlay_subnet": "10.255.255.55/32" } + ] + }`) + json.Unmarshal(respBytes, respData) + return nil + } + }) + + It("does all the right things", func() { + leases, err := client.GetActiveLeases() + Expect(err).NotTo(HaveOccurred()) + + Expect(jsonClient.DoCallCount()).To(Equal(1)) + method, route, reqData, _, token := jsonClient.DoArgsForCall(0) + Expect(method).To(Equal("GET")) + Expect(route).To(Equal("/leases")) + Expect(reqData).To(BeNil()) + Expect(token).To(BeEmpty()) + + Expect(leases).To(Equal([]controller.Lease{ + { + UnderlayIP: "10.0.3.1", + OverlaySubnet: "10.255.90.0/24", + }, + { + UnderlayIP: "10.0.5.9", + OverlaySubnet: "10.253.30.0/24", + }, + { + UnderlayIP: "10.0.0.8", + OverlaySubnet: "10.255.255.55/32", + }, + }, + )) + }) + + Context("when the json client fails", func() { + BeforeEach(func() { + jsonClient.DoReturns(errors.New("banana")) + }) + It("returns the error", func() { + _, err := client.GetActiveLeases() + Expect(err).To(MatchError("banana")) + }) + }) + }) + + Describe("AcquireSubnetLease", func() { + Context("when acquring a single overlay IP", func() { + BeforeEach(func() { + jsonClient.DoStub = func(method, route string, reqData, respData interface{}, token string) error { + respBytes := []byte(` + { + "underlay_ip": "10.0.3.7", + "overlay_subnet": "10.255.0.12/32" + }`) + json.Unmarshal(respBytes, respData) + return nil + } + }) + + It("does all the right things", func() { + lease, err := client.AcquireSingleOverlayIPLease("10.0.3.7") + Expect(err).NotTo(HaveOccurred()) + + Expect(jsonClient.DoCallCount()).To(Equal(1)) + method, route, reqData, _, token := jsonClient.DoArgsForCall(0) + Expect(method).To(Equal("PUT")) + Expect(route).To(Equal("/leases/acquire")) + Expect(reqData).To(Equal(controller.AcquireLeaseRequest{UnderlayIP: "10.0.3.7", SingleOverlayIP: true})) + Expect(token).To(BeEmpty()) + + Expect(lease).To(Equal(controller.Lease{ + UnderlayIP: "10.0.3.7", + OverlaySubnet: "10.255.0.12/32", + }, + )) + }) + }) + + Context("when acquiring a /24 lease", func() { + BeforeEach(func() { + jsonClient.DoStub = func(method, route string, reqData, respData interface{}, token string) error { + respBytes := []byte(` + { + "underlay_ip": "10.0.3.1", + "overlay_subnet": "10.255.90.0/24" + }`) + json.Unmarshal(respBytes, respData) + return nil + } + }) + + It("does all the right things", func() { + lease, err := client.AcquireSubnetLease("10.0.3.1") + Expect(err).NotTo(HaveOccurred()) + + Expect(jsonClient.DoCallCount()).To(Equal(1)) + method, route, reqData, _, token := jsonClient.DoArgsForCall(0) + Expect(method).To(Equal("PUT")) + Expect(route).To(Equal("/leases/acquire")) + Expect(reqData).To(Equal(controller.AcquireLeaseRequest{UnderlayIP: "10.0.3.1", SingleOverlayIP: false})) + Expect(token).To(BeEmpty()) + + Expect(lease).To(Equal(controller.Lease{ + UnderlayIP: "10.0.3.1", + OverlaySubnet: "10.255.90.0/24", + }, + )) + }) + + }) + + Context("when the json client fails", func() { + BeforeEach(func() { + jsonClient.DoReturns(errors.New("carrot")) + }) + It("returns the error", func() { + _, err := client.AcquireSubnetLease("10.0.3.1") + Expect(err).To(MatchError("carrot")) + }) + }) + }) + + Describe("RenewSubnetLease", func() { + var lease controller.Lease + BeforeEach(func() { + lease = controller.Lease{ + UnderlayIP: "10.0.3.1", + OverlaySubnet: "10.255.90.0/24", + OverlayHardwareAddr: "ee:ee:0a:ff:5a:00", + } + }) + + It("calls the controller to renew the subnet lease", func() { + err := client.RenewSubnetLease(lease) + Expect(err).NotTo(HaveOccurred()) + + Expect(jsonClient.DoCallCount()).To(Equal(1)) + method, route, reqData, _, token := jsonClient.DoArgsForCall(0) + Expect(method).To(Equal("PUT")) + Expect(route).To(Equal("/leases/renew")) + Expect(reqData).To(Equal(lease)) + Expect(token).To(BeEmpty()) + }) + + Context("when the json client fails due to a HTTP 409 Conflict", func() { + BeforeEach(func() { + jsonClient.DoReturns(&json_client.HttpResponseCodeError{ + StatusCode: http.StatusConflict, + Message: "banana", + }) + }) + + It("returns a non-retriable error", func() { + err := client.RenewSubnetLease(lease) + Expect(err).NotTo(BeNil()) + typedErr, ok := err.(controller.NonRetriableError) + Expect(ok).To(BeTrue()) + Expect(typedErr.Error()).To(Equal("non-retriable: banana")) + }) + }) + + Context("when the json client returns any other error", func() { + BeforeEach(func() { + jsonClient.DoReturns(errors.New("no you're a teapot")) + }) + + It("returns the error", func() { + err := client.RenewSubnetLease(lease) + Expect(err).To(MatchError("no you're a teapot")) + }) + }) + }) + + Describe("ReleaseSubnetLease", func() { + BeforeEach(func() { + jsonClient.DoStub = func(method, route string, reqData, respData interface{}, token string) error { + respBytes := []byte(` + { + "underlay_ip": "10.0.3.1", + "overlay_subnet": "10.255.90.0/24", + "overlay_hardware_addr": "ee:ee:0a:ff:5a:00" + }`) + json.Unmarshal(respBytes, respData) + return nil + } + }) + It("calls the controller to release the subnet lease", func() { + err := client.ReleaseSubnetLease("10.0.3.1") + Expect(err).NotTo(HaveOccurred()) + + Expect(jsonClient.DoCallCount()).To(Equal(1)) + method, route, reqData, response, token := jsonClient.DoArgsForCall(0) + Expect(method).To(Equal("PUT")) + Expect(route).To(Equal("/leases/release")) + Expect(reqData).To(Equal(controller.ReleaseLeaseRequest{UnderlayIP: "10.0.3.1"})) + Expect(response).To(BeNil()) + Expect(token).To(BeEmpty()) + }) + + Context("when the json client returns an error", func() { + BeforeEach(func() { + jsonClient.DoReturns(errors.New("no you're a teapot")) + }) + + It("returns the error", func() { + err := client.ReleaseSubnetLease("10.0.3.1") + Expect(err).To(MatchError("no you're a teapot")) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/config/config.go b/src/code.cloudfoundry.org/silk/controller/config/config.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/config/config.go rename to src/code.cloudfoundry.org/silk/controller/config/config.go diff --git a/src/code.cloudfoundry.org/silk/controller/config/config_suite_test.go b/src/code.cloudfoundry.org/silk/controller/config/config_suite_test.go new file mode 100644 index 00000000..a42c99df --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/config/config_suite_test.go @@ -0,0 +1,13 @@ +package config_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Controller Config Suite") +} diff --git a/src/code.cloudfoundry.org/silk/controller/config/config_test.go b/src/code.cloudfoundry.org/silk/controller/config/config_test.go new file mode 100644 index 00000000..c930e703 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/config/config_test.go @@ -0,0 +1,118 @@ +package config_test + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + + "code.cloudfoundry.org/cf-networking-helpers/db" + "code.cloudfoundry.org/silk/controller/config" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func cloneMap(original map[string]interface{}) map[string]interface{} { + new := map[string]interface{}{} + for k, v := range original { + new[k] = v + } + return new +} + +var _ = Describe("Config.ReadFromFile", func() { + var ( + requiredFields map[string]interface{} + ) + + BeforeEach(func() { + requiredFields = map[string]interface{}{ + "debug_server_port": 234, + "listen_host": "0.0.0.0", + "listen_port": 678, + "ca_cert_file": "/some/cert/file", + "server_cert_file": "/some/other/cert/file", + "server_key_file": "/some/key/file", + "network": "10.255.0.0/16", + "subnet_prefix_length": 24, + "database": db.Config{ + Type: "mysql", + User: "user", + Password: "password", + Host: "127.0.0.1", + Port: uint16(234), + Timeout: 1234, + DatabaseName: "database", + }, + "lease_expiration_seconds": 12, + "metron_port": 12, + "metrics_emit_seconds": 5, + "staleness_threshold_seconds": 5, + "health_check_port": 999, + "log_prefix": "potato", + } + }) + + It("does not error on a valid config", func() { + cfg := cloneMap(requiredFields) + + file, err := ioutil.TempFile(os.TempDir(), "config-") + Expect(err).NotTo(HaveOccurred()) + + Expect(json.NewEncoder(file).Encode(cfg)).To(Succeed()) + + _, err = config.ReadFromFile(file.Name()) + Expect(err).NotTo(HaveOccurred()) + }) + + DescribeTable("when config file is missing a member", + func(missingFlag, errorString string) { + cfg := cloneMap(requiredFields) + + delete(cfg, missingFlag) + + file, err := ioutil.TempFile(os.TempDir(), "config-") + Expect(err).NotTo(HaveOccurred()) + + Expect(json.NewEncoder(file).Encode(cfg)).To(Succeed()) + + _, err = config.ReadFromFile(file.Name()) + Expect(err).To(MatchError(ContainSubstring(fmt.Sprintf("invalid config: %s", errorString)))) + }, + + Entry("missing debug_server_port", "debug_server_port", "DebugServerPort: less than min"), + Entry("missing listen_host", "listen_host", "ListenHost: zero value"), + Entry("missing listen_port", "listen_port", "ListenPort: zero value"), + Entry("missing ca_cert_file", "ca_cert_file", "CACertFile: zero value"), + Entry("missing server_cert_file", "server_cert_file", "ServerCertFile: zero value"), + Entry("missing server_key_file", "server_key_file", "ServerKeyFile: zero value"), + Entry("missing network", "network", "Network: zero value"), + Entry("missing subnet_prefix_length", "subnet_prefix_length", "SubnetPrefixLength: zero value"), + Entry("missing database", "database", ""), + Entry("missing lease_expiration_seconds", "lease_expiration_seconds", "LeaseExpirationSeconds: less than min"), + Entry("missing metron_port", "metron_port", "MetronPort: less than min"), + Entry("missing metrics_emit_seconds", "metrics_emit_seconds", "MetricsEmitSeconds: less than min"), + Entry("missing staleness_threshold_seconds", "staleness_threshold_seconds", "StalenessThresholdSeconds: less than min"), + Entry("missing health_check_port", "health_check_port", "HealthCheckPort: less than min"), + Entry("missing log_prefix", "log_prefix", "LogPrefix: zero value"), + ) + + DescribeTable("when config file field is an invalid value", + func(invalidField string, value interface{}, errorString string) { + cfg := cloneMap(requiredFields) + cfg[invalidField] = value + + file, err := ioutil.TempFile(os.TempDir(), "config-") + Expect(err).NotTo(HaveOccurred()) + + Expect(json.NewEncoder(file).Encode(cfg)).To(Succeed()) + + _, err = config.ReadFromFile(file.Name()) + Expect(err).To(MatchError(fmt.Sprintf("invalid config: %s", errorString))) + }, + + Entry("invalid max_open_connections", "max_open_connections", -2, "MaxOpenConnections: less than min"), + Entry("invalid max_idle_connections", "max_idle_connections", -2, "MaxIdleConnections: less than min"), + Entry("invalid connections_max_lifetime_seconds", "connections_max_lifetime_seconds", -2, "MaxConnectionsLifetimeSeconds: less than min"), + ) +}) diff --git a/src/code.cloudfoundry.org/silk/controller/controller_suite_test.go b/src/code.cloudfoundry.org/silk/controller/controller_suite_test.go new file mode 100644 index 00000000..cee3e324 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/controller_suite_test.go @@ -0,0 +1,13 @@ +package controller_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" +) + +func TestController(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Controller Suite") +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/database/database_handler.go b/src/code.cloudfoundry.org/silk/controller/database/database_handler.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/database/database_handler.go rename to src/code.cloudfoundry.org/silk/controller/database/database_handler.go diff --git a/src/code.cloudfoundry.org/silk/controller/database/database_handler_test.go b/src/code.cloudfoundry.org/silk/controller/database/database_handler_test.go new file mode 100644 index 00000000..6dc174fb --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/database/database_handler_test.go @@ -0,0 +1,832 @@ +package database_test + +import ( + "database/sql" + "errors" + "fmt" + "math/rand" + "sync/atomic" + "time" + + "code.cloudfoundry.org/cf-networking-helpers/db" + "code.cloudfoundry.org/cf-networking-helpers/testsupport" + "code.cloudfoundry.org/lager/v3/lagertest" + + "code.cloudfoundry.org/silk/controller" + "code.cloudfoundry.org/silk/controller/database" + "code.cloudfoundry.org/silk/controller/database/fakes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + migrate "github.com/rubenv/sql-migrate" +) + +var _ = Describe("DatabaseHandler", func() { + var ( + databaseHandler *database.DatabaseHandler + realDb *db.ConnWrapper + realMigrateAdapter *database.MigrateAdapter + dbConfig db.Config + mockDb *fakes.Db + mockMigrateAdapter *fakes.MigrateAdapter + lease controller.Lease + lease2 controller.Lease + singleIPLease controller.Lease + singleIPLease2 controller.Lease + ) + BeforeEach(func() { + mockDb = &fakes.Db{} + mockMigrateAdapter = &fakes.MigrateAdapter{} + + dbConfig = testsupport.GetDBConfig() + dbConfig.DatabaseName = fmt.Sprintf("test_db_%03d_%x", GinkgoParallelProcess(), rand.Int()) + testsupport.CreateDatabase(dbConfig) + + var err error + realDb, err = db.NewConnectionPool( + dbConfig, + 200, + 200, + 5*time.Minute, + "controller", + "database-handler", + lagertest.NewTestLogger("test"), + ) + Expect(err).NotTo(HaveOccurred()) + + realMigrateAdapter = &database.MigrateAdapter{} + + mockDb.DriverNameReturns(realDb.DriverName()) + + lease = controller.Lease{ + UnderlayIP: "10.244.11.22", + OverlaySubnet: "10.255.17.0/24", + OverlayHardwareAddr: "ee:ee:0a:ff:11:00", + } + lease2 = controller.Lease{ + UnderlayIP: "10.244.22.33", + OverlaySubnet: "10.255.93.0/24", + OverlayHardwareAddr: "ee:ee:0a:ff:5d:0f", + } + singleIPLease = controller.Lease{ + UnderlayIP: "10.244.11.26", + OverlaySubnet: "10.255.0.12/32", + OverlayHardwareAddr: "ee:ee:0a:ff:11:11", + } + singleIPLease2 = controller.Lease{ + UnderlayIP: "10.244.11.28", + OverlaySubnet: "10.255.0.19/32", + OverlayHardwareAddr: "ee:ee:0a:ff:11:12", + } + }) + + AfterEach(func() { + if realDb != nil { + Expect(realDb.Close()).To(Succeed()) + } + testsupport.RemoveDatabase(dbConfig) + }) + + Describe("Migrate", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + mockMigrateAdapter.ExecReturns(43, nil) + }) + It("calls the migrate adapter", func() { + By("returning the results from the migrator") + numMigrations, err := databaseHandler.Migrate() + Expect(err).NotTo(HaveOccurred()) + Expect(numMigrations).To(Equal(43)) + + By("calling the migrator") + Expect(mockMigrateAdapter.ExecCallCount()).To(Equal(1)) + db, dbType, migrations, dir := mockMigrateAdapter.ExecArgsForCall(0) + Expect(db).To(Equal(mockDb)) + Expect(dbType).To(Equal(realDb.DriverName())) + if dbType == "postgres" { + Expect(migrations).To(Equal(migrate.MemoryMigrationSource{ + Migrations: []*migrate.Migration{ + { + Id: "1", + Up: []string{"CREATE TABLE IF NOT EXISTS subnets (id SERIAL PRIMARY KEY, underlay_ip varchar(15) NOT NULL, overlay_subnet varchar(18) NOT NULL, overlay_hwaddr varchar(17) NOT NULL, last_renewed_at bigint NOT NULL, UNIQUE (underlay_ip), UNIQUE (overlay_subnet), UNIQUE (overlay_hwaddr));"}, + Down: []string{"DROP TABLE subnets"}, + }, + }, + })) + } else { + Expect(migrations).To(Equal(migrate.MemoryMigrationSource{ + Migrations: []*migrate.Migration{ + { + Id: "1", + Up: []string{"CREATE TABLE IF NOT EXISTS subnets (id int NOT NULL AUTO_INCREMENT, PRIMARY KEY (id), underlay_ip varchar(15) NOT NULL, overlay_subnet varchar(18) NOT NULL, overlay_hwaddr varchar(17) NOT NULL, last_renewed_at bigint NOT NULL, UNIQUE (underlay_ip), UNIQUE (overlay_subnet), UNIQUE (overlay_hwaddr));"}, + Down: []string{"DROP TABLE subnets"}, + }, + }, + })) + } + Expect(dir).To(Equal(migrate.Up)) + }) + Context("when the migrator fails", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + mockMigrateAdapter.ExecReturns(0, errors.New("guava")) + }) + It("returns the error", func() { + _, err := databaseHandler.Migrate() + Expect(err).To(MatchError("migrating: guava")) + }) + }) + }) + + Describe("AddEntry", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(realMigrateAdapter, realDb) + _, err := databaseHandler.Migrate() + Expect(err).NotTo(HaveOccurred()) + }) + + It("adds an entry to the DB", func() { + err := databaseHandler.AddEntry(lease) + Expect(err).NotTo(HaveOccurred()) + + leases, err := databaseHandler.All() + Expect(err).NotTo(HaveOccurred()) + Expect(leases).To(ContainElement(lease)) + }) + + Context("when the database type is postgres", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + mockDb.RebindReturns("INSERT INTO subnets (underlay_ip, overlay_subnet, overlay_hwaddr, last_renewed_at) VALUES ($1, $2, $3, EXTRACT(EPOCH FROM now())::numeric::integer)") + mockDb.DriverNameReturns("postgres") + }) + It("adds an entry to the DB", func() { + err := databaseHandler.AddEntry(lease) + Expect(err).NotTo(HaveOccurred()) + + Expect(mockDb.ExecCallCount()).To(Equal(1)) + query, args := mockDb.ExecArgsForCall(0) + Expect(mockDb.RebindArgsForCall(0)).To(Equal("INSERT INTO subnets (underlay_ip, overlay_subnet, overlay_hwaddr, last_renewed_at) VALUES (?, ?, ?, EXTRACT(EPOCH FROM now())::numeric::integer)")) + Expect(query).To(Equal("INSERT INTO subnets (underlay_ip, overlay_subnet, overlay_hwaddr, last_renewed_at) VALUES ($1, $2, $3, EXTRACT(EPOCH FROM now())::numeric::integer)")) + Expect(args).To(Equal([]interface{}{"10.244.11.22", "10.255.17.0/24", "ee:ee:0a:ff:11:00"})) + }) + }) + + Context("when the database type is mysql", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + mockDb.DriverNameReturns("mysql") + mockDb.RebindReturns("INSERT INTO subnets (underlay_ip, overlay_subnet, overlay_hwaddr, last_renewed_at) VALUES (?, ?, ?, UNIX_TIMESTAMP())") + }) + It("adds an entry to the DB", func() { + err := databaseHandler.AddEntry(lease) + Expect(err).NotTo(HaveOccurred()) + + Expect(mockDb.ExecCallCount()).To(Equal(1)) + query, args := mockDb.ExecArgsForCall(0) + Expect(mockDb.RebindArgsForCall(0)).To(Equal("INSERT INTO subnets (underlay_ip, overlay_subnet, overlay_hwaddr, last_renewed_at) VALUES (?, ?, ?, UNIX_TIMESTAMP())")) + Expect(query).To(Equal("INSERT INTO subnets (underlay_ip, overlay_subnet, overlay_hwaddr, last_renewed_at) VALUES (?, ?, ?, UNIX_TIMESTAMP())")) + Expect(args).To(Equal([]interface{}{"10.244.11.22", "10.255.17.0/24", "ee:ee:0a:ff:11:00"})) + }) + }) + + Context("when the database type is not supported", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + mockDb.DriverNameReturns("foo") + }) + It("returns an error", func() { + err := databaseHandler.RenewLeaseForUnderlayIP("1.2.3.4") + Expect(err).To(MatchError("database type foo is not supported")) + }) + }) + + Context("when the database exec returns an error", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + mockDb.ExecReturns(nil, errors.New("apple")) + }) + It("returns a sensible error", func() { + err := databaseHandler.AddEntry(lease) + Expect(err).To(MatchError("adding entry: apple")) + }) + }) + + }) + + Describe("DeleteEntry", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(realMigrateAdapter, realDb) + _, err := databaseHandler.Migrate() + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(lease) + Expect(err).NotTo(HaveOccurred()) + + By("checking that the lease is present") + leases, err := databaseHandler.All() + Expect(err).NotTo(HaveOccurred()) + Expect(leases).To(ContainElement(lease)) + }) + + Context("when the database exec returns some other error", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + mockDb.ExecReturns(nil, errors.New("carrot")) + mockDb.RebindReturns("DELETE FROM subnets WHERE underlay_ip = $1") + mockDb.DriverNameReturns("postgres") + + }) + It("returns a sensible error", func() { + err := databaseHandler.DeleteEntry("some-underlay") + Expect(err).To(MatchError("deleting entry: carrot")) + + Expect(mockDb.ExecCallCount()).To(Equal(1)) + + query, args := mockDb.ExecArgsForCall(0) + Expect(mockDb.RebindArgsForCall(0)).To(Equal("DELETE FROM subnets WHERE underlay_ip = ?")) + Expect(query).To(Equal("DELETE FROM subnets WHERE underlay_ip = $1")) + Expect(args).To(Equal([]interface{}{"some-underlay"})) + }) + }) + + Context("when the parsing the result fails", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + badResult := &fakes.SqlResult{} + badResult.RowsAffectedReturns(0, errors.New("potato")) + mockDb.ExecReturns(badResult, nil) + }) + + It("returns an error", func() { + err := databaseHandler.DeleteEntry("10.244.11.22") + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError("parse result: potato")) + }) + }) + + It("deletes an entry from the DB", func() { + err := databaseHandler.DeleteEntry("10.244.11.22") + Expect(err).NotTo(HaveOccurred()) + + By("checking that the lease is not present") + leases, err := databaseHandler.All() + Expect(err).NotTo(HaveOccurred()) + Expect(leases).NotTo(ContainElement(lease)) + }) + + Context("when no entry exists", func() { + It("returns a RecordNotAffectedError", func() { + err := databaseHandler.DeleteEntry("8.8.8.8") + Expect(err).To(Equal(database.RecordNotAffectedError)) + }) + }) + }) + + Describe("LeaseForUnderlayIP", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(realMigrateAdapter, realDb) + _, err := databaseHandler.Migrate() + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(lease) + Expect(err).NotTo(HaveOccurred()) + }) + It("returns the subnet for the given underlay IP", func() { + found, err := databaseHandler.LeaseForUnderlayIP("10.244.11.22") + Expect(err).NotTo(HaveOccurred()) + Expect(*found).To(Equal(lease)) + }) + + Context("when there is no entry for the underlay ip", func() { + It("returns nil", func() { + entry, err := databaseHandler.LeaseForUnderlayIP("10.244.11.23") + Expect(err).NotTo(HaveOccurred()) + Expect(entry).To(BeNil()) + }) + }) + }) + + Describe("RenewLeaseForUnderlayIP", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + }) + + Context("when the database is postgres", func() { + BeforeEach(func() { + mockDb.DriverNameReturns("postgres") + mockDb.RebindReturns("UPDATE subnets SET last_renewed_at = EXTRACT(EPOCH FROM now())::numeric::integer WHERE underlay_ip = $1") + }) + It("updates the last renewed at time", func() { + err := databaseHandler.RenewLeaseForUnderlayIP("1.2.3.4") + Expect(err).NotTo(HaveOccurred()) + + Expect(mockDb.ExecCallCount()).To(Equal(1)) + query, args := mockDb.ExecArgsForCall(0) + + Expect(mockDb.RebindArgsForCall(0)).To(Equal("UPDATE subnets SET last_renewed_at = EXTRACT(EPOCH FROM now())::numeric::integer WHERE underlay_ip = ?")) + Expect(query).To(Equal("UPDATE subnets SET last_renewed_at = EXTRACT(EPOCH FROM now())::numeric::integer WHERE underlay_ip = $1")) + Expect(args).To(ContainElement("1.2.3.4")) + }) + }) + + Context("when the database is mysql", func() { + BeforeEach(func() { + mockDb.DriverNameReturns("mysql") + mockDb.RebindReturns("UPDATE subnets SET last_renewed_at = UNIX_TIMESTAMP() WHERE underlay_ip = ?") + }) + It("updates the last renewed at time", func() { + err := databaseHandler.RenewLeaseForUnderlayIP("1.2.3.4") + Expect(err).NotTo(HaveOccurred()) + + query, args := mockDb.ExecArgsForCall(0) + Expect(mockDb.RebindArgsForCall(0)).To(Equal("UPDATE subnets SET last_renewed_at = UNIX_TIMESTAMP() WHERE underlay_ip = ?")) + Expect(query).To(Equal("UPDATE subnets SET last_renewed_at = UNIX_TIMESTAMP() WHERE underlay_ip = ?")) + Expect(args).To(ContainElement("1.2.3.4")) + }) + }) + + Context("when the database type is not supported", func() { + BeforeEach(func() { + mockDb.DriverNameReturns("foo") + }) + It("returns an error", func() { + err := databaseHandler.RenewLeaseForUnderlayIP("1.2.3.4") + Expect(err).To(MatchError("database type foo is not supported")) + }) + }) + + Context("when the database exec returns an error", func() { + BeforeEach(func() { + mockDb.ExecReturns(nil, errors.New("apple")) + }) + It("returns a sensible error", func() { + err := databaseHandler.RenewLeaseForUnderlayIP("1.2.3.4") + Expect(err).To(MatchError("renewing lease: apple")) + }) + }) + }) + + Describe("LastRenewedAtForUnderlayIP", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(realMigrateAdapter, realDb) + _, err := databaseHandler.Migrate() + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(lease) + Expect(err).NotTo(HaveOccurred()) + }) + It("selects the last_renewed_at time for the lease", func() { + lastRenewedAt, err := databaseHandler.LastRenewedAtForUnderlayIP("10.244.11.22") + Expect(err).NotTo(HaveOccurred()) + Expect(lastRenewedAt).To(BeNumerically(">", 0)) + }) + It("gets updated when the lease is renewed", func() { + createdAt, err := databaseHandler.LastRenewedAtForUnderlayIP("10.244.11.22") + Expect(err).NotTo(HaveOccurred()) + time.Sleep(1 * time.Second) + err = databaseHandler.RenewLeaseForUnderlayIP("10.244.11.22") + Expect(err).NotTo(HaveOccurred()) + updatedAt, err := databaseHandler.LastRenewedAtForUnderlayIP("10.244.11.22") + Expect(err).NotTo(HaveOccurred()) + Expect(updatedAt).To(BeNumerically(">", createdAt)) + }) + + Context("when there is no entry for the underlay ip", func() { + It("returns an error", func() { + _, err := databaseHandler.LastRenewedAtForUnderlayIP("10.244.11.23") + Expect(err).To(MatchError("sql: no rows in result set")) + }) + }) + }) + + Describe("All", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(realMigrateAdapter, realDb) + _, err := databaseHandler.Migrate() + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(lease) + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(lease2) + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(singleIPLease) + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(singleIPLease2) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns all the saved subnets", func() { + leases, err := databaseHandler.All() + Expect(err).NotTo(HaveOccurred()) + + Expect(len(leases)).To(Equal(4)) + Expect(leases).To(ConsistOf([]controller.Lease{ + lease, + lease2, + singleIPLease, + singleIPLease2, + })) + }) + + Context("when the query fails", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + mockDb.QueryReturns(nil, errors.New("strawberry")) + }) + It("returns an error", func() { + _, err := databaseHandler.All() + Expect(err).To(MatchError("selecting all subnets: strawberry")) + }) + }) + + Context("when the parsing the result fails", func() { + var rows *sql.Rows + BeforeEach(func() { + var err error + rows, err = realDb.Query("SELECT 1") + Expect(err).NotTo(HaveOccurred()) + + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + mockDb.QueryReturns(rows, nil) + }) + + AfterEach(func() { + Expect(rows.Close()).To(Succeed()) + }) + + It("returns an error", func() { + _, err := databaseHandler.All() + Expect(err.Error()).To(ContainSubstring("selecting all subnets: parsing result")) + }) + }) + }) + + Describe("AllBlockSubnets", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(realMigrateAdapter, realDb) + _, err := databaseHandler.Migrate() + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(lease) + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(lease2) + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(singleIPLease) + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(singleIPLease2) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns all the saved subnets", func() { + leases, err := databaseHandler.AllBlockSubnets() + Expect(err).NotTo(HaveOccurred()) + + Expect(len(leases)).To(Equal(2)) + Expect(leases).To(ConsistOf([]controller.Lease{ + lease, + lease2, + })) + }) + + Context("when the query fails", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + mockDb.QueryReturns(nil, errors.New("strawberry")) + }) + It("returns an error", func() { + _, err := databaseHandler.AllBlockSubnets() + Expect(err).To(MatchError("selecting all block subnets: strawberry")) + }) + }) + + Context("when the parsing the result fails", func() { + var rows *sql.Rows + BeforeEach(func() { + var err error + rows, err = realDb.Query("SELECT 1") + Expect(err).NotTo(HaveOccurred()) + + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + mockDb.QueryReturns(rows, nil) + }) + + AfterEach(func() { + Expect(rows.Close()).To(Succeed()) + }) + + It("returns an error", func() { + _, err := databaseHandler.AllBlockSubnets() + Expect(err.Error()).To(ContainSubstring("selecting all block subnets: parsing result")) + }) + }) + }) + + Describe("AllSingleIPSubnets", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(realMigrateAdapter, realDb) + _, err := databaseHandler.Migrate() + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(lease) + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(singleIPLease) + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(singleIPLease2) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns all singleIP subnets", func() { + leases, err := databaseHandler.AllSingleIPSubnets() + Expect(err).NotTo(HaveOccurred()) + + Expect(leases).To(HaveLen(2)) + Expect(leases).To(ConsistOf([]controller.Lease{ + singleIPLease, + singleIPLease2, + })) + }) + + Context("when the query fails", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + mockDb.QueryReturns(nil, errors.New("strawberry")) + }) + + It("returns an error", func() { + _, err := databaseHandler.AllSingleIPSubnets() + Expect(err).To(MatchError("selecting all single ip subnets: strawberry")) + }) + }) + + Context("when the parsing the result fails", func() { + var rows *sql.Rows + BeforeEach(func() { + var err error + rows, err = realDb.Query("SELECT 1") + Expect(err).NotTo(HaveOccurred()) + + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + mockDb.QueryReturns(rows, nil) + }) + + AfterEach(func() { + Expect(rows.Close()).To(Succeed()) + }) + + It("returns an error", func() { + _, err := databaseHandler.AllSingleIPSubnets() + Expect(err.Error()).To(ContainSubstring("selecting all single ip subnets: parsing result")) + }) + }) + }) + + Describe("AllActive", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(realMigrateAdapter, realDb) + _, err := databaseHandler.Migrate() + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(lease) + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(lease2) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns the leases which have been renewed within the expiration time", func() { + leases, err := databaseHandler.AllActive(1000) + Expect(err).NotTo(HaveOccurred()) + + Expect(leases).To(HaveLen(2)) + Expect(leases).To(ConsistOf([]controller.Lease{ + lease, + lease2, + })) + + leases, err = databaseHandler.AllActive(0) + Expect(err).NotTo(HaveOccurred()) + + Expect(leases).To(HaveLen(0)) + }) + + Context("when the db driver name is not supported", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + mockDb.DriverNameReturns("foo") + }) + It("should return an error", func() { + _, err := databaseHandler.AllActive(1000) + Expect(err).To(MatchError("database type foo is not supported")) + }) + }) + + Context("when the query fails", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + mockDb.QueryReturns(nil, errors.New("strawberry")) + }) + It("returns an error", func() { + _, err := databaseHandler.AllActive(100) + Expect(err).To(MatchError("selecting all active subnets: strawberry")) + }) + }) + + Context("when the parsing the result fails", func() { + var rows *sql.Rows + BeforeEach(func() { + var err error + rows, err = realDb.Query("SELECT 1") + Expect(err).NotTo(HaveOccurred()) + + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + mockDb.QueryReturns(rows, nil) + }) + + AfterEach(func() { + Expect(rows.Close()).To(Succeed()) + }) + + It("returns an error", func() { + _, err := databaseHandler.AllActive(100) + Expect(err.Error()).To(ContainSubstring("selecting all active subnets: parsing result")) + }) + }) + }) + + Describe("CheckDatabase", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(realMigrateAdapter, realDb) + _, err := databaseHandler.Migrate() + Expect(err).NotTo(HaveOccurred()) + }) + + It("checks the database", func() { + err := databaseHandler.CheckDatabase() + Expect(err).NotTo(HaveOccurred()) + }) + + Context("when the connection to the database is closed", func() { + BeforeEach(func() { + if realDb != nil { + Expect(realDb.Close()).To(Succeed()) + } + }) + + It("returns an error", func() { + err := databaseHandler.CheckDatabase() + Expect(err).To(HaveOccurred()) + }) + }) + }) + + Describe("OldestExpiredBlockSubnet", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(realMigrateAdapter, realDb) + _, err := databaseHandler.Migrate() + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(singleIPLease) + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(lease) + Expect(err).NotTo(HaveOccurred()) + }) + + It("gets the oldest lease that is expired", func() { + expiredLease, err := databaseHandler.OldestExpiredBlockSubnet(0) + Expect(err).NotTo(HaveOccurred()) + + Expect(expiredLease).To(Equal(&lease)) + }) + + Context("when the database type is not supported", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + mockDb.DriverNameReturns("foo") + }) + It("returns an error", func() { + _, err := databaseHandler.OldestExpiredBlockSubnet(23) + Expect(err).To(MatchError("database type foo is not supported")) + }) + }) + + Context("when no lease is expired", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(realMigrateAdapter, realDb) + _, err := databaseHandler.Migrate() + Expect(err).NotTo(HaveOccurred()) + }) + It("returns nil and does not error", func() { + lease, err := databaseHandler.OldestExpiredBlockSubnet(23) + Expect(err).NotTo(HaveOccurred()) + Expect(lease).To(BeNil()) + }) + }) + + Context("when parsing the result fails", func() { + var result *sql.Row + BeforeEach(func() { + result = realDb.QueryRow("SELECT 1") + + mockDb.QueryRowReturns(result) + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + }) + It("returns an error", func() { + _, err := databaseHandler.OldestExpiredBlockSubnet(23) + Expect(err).To(MatchError(ContainSubstring("scan result:"))) + }) + }) + }) + + Describe("OldestExipredSingleIP", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(realMigrateAdapter, realDb) + _, err := databaseHandler.Migrate() + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(lease) + Expect(err).NotTo(HaveOccurred()) + err = databaseHandler.AddEntry(singleIPLease) + Expect(err).NotTo(HaveOccurred()) + }) + + It("gets the oldest lease that is expired", func() { + expiredLease, err := databaseHandler.OldestExpiredSingleIP(0) + Expect(err).NotTo(HaveOccurred()) + + Expect(expiredLease).To(Equal(&singleIPLease)) + }) + + Context("when the database type is not supported", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + mockDb.DriverNameReturns("foo") + }) + It("returns an error", func() { + _, err := databaseHandler.OldestExpiredSingleIP(23) + Expect(err).To(MatchError("database type foo is not supported")) + }) + }) + + Context("when no lease is expired", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(realMigrateAdapter, realDb) + _, err := databaseHandler.Migrate() + Expect(err).NotTo(HaveOccurred()) + }) + It("returns nil and does not error", func() { + lease, err := databaseHandler.OldestExpiredSingleIP(23) + Expect(err).NotTo(HaveOccurred()) + Expect(lease).To(BeNil()) + }) + }) + + Context("when parsing the result fails", func() { + var result *sql.Row + BeforeEach(func() { + result = realDb.QueryRow("SELECT 1") + + mockDb.QueryRowReturns(result) + databaseHandler = database.NewDatabaseHandler(mockMigrateAdapter, mockDb) + }) + It("returns an error", func() { + _, err := databaseHandler.OldestExpiredSingleIP(23) + Expect(err).To(MatchError(ContainSubstring("scan result:"))) + }) + }) + }) + + Describe("concurrent add and delete requests", func() { + BeforeEach(func() { + databaseHandler = database.NewDatabaseHandler(realMigrateAdapter, realDb) + _, err := databaseHandler.Migrate() + Expect(err).NotTo(HaveOccurred()) + }) + It("remains consistent", func() { + nLeases := 1000 + leases := []interface{}{} + for i := 0; i < nLeases; i++ { + leases = append(leases, controller.Lease{ + UnderlayIP: fmt.Sprintf("underlay-%d", i), + OverlaySubnet: fmt.Sprintf("subnet-%d", i), + OverlayHardwareAddr: fmt.Sprintf("hardware-%x", i), + }) + } + parallelRunner := &testsupport.ParallelRunner{ + NumWorkers: 4, + } + toDelete := make(chan (interface{}), nLeases) + go func() { + parallelRunner.RunOnSlice(leases, func(lease interface{}) { + l := lease.(controller.Lease) + Expect(databaseHandler.AddEntry(l)).To(Succeed()) + toDelete <- l + }) + close(toDelete) + }() + + var nDeleted int32 + parallelRunner.RunOnChannel(toDelete, func(lease interface{}) { + l := lease.(controller.Lease) + Expect(databaseHandler.DeleteEntry(l.UnderlayIP)).To(Succeed()) + atomic.AddInt32(&nDeleted, 1) + }) + + Expect(nDeleted).To(Equal(int32(nLeases))) + + allLeases, err := databaseHandler.All() + Expect(err).NotTo(HaveOccurred()) + + Expect(allLeases).To(BeEmpty()) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/controller/database/database_suite_test.go b/src/code.cloudfoundry.org/silk/controller/database/database_suite_test.go new file mode 100644 index 00000000..65dcd412 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/database/database_suite_test.go @@ -0,0 +1,19 @@ +package database_test + +import ( + "math/rand" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" +) + +func TestDatabase(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Controller Database Suite") +} + +var _ = BeforeSuite(func() { + rand.Seed(GinkgoRandomSeed() + int64(GinkgoParallelProcess())) +}) diff --git a/src/code.cloudfoundry.org/silk/controller/database/fakes/database_migrator.go b/src/code.cloudfoundry.org/silk/controller/database/fakes/database_migrator.go new file mode 100644 index 00000000..b939dc23 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/database/fakes/database_migrator.go @@ -0,0 +1,89 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" +) + +type DatabaseMigrator struct { + MigrateStub func() (int, error) + migrateMutex sync.RWMutex + migrateArgsForCall []struct{} + migrateReturns struct { + result1 int + result2 error + } + migrateReturnsOnCall map[int]struct { + result1 int + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *DatabaseMigrator) Migrate() (int, error) { + fake.migrateMutex.Lock() + ret, specificReturn := fake.migrateReturnsOnCall[len(fake.migrateArgsForCall)] + fake.migrateArgsForCall = append(fake.migrateArgsForCall, struct{}{}) + fake.recordInvocation("Migrate", []interface{}{}) + fake.migrateMutex.Unlock() + if fake.MigrateStub != nil { + return fake.MigrateStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.migrateReturns.result1, fake.migrateReturns.result2 +} + +func (fake *DatabaseMigrator) MigrateCallCount() int { + fake.migrateMutex.RLock() + defer fake.migrateMutex.RUnlock() + return len(fake.migrateArgsForCall) +} + +func (fake *DatabaseMigrator) MigrateReturns(result1 int, result2 error) { + fake.MigrateStub = nil + fake.migrateReturns = struct { + result1 int + result2 error + }{result1, result2} +} + +func (fake *DatabaseMigrator) MigrateReturnsOnCall(i int, result1 int, result2 error) { + fake.MigrateStub = nil + if fake.migrateReturnsOnCall == nil { + fake.migrateReturnsOnCall = make(map[int]struct { + result1 int + result2 error + }) + } + fake.migrateReturnsOnCall[i] = struct { + result1 int + result2 error + }{result1, result2} +} + +func (fake *DatabaseMigrator) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.migrateMutex.RLock() + defer fake.migrateMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *DatabaseMigrator) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/controller/database/fakes/db.go b/src/code.cloudfoundry.org/silk/controller/database/fakes/db.go new file mode 100644 index 00000000..14cc5877 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/database/fakes/db.go @@ -0,0 +1,401 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "database/sql" + "sync" + + "code.cloudfoundry.org/silk/controller/database" + "github.com/jmoiron/sqlx" +) + +type Db struct { + ExecStub func(query string, args ...interface{}) (sql.Result, error) + execMutex sync.RWMutex + execArgsForCall []struct { + query string + args []interface{} + } + execReturns struct { + result1 sql.Result + result2 error + } + execReturnsOnCall map[int]struct { + result1 sql.Result + result2 error + } + RebindStub func(query string) string + rebindMutex sync.RWMutex + rebindArgsForCall []struct { + query string + } + rebindReturns struct { + result1 string + } + rebindReturnsOnCall map[int]struct { + result1 string + } + QueryStub func(query string, args ...interface{}) (*sql.Rows, error) + queryMutex sync.RWMutex + queryArgsForCall []struct { + query string + args []interface{} + } + queryReturns struct { + result1 *sql.Rows + result2 error + } + queryReturnsOnCall map[int]struct { + result1 *sql.Rows + result2 error + } + QueryRowStub func(query string, args ...interface{}) *sql.Row + queryRowMutex sync.RWMutex + queryRowArgsForCall []struct { + query string + args []interface{} + } + queryRowReturns struct { + result1 *sql.Row + } + queryRowReturnsOnCall map[int]struct { + result1 *sql.Row + } + DriverNameStub func() string + driverNameMutex sync.RWMutex + driverNameArgsForCall []struct{} + driverNameReturns struct { + result1 string + } + driverNameReturnsOnCall map[int]struct { + result1 string + } + RawConnectionStub func() *sqlx.DB + rawConnectionMutex sync.RWMutex + rawConnectionArgsForCall []struct{} + rawConnectionReturns struct { + result1 *sqlx.DB + } + rawConnectionReturnsOnCall map[int]struct { + result1 *sqlx.DB + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *Db) Exec(query string, args ...interface{}) (sql.Result, error) { + fake.execMutex.Lock() + ret, specificReturn := fake.execReturnsOnCall[len(fake.execArgsForCall)] + fake.execArgsForCall = append(fake.execArgsForCall, struct { + query string + args []interface{} + }{query, args}) + fake.recordInvocation("Exec", []interface{}{query, args}) + fake.execMutex.Unlock() + if fake.ExecStub != nil { + return fake.ExecStub(query, args...) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.execReturns.result1, fake.execReturns.result2 +} + +func (fake *Db) ExecCallCount() int { + fake.execMutex.RLock() + defer fake.execMutex.RUnlock() + return len(fake.execArgsForCall) +} + +func (fake *Db) ExecArgsForCall(i int) (string, []interface{}) { + fake.execMutex.RLock() + defer fake.execMutex.RUnlock() + return fake.execArgsForCall[i].query, fake.execArgsForCall[i].args +} + +func (fake *Db) ExecReturns(result1 sql.Result, result2 error) { + fake.ExecStub = nil + fake.execReturns = struct { + result1 sql.Result + result2 error + }{result1, result2} +} + +func (fake *Db) ExecReturnsOnCall(i int, result1 sql.Result, result2 error) { + fake.ExecStub = nil + if fake.execReturnsOnCall == nil { + fake.execReturnsOnCall = make(map[int]struct { + result1 sql.Result + result2 error + }) + } + fake.execReturnsOnCall[i] = struct { + result1 sql.Result + result2 error + }{result1, result2} +} + +func (fake *Db) Rebind(query string) string { + fake.rebindMutex.Lock() + ret, specificReturn := fake.rebindReturnsOnCall[len(fake.rebindArgsForCall)] + fake.rebindArgsForCall = append(fake.rebindArgsForCall, struct { + query string + }{query}) + fake.recordInvocation("Rebind", []interface{}{query}) + fake.rebindMutex.Unlock() + if fake.RebindStub != nil { + return fake.RebindStub(query) + } + if specificReturn { + return ret.result1 + } + return fake.rebindReturns.result1 +} + +func (fake *Db) RebindCallCount() int { + fake.rebindMutex.RLock() + defer fake.rebindMutex.RUnlock() + return len(fake.rebindArgsForCall) +} + +func (fake *Db) RebindArgsForCall(i int) string { + fake.rebindMutex.RLock() + defer fake.rebindMutex.RUnlock() + return fake.rebindArgsForCall[i].query +} + +func (fake *Db) RebindReturns(result1 string) { + fake.RebindStub = nil + fake.rebindReturns = struct { + result1 string + }{result1} +} + +func (fake *Db) RebindReturnsOnCall(i int, result1 string) { + fake.RebindStub = nil + if fake.rebindReturnsOnCall == nil { + fake.rebindReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.rebindReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *Db) Query(query string, args ...interface{}) (*sql.Rows, error) { + fake.queryMutex.Lock() + ret, specificReturn := fake.queryReturnsOnCall[len(fake.queryArgsForCall)] + fake.queryArgsForCall = append(fake.queryArgsForCall, struct { + query string + args []interface{} + }{query, args}) + fake.recordInvocation("Query", []interface{}{query, args}) + fake.queryMutex.Unlock() + if fake.QueryStub != nil { + return fake.QueryStub(query, args...) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.queryReturns.result1, fake.queryReturns.result2 +} + +func (fake *Db) QueryCallCount() int { + fake.queryMutex.RLock() + defer fake.queryMutex.RUnlock() + return len(fake.queryArgsForCall) +} + +func (fake *Db) QueryArgsForCall(i int) (string, []interface{}) { + fake.queryMutex.RLock() + defer fake.queryMutex.RUnlock() + return fake.queryArgsForCall[i].query, fake.queryArgsForCall[i].args +} + +func (fake *Db) QueryReturns(result1 *sql.Rows, result2 error) { + fake.QueryStub = nil + fake.queryReturns = struct { + result1 *sql.Rows + result2 error + }{result1, result2} +} + +func (fake *Db) QueryReturnsOnCall(i int, result1 *sql.Rows, result2 error) { + fake.QueryStub = nil + if fake.queryReturnsOnCall == nil { + fake.queryReturnsOnCall = make(map[int]struct { + result1 *sql.Rows + result2 error + }) + } + fake.queryReturnsOnCall[i] = struct { + result1 *sql.Rows + result2 error + }{result1, result2} +} + +func (fake *Db) QueryRow(query string, args ...interface{}) *sql.Row { + fake.queryRowMutex.Lock() + ret, specificReturn := fake.queryRowReturnsOnCall[len(fake.queryRowArgsForCall)] + fake.queryRowArgsForCall = append(fake.queryRowArgsForCall, struct { + query string + args []interface{} + }{query, args}) + fake.recordInvocation("QueryRow", []interface{}{query, args}) + fake.queryRowMutex.Unlock() + if fake.QueryRowStub != nil { + return fake.QueryRowStub(query, args...) + } + if specificReturn { + return ret.result1 + } + return fake.queryRowReturns.result1 +} + +func (fake *Db) QueryRowCallCount() int { + fake.queryRowMutex.RLock() + defer fake.queryRowMutex.RUnlock() + return len(fake.queryRowArgsForCall) +} + +func (fake *Db) QueryRowArgsForCall(i int) (string, []interface{}) { + fake.queryRowMutex.RLock() + defer fake.queryRowMutex.RUnlock() + return fake.queryRowArgsForCall[i].query, fake.queryRowArgsForCall[i].args +} + +func (fake *Db) QueryRowReturns(result1 *sql.Row) { + fake.QueryRowStub = nil + fake.queryRowReturns = struct { + result1 *sql.Row + }{result1} +} + +func (fake *Db) QueryRowReturnsOnCall(i int, result1 *sql.Row) { + fake.QueryRowStub = nil + if fake.queryRowReturnsOnCall == nil { + fake.queryRowReturnsOnCall = make(map[int]struct { + result1 *sql.Row + }) + } + fake.queryRowReturnsOnCall[i] = struct { + result1 *sql.Row + }{result1} +} + +func (fake *Db) DriverName() string { + fake.driverNameMutex.Lock() + ret, specificReturn := fake.driverNameReturnsOnCall[len(fake.driverNameArgsForCall)] + fake.driverNameArgsForCall = append(fake.driverNameArgsForCall, struct{}{}) + fake.recordInvocation("DriverName", []interface{}{}) + fake.driverNameMutex.Unlock() + if fake.DriverNameStub != nil { + return fake.DriverNameStub() + } + if specificReturn { + return ret.result1 + } + return fake.driverNameReturns.result1 +} + +func (fake *Db) DriverNameCallCount() int { + fake.driverNameMutex.RLock() + defer fake.driverNameMutex.RUnlock() + return len(fake.driverNameArgsForCall) +} + +func (fake *Db) DriverNameReturns(result1 string) { + fake.DriverNameStub = nil + fake.driverNameReturns = struct { + result1 string + }{result1} +} + +func (fake *Db) DriverNameReturnsOnCall(i int, result1 string) { + fake.DriverNameStub = nil + if fake.driverNameReturnsOnCall == nil { + fake.driverNameReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.driverNameReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *Db) RawConnection() *sqlx.DB { + fake.rawConnectionMutex.Lock() + ret, specificReturn := fake.rawConnectionReturnsOnCall[len(fake.rawConnectionArgsForCall)] + fake.rawConnectionArgsForCall = append(fake.rawConnectionArgsForCall, struct{}{}) + fake.recordInvocation("RawConnection", []interface{}{}) + fake.rawConnectionMutex.Unlock() + if fake.RawConnectionStub != nil { + return fake.RawConnectionStub() + } + if specificReturn { + return ret.result1 + } + return fake.rawConnectionReturns.result1 +} + +func (fake *Db) RawConnectionCallCount() int { + fake.rawConnectionMutex.RLock() + defer fake.rawConnectionMutex.RUnlock() + return len(fake.rawConnectionArgsForCall) +} + +func (fake *Db) RawConnectionReturns(result1 *sqlx.DB) { + fake.RawConnectionStub = nil + fake.rawConnectionReturns = struct { + result1 *sqlx.DB + }{result1} +} + +func (fake *Db) RawConnectionReturnsOnCall(i int, result1 *sqlx.DB) { + fake.RawConnectionStub = nil + if fake.rawConnectionReturnsOnCall == nil { + fake.rawConnectionReturnsOnCall = make(map[int]struct { + result1 *sqlx.DB + }) + } + fake.rawConnectionReturnsOnCall[i] = struct { + result1 *sqlx.DB + }{result1} +} + +func (fake *Db) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.execMutex.RLock() + defer fake.execMutex.RUnlock() + fake.rebindMutex.RLock() + defer fake.rebindMutex.RUnlock() + fake.queryMutex.RLock() + defer fake.queryMutex.RUnlock() + fake.queryRowMutex.RLock() + defer fake.queryRowMutex.RUnlock() + fake.driverNameMutex.RLock() + defer fake.driverNameMutex.RUnlock() + fake.rawConnectionMutex.RLock() + defer fake.rawConnectionMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *Db) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ database.Db = new(Db) diff --git a/src/code.cloudfoundry.org/silk/controller/database/fakes/migrateAdapter.go b/src/code.cloudfoundry.org/silk/controller/database/fakes/migrateAdapter.go new file mode 100644 index 00000000..32667374 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/database/fakes/migrateAdapter.go @@ -0,0 +1,108 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + + "code.cloudfoundry.org/silk/controller/database" + "github.com/rubenv/sql-migrate" +) + +type MigrateAdapter struct { + ExecStub func(db database.Db, dialect string, m migrate.MigrationSource, dir migrate.MigrationDirection) (int, error) + execMutex sync.RWMutex + execArgsForCall []struct { + db database.Db + dialect string + m migrate.MigrationSource + dir migrate.MigrationDirection + } + execReturns struct { + result1 int + result2 error + } + execReturnsOnCall map[int]struct { + result1 int + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *MigrateAdapter) Exec(db database.Db, dialect string, m migrate.MigrationSource, dir migrate.MigrationDirection) (int, error) { + fake.execMutex.Lock() + ret, specificReturn := fake.execReturnsOnCall[len(fake.execArgsForCall)] + fake.execArgsForCall = append(fake.execArgsForCall, struct { + db database.Db + dialect string + m migrate.MigrationSource + dir migrate.MigrationDirection + }{db, dialect, m, dir}) + fake.recordInvocation("Exec", []interface{}{db, dialect, m, dir}) + fake.execMutex.Unlock() + if fake.ExecStub != nil { + return fake.ExecStub(db, dialect, m, dir) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.execReturns.result1, fake.execReturns.result2 +} + +func (fake *MigrateAdapter) ExecCallCount() int { + fake.execMutex.RLock() + defer fake.execMutex.RUnlock() + return len(fake.execArgsForCall) +} + +func (fake *MigrateAdapter) ExecArgsForCall(i int) (database.Db, string, migrate.MigrationSource, migrate.MigrationDirection) { + fake.execMutex.RLock() + defer fake.execMutex.RUnlock() + return fake.execArgsForCall[i].db, fake.execArgsForCall[i].dialect, fake.execArgsForCall[i].m, fake.execArgsForCall[i].dir +} + +func (fake *MigrateAdapter) ExecReturns(result1 int, result2 error) { + fake.ExecStub = nil + fake.execReturns = struct { + result1 int + result2 error + }{result1, result2} +} + +func (fake *MigrateAdapter) ExecReturnsOnCall(i int, result1 int, result2 error) { + fake.ExecStub = nil + if fake.execReturnsOnCall == nil { + fake.execReturnsOnCall = make(map[int]struct { + result1 int + result2 error + }) + } + fake.execReturnsOnCall[i] = struct { + result1 int + result2 error + }{result1, result2} +} + +func (fake *MigrateAdapter) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.execMutex.RLock() + defer fake.execMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *MigrateAdapter) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/controller/database/fakes/sqlResult.go b/src/code.cloudfoundry.org/silk/controller/database/fakes/sqlResult.go new file mode 100644 index 00000000..ca188246 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/database/fakes/sqlResult.go @@ -0,0 +1,145 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" +) + +type SqlResult struct { + LastInsertIdStub func() (int64, error) + lastInsertIdMutex sync.RWMutex + lastInsertIdArgsForCall []struct{} + lastInsertIdReturns struct { + result1 int64 + result2 error + } + lastInsertIdReturnsOnCall map[int]struct { + result1 int64 + result2 error + } + RowsAffectedStub func() (int64, error) + rowsAffectedMutex sync.RWMutex + rowsAffectedArgsForCall []struct{} + rowsAffectedReturns struct { + result1 int64 + result2 error + } + rowsAffectedReturnsOnCall map[int]struct { + result1 int64 + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *SqlResult) LastInsertId() (int64, error) { + fake.lastInsertIdMutex.Lock() + ret, specificReturn := fake.lastInsertIdReturnsOnCall[len(fake.lastInsertIdArgsForCall)] + fake.lastInsertIdArgsForCall = append(fake.lastInsertIdArgsForCall, struct{}{}) + fake.recordInvocation("LastInsertId", []interface{}{}) + fake.lastInsertIdMutex.Unlock() + if fake.LastInsertIdStub != nil { + return fake.LastInsertIdStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.lastInsertIdReturns.result1, fake.lastInsertIdReturns.result2 +} + +func (fake *SqlResult) LastInsertIdCallCount() int { + fake.lastInsertIdMutex.RLock() + defer fake.lastInsertIdMutex.RUnlock() + return len(fake.lastInsertIdArgsForCall) +} + +func (fake *SqlResult) LastInsertIdReturns(result1 int64, result2 error) { + fake.LastInsertIdStub = nil + fake.lastInsertIdReturns = struct { + result1 int64 + result2 error + }{result1, result2} +} + +func (fake *SqlResult) LastInsertIdReturnsOnCall(i int, result1 int64, result2 error) { + fake.LastInsertIdStub = nil + if fake.lastInsertIdReturnsOnCall == nil { + fake.lastInsertIdReturnsOnCall = make(map[int]struct { + result1 int64 + result2 error + }) + } + fake.lastInsertIdReturnsOnCall[i] = struct { + result1 int64 + result2 error + }{result1, result2} +} + +func (fake *SqlResult) RowsAffected() (int64, error) { + fake.rowsAffectedMutex.Lock() + ret, specificReturn := fake.rowsAffectedReturnsOnCall[len(fake.rowsAffectedArgsForCall)] + fake.rowsAffectedArgsForCall = append(fake.rowsAffectedArgsForCall, struct{}{}) + fake.recordInvocation("RowsAffected", []interface{}{}) + fake.rowsAffectedMutex.Unlock() + if fake.RowsAffectedStub != nil { + return fake.RowsAffectedStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.rowsAffectedReturns.result1, fake.rowsAffectedReturns.result2 +} + +func (fake *SqlResult) RowsAffectedCallCount() int { + fake.rowsAffectedMutex.RLock() + defer fake.rowsAffectedMutex.RUnlock() + return len(fake.rowsAffectedArgsForCall) +} + +func (fake *SqlResult) RowsAffectedReturns(result1 int64, result2 error) { + fake.RowsAffectedStub = nil + fake.rowsAffectedReturns = struct { + result1 int64 + result2 error + }{result1, result2} +} + +func (fake *SqlResult) RowsAffectedReturnsOnCall(i int, result1 int64, result2 error) { + fake.RowsAffectedStub = nil + if fake.rowsAffectedReturnsOnCall == nil { + fake.rowsAffectedReturnsOnCall = make(map[int]struct { + result1 int64 + result2 error + }) + } + fake.rowsAffectedReturnsOnCall[i] = struct { + result1 int64 + result2 error + }{result1, result2} +} + +func (fake *SqlResult) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.lastInsertIdMutex.RLock() + defer fake.lastInsertIdMutex.RUnlock() + fake.rowsAffectedMutex.RLock() + defer fake.rowsAffectedMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *SqlResult) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/database/migrate_adapter.go b/src/code.cloudfoundry.org/silk/controller/database/migrate_adapter.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/database/migrate_adapter.go rename to src/code.cloudfoundry.org/silk/controller/database/migrate_adapter.go diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/database/migrator.go b/src/code.cloudfoundry.org/silk/controller/database/migrator.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/database/migrator.go rename to src/code.cloudfoundry.org/silk/controller/database/migrator.go diff --git a/src/code.cloudfoundry.org/silk/controller/database/migrator_test.go b/src/code.cloudfoundry.org/silk/controller/database/migrator_test.go new file mode 100644 index 00000000..23ebc7e0 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/database/migrator_test.go @@ -0,0 +1,56 @@ +package database_test + +import ( + "errors" + "time" + + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/controller/database" + "code.cloudfoundry.org/silk/controller/database/fakes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Migrator", func() { + var ( + logger *lagertest.TestLogger + migrator *database.Migrator + databaseMigrator *fakes.DatabaseMigrator + ) + + Describe("TryMigrations", func() { + BeforeEach(func() { + databaseMigrator = &fakes.DatabaseMigrator{} + logger = lagertest.NewTestLogger("test") + migrator = &database.Migrator{ + DatabaseMigrator: databaseMigrator, + MaxMigrationAttempts: 5, + MigrationAttemptSleepDuration: time.Nanosecond, + Logger: logger, + } + }) + + It("calls migrate and logs the success", func() { + databaseMigrator.MigrateReturns(1, nil) + + err := migrator.TryMigrations() + Expect(err).NotTo(HaveOccurred()) + Expect(logger.Logs()[0].Data["num-applied"]).To(BeEquivalentTo(1)) + Expect(logger.Logs()[0].Message).To(Equal("test.db-migration-complete")) + + Expect(databaseMigrator.MigrateCallCount()).To(Equal(1)) + }) + + Context("when the database cannot be migrated within the max migration attempts", func() { + It("returns an error", func() { + databaseMigrator.MigrateReturns(1, errors.New("peach")) + err := migrator.TryMigrations() + + Expect(err).To(MatchError("creating table: peach")) + Expect(databaseMigrator.MigrateCallCount()).To(Equal(5)) + }) + }) + }) + +}) diff --git a/src/code.cloudfoundry.org/silk/controller/handlers/fakes/database_checker.go b/src/code.cloudfoundry.org/silk/controller/handlers/fakes/database_checker.go new file mode 100644 index 00000000..6b08c1b2 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/handlers/fakes/database_checker.go @@ -0,0 +1,84 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" +) + +type DatabaseChecker struct { + CheckDatabaseStub func() error + checkDatabaseMutex sync.RWMutex + checkDatabaseArgsForCall []struct{} + checkDatabaseReturns struct { + result1 error + } + checkDatabaseReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *DatabaseChecker) CheckDatabase() error { + fake.checkDatabaseMutex.Lock() + ret, specificReturn := fake.checkDatabaseReturnsOnCall[len(fake.checkDatabaseArgsForCall)] + fake.checkDatabaseArgsForCall = append(fake.checkDatabaseArgsForCall, struct{}{}) + fake.recordInvocation("CheckDatabase", []interface{}{}) + fake.checkDatabaseMutex.Unlock() + if fake.CheckDatabaseStub != nil { + return fake.CheckDatabaseStub() + } + if specificReturn { + return ret.result1 + } + return fake.checkDatabaseReturns.result1 +} + +func (fake *DatabaseChecker) CheckDatabaseCallCount() int { + fake.checkDatabaseMutex.RLock() + defer fake.checkDatabaseMutex.RUnlock() + return len(fake.checkDatabaseArgsForCall) +} + +func (fake *DatabaseChecker) CheckDatabaseReturns(result1 error) { + fake.CheckDatabaseStub = nil + fake.checkDatabaseReturns = struct { + result1 error + }{result1} +} + +func (fake *DatabaseChecker) CheckDatabaseReturnsOnCall(i int, result1 error) { + fake.CheckDatabaseStub = nil + if fake.checkDatabaseReturnsOnCall == nil { + fake.checkDatabaseReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.checkDatabaseReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *DatabaseChecker) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.checkDatabaseMutex.RLock() + defer fake.checkDatabaseMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *DatabaseChecker) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/controller/handlers/fakes/error_response.go b/src/code.cloudfoundry.org/silk/controller/handlers/fakes/error_response.go new file mode 100644 index 00000000..f159c741 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/handlers/fakes/error_response.go @@ -0,0 +1,147 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "net/http" + "sync" + + "code.cloudfoundry.org/lager/v3" +) + +type ErrorResponse struct { + InternalServerErrorStub func(lager.Logger, http.ResponseWriter, error, string) + internalServerErrorMutex sync.RWMutex + internalServerErrorArgsForCall []struct { + arg1 lager.Logger + arg2 http.ResponseWriter + arg3 error + arg4 string + } + BadRequestStub func(lager.Logger, http.ResponseWriter, error, string) + badRequestMutex sync.RWMutex + badRequestArgsForCall []struct { + arg1 lager.Logger + arg2 http.ResponseWriter + arg3 error + arg4 string + } + ConflictStub func(lager.Logger, http.ResponseWriter, error, string) + conflictMutex sync.RWMutex + conflictArgsForCall []struct { + arg1 lager.Logger + arg2 http.ResponseWriter + arg3 error + arg4 string + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *ErrorResponse) InternalServerError(arg1 lager.Logger, arg2 http.ResponseWriter, arg3 error, arg4 string) { + fake.internalServerErrorMutex.Lock() + fake.internalServerErrorArgsForCall = append(fake.internalServerErrorArgsForCall, struct { + arg1 lager.Logger + arg2 http.ResponseWriter + arg3 error + arg4 string + }{arg1, arg2, arg3, arg4}) + fake.recordInvocation("InternalServerError", []interface{}{arg1, arg2, arg3, arg4}) + fake.internalServerErrorMutex.Unlock() + if fake.InternalServerErrorStub != nil { + fake.InternalServerErrorStub(arg1, arg2, arg3, arg4) + } +} + +func (fake *ErrorResponse) InternalServerErrorCallCount() int { + fake.internalServerErrorMutex.RLock() + defer fake.internalServerErrorMutex.RUnlock() + return len(fake.internalServerErrorArgsForCall) +} + +func (fake *ErrorResponse) InternalServerErrorArgsForCall(i int) (lager.Logger, http.ResponseWriter, error, string) { + fake.internalServerErrorMutex.RLock() + defer fake.internalServerErrorMutex.RUnlock() + return fake.internalServerErrorArgsForCall[i].arg1, fake.internalServerErrorArgsForCall[i].arg2, fake.internalServerErrorArgsForCall[i].arg3, fake.internalServerErrorArgsForCall[i].arg4 +} + +func (fake *ErrorResponse) BadRequest(arg1 lager.Logger, arg2 http.ResponseWriter, arg3 error, arg4 string) { + fake.badRequestMutex.Lock() + fake.badRequestArgsForCall = append(fake.badRequestArgsForCall, struct { + arg1 lager.Logger + arg2 http.ResponseWriter + arg3 error + arg4 string + }{arg1, arg2, arg3, arg4}) + fake.recordInvocation("BadRequest", []interface{}{arg1, arg2, arg3, arg4}) + fake.badRequestMutex.Unlock() + if fake.BadRequestStub != nil { + fake.BadRequestStub(arg1, arg2, arg3, arg4) + } +} + +func (fake *ErrorResponse) BadRequestCallCount() int { + fake.badRequestMutex.RLock() + defer fake.badRequestMutex.RUnlock() + return len(fake.badRequestArgsForCall) +} + +func (fake *ErrorResponse) BadRequestArgsForCall(i int) (lager.Logger, http.ResponseWriter, error, string) { + fake.badRequestMutex.RLock() + defer fake.badRequestMutex.RUnlock() + return fake.badRequestArgsForCall[i].arg1, fake.badRequestArgsForCall[i].arg2, fake.badRequestArgsForCall[i].arg3, fake.badRequestArgsForCall[i].arg4 +} + +func (fake *ErrorResponse) Conflict(arg1 lager.Logger, arg2 http.ResponseWriter, arg3 error, arg4 string) { + fake.conflictMutex.Lock() + fake.conflictArgsForCall = append(fake.conflictArgsForCall, struct { + arg1 lager.Logger + arg2 http.ResponseWriter + arg3 error + arg4 string + }{arg1, arg2, arg3, arg4}) + fake.recordInvocation("Conflict", []interface{}{arg1, arg2, arg3, arg4}) + fake.conflictMutex.Unlock() + if fake.ConflictStub != nil { + fake.ConflictStub(arg1, arg2, arg3, arg4) + } +} + +func (fake *ErrorResponse) ConflictCallCount() int { + fake.conflictMutex.RLock() + defer fake.conflictMutex.RUnlock() + return len(fake.conflictArgsForCall) +} + +func (fake *ErrorResponse) ConflictArgsForCall(i int) (lager.Logger, http.ResponseWriter, error, string) { + fake.conflictMutex.RLock() + defer fake.conflictMutex.RUnlock() + return fake.conflictArgsForCall[i].arg1, fake.conflictArgsForCall[i].arg2, fake.conflictArgsForCall[i].arg3, fake.conflictArgsForCall[i].arg4 +} + +func (fake *ErrorResponse) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.internalServerErrorMutex.RLock() + defer fake.internalServerErrorMutex.RUnlock() + fake.badRequestMutex.RLock() + defer fake.badRequestMutex.RUnlock() + fake.conflictMutex.RLock() + defer fake.conflictMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *ErrorResponse) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/controller/handlers/fakes/hardwareAddressGenerator.go b/src/code.cloudfoundry.org/silk/controller/handlers/fakes/hardwareAddressGenerator.go new file mode 100644 index 00000000..acf07041 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/handlers/fakes/hardwareAddressGenerator.go @@ -0,0 +1,96 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "net" + "sync" +) + +type HardwareAddressGenerator struct { + GenerateForVTEPStub func(containerIP net.IP) (net.HardwareAddr, error) + generateForVTEPMutex sync.RWMutex + generateForVTEPArgsForCall []struct { + containerIP net.IP + } + generateForVTEPReturns struct { + result1 net.HardwareAddr + result2 error + } + generateForVTEPReturnsOnCall map[int]struct { + result1 net.HardwareAddr + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *HardwareAddressGenerator) GenerateForVTEP(containerIP net.IP) (net.HardwareAddr, error) { + fake.generateForVTEPMutex.Lock() + ret, specificReturn := fake.generateForVTEPReturnsOnCall[len(fake.generateForVTEPArgsForCall)] + fake.generateForVTEPArgsForCall = append(fake.generateForVTEPArgsForCall, struct { + containerIP net.IP + }{containerIP}) + fake.recordInvocation("GenerateForVTEP", []interface{}{containerIP}) + fake.generateForVTEPMutex.Unlock() + if fake.GenerateForVTEPStub != nil { + return fake.GenerateForVTEPStub(containerIP) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.generateForVTEPReturns.result1, fake.generateForVTEPReturns.result2 +} + +func (fake *HardwareAddressGenerator) GenerateForVTEPCallCount() int { + fake.generateForVTEPMutex.RLock() + defer fake.generateForVTEPMutex.RUnlock() + return len(fake.generateForVTEPArgsForCall) +} + +func (fake *HardwareAddressGenerator) GenerateForVTEPArgsForCall(i int) net.IP { + fake.generateForVTEPMutex.RLock() + defer fake.generateForVTEPMutex.RUnlock() + return fake.generateForVTEPArgsForCall[i].containerIP +} + +func (fake *HardwareAddressGenerator) GenerateForVTEPReturns(result1 net.HardwareAddr, result2 error) { + fake.GenerateForVTEPStub = nil + fake.generateForVTEPReturns = struct { + result1 net.HardwareAddr + result2 error + }{result1, result2} +} + +func (fake *HardwareAddressGenerator) GenerateForVTEPReturnsOnCall(i int, result1 net.HardwareAddr, result2 error) { + fake.GenerateForVTEPStub = nil + if fake.generateForVTEPReturnsOnCall == nil { + fake.generateForVTEPReturnsOnCall = make(map[int]struct { + result1 net.HardwareAddr + result2 error + }) + } + fake.generateForVTEPReturnsOnCall[i] = struct { + result1 net.HardwareAddr + result2 error + }{result1, result2} +} + +func (fake *HardwareAddressGenerator) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.generateForVTEPMutex.RLock() + defer fake.generateForVTEPMutex.RUnlock() + return fake.invocations +} + +func (fake *HardwareAddressGenerator) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/controller/handlers/fakes/lease_acquirer.go b/src/code.cloudfoundry.org/silk/controller/handlers/fakes/lease_acquirer.go new file mode 100644 index 00000000..582740e7 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/handlers/fakes/lease_acquirer.go @@ -0,0 +1,103 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + + "code.cloudfoundry.org/silk/controller" +) + +type LeaseAcquirer struct { + AcquireSubnetLeaseStub func(underlayIP string, singleOverlayIP bool) (*controller.Lease, error) + acquireSubnetLeaseMutex sync.RWMutex + acquireSubnetLeaseArgsForCall []struct { + underlayIP string + singleOverlayIP bool + } + acquireSubnetLeaseReturns struct { + result1 *controller.Lease + result2 error + } + acquireSubnetLeaseReturnsOnCall map[int]struct { + result1 *controller.Lease + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *LeaseAcquirer) AcquireSubnetLease(underlayIP string, singleOverlayIP bool) (*controller.Lease, error) { + fake.acquireSubnetLeaseMutex.Lock() + ret, specificReturn := fake.acquireSubnetLeaseReturnsOnCall[len(fake.acquireSubnetLeaseArgsForCall)] + fake.acquireSubnetLeaseArgsForCall = append(fake.acquireSubnetLeaseArgsForCall, struct { + underlayIP string + singleOverlayIP bool + }{underlayIP, singleOverlayIP}) + fake.recordInvocation("AcquireSubnetLease", []interface{}{underlayIP, singleOverlayIP}) + fake.acquireSubnetLeaseMutex.Unlock() + if fake.AcquireSubnetLeaseStub != nil { + return fake.AcquireSubnetLeaseStub(underlayIP, singleOverlayIP) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.acquireSubnetLeaseReturns.result1, fake.acquireSubnetLeaseReturns.result2 +} + +func (fake *LeaseAcquirer) AcquireSubnetLeaseCallCount() int { + fake.acquireSubnetLeaseMutex.RLock() + defer fake.acquireSubnetLeaseMutex.RUnlock() + return len(fake.acquireSubnetLeaseArgsForCall) +} + +func (fake *LeaseAcquirer) AcquireSubnetLeaseArgsForCall(i int) (string, bool) { + fake.acquireSubnetLeaseMutex.RLock() + defer fake.acquireSubnetLeaseMutex.RUnlock() + return fake.acquireSubnetLeaseArgsForCall[i].underlayIP, fake.acquireSubnetLeaseArgsForCall[i].singleOverlayIP +} + +func (fake *LeaseAcquirer) AcquireSubnetLeaseReturns(result1 *controller.Lease, result2 error) { + fake.AcquireSubnetLeaseStub = nil + fake.acquireSubnetLeaseReturns = struct { + result1 *controller.Lease + result2 error + }{result1, result2} +} + +func (fake *LeaseAcquirer) AcquireSubnetLeaseReturnsOnCall(i int, result1 *controller.Lease, result2 error) { + fake.AcquireSubnetLeaseStub = nil + if fake.acquireSubnetLeaseReturnsOnCall == nil { + fake.acquireSubnetLeaseReturnsOnCall = make(map[int]struct { + result1 *controller.Lease + result2 error + }) + } + fake.acquireSubnetLeaseReturnsOnCall[i] = struct { + result1 *controller.Lease + result2 error + }{result1, result2} +} + +func (fake *LeaseAcquirer) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.acquireSubnetLeaseMutex.RLock() + defer fake.acquireSubnetLeaseMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *LeaseAcquirer) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/controller/handlers/fakes/lease_releaser.go b/src/code.cloudfoundry.org/silk/controller/handlers/fakes/lease_releaser.go new file mode 100644 index 00000000..3552367c --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/handlers/fakes/lease_releaser.go @@ -0,0 +1,94 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" +) + +type LeaseReleaser struct { + ReleaseSubnetLeaseStub func(underlayIP string) error + releaseSubnetLeaseMutex sync.RWMutex + releaseSubnetLeaseArgsForCall []struct { + underlayIP string + } + releaseSubnetLeaseReturns struct { + result1 error + } + releaseSubnetLeaseReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *LeaseReleaser) ReleaseSubnetLease(underlayIP string) error { + fake.releaseSubnetLeaseMutex.Lock() + ret, specificReturn := fake.releaseSubnetLeaseReturnsOnCall[len(fake.releaseSubnetLeaseArgsForCall)] + fake.releaseSubnetLeaseArgsForCall = append(fake.releaseSubnetLeaseArgsForCall, struct { + underlayIP string + }{underlayIP}) + fake.recordInvocation("ReleaseSubnetLease", []interface{}{underlayIP}) + fake.releaseSubnetLeaseMutex.Unlock() + if fake.ReleaseSubnetLeaseStub != nil { + return fake.ReleaseSubnetLeaseStub(underlayIP) + } + if specificReturn { + return ret.result1 + } + return fake.releaseSubnetLeaseReturns.result1 +} + +func (fake *LeaseReleaser) ReleaseSubnetLeaseCallCount() int { + fake.releaseSubnetLeaseMutex.RLock() + defer fake.releaseSubnetLeaseMutex.RUnlock() + return len(fake.releaseSubnetLeaseArgsForCall) +} + +func (fake *LeaseReleaser) ReleaseSubnetLeaseArgsForCall(i int) string { + fake.releaseSubnetLeaseMutex.RLock() + defer fake.releaseSubnetLeaseMutex.RUnlock() + return fake.releaseSubnetLeaseArgsForCall[i].underlayIP +} + +func (fake *LeaseReleaser) ReleaseSubnetLeaseReturns(result1 error) { + fake.ReleaseSubnetLeaseStub = nil + fake.releaseSubnetLeaseReturns = struct { + result1 error + }{result1} +} + +func (fake *LeaseReleaser) ReleaseSubnetLeaseReturnsOnCall(i int, result1 error) { + fake.ReleaseSubnetLeaseStub = nil + if fake.releaseSubnetLeaseReturnsOnCall == nil { + fake.releaseSubnetLeaseReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.releaseSubnetLeaseReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *LeaseReleaser) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.releaseSubnetLeaseMutex.RLock() + defer fake.releaseSubnetLeaseMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *LeaseReleaser) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/controller/handlers/fakes/lease_renewer.go b/src/code.cloudfoundry.org/silk/controller/handlers/fakes/lease_renewer.go new file mode 100644 index 00000000..62a20202 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/handlers/fakes/lease_renewer.go @@ -0,0 +1,96 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + + "code.cloudfoundry.org/silk/controller" +) + +type LeaseRenewer struct { + RenewSubnetLeaseStub func(lease controller.Lease) error + renewSubnetLeaseMutex sync.RWMutex + renewSubnetLeaseArgsForCall []struct { + lease controller.Lease + } + renewSubnetLeaseReturns struct { + result1 error + } + renewSubnetLeaseReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *LeaseRenewer) RenewSubnetLease(lease controller.Lease) error { + fake.renewSubnetLeaseMutex.Lock() + ret, specificReturn := fake.renewSubnetLeaseReturnsOnCall[len(fake.renewSubnetLeaseArgsForCall)] + fake.renewSubnetLeaseArgsForCall = append(fake.renewSubnetLeaseArgsForCall, struct { + lease controller.Lease + }{lease}) + fake.recordInvocation("RenewSubnetLease", []interface{}{lease}) + fake.renewSubnetLeaseMutex.Unlock() + if fake.RenewSubnetLeaseStub != nil { + return fake.RenewSubnetLeaseStub(lease) + } + if specificReturn { + return ret.result1 + } + return fake.renewSubnetLeaseReturns.result1 +} + +func (fake *LeaseRenewer) RenewSubnetLeaseCallCount() int { + fake.renewSubnetLeaseMutex.RLock() + defer fake.renewSubnetLeaseMutex.RUnlock() + return len(fake.renewSubnetLeaseArgsForCall) +} + +func (fake *LeaseRenewer) RenewSubnetLeaseArgsForCall(i int) controller.Lease { + fake.renewSubnetLeaseMutex.RLock() + defer fake.renewSubnetLeaseMutex.RUnlock() + return fake.renewSubnetLeaseArgsForCall[i].lease +} + +func (fake *LeaseRenewer) RenewSubnetLeaseReturns(result1 error) { + fake.RenewSubnetLeaseStub = nil + fake.renewSubnetLeaseReturns = struct { + result1 error + }{result1} +} + +func (fake *LeaseRenewer) RenewSubnetLeaseReturnsOnCall(i int, result1 error) { + fake.RenewSubnetLeaseStub = nil + if fake.renewSubnetLeaseReturnsOnCall == nil { + fake.renewSubnetLeaseReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.renewSubnetLeaseReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *LeaseRenewer) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.renewSubnetLeaseMutex.RLock() + defer fake.renewSubnetLeaseMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *LeaseRenewer) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/controller/handlers/fakes/lease_repository.go b/src/code.cloudfoundry.org/silk/controller/handlers/fakes/lease_repository.go new file mode 100644 index 00000000..a2249e46 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/handlers/fakes/lease_repository.go @@ -0,0 +1,91 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + + "code.cloudfoundry.org/silk/controller" +) + +type LeaseRepository struct { + RoutableLeasesStub func() ([]controller.Lease, error) + routableLeasesMutex sync.RWMutex + routableLeasesArgsForCall []struct{} + routableLeasesReturns struct { + result1 []controller.Lease + result2 error + } + routableLeasesReturnsOnCall map[int]struct { + result1 []controller.Lease + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *LeaseRepository) RoutableLeases() ([]controller.Lease, error) { + fake.routableLeasesMutex.Lock() + ret, specificReturn := fake.routableLeasesReturnsOnCall[len(fake.routableLeasesArgsForCall)] + fake.routableLeasesArgsForCall = append(fake.routableLeasesArgsForCall, struct{}{}) + fake.recordInvocation("RoutableLeases", []interface{}{}) + fake.routableLeasesMutex.Unlock() + if fake.RoutableLeasesStub != nil { + return fake.RoutableLeasesStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.routableLeasesReturns.result1, fake.routableLeasesReturns.result2 +} + +func (fake *LeaseRepository) RoutableLeasesCallCount() int { + fake.routableLeasesMutex.RLock() + defer fake.routableLeasesMutex.RUnlock() + return len(fake.routableLeasesArgsForCall) +} + +func (fake *LeaseRepository) RoutableLeasesReturns(result1 []controller.Lease, result2 error) { + fake.RoutableLeasesStub = nil + fake.routableLeasesReturns = struct { + result1 []controller.Lease + result2 error + }{result1, result2} +} + +func (fake *LeaseRepository) RoutableLeasesReturnsOnCall(i int, result1 []controller.Lease, result2 error) { + fake.RoutableLeasesStub = nil + if fake.routableLeasesReturnsOnCall == nil { + fake.routableLeasesReturnsOnCall = make(map[int]struct { + result1 []controller.Lease + result2 error + }) + } + fake.routableLeasesReturnsOnCall[i] = struct { + result1 []controller.Lease + result2 error + }{result1, result2} +} + +func (fake *LeaseRepository) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.routableLeasesMutex.RLock() + defer fake.routableLeasesMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *LeaseRepository) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/controller/handlers/handlers_suite_test.go b/src/code.cloudfoundry.org/silk/controller/handlers/handlers_suite_test.go new file mode 100644 index 00000000..ac6ea8af --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/handlers/handlers_suite_test.go @@ -0,0 +1,32 @@ +package handlers_test + +import ( + "code.cloudfoundry.org/lager/v3" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" + + "testing" +) + +func TestHandlers(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Handlers Suite") +} + +func LogsWith(level lager.LogLevel, msg string) types.GomegaMatcher { + return And( + WithTransform(func(log lager.LogFormat) string { + return log.Message + }, Equal(msg)), + WithTransform(func(log lager.LogFormat) lager.LogLevel { + return log.LogLevel + }, Equal(level)), + ) +} + +func HaveLogData(nextMatcher types.GomegaMatcher) types.GomegaMatcher { + return WithTransform(func(log lager.LogFormat) lager.Data { + return log.Data + }, nextMatcher) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/handlers/health.go b/src/code.cloudfoundry.org/silk/controller/handlers/health.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/handlers/health.go rename to src/code.cloudfoundry.org/silk/controller/handlers/health.go diff --git a/src/code.cloudfoundry.org/silk/controller/handlers/health_test.go b/src/code.cloudfoundry.org/silk/controller/handlers/health_test.go new file mode 100644 index 00000000..c25bf9fa --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/handlers/health_test.go @@ -0,0 +1,73 @@ +package handlers_test + +import ( + "errors" + "net/http" + "net/http/httptest" + + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/controller/handlers" + "code.cloudfoundry.org/silk/controller/handlers/fakes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Health handler", func() { + var ( + expectedLogger lager.Logger + logger *lagertest.TestLogger + handler *handlers.Health + request *http.Request + fakeDatabaseChecker *fakes.DatabaseChecker + fakeErrorResponse *fakes.ErrorResponse + resp *httptest.ResponseRecorder + ) + + BeforeEach(func() { + expectedLogger = lager.NewLogger("test").Session("health") + + testSink := lagertest.NewTestSink() + expectedLogger.RegisterSink(testSink) + expectedLogger.RegisterSink(lager.NewWriterSink(GinkgoWriter, lager.DEBUG)) + logger = lagertest.NewTestLogger("test") + + var err error + request, err = http.NewRequest("GET", "/health", nil) + Expect(err).NotTo(HaveOccurred()) + + fakeDatabaseChecker = &fakes.DatabaseChecker{} + fakeErrorResponse = &fakes.ErrorResponse{} + + handler = &handlers.Health{ + DatabaseChecker: fakeDatabaseChecker, + ErrorResponse: fakeErrorResponse, + } + resp = httptest.NewRecorder() + }) + + It("checks the database is up and returns a 200", func() { + handler.ServeHTTP(logger, resp, request) + Expect(fakeDatabaseChecker.CheckDatabaseCallCount()).To(Equal(1)) + Expect(resp.Code).To(Equal(http.StatusOK)) + }) + + Context("when the database returns an error", func() { + BeforeEach(func() { + fakeDatabaseChecker.CheckDatabaseReturns(errors.New("pineapple")) + }) + + It("calls the internal server error handler", func() { + handler.ServeHTTP(logger, resp, request) + Expect(fakeDatabaseChecker.CheckDatabaseCallCount()).To(Equal(1)) + Expect(fakeErrorResponse.InternalServerErrorCallCount()).To(Equal(1)) + + l, w, err, description := fakeErrorResponse.InternalServerErrorArgsForCall(0) + Expect(l).To(Equal(expectedLogger)) + Expect(w).To(Equal(resp)) + Expect(err).To(MatchError("pineapple")) + Expect(description).To(Equal("check database failed")) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/handlers/lease_acquire.go b/src/code.cloudfoundry.org/silk/controller/handlers/lease_acquire.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/handlers/lease_acquire.go rename to src/code.cloudfoundry.org/silk/controller/handlers/lease_acquire.go diff --git a/src/code.cloudfoundry.org/silk/controller/handlers/lease_acquire_test.go b/src/code.cloudfoundry.org/silk/controller/handlers/lease_acquire_test.go new file mode 100644 index 00000000..0f5d6bb0 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/handlers/lease_acquire_test.go @@ -0,0 +1,214 @@ +package handlers_test + +import ( + "bytes" + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "net/http/httptest" + + hfakes "code.cloudfoundry.org/cf-networking-helpers/fakes" + "code.cloudfoundry.org/cf-networking-helpers/testsupport" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/controller" + "code.cloudfoundry.org/silk/controller/handlers" + "code.cloudfoundry.org/silk/controller/handlers/fakes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("LeasesAcquire", func() { + var ( + logger *lagertest.TestLogger + expectedLogger lager.Logger + handler *handlers.LeasesAcquire + resp *httptest.ResponseRecorder + marshaler *hfakes.Marshaler + unmarshaler *hfakes.Unmarshaler + leaseAcquirer *fakes.LeaseAcquirer + fakeErrorResponse *fakes.ErrorResponse + ) + + BeforeEach(func() { + expectedLogger = lager.NewLogger("test").Session("leases-acquire") + + testSink := lagertest.NewTestSink() + expectedLogger.RegisterSink(testSink) + expectedLogger.RegisterSink(lager.NewWriterSink(GinkgoWriter, lager.DEBUG)) + + logger = lagertest.NewTestLogger("test") + marshaler = &hfakes.Marshaler{} + marshaler.MarshalStub = json.Marshal + unmarshaler = &hfakes.Unmarshaler{} + unmarshaler.UnmarshalStub = json.Unmarshal + leaseAcquirer = &fakes.LeaseAcquirer{} + fakeErrorResponse = &fakes.ErrorResponse{} + + handler = &handlers.LeasesAcquire{ + Marshaler: marshaler, + Unmarshaler: unmarshaler, + LeaseAcquirer: leaseAcquirer, + ErrorResponse: fakeErrorResponse, + } + resp = httptest.NewRecorder() + + lease := &controller.Lease{ + UnderlayIP: "10.244.16.11", + OverlaySubnet: "10.255.17.0/24", + OverlayHardwareAddr: "ee:ee:0a:ff:11:00", + } + leaseAcquirer.AcquireSubnetLeaseReturns(lease, nil) + }) + + It("acquires a lease for subnet", func() { + expectedResponseJSON := `{ "underlay_ip": "10.244.16.11", "overlay_subnet": "10.255.17.0/24", "overlay_hardware_addr": "ee:ee:0a:ff:11:00" }` + requestBody := bytes.NewBuffer([]byte(`{ "underlay_ip": "10.244.16.11" }`)) + request, err := http.NewRequest("PUT", "/leases/acquire", requestBody) + Expect(err).NotTo(HaveOccurred()) + + request.RemoteAddr = "some-host:some-port" + + handler.ServeHTTP(logger, resp, request) + Expect(leaseAcquirer.AcquireSubnetLeaseCallCount()).To(Equal(1)) + underlayIP, singleOverlayIP := leaseAcquirer.AcquireSubnetLeaseArgsForCall(0) + Expect(underlayIP).To(Equal("10.244.16.11")) + Expect(singleOverlayIP).To(Equal(false)) + + Expect(resp.Code).To(Equal(http.StatusOK)) + Expect(resp.Body).To(MatchJSON(expectedResponseJSON)) + }) + + It("acquires a lease for a single overlay IP", func() { + lease := &controller.Lease{ + UnderlayIP: "10.244.0.12", + OverlaySubnet: "10.255.0.17/32", + OverlayHardwareAddr: "ee:ee:0a:fb:00:11", + } + leaseAcquirer.AcquireSubnetLeaseReturns(lease, nil) + + expectedResponseJSON := `{ "underlay_ip": "10.244.0.12", "overlay_subnet": "10.255.0.17/32", "overlay_hardware_addr": "ee:ee:0a:fb:00:11" }` + requestBody := bytes.NewBuffer([]byte(`{ "underlay_ip": "10.244.0.12", "single_overlay_ip": true }`)) + request, err := http.NewRequest("PUT", "/leases/acquire", requestBody) + Expect(err).NotTo(HaveOccurred()) + + request.RemoteAddr = "remote-host:remote-port" + + handler.ServeHTTP(logger, resp, request) + Expect(leaseAcquirer.AcquireSubnetLeaseCallCount()).To(Equal(1)) + underlayIP, singleOverlayIP := leaseAcquirer.AcquireSubnetLeaseArgsForCall(0) + Expect(underlayIP).To(Equal("10.244.0.12")) + Expect(singleOverlayIP).To(Equal(true)) + + Expect(resp.Code).To(Equal(http.StatusOK)) + Expect(resp.Body).To(MatchJSON(expectedResponseJSON)) + }) + + Context("when there are errors reading the body bytes", func() { + var request *http.Request + BeforeEach(func() { + var err error + request, err = http.NewRequest("PUT", "/leases/acquire", ioutil.NopCloser(&testsupport.BadReader{})) + Expect(err).NotTo(HaveOccurred()) + }) + + It("calls the BadRequest error handler", func() { + handler.ServeHTTP(logger, resp, request) + + Expect(fakeErrorResponse.BadRequestCallCount()).To(Equal(1)) + l, w, err, description := fakeErrorResponse.BadRequestArgsForCall(0) + Expect(l).To(Equal(expectedLogger)) + Expect(w).To(Equal(resp)) + Expect(err).To(MatchError("banana")) + Expect(description).To(Equal("read-body: banana")) + }) + }) + + Context("when the request cannot be unmarshaled", func() { + BeforeEach(func() { + unmarshaler.UnmarshalReturns(errors.New("fig")) + }) + + It("logs the error and returns a 400", func() { + requestBody := bytes.NewBuffer([]byte(`{ "underlay_ip": "10.244.16.11" }`)) + request, err := http.NewRequest("PUT", "/leases/acquire", requestBody) + Expect(err).NotTo(HaveOccurred()) + + handler.ServeHTTP(logger, resp, request) + + Expect(fakeErrorResponse.BadRequestCallCount()).To(Equal(1)) + l, w, err, description := fakeErrorResponse.BadRequestArgsForCall(0) + Expect(l).To(Equal(expectedLogger)) + Expect(w).To(Equal(resp)) + Expect(err).To(MatchError("fig")) + Expect(description).To(Equal("unmarshal-request: fig")) + }) + }) + + Context("when acquiring a lease fails", func() { + BeforeEach(func() { + leaseAcquirer.AcquireSubnetLeaseReturns(nil, errors.New("kiwi")) + }) + + It("logs the error and returns a 500", func() { + requestBody := bytes.NewBuffer([]byte(`{ "underlay_ip": "10.244.16.11" }`)) + request, err := http.NewRequest("PUT", "/leases/acquire", requestBody) + Expect(err).NotTo(HaveOccurred()) + + handler.ServeHTTP(logger, resp, request) + + Expect(fakeErrorResponse.InternalServerErrorCallCount()).To(Equal(1)) + l, w, err, description := fakeErrorResponse.InternalServerErrorArgsForCall(0) + Expect(l).To(Equal(expectedLogger)) + Expect(w).To(Equal(resp)) + Expect(err).To(MatchError("kiwi")) + Expect(description).To(Equal("kiwi")) + }) + }) + + Context("when no leases are available", func() { + BeforeEach(func() { + leaseAcquirer.AcquireSubnetLeaseReturns(nil, nil) + }) + + It("logs the error and returns a 503", func() { + requestBody := bytes.NewBuffer([]byte(`{ "underlay_ip": "10.244.16.11" }`)) + request, err := http.NewRequest("PUT", "/leases/acquire", requestBody) + Expect(err).NotTo(HaveOccurred()) + + handler.ServeHTTP(logger, resp, request) + + Expect(fakeErrorResponse.ConflictCallCount()).To(Equal(1)) + l, w, err, description := fakeErrorResponse.ConflictArgsForCall(0) + Expect(l).To(Equal(expectedLogger)) + Expect(w).To(Equal(resp)) + Expect(err).To(MatchError("no lease available")) + Expect(description).To(Equal("no lease available")) + }) + }) + + Context("when the response cannot be marshaled", func() { + BeforeEach(func() { + marshaler.MarshalStub = func(interface{}) ([]byte, error) { + return nil, errors.New("grapes") + } + }) + + It("logs the error and returns a 500", func() { + requestBody := bytes.NewBuffer([]byte(`{ "underlay_ip": "10.244.16.11" }`)) + request, err := http.NewRequest("PUT", "/leases/acquire", requestBody) + Expect(err).NotTo(HaveOccurred()) + + handler.ServeHTTP(logger, resp, request) + + Expect(fakeErrorResponse.InternalServerErrorCallCount()).To(Equal(1)) + l, w, err, description := fakeErrorResponse.InternalServerErrorArgsForCall(0) + Expect(l).To(Equal(expectedLogger)) + Expect(w).To(Equal(resp)) + Expect(err).To(MatchError("grapes")) + Expect(description).To(Equal("marshal-response: grapes")) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/handlers/leases_index.go b/src/code.cloudfoundry.org/silk/controller/handlers/leases_index.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/handlers/leases_index.go rename to src/code.cloudfoundry.org/silk/controller/handlers/leases_index.go diff --git a/src/code.cloudfoundry.org/silk/controller/handlers/leases_index_test.go b/src/code.cloudfoundry.org/silk/controller/handlers/leases_index_test.go new file mode 100644 index 00000000..a460e9a3 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/handlers/leases_index_test.go @@ -0,0 +1,120 @@ +package handlers_test + +import ( + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + + hfakes "code.cloudfoundry.org/cf-networking-helpers/fakes" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/controller" + "code.cloudfoundry.org/silk/controller/handlers" + "code.cloudfoundry.org/silk/controller/handlers/fakes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("LeasesIndex", func() { + var ( + logger *lagertest.TestLogger + expectedLogger lager.Logger + handler *handlers.LeasesIndex + leaseRepository *fakes.LeaseRepository + resp *httptest.ResponseRecorder + marshaler *hfakes.Marshaler + fakeErrorResponse *fakes.ErrorResponse + ) + + BeforeEach(func() { + expectedLogger = lager.NewLogger("test").Session("leases-index") + + testSink := lagertest.NewTestSink() + expectedLogger.RegisterSink(testSink) + expectedLogger.RegisterSink(lager.NewWriterSink(GinkgoWriter, lager.DEBUG)) + + logger = lagertest.NewTestLogger("test") + marshaler = &hfakes.Marshaler{} + marshaler.MarshalStub = json.Marshal + leaseRepository = &fakes.LeaseRepository{} + fakeErrorResponse = &fakes.ErrorResponse{} + handler = &handlers.LeasesIndex{ + Marshaler: marshaler, + LeaseRepository: leaseRepository, + ErrorResponse: fakeErrorResponse, + } + resp = httptest.NewRecorder() + leaseRepository.RoutableLeasesReturns([]controller.Lease{ + { + UnderlayIP: "10.244.5.9", + OverlaySubnet: "10.255.16.0/24", + OverlayHardwareAddr: "ee:ee:0a:ff:10:00", + }, + { + UnderlayIP: "10.244.22.33", + OverlaySubnet: "10.255.75.0/32", + OverlayHardwareAddr: "ee:ee:0a:ff:4b:00", + }, + }, nil) + }) + + It("returns the routable leases", func() { + expectedResponseJSON := `{ "leases": [ + { "underlay_ip": "10.244.5.9", "overlay_subnet": "10.255.16.0/24", "overlay_hardware_addr": "ee:ee:0a:ff:10:00" }, + { "underlay_ip": "10.244.22.33", "overlay_subnet": "10.255.75.0/32", "overlay_hardware_addr": "ee:ee:0a:ff:4b:00" } + ] }` + request, err := http.NewRequest("GET", "/leases", nil) + Expect(err).NotTo(HaveOccurred()) + + request.RemoteAddr = "some-host:some-port" + + handler.ServeHTTP(logger, resp, request) + Expect(leaseRepository.RoutableLeasesCallCount()).To(Equal(1)) + Expect(resp.Code).To(Equal(http.StatusOK)) + Expect(resp.Body).To(MatchJSON(expectedResponseJSON)) + }) + + Context("when getting the routable leases fails", func() { + BeforeEach(func() { + leaseRepository.RoutableLeasesReturns(nil, errors.New("butter")) + }) + + It("calls the internal server error handler", func() { + request, err := http.NewRequest("GET", "/leases", nil) + Expect(err).NotTo(HaveOccurred()) + + handler.ServeHTTP(logger, resp, request) + + Expect(fakeErrorResponse.InternalServerErrorCallCount()).To(Equal(1)) + l, w, err, description := fakeErrorResponse.InternalServerErrorArgsForCall(0) + Expect(l).To(Equal(expectedLogger)) + Expect(w).To(Equal(resp)) + Expect(err).To(MatchError("butter")) + Expect(description).To(Equal("all-routable-leases: butter")) + }) + }) + + Context("when the response cannot be marshaled", func() { + BeforeEach(func() { + marshaler.MarshalStub = func(interface{}) ([]byte, error) { + return nil, errors.New("grapes") + } + }) + + It("calls the internal server error handler", func() { + request, err := http.NewRequest("GET", "/leases", nil) + Expect(err).NotTo(HaveOccurred()) + + handler.ServeHTTP(logger, resp, request) + + Expect(fakeErrorResponse.InternalServerErrorCallCount()).To(Equal(1)) + l, w, err, description := fakeErrorResponse.InternalServerErrorArgsForCall(0) + Expect(l).To(Equal(expectedLogger)) + Expect(w).To(Equal(resp)) + Expect(err).To(MatchError("grapes")) + Expect(description).To(Equal("marshal-response: grapes")) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/handlers/loggable_handler_wrapper.go b/src/code.cloudfoundry.org/silk/controller/handlers/loggable_handler_wrapper.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/handlers/loggable_handler_wrapper.go rename to src/code.cloudfoundry.org/silk/controller/handlers/loggable_handler_wrapper.go diff --git a/src/code.cloudfoundry.org/silk/controller/handlers/loggable_handler_wrapper_test.go b/src/code.cloudfoundry.org/silk/controller/handlers/loggable_handler_wrapper_test.go new file mode 100644 index 00000000..00cf945e --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/handlers/loggable_handler_wrapper_test.go @@ -0,0 +1,57 @@ +package handlers_test + +import ( + "net/http" + + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/controller/handlers" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("LogWrap", func() { + var ( + logger *lagertest.TestLogger + loggableHandlerFunc handlers.LoggableHandlerFunc + ) + BeforeEach(func() { + logger = lagertest.NewTestLogger("test-session") + logger.RegisterSink(lager.NewWriterSink(GinkgoWriter, lager.DEBUG)) + loggableHandlerFunc = func(logger lager.Logger, w http.ResponseWriter, r *http.Request) { + logger = logger.Session("logger-group") + logger.Info("written-in-loggable-handler") + } + }) + It("creates \"request\" session and passes it to LoggableHandlerFunc", func() { + handler := handlers.LogWrap(logger, loggableHandlerFunc) + req, err := http.NewRequest("GET", "http://example.com", nil) + Expect(err).NotTo(HaveOccurred()) + handler.ServeHTTP(nil, req) + + Expect(logger.Logs()).To(HaveLen(3)) + Expect(logger.Logs()[0]).To(SatisfyAll( + LogsWith(lager.DEBUG, "test-session.request.serving"), + HaveLogData(SatisfyAll( + HaveKeyWithValue("session", Equal("1")), + HaveKeyWithValue("method", Equal("GET")), + HaveKeyWithValue("request", Equal("http://example.com")), + )), + )) + + Expect(logger.Logs()[1]).To(SatisfyAll( + LogsWith(lager.INFO, "test-session.request.logger-group.written-in-loggable-handler"), + HaveLogData(HaveKeyWithValue("session", Equal("1.1"))), + )) + + Expect(logger.Logs()[2]).To(SatisfyAll( + LogsWith(lager.DEBUG, "test-session.request.done"), + HaveLogData(SatisfyAll( + HaveKeyWithValue("session", Equal("1")), + HaveKeyWithValue("method", Equal("GET")), + HaveKeyWithValue("request", Equal("http://example.com")), + )), + )) + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/handlers/release_lease.go b/src/code.cloudfoundry.org/silk/controller/handlers/release_lease.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/handlers/release_lease.go rename to src/code.cloudfoundry.org/silk/controller/handlers/release_lease.go diff --git a/src/code.cloudfoundry.org/silk/controller/handlers/release_lease_test.go b/src/code.cloudfoundry.org/silk/controller/handlers/release_lease_test.go new file mode 100644 index 00000000..0ff63818 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/handlers/release_lease_test.go @@ -0,0 +1,124 @@ +package handlers_test + +import ( + "bytes" + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "net/http/httptest" + + hfakes "code.cloudfoundry.org/cf-networking-helpers/fakes" + "code.cloudfoundry.org/cf-networking-helpers/testsupport" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/controller/handlers" + "code.cloudfoundry.org/silk/controller/handlers/fakes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ReleaseLease", func() { + var ( + logger *lagertest.TestLogger + expectedLogger lager.Logger + handler *handlers.ReleaseLease + resp *httptest.ResponseRecorder + marshaler *hfakes.Marshaler + unmarshaler *hfakes.Unmarshaler + leaseReleaser *fakes.LeaseReleaser + fakeErrorResponse *fakes.ErrorResponse + + request *http.Request + ) + + BeforeEach(func() { + expectedLogger = lager.NewLogger("test").Session("leases-release") + testSink := lagertest.NewTestSink() + expectedLogger.RegisterSink(testSink) + expectedLogger.RegisterSink(lager.NewWriterSink(GinkgoWriter, lager.DEBUG)) + + logger = lagertest.NewTestLogger("test") + marshaler = &hfakes.Marshaler{} + marshaler.MarshalStub = json.Marshal + unmarshaler = &hfakes.Unmarshaler{} + unmarshaler.UnmarshalStub = json.Unmarshal + leaseReleaser = &fakes.LeaseReleaser{} + fakeErrorResponse = &fakes.ErrorResponse{} + + handler = &handlers.ReleaseLease{ + Marshaler: marshaler, + Unmarshaler: unmarshaler, + LeaseReleaser: leaseReleaser, + ErrorResponse: fakeErrorResponse, + } + resp = httptest.NewRecorder() + + requestBody := bytes.NewBuffer([]byte(`{ "underlay_ip": "10.244.16.11" }`)) + var err error + request, err = http.NewRequest("PUT", "/leases/release", requestBody) + Expect(err).NotTo(HaveOccurred()) + request.RemoteAddr = "some-host:some-port" + }) + + It("releases a lease for subnet", func() { + handler.ServeHTTP(logger, resp, request) + Expect(leaseReleaser.ReleaseSubnetLeaseCallCount()).To(Equal(1)) + Expect(leaseReleaser.ReleaseSubnetLeaseArgsForCall(0)).To(Equal("10.244.16.11")) + + Expect(resp.Code).To(Equal(http.StatusOK)) + Expect(resp.Body.String()).To(MatchJSON(`{}`)) + }) + + Context("when there are errors reading the body bytes", func() { + BeforeEach(func() { + request.Body = ioutil.NopCloser(&testsupport.BadReader{}) + }) + + It("logs the error and returns a 400", func() { + handler.ServeHTTP(logger, resp, request) + + Expect(fakeErrorResponse.BadRequestCallCount()).To(Equal(1)) + l, w, err, description := fakeErrorResponse.BadRequestArgsForCall(0) + Expect(l).To(Equal(expectedLogger)) + Expect(w).To(Equal(resp)) + Expect(err).To(MatchError("banana")) + Expect(description).To(Equal("read-body: banana")) + }) + }) + + Context("when the request cannot be unmarshaled", func() { + BeforeEach(func() { + unmarshaler.UnmarshalReturns(errors.New("fig")) + }) + + It("returns a BadRequest error", func() { + handler.ServeHTTP(logger, resp, request) + + Expect(fakeErrorResponse.BadRequestCallCount()).To(Equal(1)) + l, w, err, description := fakeErrorResponse.BadRequestArgsForCall(0) + Expect(l).To(Equal(expectedLogger)) + Expect(w).To(Equal(resp)) + Expect(err).To(MatchError("fig")) + Expect(description).To(Equal("unmarshal-request: fig")) + }) + }) + + Context("when releasing a lease fails", func() { + BeforeEach(func() { + leaseReleaser.ReleaseSubnetLeaseReturns(errors.New("kiwi")) + }) + + It("calls the Error Response InternalServerError() handler", func() { + handler.ServeHTTP(logger, resp, request) + + Expect(fakeErrorResponse.InternalServerErrorCallCount()).To(Equal(1)) + l, w, err, description := fakeErrorResponse.InternalServerErrorArgsForCall(0) + Expect(l).To(Equal(expectedLogger)) + Expect(w).To(Equal(resp)) + Expect(err).To(MatchError("kiwi")) + Expect(description).To(Equal("kiwi")) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/handlers/renew_lease.go b/src/code.cloudfoundry.org/silk/controller/handlers/renew_lease.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/handlers/renew_lease.go rename to src/code.cloudfoundry.org/silk/controller/handlers/renew_lease.go diff --git a/src/code.cloudfoundry.org/silk/controller/handlers/renew_lease_test.go b/src/code.cloudfoundry.org/silk/controller/handlers/renew_lease_test.go new file mode 100644 index 00000000..fbe69327 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/handlers/renew_lease_test.go @@ -0,0 +1,148 @@ +package handlers_test + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + + hfakes "code.cloudfoundry.org/cf-networking-helpers/fakes" + "code.cloudfoundry.org/cf-networking-helpers/testsupport" + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/controller" + "code.cloudfoundry.org/silk/controller/handlers" + "code.cloudfoundry.org/silk/controller/handlers/fakes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("RenewLease", func() { + var ( + logger *lagertest.TestLogger + expectedLogger lager.Logger + handler *handlers.RenewLease + resp *httptest.ResponseRecorder + unmarshaler *hfakes.Unmarshaler + leaseRenewer *fakes.LeaseRenewer + fakeErrorResponse *fakes.ErrorResponse + + expectedLease controller.Lease + request *http.Request + ) + + BeforeEach(func() { + expectedLogger = lager.NewLogger("test").Session("leases-renew") + + testSink := lagertest.NewTestSink() + expectedLogger.RegisterSink(testSink) + expectedLogger.RegisterSink(lager.NewWriterSink(GinkgoWriter, lager.DEBUG)) + + logger = lagertest.NewTestLogger("test") + unmarshaler = &hfakes.Unmarshaler{} + unmarshaler.UnmarshalStub = json.Unmarshal + leaseRenewer = &fakes.LeaseRenewer{} + fakeErrorResponse = &fakes.ErrorResponse{} + + handler = &handlers.RenewLease{ + Unmarshaler: unmarshaler, + LeaseRenewer: leaseRenewer, + ErrorResponse: fakeErrorResponse, + } + resp = httptest.NewRecorder() + + expectedLease = controller.Lease{ + UnderlayIP: "10.244.16.11", + OverlaySubnet: "10.255.17.0/24", + OverlayHardwareAddr: "ee:ee:0a:ff:11:00", + } + requestBody := bytes.NewBuffer([]byte(`{ "underlay_ip": "10.244.16.11", "overlay_subnet": "10.255.17.0/24", "overlay_hardware_addr": "ee:ee:0a:ff:11:00" }`)) + var err error + request, err = http.NewRequest("PUT", "/leases/renew", requestBody) + Expect(err).NotTo(HaveOccurred()) + request.RemoteAddr = "some-host:some-port" + }) + + It("renews a lease for subnet", func() { + handler.ServeHTTP(logger, resp, request) + Expect(leaseRenewer.RenewSubnetLeaseCallCount()).To(Equal(1)) + Expect(leaseRenewer.RenewSubnetLeaseArgsForCall(0)).To(Equal(expectedLease)) + + Expect(resp.Code).To(Equal(http.StatusOK)) + Expect(resp.Body.String()).To(Equal("{}")) + }) + + Context("when there are errors reading the body bytes", func() { + BeforeEach(func() { + request.Body = ioutil.NopCloser(&testsupport.BadReader{}) + }) + + It("logs the error and returns a 400", func() { + handler.ServeHTTP(logger, resp, request) + + Expect(fakeErrorResponse.BadRequestCallCount()).To(Equal(1)) + l, w, err, description := fakeErrorResponse.BadRequestArgsForCall(0) + Expect(l).To(Equal(expectedLogger)) + Expect(w).To(Equal(resp)) + Expect(err).To(MatchError("banana")) + Expect(description).To(Equal("read-body: banana")) + }) + }) + + Context("when the request cannot be unmarshaled", func() { + BeforeEach(func() { + unmarshaler.UnmarshalReturns(errors.New("fig")) + }) + + It("returns a BadRequest error", func() { + handler.ServeHTTP(logger, resp, request) + + Expect(fakeErrorResponse.BadRequestCallCount()).To(Equal(1)) + l, w, err, description := fakeErrorResponse.BadRequestArgsForCall(0) + Expect(l).To(Equal(expectedLogger)) + Expect(w).To(Equal(resp)) + Expect(err).To(MatchError("fig")) + Expect(description).To(Equal("unmarshal-request: fig")) + }) + }) + + Context("when renewing a lease fails due to a non-retriable error", func() { + var terr controller.NonRetriableError + BeforeEach(func() { + terr = controller.NonRetriableError("kiwi") + leaseRenewer.RenewSubnetLeaseReturns(terr) + }) + + It("calls the Error Response Conflict() handler", func() { + handler.ServeHTTP(logger, resp, request) + + Expect(fakeErrorResponse.ConflictCallCount()).To(Equal(1)) + l, w, err, description := fakeErrorResponse.ConflictArgsForCall(0) + Expect(l).To(Equal(expectedLogger)) + Expect(w).To(Equal(resp)) + Expect(err).To(Equal(terr)) + Expect(description).To(Equal(fmt.Sprintf("renew-subnet-lease: %s", terr.Error()))) + }) + }) + + Context("when renewing a lease fails due to some other error", func() { + BeforeEach(func() { + leaseRenewer.RenewSubnetLeaseReturns(errors.New("kiwi")) + }) + + It("calls the Error Response InternalServerError() handler", func() { + handler.ServeHTTP(logger, resp, request) + + Expect(fakeErrorResponse.InternalServerErrorCallCount()).To(Equal(1)) + l, w, err, description := fakeErrorResponse.InternalServerErrorArgsForCall(0) + Expect(l).To(Equal(expectedLogger)) + Expect(w).To(Equal(resp)) + Expect(err).To(MatchError("kiwi")) + Expect(description).To(Equal("renew-subnet-lease: kiwi")) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/controller/integration/fixtures/ca.crl b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/ca.crl new file mode 100644 index 00000000..4c5bef03 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/ca.crl @@ -0,0 +1,16 @@ +-----BEGIN X509 CRL----- +MIICfDBmAgEBMA0GCSqGSIb3DQEBCwUAMA0xCzAJBgNVBAMTAmNhFw0yMzAzMDIx +ODA0NDZaFw0yNDA5MDIxODA0NDRaMACgIzAhMB8GA1UdIwQYMBaAFETjk8Bh6/WB +Y1nLlBrHILL4hxZMMA0GCSqGSIb3DQEBCwUAA4ICAQBp6CZ2QKw++f3iNFLAYjV7 +jODnornSOqUBD9+txjT51z8/aV+wvGCaPyz32gBykmikQbtbj1EbzTmaMm53hRmA +PSxaTKITQoWSb8UJ9mURb0dX6h/moYPiFA9yyPcKFRA+4SmyQDKfY/AnNoslmSUG +3RPZVYyoA+buUqbwYVy0fbOfgiqsjHctD1aj9ET0kEzsX4am8Py7ts1xsV35tcwK +uBtFhW6eY8LeHLWxZw5rYOEThFBPbsRWQdF1IBJmYjpYFj6QV/EWxkjP/zLR3Fe2 +XVtVjRVDqpJhsvTeoIffoSves5kymF45gc6TGI8xlvGlF7NVfHTRy+vvDHjRFCJR +fOi+Y6oZknLNMXfdoLHkh3EZ5PgZ1McgBldTwr8Qiu0mwc43pNFKVKZMNYu9+IlA +ATnNbA7EairbjE3GHlFycASxDZArgSy9yP0Addaql7BZRfaGUGyiXw4L8ntPAmKi +5kQiW2Tqp4uYXqVBmg2zFz9Enxuoj555WGEIDbsqgk0WRX8E/RnuBDU6KMjF8RdY +dSFgpH8HwVR8vK35JZ5suFbh/rCtd5zXTj8rUWO8oLwvyMuGwwxtDXtyZXi4Pe7c +XO+Jw9mSimPfVuNwSntqrONJ9WaskYh/H+q6GPm8RMerKyCN0OQXDkS3MIbYSUkr +rAGW6uKNeTr9riv+ZV4ayw== +-----END X509 CRL----- diff --git a/src/code.cloudfoundry.org/silk/controller/integration/fixtures/ca.crt b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/ca.crt new file mode 100644 index 00000000..a6b51452 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/ca.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIE2jCCAsKgAwIBAgIBATANBgkqhkiG9w0BAQsFADANMQswCQYDVQQDEwJjYTAe +Fw0yMzAzMDIxNzU0NDZaFw0yNDA5MDIxODA0NDRaMA0xCzAJBgNVBAMTAmNhMIIC +IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvRufIGPkztDhczSUfTVoCtnt +implUc+Glmb3sedbtwSjZS0uNG6j+pxKQzOk6sKIBPgQp/yGG9KUw/7CXdeDwGcN +vXDSHuXPDo/NB+O0FohjzJyOa5VGyUq1eFFmM9S8fCt412bjf3dKAZIAq1xpdcrB +b9U1eAcUmORF6b5iNW1WuLHtZ4EJFEmU+0/ZqMeb+WWlhFDMZu8I36bEDBEWfnqI +s83RWYDLe17z8ytnrqkaMkvvEzDxZdmqtgBKX9JBAsIGYanQhDhc2JKb4v3A/n2Y +vvoSnMsPwpm60EXgxqXkZU+dpRYUJTSLMde9W2mpiZsZCCuZ2PD0c/Ht7K8dhhGb +pe/LDNQ8btuD7QlfQOvmQ8SMFpTDWYFnSwFWEBKVFOskJpgu9+XaVftupF8QkPbc +5SX80382AnacfMkC15RJnJtDxghXTFF+Nb3KE4KTEKdfN1glVuX/GSkpYSFKGaf+ +t0BFnOpN/T7jLWevSWiIfpVLFgOyTEN+87WuECcIL1I1HdGVUFFGUNYZZQFYnaXg +kOnL7heCTpZV4jjdn9eY6BILu99u1Q8LqdP1vrkYOrACHGk9wuwE9+IaVeJXTPF7 +qZi5ZpHTIAv9QvfetKE7NT5OVBcF1qE3lbvbv7Lm8oCr9DsabUsh0si1uxd8PnTh +e2HiSUUX/DfRwWd+l/kCAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB +/wQIMAYBAf8CAQAwHQYDVR0OBBYEFETjk8Bh6/WBY1nLlBrHILL4hxZMMA0GCSqG +SIb3DQEBCwUAA4ICAQCIcTPpii98FZQTkz7QsmeCZ9VPXwYvkA5AbZRK4OMTF2sJ +coN2toI1Fy67n6qHaS/X4LfKUcVUVAFgC4AzRgXLTx2scdL1/i5D5uvKpDZL7F09 +qhxKgO0AOExPbxdn0SnXAaBNFE7J/0LDoFOBKMLEFJNGHjaBY4xWbtcL4gF+oFZV +1dV/jko/5iEqRSLAsiIwNstVW5cC82bqSwKpPJLziZpFHxB+qSPl6jD3SlICIOdw +BlyhGZzNHML6BWscldBQTVLiLObl9En8G8QrSTj1NLFpGkOrjAdzlpanoB371nPV +utFnbMIysl0v6LRyq5HdNZqzl5BhXg5Wbd0vj2lDFPNrA9RfojeqOZChft3Nwjet +jfcGEIz0N2JOmDKn1Mvup0icoAnnIL0f8J7G28ZIzrtIZw8UbAE42QZSiCNp9pUH +mMYN8Jl9pLp9fz1t2VZOouIwpZnZgwXXsu85y9T+6PbNk8npEbzIn8Q0quiW8uY/ +Fsh9GwG3bP0fg1Vz6zp1XLOBdosMl7L6YwsaZwiZtmlwyTx9P405bG+yRehKPlJh +zutEmEsRhlhQL0EI0tHokPN4L3l1W5IW6z8aercUMazm8PqN29lnxs3fLSov1Wuw +0cB2ZIYq4FGZGWdAcXdaMCiZIEBK73UX1bj4Q4y6s3vjBepmdZzpxGjYviyRGA== +-----END CERTIFICATE----- diff --git a/src/code.cloudfoundry.org/silk/controller/integration/fixtures/ca.key b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/ca.key new file mode 100644 index 00000000..4b4edbbc --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/ca.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/src/code.cloudfoundry.org/silk/controller/integration/fixtures/client.crt b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/client.crt new file mode 100644 index 00000000..83e91d78 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/client.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIELTCCAhWgAwIBAgIRAOcXQn2NCAYr16lT1Zxe1i0wDQYJKoZIhvcNAQELBQAw +DTELMAkGA1UEAxMCY2EwHhcNMjMwMzAyMTc1NDQ2WhcNMjQwOTAyMTgwNDQzWjAR +MQ8wDQYDVQQDEwZjbGllbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQC4ccmYvE/yfdaR+tNa8P4LZJxPRTl8JiTqFWNYCdoN+p10sWrYhPuMzg3y2L0F ++IB7aw33t8kW4YvmSFsA9/F7ozn9bDFVAk/fMKajdAJVuvRrIhS45lGgzJIYUsbk +a84KQKoEyJ7XVFIc+F6Zkttnun6jsAaRmp6MaDyIsHllrNhg2r8IryqFUietTL09 +UQof5Arv0iaeRtceu4qBSuNi5giV92fwj3v1yS1i4muKUstscpJ4xpTfH1rlv1Et +JArHux+L7rj4HeuHzKKcNFCspjDPMSctEYn862fPvmocTSQJVIGR6qg4nn7oqUIc +rBkkJyy1To7JF0Q0WL7hdxVpAgMBAAGjgYMwgYAwDgYDVR0PAQH/BAQDAgO4MB0G +A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUmIghxx+tDJgx +G5HwOHfMRQqwBr0wHwYDVR0jBBgwFoAUROOTwGHr9YFjWcuUGscgsviHFkwwDwYD +VR0RBAgwBocEfwAAATANBgkqhkiG9w0BAQsFAAOCAgEAjTh4RjUahi6+uUOo/nEQ +ZkjaLp/rjxKhLPyPbAgLJMfvbnw3jtX/EEm4BQ2EiNtgwEZ23+p4EJqQLuH1jAvl +jehQeVW9cqMIIIr2elwbLDgkbm1m/I+txoG176qDfBN8D/rK2QXI6lqFKpu6CuyR +di/T4OkTLFu9JS/cCKGQWvZlkSOMPwLVOxkqOxRy1gEFhuyS76TK8rDaQZIPzf4e +xnRrVIrFHbHE2pNk9VxL7QF819azKOEZ2jZUH9oPwKMowCpjMe1Hlq8zLg1tcSSx +n8ypWMPdPfR8qKNz27meg7D/9KtLbujHN2KzOlHfElFFskyYube13F3cTcLxhBG0 +e5jxBrgIkRBiNG5p4pxwHZMbEzzrB2BKFPcpbQe+PjKofqyfNrag2OyweDy2VVRn +o5oTYDTf4F50hfUEFK6eW92nOuAkzrjcaQoKfTTUQFTgI40tUJjNYZX87IO+C/tS +EcrI+SF0y5LKvM9L3IYJ0xx4z3i3Kgq/m6rQyc9wLQdb4m8+Hxa1kwaRCc+pE6KU +qBi2Geq7TaBirSIf7t9X3OMM+iIJ52il/EcBr1V4ldc6MFCYW6FULS+GSUSyl2YD +FHYfnucZrzL7AAkrCpOUWArgaEA5dtJKzTrJFITr/lAOak7KvXXqNSEKD3+exBcc +dQZOuVSsQQWf+KtE/h1LARc= +-----END CERTIFICATE----- diff --git a/src/code.cloudfoundry.org/silk/controller/integration/fixtures/client.csr b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/client.csr new file mode 100644 index 00000000..ff3d84fc --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/client.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICeDCCAWACAQAwETEPMA0GA1UEAxMGY2xpZW50MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAuHHJmLxP8n3WkfrTWvD+C2ScT0U5fCYk6hVjWAnaDfqd +dLFq2IT7jM4N8ti9BfiAe2sN97fJFuGL5khbAPfxe6M5/WwxVQJP3zCmo3QCVbr0 +ayIUuOZRoMySGFLG5GvOCkCqBMie11RSHPhemZLbZ7p+o7AGkZqejGg8iLB5ZazY +YNq/CK8qhVInrUy9PVEKH+QK79ImnkbXHruKgUrjYuYIlfdn8I979cktYuJrilLL +bHKSeMaU3x9a5b9RLSQKx7sfi+64+B3rh8yinDRQrKYwzzEnLRGJ/Otnz75qHE0k +CVSBkeqoOJ5+6KlCHKwZJCcstU6OyRdENFi+4XcVaQIDAQABoCIwIAYJKoZIhvcN +AQkOMRMwETAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQBg7lVW +gViygyrPHlB+sftwCsFgoQMfNI+79owMnr4UWeHgvdl7gxgONNEE3AqR3xbf/ruM +wClWM112TNe5bcBMhrEUj5r/TNAdRM7VCH3TdIeodAnsb3WY6VM63gEOhjoj9Hmv +ttHhRrVyoJmdAulJzUT+b3OCfhXAp8rHirsoe6R5+9R4DElt6wInm7jBqs/jH0F5 +sqvI6qsG4OkT4eiVQqpmk/aiYvbXN8GqcwUaZK7Cw4U/f3+whCVSlGZMxoS+gj8/ +Vrry+H5oAyDRZU3/ssSq0ocKZaOa1iLzuOQR/fOjwW3mY3Xbn5mALJf/O5E1xzz5 +aTX9/m3Q4sDpwXbh +-----END CERTIFICATE REQUEST----- diff --git a/src/code.cloudfoundry.org/silk/controller/integration/fixtures/client.key b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/client.key new file mode 100644 index 00000000..329d0883 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAuHHJmLxP8n3WkfrTWvD+C2ScT0U5fCYk6hVjWAnaDfqddLFq +2IT7jM4N8ti9BfiAe2sN97fJFuGL5khbAPfxe6M5/WwxVQJP3zCmo3QCVbr0ayIU +uOZRoMySGFLG5GvOCkCqBMie11RSHPhemZLbZ7p+o7AGkZqejGg8iLB5ZazYYNq/ +CK8qhVInrUy9PVEKH+QK79ImnkbXHruKgUrjYuYIlfdn8I979cktYuJrilLLbHKS +eMaU3x9a5b9RLSQKx7sfi+64+B3rh8yinDRQrKYwzzEnLRGJ/Otnz75qHE0kCVSB +keqoOJ5+6KlCHKwZJCcstU6OyRdENFi+4XcVaQIDAQABAoIBAQCui+BADlWKIdQh +KcFCWrXPilLyIYyZFG/ypnLxKcGVYQoJGK0IYjC7u3Qv0TRwpg7QvSrmtqIyaRe/ +wdF9RSbd3kZOsUiuLhXwHVZVcL+U6evMRuBdanjtNys6oXtfRzYhNbiv9jt7g2yB +xfmgU/4WV8LonPhaanyIW0yN/pOjxyaGd/eHasODZ7a6dR+YdcQNycDjDP1A0/1E +lridupEu/X9yn4oJLgiBgIewyAAs+Er4WPjWFcf9lt5NOr0ExACxnCB8u9P83kiY +JMZJIYYtMFRxkniSuTWmIkddRHjJsnjEoiTakgn4vHU9QzboAbsQzHHeIeXqux0m +UhTdbAxRAoGBAOf0tsJPnW39RqmDB5YC92pTB/aSRzkZXJa2y7JLE8PC7922hcNK +YD9sf7DpxW0qJgX72YdMOvVLgRf8j3sqByusiaS2tXsPQv7jP/Z6BqVF92rB43li +GxYjtESeP7E/0pGdI5keiaWEsTU0XMIS1po2xEMLjSWKqTANEk+XigYfAoGBAMuQ +SfhmTsPJh6lqGfxLFiA9Vx+fWNUw+A/+cw0LY8G4s0FxTfXpkJf91SEyKuiHI3Fh +dSkpWRd7Dgo6mOr/2x6VZyGa34TnNhMR5RgfcDo5kJoGvJK1zhUDXiV1CpfY4ete +uQRVoYyOfKSfR1kb8ZWivjkWAFVlMW9eo5ePiCN3AoGBAJP6iMW6KQNMe9IU57gK +OHG6evARLiL9TRZRk+4w+3D+YiyeDwQh5PlRbL0exnb4Vr2miIZKWaq3+qmk1JyM +mXqBXmaYsXrKp8EYlWvXOXojAs821rpPnlwndw/3rUaVIu8GwvkihEn2N19sAmlF +v2cu3VKwgeGbbohbLn1cfg+dAoGAapKtF15AqGIvIzQITLzm5ZQXs9eSA3Nyu7Tk +eRTYckiHmKCAR1mzuM9MsOd57Wkq1iZG13jtSszC6foJqyauilF/v+k8UnFWLx+0 +SC/VarZOjUUvnsOOFBRCSuv+0sbaaIab6LgILvU+LlWxXeayUTv1yR9Kp2qmOf9b +OHMNvDsCgYAdNwAsnEnYtswhJp4njCzfrroM21mQnnugcDvSwg2alcHL0+rs3bvF +8kATNcUpz5/isB1UXSGKoA2e2b2+smVJCIP166Lwi9D+HYGNBW1tdzLpbPBODePW +GdtyzRv1Im8/NdcKnNr+5h7RU+r3izHasJRt6KAH4CkvQy6NJphAmA== +-----END RSA PRIVATE KEY----- diff --git a/src/code.cloudfoundry.org/silk/controller/integration/fixtures/regenerate-certs.sh b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/regenerate-certs.sh new file mode 100755 index 00000000..64fe7008 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/regenerate-certs.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -e + +this_dir="$(cd $(dirname $0) && pwd)" + +pushd "$this_dir" + +rm -rf out +certstrap init --common-name "ca" --passphrase "" +certstrap request-cert --common-name "client" --passphrase "" --ip "127.0.0.1" +certstrap sign client --CA "ca" + +certstrap request-cert --common-name "server" --passphrase "" --ip "127.0.0.1" +certstrap sign server --CA "ca" + +mv -f out/* ./ +rm -rf out + +popd diff --git a/src/code.cloudfoundry.org/silk/controller/integration/fixtures/server.crt b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/server.crt new file mode 100644 index 00000000..349eaf75 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/server.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIELTCCAhWgAwIBAgIRALcu7rVdfepIfTrpCnTyErowDQYJKoZIhvcNAQELBQAw +DTELMAkGA1UEAxMCY2EwHhcNMjMwMzAyMTc1NDQ3WhcNMjQwOTAyMTgwNDQzWjAR +MQ8wDQYDVQQDEwZzZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwHNdGFxuYiad+foH/Z8Mfncf4ESINk1vW1Qe3qx1+6K5ayrjDjJIrXDswEsFP +BMi6Yq89dHuJAEJhM8zL0tcs7pBBR7X2T9H/YXFPfV7p+UOm9dmSUP7Mr02LQJh6 +F0ZO4Xu2hM60D910HmF+RgSTxnLzo9fON9Bge/xnPhRyL918c7VyGpyYS53Z757n +gusk4cKmkhGB+kyDGAmlIvT4IKSRLoRDl8HmEZ/+J46CVFIN5GCqlXDHJp+/af4y +XCbMxGQcrA0BwbvVBTW8cWyW68T4TVX5BSbOcZklMHRc4DEy8gZFUEgFZIqq5YXu +XRMNCwP5LM4jdHWDA6VsHFhpAgMBAAGjgYMwgYAwDgYDVR0PAQH/BAQDAgO4MB0G +A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUwFTq9oK4LDsr +zQttIb3+J45MivUwHwYDVR0jBBgwFoAUROOTwGHr9YFjWcuUGscgsviHFkwwDwYD +VR0RBAgwBocEfwAAATANBgkqhkiG9w0BAQsFAAOCAgEAOLwcgOFsYj10Z2Mao9Mj +YZsgka3q9a5kfXh7DWzcLRh8o5icY1VOTexjc2H6/dpBAK/3AsR8AzkB87FLtWhR +KhhE2Sl6x4toQ8eko1elJTAAG1EUzl4sp0zbOwrZiHr0xZgw2gXVw2QxIH1pEU26 +WlkYHFSQocyAuLP5BlGGlPgUqvvoDImv781VuJKXB0SvNhsdUCH5ymJjbb/XpueA +VcEatUxN/9Dbe9QPJsKYGof6gj1nTyE0a7boatUTV7luVlzyNafSLORmb2XmVOja +50MGLofZA9YBnI394M2iiLY6N8fj0agAuZygXljyxbRi0oD/ZSuFMFVoUV0IQPZT +oSRk2apmcu5rHa2Sp+bDPWh9v6kAZlWUM7YOU5xpP5Ia0rywhRClHEn2I0WqpI0V +8lvUcyH8yk2+2s7UPw5L8jkQJkF9VT1ZeIMIGxuukNqMWXLvCfx5t6BZBfiX7a4X +Rm144re/LIU7FexOBIwClB1aipX0xnBJav4wwLAHSPe5/rJ9hWPzZd/VNesPC9xZ +X+MRrN62RtSZjNSPNeb6XVevuREMBoXK0Ih1Ccp6QNlYIiMfcStH85WdSDzewXN8 +zJP0M/e7JfIl17mfslpukeH52gkDvQe2gzCF2k/7/ExL64VOuk3HHcjgioqchAzQ +RmsAuA9LEyitxc+V4SXrY3M= +-----END CERTIFICATE----- diff --git a/src/code.cloudfoundry.org/silk/controller/integration/fixtures/server.csr b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/server.csr new file mode 100644 index 00000000..b20a4fb9 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/server.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICeDCCAWACAQAwETEPMA0GA1UEAxMGc2VydmVyMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAsBzXRhcbmImnfn6B/2fDH53H+BEiDZNb1tUHt6sdfuiu +Wsq4w4ySK1w7MBLBTwTIumKvPXR7iQBCYTPMy9LXLO6QQUe19k/R/2FxT31e6flD +pvXZklD+zK9Ni0CYehdGTuF7toTOtA/ddB5hfkYEk8Zy86PXzjfQYHv8Zz4Uci/d +fHO1chqcmEud2e+e54LrJOHCppIRgfpMgxgJpSL0+CCkkS6EQ5fB5hGf/ieOglRS +DeRgqpVwxyafv2n+MlwmzMRkHKwNAcG71QU1vHFsluvE+E1V+QUmznGZJTB0XOAx +MvIGRVBIBWSKquWF7l0TDQsD+SzOI3R1gwOlbBxYaQIDAQABoCIwIAYJKoZIhvcN +AQkOMRMwETAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQAoMqjO +w1lHXFhws/hCRLnsPujo0EyL1yR7DO+AAPZST5zwV0wwSia4wo97rx1HtBssy5Ka +7+XLzdnR5vf8JDTy2/oDM+dNQ+taPAPjFM240hCe9/VJT20tTqQQ0pvJtErmByxt +wZAGfnmv8QJF1Yh2jPvmCyhIegGRkLwFfDRXUKY2eP86oio7CxfVqK4gWpNEjmYj +q/nIietuzBr3co7+wzWb/wsVw/sUIoHbktSILVjrzv6nsQtHc1jTYnICTgtMMz3r +0DU12GQIsk76S+2OazrlAvVUpF8mtbcGBBKd3gjkIHhX3JMgp6PJ66vIhgi2NUrX +kQcWOzi9Gp9gNVLz +-----END CERTIFICATE REQUEST----- diff --git a/src/code.cloudfoundry.org/silk/controller/integration/fixtures/server.key b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/server.key new file mode 100644 index 00000000..f95c3612 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/integration/fixtures/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAsBzXRhcbmImnfn6B/2fDH53H+BEiDZNb1tUHt6sdfuiuWsq4 +w4ySK1w7MBLBTwTIumKvPXR7iQBCYTPMy9LXLO6QQUe19k/R/2FxT31e6flDpvXZ +klD+zK9Ni0CYehdGTuF7toTOtA/ddB5hfkYEk8Zy86PXzjfQYHv8Zz4Uci/dfHO1 +chqcmEud2e+e54LrJOHCppIRgfpMgxgJpSL0+CCkkS6EQ5fB5hGf/ieOglRSDeRg +qpVwxyafv2n+MlwmzMRkHKwNAcG71QU1vHFsluvE+E1V+QUmznGZJTB0XOAxMvIG +RVBIBWSKquWF7l0TDQsD+SzOI3R1gwOlbBxYaQIDAQABAoIBAEtLGVJzAK5Xc8+l +4IhEYWy79Ul+cbOiI6qWi2Uf+E1Qx3Izp4ibKcb1+KwV6KWS4wqktE0u5r3s6XRL +RCyEKNCh8fSm5wm2mI55p4sXi6O2mRDhnTJsBHOYIyv84vOXzcxdpJmk8AaVndVO +Q9pLrxtKxcyaaOJWhHUrrP6q0Oz8Cf4/nf1CkaG/dbTXX+6CjnVgqdSiav5q55V0 +BgnN1IIRIn6aZCm+pIcwjV360q7EoWEMOgAn6sHHAj+yL6dqw/8kfq7xoCSOpdWW +qMIoULgVL3QjE9IWWsbn+YHYrDJP7QnoMyQUttvzZQaRwrzJDa+U6D/xWqePZZN5 +DimeifECgYEAyREzEFSbZWgcipM2L3FFu5VUPe9Dn77VXPbSX03g1UWCrSEiJyeH ++wVvPRNRCmWTWZiNQU9U9SAk7gbG4bDK+8ZzPXhvMZMmVOawQ8k3OcBP85u30atP +KBSzbX3V5RRuColmpSMlYMkdemj+gujwlczYu/gDtfP6cnI/n9UQPjUCgYEA4DpN +R1/OQf52G9D4apyCDI8O7ZomKgzXa5op1vvwSK+KkHX4Suh8qkD9IGggkAe+6de3 +42z1oI7g/Bio0Y134hZroWzSMe1X8UtbVBavn8C1fv3jCq8bKEbVWLCgNUJ4Kztt +YiLMobxoYpaS4O+8Os2eCApIU8aIUZij60CER+UCgYBaR2C4KmUDqTV3exXPQ3rd ++PS0QmQIq/S9Iw3eQOd8mxNLjR76maNSsGP79lpi4qzAg+CYvHgW//HU9jICUBEz +7yz7IsI6bim94T6vkP887P55ESf9+n82LeVKej+59exaysrNxvQXevXDarUsLk8n +06xU6c0wu3VuQaITr+T1KQKBgFJoWvp+VXmfoZy7gnD8MDeZZC0i2B/gNFPRhssC +MN8l1NEe6i/tndOcqur8GeuVTS74Hxcwdl2Z5UoBbhoF2id3gfsD2YSL7CQ8Q8Z1 +x/n4gzu253iHvn1g+wvrbhXk5fFNl/fXPkm6yspg+H4+3XjnFMtzq/1OG8+RP704 +KuWZAoGADtFeVw+B4khoXEh9Chhu4lNwYBuClepTrB6jUw38+UxcgtV6UkFYyXLe +fjmNffDyR258nqX4U0vxkRYq2ITQ/z3YIQPgWF97IZtnO0iST6Q9CSZkZXFvz1if +QOX48wKz6jhZ3B28moJgjM23Nv/pYfC9HwQNf/Lt6002K2TeyUI= +-----END RSA PRIVATE KEY----- diff --git a/src/code.cloudfoundry.org/silk/controller/integration/helpers/helpers.go b/src/code.cloudfoundry.org/silk/controller/integration/helpers/helpers.go new file mode 100644 index 00000000..587b6dbd --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/integration/helpers/helpers.go @@ -0,0 +1,109 @@ +package helpers + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "net/http" + "os/exec" + "path/filepath" + + "code.cloudfoundry.org/cf-networking-helpers/db" + "code.cloudfoundry.org/cf-networking-helpers/testsupport/ports" + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/controller" + "code.cloudfoundry.org/silk/controller/config" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +const DEFAULT_TIMEOUT = "5s" + +func DefaultTestConfig(dbConf db.Config, fixturesPath string) config.Config { + return config.Config{ + ListenHost: "127.0.0.1", + ListenPort: ports.PickAPort(), + DebugServerPort: ports.PickAPort(), + HealthCheckPort: ports.PickAPort(), + CACertFile: filepath.Join(fixturesPath, "ca.crt"), + ServerCertFile: filepath.Join(fixturesPath, "server.crt"), + ServerKeyFile: filepath.Join(fixturesPath, "server.key"), + Network: "10.255.0.0/16", + SubnetPrefixLength: 24, + Database: dbConf, + LeaseExpirationSeconds: 60, + StalenessThresholdSeconds: 5, + MetronPort: 5432, + MetricsEmitSeconds: 1, + LogPrefix: "potato-prefix", + } +} + +func StartAndWaitForServer(controllerBinaryPath string, conf config.Config, client *controller.Client) *gexec.Session { + configFilePath := WriteConfigFile(conf) + + session := StartServer(controllerBinaryPath, configFilePath) + + By("waiting for the http server to boot") + serverIsUp := func() error { + _, err := client.GetActiveLeases() + return err + } + Eventually(serverIsUp, DEFAULT_TIMEOUT).Should(Succeed()) + return session +} + +func WriteConfigFile(conf config.Config) string { + configFile, err := ioutil.TempFile("", "config-") + Expect(err).NotTo(HaveOccurred()) + configFilePath := configFile.Name() + Expect(configFile.Close()).To(Succeed()) + Expect(conf.WriteToFile(configFilePath)).To(Succeed()) + return configFilePath +} + +func StartServer(controllerBinaryPath, configFilePath string) *gexec.Session { + cmd := exec.Command(controllerBinaryPath, "-config", configFilePath) + s, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + return s +} + +func StopServer(session *gexec.Session) { + session.Interrupt() + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit(0)) +} + +func TestClient(conf config.Config, fixturesPath string) *controller.Client { + baseURL := fmt.Sprintf("https://%s:%d", conf.ListenHost, conf.ListenPort) + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: makeClientTLSConfig(fixturesPath), + }, + } + return controller.NewClient(lagertest.NewTestLogger("test"), httpClient, baseURL) +} + +func makeClientTLSConfig(fixturesPath string) *tls.Config { + clientCertPath := filepath.Join(fixturesPath, "client.crt") + clientKeyPath := filepath.Join(fixturesPath, "client.key") + cert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath) + Expect(err).NotTo(HaveOccurred()) + + clientCAPath := filepath.Join(fixturesPath, "ca.crt") + clientCACert, err := ioutil.ReadFile(clientCAPath) + Expect(err).NotTo(HaveOccurred()) + + clientCertPool := x509.NewCertPool() + clientCertPool.AppendCertsFromPEM(clientCACert) + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: clientCertPool, + } + tlsConfig.BuildNameToCertificate() + return tlsConfig +} diff --git a/src/code.cloudfoundry.org/silk/controller/integration/integration_suite_test.go b/src/code.cloudfoundry.org/silk/controller/integration/integration_suite_test.go new file mode 100644 index 00000000..b0f5967f --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/integration/integration_suite_test.go @@ -0,0 +1,44 @@ +package integration_test + +import ( + "math/rand" + + "code.cloudfoundry.org/cf-networking-helpers/testsupport/metrics" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" + "github.com/onsi/gomega/types" + + "testing" +) + +var controllerBinaryPath string + +func TestIntegration(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Controller Integration Suite") +} + +var HaveName = func(name string) types.GomegaMatcher { + return WithTransform(func(ev metrics.Event) string { + return ev.Name + }, Equal(name)) +} + +var _ = SynchronizedBeforeSuite(func() []byte { + controllerBinaryPath, err := gexec.Build( + "code.cloudfoundry.org/silk/cmd/silk-controller", + "-race", + "-buildvcs=false", + ) + Expect(err).NotTo(HaveOccurred()) + return []byte(controllerBinaryPath) +}, func(data []byte) { + controllerBinaryPath = string(data) + rand.Seed(GinkgoRandomSeed() + int64(GinkgoParallelProcess())) +}) + +var _ = SynchronizedAfterSuite(func() {}, func() { + gexec.CleanupBuildArtifacts() +}) diff --git a/src/code.cloudfoundry.org/silk/controller/integration/integration_test.go b/src/code.cloudfoundry.org/silk/controller/integration/integration_test.go new file mode 100644 index 00000000..db1a3f5f --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/integration/integration_test.go @@ -0,0 +1,542 @@ +package integration_test + +import ( + "fmt" + "net" + "net/http" + "time" + + "code.cloudfoundry.org/cf-networking-helpers/db" + "code.cloudfoundry.org/cf-networking-helpers/testsupport" + "code.cloudfoundry.org/cf-networking-helpers/testsupport/metrics" + "code.cloudfoundry.org/cf-networking-helpers/testsupport/ports" + "code.cloudfoundry.org/silk/controller" + "code.cloudfoundry.org/silk/controller/config" + "code.cloudfoundry.org/silk/controller/integration/helpers" + "code.cloudfoundry.org/silk/controller/leaser" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gexec" + "github.com/onsi/gomega/types" +) + +var ( + dbConfig db.Config + session *gexec.Session + conf config.Config + testClient *controller.Client + fakeMetron metrics.FakeMetron +) + +var _ = BeforeEach(func() { + fakeMetron = metrics.NewFakeMetron() + dbConfig = testsupport.GetDBConfig() + dbConfig.DatabaseName = fmt.Sprintf("test_%d", ports.PickAPort()) + testsupport.CreateDatabase(dbConfig) + + conf = helpers.DefaultTestConfig(dbConfig, "fixtures") + conf.MetronPort = fakeMetron.Port() + testClient = helpers.TestClient(conf, "fixtures") +}) + +var _ = Describe("Silk Controller", func() { + BeforeEach(func() { + session = helpers.StartAndWaitForServer(controllerBinaryPath, conf, testClient) + }) + + AfterEach(func() { + testClient.JsonClient.CloseIdleConnections() + helpers.StopServer(session) + testsupport.RemoveDatabase(dbConfig) + }) + + It("gracefully terminates when sent an interrupt signal", func() { + Consistently(session).ShouldNot(gexec.Exit()) + Expect(session.Out).To(gbytes.Say(`potato-prefix\.silk-controller\.starting-servers`)) + Expect(session.Out).To(gbytes.Say(`potato-prefix\.silk-controller\.running`)) + + session.Interrupt() + + Eventually(session, "5s").Should(gexec.Exit(0)) + Expect(session.Out).To(gbytes.Say(`potato-prefix\.silk-controller\.exited`)) + }) + + It("runs the cf debug server on the configured port", func() { + resp, err := http.Get( + fmt.Sprintf("http://127.0.0.1:%d/debug/pprof", conf.DebugServerPort), + ) + Expect(err).NotTo(HaveOccurred()) + defer resp.Body.Close() + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + }) + + It("runs the health check server on the configured port", func() { + resp, err := http.Get( + fmt.Sprintf("http://127.0.0.1:%d/health", conf.HealthCheckPort), + ) + Expect(err).NotTo(HaveOccurred()) + defer resp.Body.Close() + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + }) + + Describe("acquiring", func() { + It("provides an endpoint to acquire a subnet leases", func() { + lease, err := testClient.AcquireSubnetLease("10.244.4.5") + Expect(err).NotTo(HaveOccurred()) + Expect(lease.UnderlayIP).To(Equal("10.244.4.5")) + _, subnet, err := net.ParseCIDR(lease.OverlaySubnet) + Expect(err).NotTo(HaveOccurred()) + _, network, err := net.ParseCIDR(conf.Network) + Expect(network.Contains(subnet.IP)).To(BeTrue()) + expectedHardwareAddr, err := (&leaser.HardwareAddressGenerator{}).GenerateForVTEP(subnet.IP) + Expect(err).NotTo(HaveOccurred()) + Expect(lease.OverlayHardwareAddr).To(Equal(expectedHardwareAddr.String())) + + Eventually(fakeMetron.AllEvents, "5s").Should(ContainElement( + HaveName("LeasesAcquireRequestTime"), + )) + }) + + Context("when there is an existing lease for the underlay IP", func() { + var existingLease controller.Lease + BeforeEach(func() { + var err error + existingLease, err = testClient.AcquireSubnetLease("10.244.4.5") + Expect(err).NotTo(HaveOccurred()) + }) + It("returns the same lease", func() { + lease, err := testClient.AcquireSubnetLease("10.244.4.5") + Expect(err).NotTo(HaveOccurred()) + + Expect(lease).To(Equal(existingLease)) + }) + + Context("when the existing lease is in a different overlay network", func() { + BeforeEach(func() { + helpers.StopServer(session) + conf.Network = "10.254.0.0/16" + session = helpers.StartAndWaitForServer(controllerBinaryPath, conf, testClient) + }) + It("returns a new lease in the new network", func() { + lease, err := testClient.AcquireSubnetLease("10.244.4.5") + Expect(err).NotTo(HaveOccurred()) + + Expect(lease).NotTo(Equal(existingLease)) + _, subnet, err := net.ParseCIDR(lease.OverlaySubnet) + Expect(err).NotTo(HaveOccurred()) + _, network, err := net.ParseCIDR(conf.Network) + Expect(network.Contains(subnet.IP)).To(BeTrue()) + }) + }) + }) + }) + + Describe("releasing", func() { + It("releases a subnet lease", func() { + By("getting a valid lease") + lease, err := testClient.AcquireSubnetLease("10.244.4.5") + Expect(err).NotTo(HaveOccurred()) + + By("checking that the lease is present in the list of routable leases") + leases, err := testClient.GetActiveLeases() + Expect(err).NotTo(HaveOccurred()) + Expect(len(leases)).To(Equal(1)) + Expect(leases[0]).To(Equal(lease)) + + By("attempting to release it") + err = testClient.ReleaseSubnetLease("10.244.4.5") + Expect(err).NotTo(HaveOccurred()) + + By("checking that the lease is not present in the list of routable leases") + leases, err = testClient.GetActiveLeases() + Expect(err).NotTo(HaveOccurred()) + Expect(len(leases)).To(Equal(0)) + + Eventually(fakeMetron.AllEvents, "5s").Should(ContainElement( + HaveName("LeasesReleaseRequestTime"), + )) + }) + + Context("when trying to release a single ip", func() { + It("releases a subnet lease", func() { + By("getting a valid lease") + lease, err := testClient.AcquireSingleOverlayIPLease("10.244.4.5") + Expect(err).NotTo(HaveOccurred()) + + By("checking that the lease is present in the list of routable leases") + leases, err := testClient.GetActiveLeases() + Expect(err).NotTo(HaveOccurred()) + Expect(len(leases)).To(Equal(1)) + Expect(leases[0]).To(Equal(lease)) + + By("attempting to release it") + err = testClient.ReleaseSubnetLease("10.244.4.5") + Expect(err).NotTo(HaveOccurred()) + + By("checking that the lease is not present in the list of routable leases") + leases, err = testClient.GetActiveLeases() + Expect(err).NotTo(HaveOccurred()) + Expect(len(leases)).To(Equal(0)) + + Eventually(fakeMetron.AllEvents, "5s").Should(ContainElement( + HaveName("LeasesReleaseRequestTime"), + )) + }) + }) + + Context("when lease is not present in database", func() { + It("does not error", func() { + err := testClient.ReleaseSubnetLease("9.9.9.9") + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) + + Describe("lease expiration", func() { + BeforeEach(func() { + helpers.StopServer(session) + conf.Network = "10.255.0.0/29" + conf.SubnetPrefixLength = 30 + conf.LeaseExpirationSeconds = 3 + session = helpers.StartAndWaitForServer(controllerBinaryPath, conf, testClient) + }) + + It("reclaims expired leases", func() { + oldLease, err := testClient.AcquireSubnetLease("10.244.4.5") + Expect(err).NotTo(HaveOccurred()) + + _, err = testClient.AcquireSubnetLease("10.244.4.15") + Expect(err).To(MatchError(ContainSubstring("no lease available"))) + + // wait for lease to expire + time.Sleep(time.Duration(conf.LeaseExpirationSeconds+1) * time.Second) + + newLease, err := testClient.AcquireSubnetLease("10.244.4.15") + Expect(err).NotTo(HaveOccurred()) + Expect(newLease.OverlaySubnet).To(Equal(oldLease.OverlaySubnet)) + }) + }) + + Describe("renewal", func() { + It("successfully renews", func() { + By("getting a valid lease") + lease, err := testClient.AcquireSubnetLease("10.244.4.5") + Expect(err).NotTo(HaveOccurred()) + + By("attempting to renew it") + err = testClient.RenewSubnetLease(lease) + Expect(err).NotTo(HaveOccurred()) + + By("checking that the lease is present in the list of routable leases") + leases, err := testClient.GetActiveLeases() + Expect(err).NotTo(HaveOccurred()) + Expect(len(leases)).To(Equal(1)) + Expect(leases[0]).To(Equal(lease)) + + Eventually(fakeMetron.AllEvents, "5s").Should(ContainElement( + HaveName("LeasesRenewRequestTime"), + )) + }) + + Context("when trying to renew a single ip", func() { + It("successfully renews", func() { + By("getting a valid lease") + lease, err := testClient.AcquireSingleOverlayIPLease("10.244.4.5") + Expect(err).NotTo(HaveOccurred()) + + By("attempting to renew it") + err = testClient.RenewSubnetLease(lease) + Expect(err).NotTo(HaveOccurred()) + + By("checking that the lease is present in the list of routable leases") + leases, err := testClient.GetActiveLeases() + Expect(err).NotTo(HaveOccurred()) + Expect(len(leases)).To(Equal(1)) + Expect(leases[0]).To(Equal(lease)) + + Eventually(fakeMetron.AllEvents, "5s").Should(ContainElement( + HaveName("LeasesRenewRequestTime"), + )) + }) + }) + + Context("when the lease is not valid for some reason", func() { + It("returns a non-retriable error", func() { + By("getting a valid lease") + validLease, err := testClient.AcquireSubnetLease("10.244.4.5") + Expect(err).NotTo(HaveOccurred()) + + By("corrupting it somehow") + invalidLease := controller.Lease{ + UnderlayIP: validLease.UnderlayIP, + OverlaySubnet: "10.9.9.9/24", + OverlayHardwareAddr: validLease.OverlayHardwareAddr, + } + + By("attempting to renew it") + err = testClient.RenewSubnetLease(invalidLease) + Expect(err).To(BeAssignableToTypeOf(controller.NonRetriableError(""))) + typedError := err.(controller.NonRetriableError) + Expect(typedError.Error()).To(Equal("non-retriable: renew-subnet-lease: lease mismatch")) + + By("checking that the corrupted lease is not present in the list of routable leases") + leases, err := testClient.GetActiveLeases() + Expect(err).NotTo(HaveOccurred()) + Expect(len(leases)).To(Equal(1)) + Expect(leases[0]).To(Equal(validLease)) + }) + }) + + Context("when there is an existing lease for the underlay IP", func() { + var existingLease controller.Lease + BeforeEach(func() { + var err error + existingLease, err = testClient.AcquireSubnetLease("10.244.4.5") + Expect(err).NotTo(HaveOccurred()) + }) + + Context("when the existing lease is in a different overlay network", func() { + BeforeEach(func() { + helpers.StopServer(session) + conf.Network = "10.254.0.0/16" + session = helpers.StartAndWaitForServer(controllerBinaryPath, conf, testClient) + }) + It("renews the same lease in the old network", func() { + err := testClient.RenewSubnetLease(existingLease) + Expect(err).NotTo(HaveOccurred()) + + By("checking that the lease is present in the list of routable leases") + leases, err := testClient.GetActiveLeases() + Expect(err).NotTo(HaveOccurred()) + Expect(len(leases)).To(Equal(1)) + Expect(leases[0]).To(Equal(existingLease)) + }) + }) + }) + + Context("when the local lease is not present in the database", func() { + It("the renew succeeds (even though its really more of an acquire)", func() { + lease := controller.Lease{ + UnderlayIP: "10.244.9.9", + OverlaySubnet: "10.255.9.0/24", + OverlayHardwareAddr: "ee:ee:0a:ff:09:00", + } + + By("attempting to renew something new but ok") + err := testClient.RenewSubnetLease(lease) + Expect(err).NotTo(HaveOccurred()) + + By("checking that the lease is present in the list of routable leases") + leases, err := testClient.GetActiveLeases() + Expect(err).NotTo(HaveOccurred()) + Expect(len(leases)).To(Equal(1)) + Expect(leases[0]).To(Equal(lease)) + }) + }) + }) + + Describe("listing leases", func() { + It("list the current routable leases", func() { + lease, err := testClient.AcquireSubnetLease("10.244.4.5") + Expect(err).NotTo(HaveOccurred()) + + singleIPLease, err := testClient.AcquireSingleOverlayIPLease("10.244.4.6") + Expect(err).NotTo(HaveOccurred()) + + leases, err := testClient.GetActiveLeases() + Expect(err).NotTo(HaveOccurred()) + Expect(len(leases)).To(Equal(2)) + Expect(leases).To(ConsistOf([]controller.Lease{ + lease, + singleIPLease, + })) + + Eventually(fakeMetron.AllEvents, "5s").Should(ContainElement( + HaveName("LeasesIndexRequestTime"), + )) + }) + + Context("when a lease expires", func() { + BeforeEach(func() { + helpers.StopServer(session) + conf.LeaseExpirationSeconds = 2 + session = helpers.StartAndWaitForServer(controllerBinaryPath, conf, testClient) + }) + + It("does not return expired leases", func() { + lease1, err := testClient.AcquireSubnetLease("10.244.4.5") + Expect(err).NotTo(HaveOccurred()) + lease2, err := testClient.AcquireSubnetLease("10.244.4.6") + Expect(err).NotTo(HaveOccurred()) + + leases, err := testClient.GetActiveLeases() + Expect(err).NotTo(HaveOccurred()) + + Expect(leases).To(ConsistOf(lease1, lease2)) + + renewAndCheck := func() []controller.Lease { + Expect(testClient.RenewSubnetLease(lease2)).To(Succeed()) + leases, err := testClient.GetActiveLeases() + Expect(err).NotTo(HaveOccurred()) + return leases + } + + Eventually(renewAndCheck, 4).Should(ConsistOf(lease2)) + Consistently(renewAndCheck).Should(ConsistOf(lease2)) + }) + }) + + Context("when there are leases from different networks", func() { + var oldNetworkLease controller.Lease + var newNetworkLease controller.Lease + BeforeEach(func() { + var err error + oldNetworkLease, err = testClient.AcquireSubnetLease("10.244.4.5") + Expect(err).NotTo(HaveOccurred()) + + helpers.StopServer(session) + conf.Network = "10.254.0.0/16" + session = helpers.StartAndWaitForServer(controllerBinaryPath, conf, testClient) + + newNetworkLease, err = testClient.AcquireSubnetLease("10.244.4.6") + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns all the leases", func() { + leases, err := testClient.GetActiveLeases() + Expect(err).NotTo(HaveOccurred()) + + Expect(leases).To(ConsistOf(oldNetworkLease, newNetworkLease)) + }) + }) + }) + + It("assigns unique leases from the whole network to multiple clients acquiring subnets concurrently", func() { + parallelRunner := &testsupport.ParallelRunner{ + NumWorkers: 4, + } + nHosts := 255 + underlayIPs := []string{} + for i := 0; i < nHosts; i++ { + underlayIPs = append(underlayIPs, fmt.Sprintf("10.244.42.%d", i)) + } + + leases := make(chan (controller.Lease), nHosts) + go func() { + parallelRunner.RunOnSliceStrings(underlayIPs, func(underlayIP string) { + lease, err := testClient.AcquireSubnetLease(underlayIP) + Expect(err).NotTo(HaveOccurred()) + leases <- lease + }) + close(leases) + }() + + leaseIPs := make(map[string]struct{}) + leaseSubnets := make(map[string]struct{}) + _, network, err := net.ParseCIDR(conf.Network) + Expect(err).NotTo(HaveOccurred()) + + for lease := range leases { + _, subnet, err := net.ParseCIDR(lease.OverlaySubnet) + Expect(err).NotTo(HaveOccurred()) + Expect(network.Contains(subnet.IP)).To(BeTrue()) + + leaseIPs[lease.UnderlayIP] = struct{}{} + leaseSubnets[lease.OverlaySubnet] = struct{}{} + } + Expect(len(leaseIPs)).To(Equal(nHosts)) + Expect(len(leaseSubnets)).To(Equal(nHosts)) + }) + + It("assigns unique leases to multiple clients acquiring single ips concurrently", func() { + parallelRunner := &testsupport.ParallelRunner{ + NumWorkers: 4, + } + nHosts := 255 + var underlayIPs []string + for i := 0; i < nHosts; i++ { + underlayIPs = append(underlayIPs, fmt.Sprintf("10.244.0.%d", i)) + } + + leases := make(chan controller.Lease, nHosts) + go func() { + parallelRunner.RunOnSliceStrings(underlayIPs, func(underlayIP string) { + lease, err := testClient.AcquireSingleOverlayIPLease(underlayIP) + Expect(err).NotTo(HaveOccurred()) + leases <- lease + }) + close(leases) + }() + + leaseUnderlays := make(map[string]struct{}) + leaseIPs := make(map[string]struct{}) + _, network, err := net.ParseCIDR(conf.Network) + Expect(err).NotTo(HaveOccurred()) + + for lease := range leases { + _, subnet, err := net.ParseCIDR(lease.OverlaySubnet) + Expect(err).NotTo(HaveOccurred()) + Expect(network.Contains(subnet.IP)).To(BeTrue()) + Expect(lease.OverlaySubnet).To(ContainSubstring("/32")) + + leaseUnderlays[lease.UnderlayIP] = struct{}{} + leaseIPs[lease.OverlaySubnet] = struct{}{} + } + Expect(len(leaseUnderlays)).To(Equal(nHosts)) + Expect(len(leaseIPs)).To(Equal(nHosts)) + }) + + withName := func(name string) types.GomegaMatcher { + return WithTransform(func(ev metrics.Event) string { + return ev.Name + }, Equal(name)) + } + + withValue := func(value interface{}) types.GomegaMatcher { + return WithTransform(func(ev metrics.Event) float64 { + return ev.Value + }, BeEquivalentTo(value)) + } + + hasMetricWithValue := func(name string, value interface{}) types.GomegaMatcher { + return SatisfyAll(withName(name), withValue(value)) + } + + Describe("metrics", func() { + It("emits an uptime metric", func() { + Eventually(fakeMetron.AllEvents, "5s").Should(ContainElement(withName("uptime"))) + }) + + It("emits database metric", func() { + Eventually(fakeMetron.AllEvents, "5s").Should(ContainElement(withName("DBOpenConnections"))) + Eventually(fakeMetron.AllEvents, "5s").Should(ContainElement(withName("DBQueriesTotal"))) + Eventually(fakeMetron.AllEvents, "5s").Should(ContainElement(withName("DBQueriesSucceeded"))) + Eventually(fakeMetron.AllEvents, "5s").Should(ContainElement(withName("DBQueriesFailed"))) + Eventually(fakeMetron.AllEvents, "5s").Should(ContainElement(withName("DBQueriesInFlight"))) + Eventually(fakeMetron.AllEvents, "5s").Should(ContainElement(withName("DBQueryDurationMax"))) + }) + + Context("when some leases have been claimed", func() { + BeforeEach(func() { + _, err := testClient.AcquireSubnetLease("10.244.4.5") + Expect(err).NotTo(HaveOccurred()) + _, err = testClient.AcquireSubnetLease("10.244.4.6") + Expect(err).NotTo(HaveOccurred()) + }) + It("emits number of total leases", func() { + Eventually(fakeMetron.AllEvents, "10s").Should(ContainElement(hasMetricWithValue("totalLeases", 2))) + }) + It("emits number of free leases", func() { + Eventually(fakeMetron.AllEvents, "10s").Should(ContainElement(hasMetricWithValue("freeLeases", 253))) + }) + It("emits number of stale leases", func() { + Eventually(fakeMetron.AllEvents, "2s").Should(ContainElement(hasMetricWithValue("staleLeases", 0))) + Consistently(fakeMetron.AllEvents, "2s").Should(ContainElement(hasMetricWithValue("staleLeases", 0))) + Eventually(fakeMetron.AllEvents, "10s").Should(ContainElement(hasMetricWithValue("staleLeases", 2))) + }) + }) + + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/leaser/cidrpool.go b/src/code.cloudfoundry.org/silk/controller/leaser/cidrpool.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/leaser/cidrpool.go rename to src/code.cloudfoundry.org/silk/controller/leaser/cidrpool.go diff --git a/src/code.cloudfoundry.org/silk/controller/leaser/cidrpool_test.go b/src/code.cloudfoundry.org/silk/controller/leaser/cidrpool_test.go new file mode 100644 index 00000000..ccba84ba --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/leaser/cidrpool_test.go @@ -0,0 +1,213 @@ +package leaser_test + +import ( + "code.cloudfoundry.org/silk/controller/leaser" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "net" +) + +var _ = Describe("CIDRPool", func() { + Describe("BlockPoolSize", func() { + DescribeTable("returns the number of subnets that can be allocated", + func(subnetRange string, subnetMask, expectedSize int) { + cidrPool := leaser.NewCIDRPool(subnetRange, subnetMask) + Expect(cidrPool.BlockPoolSize()).To(Equal(expectedSize)) + }, + Entry("when the range is /16 and mask is /24", "10.255.0.0/16", 24, 255), + Entry("when the range is /16 and mask is /20", "10.255.0.0/16", 20, 15), + Entry("when the range is /16 and mask is /16", "10.255.0.0/16", 16, 0), + ) + + DescribeTable("produces valid subnets within the correct range", + func(overlayCIDR string, subnetMask int) { + _, overlayNetwork, _ := net.ParseCIDR(overlayCIDR) + cidrPool := leaser.NewCIDRPool(overlayCIDR, subnetMask) + + for blockDividedCIDR, _ := range cidrPool.GetBlockPool() { + _, blockNetwork, _ := net.ParseCIDR(blockDividedCIDR) + Expect(overlayNetwork.Contains(blockNetwork.IP)).Should(BeTrue()) + } + }, + Entry("when ip is in the start of the cidr range", "10.240.0.0/12", 24), + Entry("when ip is in the middle of the cidr range", "10.255.0.0/12", 24), + Entry("when ip is in the end of the cidr range", "10.255.255.255/12", 24), + ) + }) + + Describe("SingleIPPoolSize", func() { + DescribeTable("returns the number of subnets that can be allocated", + func(subnetRange string, subnetMask, expectedSize int) { + cidrPool := leaser.NewCIDRPool(subnetRange, subnetMask) + Expect(cidrPool.SingleIPPoolSize()).To(Equal(expectedSize)) + }, + Entry("when the range is /16 and mask is /25", "10.255.0.0/16", 25, 127), + Entry("when the range is /16 and mask is /26", "10.255.0.0/16", 26, 63), + Entry("when the range is /16 and mask is /27", "10.255.0.0/16", 27, 31), + ) + + It("produces valid subnet starting with the first IP of the cidr", func() { + overlayCIDR := "10.255.0.0/12" + subnetMask := 24 + firstSubnet := "10.240.0.0/24" + + cidrPool := leaser.NewCIDRPool(overlayCIDR, subnetMask) + _, expectedSingleIPNetwork, _ := net.ParseCIDR(firstSubnet) + for singleIPCIDR, _ := range cidrPool.GetSinglePool() { + _, singleIPNetwork, _ := net.ParseCIDR(singleIPCIDR) + Expect(expectedSingleIPNetwork.Contains(singleIPNetwork.IP)).Should(BeTrue()) + } + }) + }) + + Describe("GetAvailableBlock", func() { + It("returns a subnet from the pool that is not taken", func() { + subnetRange := "10.255.0.0/16" + _, network, _ := net.ParseCIDR(subnetRange) + cidrPool := leaser.NewCIDRPool(subnetRange, 24) + + results := map[string]int{} + + var taken []string + for i := 0; i < 255; i++ { + s := cidrPool.GetAvailableBlock(taken) + results[s]++ + taken = append(taken, s) + } + Expect(len(results)).To(Equal(255)) + + for result := range results { + _, subnet, err := net.ParseCIDR(result) + Expect(err).NotTo(HaveOccurred()) + Expect(network.Contains(subnet.IP)).To(BeTrue()) + Expect(subnet.Mask).To(Equal(net.IPMask{255, 255, 255, 0})) + // first subnet from range is never allocated + Expect(subnet.IP.To4()).NotTo(Equal(network.IP.To4())) + } + }) + + Context("when no subnets are available", func() { + It("returns an empty string", func() { + subnetRange := "10.255.0.0/16" + cidrPool := leaser.NewCIDRPool(subnetRange, 24) + var taken []string + for i := 0; i < 255; i++ { + s := cidrPool.GetAvailableBlock(taken) + taken = append(taken, s) + } + s := cidrPool.GetAvailableBlock(taken) + Expect(s).To(Equal("")) + }) + }) + }) + + Describe("GetAvailableSingleIP", func() { + It("returns a single ip that is not taken", func() { + subnetRange := "10.255.0.0/16" + _, network, _ := net.ParseCIDR(subnetRange) + cidrPool := leaser.NewCIDRPool(subnetRange, 24) + + results := map[string]int{} + + var taken []string + for i := 0; i < 255; i++ { + s := cidrPool.GetAvailableSingleIP(taken) + results[s]++ + taken = append(taken, s) + } + Expect(len(results)).To(Equal(255)) + + for result := range results { + _, subnet, err := net.ParseCIDR(result) + Expect(err).NotTo(HaveOccurred()) + Expect(network.Contains(subnet.IP)).To(BeTrue()) + Expect(subnet.Mask).To(Equal(net.IPMask{255, 255, 255, 255})) + // 10.255.0.0 should never be allocated? Maybe? + Expect(subnet.IP.To4()).NotTo(Equal(network.IP.To4())) + } + }) + + Context("when the subnet mask is 29", func() { + It("returns ips containing only .1-.7", func() { + subnetRange := "10.255.0.0/16" + _, network, _ := net.ParseCIDR(subnetRange) + cidrPool := leaser.NewCIDRPool(subnetRange, 29) + + results := map[string]int{} + + var taken []string + for i := 0; i < 7; i++ { + s := cidrPool.GetAvailableSingleIP(taken) + results[s]++ + taken = append(taken, s) + } + Expect(len(results)).To(Equal(7)) + + Expect(results).To(Equal(map[string]int{ + "10.255.0.1/32": 1, + "10.255.0.2/32": 1, + "10.255.0.3/32": 1, + "10.255.0.4/32": 1, + "10.255.0.5/32": 1, + "10.255.0.6/32": 1, + "10.255.0.7/32": 1, + })) + + for result := range results { + _, subnet, err := net.ParseCIDR(result) + Expect(err).NotTo(HaveOccurred()) + Expect(network.Contains(subnet.IP)).To(BeTrue()) + Expect(subnet.Mask).To(Equal(net.IPMask{255, 255, 255, 255})) + // 10.255.0.0 should never be allocated? Maybe? + Expect(subnet.IP.To4()).NotTo(Equal(network.IP.To4())) + } + }) + }) + + Context("when no subnets are available", func() { + It("returns an empty string", func() { + subnetRange := "10.255.0.0/16" + cidrPool := leaser.NewCIDRPool(subnetRange, 24) + var taken []string + for i := 0; i < 255; i++ { + s := cidrPool.GetAvailableSingleIP(taken) + taken = append(taken, s) + } + s := cidrPool.GetAvailableSingleIP(taken) + Expect(s).To(Equal("")) + }) + }) + }) + + Describe("IsMember", func() { + var cidrPool *leaser.CIDRPool + BeforeEach(func() { + subnetRange := "10.255.0.0/16" + cidrPool = leaser.NewCIDRPool(subnetRange, 24) + }) + + Context("when the subnet is in the block pool and not in the single pool", func() { + It("returns true", func() { + Expect(cidrPool.IsMember("10.255.30.0/24")).To(BeTrue()) + }) + }) + + Context("when the subnet is in the single pool and not in the block pool", func() { + It("returns true", func() { + Expect(cidrPool.IsMember("10.255.0.5/32")).To(BeTrue()) + }) + }) + + Context("when the subnet start is not a match for an entry", func() { + It("returns false", func() { + Expect(cidrPool.IsMember("10.255.30.10/24")).To(BeFalse()) + }) + }) + + Context("when the subnet size is not a match", func() { + It("returns false", func() { + Expect(cidrPool.IsMember("10.255.30.0/20")).To(BeFalse()) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/controller/leaser/fakes/cidr_pool.go b/src/code.cloudfoundry.org/silk/controller/leaser/fakes/cidr_pool.go new file mode 100644 index 00000000..a0303060 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/leaser/fakes/cidr_pool.go @@ -0,0 +1,226 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" +) + +type CIDRPool struct { + GetAvailableBlockStub func([]string) string + getAvailableBlockMutex sync.RWMutex + getAvailableBlockArgsForCall []struct { + arg1 []string + } + getAvailableBlockReturns struct { + result1 string + } + getAvailableBlockReturnsOnCall map[int]struct { + result1 string + } + GetAvailableSingleIPStub func([]string) string + getAvailableSingleIPMutex sync.RWMutex + getAvailableSingleIPArgsForCall []struct { + arg1 []string + } + getAvailableSingleIPReturns struct { + result1 string + } + getAvailableSingleIPReturnsOnCall map[int]struct { + result1 string + } + IsMemberStub func(string) bool + isMemberMutex sync.RWMutex + isMemberArgsForCall []struct { + arg1 string + } + isMemberReturns struct { + result1 bool + } + isMemberReturnsOnCall map[int]struct { + result1 bool + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *CIDRPool) GetAvailableBlock(arg1 []string) string { + var arg1Copy []string + if arg1 != nil { + arg1Copy = make([]string, len(arg1)) + copy(arg1Copy, arg1) + } + fake.getAvailableBlockMutex.Lock() + ret, specificReturn := fake.getAvailableBlockReturnsOnCall[len(fake.getAvailableBlockArgsForCall)] + fake.getAvailableBlockArgsForCall = append(fake.getAvailableBlockArgsForCall, struct { + arg1 []string + }{arg1Copy}) + fake.recordInvocation("GetAvailableBlock", []interface{}{arg1Copy}) + fake.getAvailableBlockMutex.Unlock() + if fake.GetAvailableBlockStub != nil { + return fake.GetAvailableBlockStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.getAvailableBlockReturns.result1 +} + +func (fake *CIDRPool) GetAvailableBlockCallCount() int { + fake.getAvailableBlockMutex.RLock() + defer fake.getAvailableBlockMutex.RUnlock() + return len(fake.getAvailableBlockArgsForCall) +} + +func (fake *CIDRPool) GetAvailableBlockArgsForCall(i int) []string { + fake.getAvailableBlockMutex.RLock() + defer fake.getAvailableBlockMutex.RUnlock() + return fake.getAvailableBlockArgsForCall[i].arg1 +} + +func (fake *CIDRPool) GetAvailableBlockReturns(result1 string) { + fake.GetAvailableBlockStub = nil + fake.getAvailableBlockReturns = struct { + result1 string + }{result1} +} + +func (fake *CIDRPool) GetAvailableBlockReturnsOnCall(i int, result1 string) { + fake.GetAvailableBlockStub = nil + if fake.getAvailableBlockReturnsOnCall == nil { + fake.getAvailableBlockReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.getAvailableBlockReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *CIDRPool) GetAvailableSingleIP(arg1 []string) string { + var arg1Copy []string + if arg1 != nil { + arg1Copy = make([]string, len(arg1)) + copy(arg1Copy, arg1) + } + fake.getAvailableSingleIPMutex.Lock() + ret, specificReturn := fake.getAvailableSingleIPReturnsOnCall[len(fake.getAvailableSingleIPArgsForCall)] + fake.getAvailableSingleIPArgsForCall = append(fake.getAvailableSingleIPArgsForCall, struct { + arg1 []string + }{arg1Copy}) + fake.recordInvocation("GetAvailableSingleIP", []interface{}{arg1Copy}) + fake.getAvailableSingleIPMutex.Unlock() + if fake.GetAvailableSingleIPStub != nil { + return fake.GetAvailableSingleIPStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.getAvailableSingleIPReturns.result1 +} + +func (fake *CIDRPool) GetAvailableSingleIPCallCount() int { + fake.getAvailableSingleIPMutex.RLock() + defer fake.getAvailableSingleIPMutex.RUnlock() + return len(fake.getAvailableSingleIPArgsForCall) +} + +func (fake *CIDRPool) GetAvailableSingleIPArgsForCall(i int) []string { + fake.getAvailableSingleIPMutex.RLock() + defer fake.getAvailableSingleIPMutex.RUnlock() + return fake.getAvailableSingleIPArgsForCall[i].arg1 +} + +func (fake *CIDRPool) GetAvailableSingleIPReturns(result1 string) { + fake.GetAvailableSingleIPStub = nil + fake.getAvailableSingleIPReturns = struct { + result1 string + }{result1} +} + +func (fake *CIDRPool) GetAvailableSingleIPReturnsOnCall(i int, result1 string) { + fake.GetAvailableSingleIPStub = nil + if fake.getAvailableSingleIPReturnsOnCall == nil { + fake.getAvailableSingleIPReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.getAvailableSingleIPReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *CIDRPool) IsMember(arg1 string) bool { + fake.isMemberMutex.Lock() + ret, specificReturn := fake.isMemberReturnsOnCall[len(fake.isMemberArgsForCall)] + fake.isMemberArgsForCall = append(fake.isMemberArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("IsMember", []interface{}{arg1}) + fake.isMemberMutex.Unlock() + if fake.IsMemberStub != nil { + return fake.IsMemberStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.isMemberReturns.result1 +} + +func (fake *CIDRPool) IsMemberCallCount() int { + fake.isMemberMutex.RLock() + defer fake.isMemberMutex.RUnlock() + return len(fake.isMemberArgsForCall) +} + +func (fake *CIDRPool) IsMemberArgsForCall(i int) string { + fake.isMemberMutex.RLock() + defer fake.isMemberMutex.RUnlock() + return fake.isMemberArgsForCall[i].arg1 +} + +func (fake *CIDRPool) IsMemberReturns(result1 bool) { + fake.IsMemberStub = nil + fake.isMemberReturns = struct { + result1 bool + }{result1} +} + +func (fake *CIDRPool) IsMemberReturnsOnCall(i int, result1 bool) { + fake.IsMemberStub = nil + if fake.isMemberReturnsOnCall == nil { + fake.isMemberReturnsOnCall = make(map[int]struct { + result1 bool + }) + } + fake.isMemberReturnsOnCall[i] = struct { + result1 bool + }{result1} +} + +func (fake *CIDRPool) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getAvailableBlockMutex.RLock() + defer fake.getAvailableBlockMutex.RUnlock() + fake.getAvailableSingleIPMutex.RLock() + defer fake.getAvailableSingleIPMutex.RUnlock() + fake.isMemberMutex.RLock() + defer fake.isMemberMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *CIDRPool) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/controller/leaser/fakes/database_handler.go b/src/code.cloudfoundry.org/silk/controller/leaser/fakes/database_handler.go new file mode 100644 index 00000000..3b519847 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/leaser/fakes/database_handler.go @@ -0,0 +1,716 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + + "code.cloudfoundry.org/silk/controller" +) + +type DatabaseHandler struct { + AddEntryStub func(controller.Lease) error + addEntryMutex sync.RWMutex + addEntryArgsForCall []struct { + arg1 controller.Lease + } + addEntryReturns struct { + result1 error + } + addEntryReturnsOnCall map[int]struct { + result1 error + } + DeleteEntryStub func(string) error + deleteEntryMutex sync.RWMutex + deleteEntryArgsForCall []struct { + arg1 string + } + deleteEntryReturns struct { + result1 error + } + deleteEntryReturnsOnCall map[int]struct { + result1 error + } + LeaseForUnderlayIPStub func(string) (*controller.Lease, error) + leaseForUnderlayIPMutex sync.RWMutex + leaseForUnderlayIPArgsForCall []struct { + arg1 string + } + leaseForUnderlayIPReturns struct { + result1 *controller.Lease + result2 error + } + leaseForUnderlayIPReturnsOnCall map[int]struct { + result1 *controller.Lease + result2 error + } + LastRenewedAtForUnderlayIPStub func(string) (int64, error) + lastRenewedAtForUnderlayIPMutex sync.RWMutex + lastRenewedAtForUnderlayIPArgsForCall []struct { + arg1 string + } + lastRenewedAtForUnderlayIPReturns struct { + result1 int64 + result2 error + } + lastRenewedAtForUnderlayIPReturnsOnCall map[int]struct { + result1 int64 + result2 error + } + RenewLeaseForUnderlayIPStub func(string) error + renewLeaseForUnderlayIPMutex sync.RWMutex + renewLeaseForUnderlayIPArgsForCall []struct { + arg1 string + } + renewLeaseForUnderlayIPReturns struct { + result1 error + } + renewLeaseForUnderlayIPReturnsOnCall map[int]struct { + result1 error + } + AllStub func() ([]controller.Lease, error) + allMutex sync.RWMutex + allArgsForCall []struct{} + allReturns struct { + result1 []controller.Lease + result2 error + } + allReturnsOnCall map[int]struct { + result1 []controller.Lease + result2 error + } + AllBlockSubnetsStub func() ([]controller.Lease, error) + allBlockSubnetsMutex sync.RWMutex + allBlockSubnetsArgsForCall []struct{} + allBlockSubnetsReturns struct { + result1 []controller.Lease + result2 error + } + allBlockSubnetsReturnsOnCall map[int]struct { + result1 []controller.Lease + result2 error + } + AllSingleIPSubnetsStub func() ([]controller.Lease, error) + allSingleIPSubnetsMutex sync.RWMutex + allSingleIPSubnetsArgsForCall []struct{} + allSingleIPSubnetsReturns struct { + result1 []controller.Lease + result2 error + } + allSingleIPSubnetsReturnsOnCall map[int]struct { + result1 []controller.Lease + result2 error + } + AllActiveStub func(int) ([]controller.Lease, error) + allActiveMutex sync.RWMutex + allActiveArgsForCall []struct { + arg1 int + } + allActiveReturns struct { + result1 []controller.Lease + result2 error + } + allActiveReturnsOnCall map[int]struct { + result1 []controller.Lease + result2 error + } + OldestExpiredBlockSubnetStub func(int) (*controller.Lease, error) + oldestExpiredBlockSubnetMutex sync.RWMutex + oldestExpiredBlockSubnetArgsForCall []struct { + arg1 int + } + oldestExpiredBlockSubnetReturns struct { + result1 *controller.Lease + result2 error + } + oldestExpiredBlockSubnetReturnsOnCall map[int]struct { + result1 *controller.Lease + result2 error + } + OldestExpiredSingleIPStub func(int) (*controller.Lease, error) + oldestExpiredSingleIPMutex sync.RWMutex + oldestExpiredSingleIPArgsForCall []struct { + arg1 int + } + oldestExpiredSingleIPReturns struct { + result1 *controller.Lease + result2 error + } + oldestExpiredSingleIPReturnsOnCall map[int]struct { + result1 *controller.Lease + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *DatabaseHandler) AddEntry(arg1 controller.Lease) error { + fake.addEntryMutex.Lock() + ret, specificReturn := fake.addEntryReturnsOnCall[len(fake.addEntryArgsForCall)] + fake.addEntryArgsForCall = append(fake.addEntryArgsForCall, struct { + arg1 controller.Lease + }{arg1}) + fake.recordInvocation("AddEntry", []interface{}{arg1}) + fake.addEntryMutex.Unlock() + if fake.AddEntryStub != nil { + return fake.AddEntryStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.addEntryReturns.result1 +} + +func (fake *DatabaseHandler) AddEntryCallCount() int { + fake.addEntryMutex.RLock() + defer fake.addEntryMutex.RUnlock() + return len(fake.addEntryArgsForCall) +} + +func (fake *DatabaseHandler) AddEntryArgsForCall(i int) controller.Lease { + fake.addEntryMutex.RLock() + defer fake.addEntryMutex.RUnlock() + return fake.addEntryArgsForCall[i].arg1 +} + +func (fake *DatabaseHandler) AddEntryReturns(result1 error) { + fake.AddEntryStub = nil + fake.addEntryReturns = struct { + result1 error + }{result1} +} + +func (fake *DatabaseHandler) AddEntryReturnsOnCall(i int, result1 error) { + fake.AddEntryStub = nil + if fake.addEntryReturnsOnCall == nil { + fake.addEntryReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.addEntryReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *DatabaseHandler) DeleteEntry(arg1 string) error { + fake.deleteEntryMutex.Lock() + ret, specificReturn := fake.deleteEntryReturnsOnCall[len(fake.deleteEntryArgsForCall)] + fake.deleteEntryArgsForCall = append(fake.deleteEntryArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("DeleteEntry", []interface{}{arg1}) + fake.deleteEntryMutex.Unlock() + if fake.DeleteEntryStub != nil { + return fake.DeleteEntryStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.deleteEntryReturns.result1 +} + +func (fake *DatabaseHandler) DeleteEntryCallCount() int { + fake.deleteEntryMutex.RLock() + defer fake.deleteEntryMutex.RUnlock() + return len(fake.deleteEntryArgsForCall) +} + +func (fake *DatabaseHandler) DeleteEntryArgsForCall(i int) string { + fake.deleteEntryMutex.RLock() + defer fake.deleteEntryMutex.RUnlock() + return fake.deleteEntryArgsForCall[i].arg1 +} + +func (fake *DatabaseHandler) DeleteEntryReturns(result1 error) { + fake.DeleteEntryStub = nil + fake.deleteEntryReturns = struct { + result1 error + }{result1} +} + +func (fake *DatabaseHandler) DeleteEntryReturnsOnCall(i int, result1 error) { + fake.DeleteEntryStub = nil + if fake.deleteEntryReturnsOnCall == nil { + fake.deleteEntryReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteEntryReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *DatabaseHandler) LeaseForUnderlayIP(arg1 string) (*controller.Lease, error) { + fake.leaseForUnderlayIPMutex.Lock() + ret, specificReturn := fake.leaseForUnderlayIPReturnsOnCall[len(fake.leaseForUnderlayIPArgsForCall)] + fake.leaseForUnderlayIPArgsForCall = append(fake.leaseForUnderlayIPArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("LeaseForUnderlayIP", []interface{}{arg1}) + fake.leaseForUnderlayIPMutex.Unlock() + if fake.LeaseForUnderlayIPStub != nil { + return fake.LeaseForUnderlayIPStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.leaseForUnderlayIPReturns.result1, fake.leaseForUnderlayIPReturns.result2 +} + +func (fake *DatabaseHandler) LeaseForUnderlayIPCallCount() int { + fake.leaseForUnderlayIPMutex.RLock() + defer fake.leaseForUnderlayIPMutex.RUnlock() + return len(fake.leaseForUnderlayIPArgsForCall) +} + +func (fake *DatabaseHandler) LeaseForUnderlayIPArgsForCall(i int) string { + fake.leaseForUnderlayIPMutex.RLock() + defer fake.leaseForUnderlayIPMutex.RUnlock() + return fake.leaseForUnderlayIPArgsForCall[i].arg1 +} + +func (fake *DatabaseHandler) LeaseForUnderlayIPReturns(result1 *controller.Lease, result2 error) { + fake.LeaseForUnderlayIPStub = nil + fake.leaseForUnderlayIPReturns = struct { + result1 *controller.Lease + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) LeaseForUnderlayIPReturnsOnCall(i int, result1 *controller.Lease, result2 error) { + fake.LeaseForUnderlayIPStub = nil + if fake.leaseForUnderlayIPReturnsOnCall == nil { + fake.leaseForUnderlayIPReturnsOnCall = make(map[int]struct { + result1 *controller.Lease + result2 error + }) + } + fake.leaseForUnderlayIPReturnsOnCall[i] = struct { + result1 *controller.Lease + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) LastRenewedAtForUnderlayIP(arg1 string) (int64, error) { + fake.lastRenewedAtForUnderlayIPMutex.Lock() + ret, specificReturn := fake.lastRenewedAtForUnderlayIPReturnsOnCall[len(fake.lastRenewedAtForUnderlayIPArgsForCall)] + fake.lastRenewedAtForUnderlayIPArgsForCall = append(fake.lastRenewedAtForUnderlayIPArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("LastRenewedAtForUnderlayIP", []interface{}{arg1}) + fake.lastRenewedAtForUnderlayIPMutex.Unlock() + if fake.LastRenewedAtForUnderlayIPStub != nil { + return fake.LastRenewedAtForUnderlayIPStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.lastRenewedAtForUnderlayIPReturns.result1, fake.lastRenewedAtForUnderlayIPReturns.result2 +} + +func (fake *DatabaseHandler) LastRenewedAtForUnderlayIPCallCount() int { + fake.lastRenewedAtForUnderlayIPMutex.RLock() + defer fake.lastRenewedAtForUnderlayIPMutex.RUnlock() + return len(fake.lastRenewedAtForUnderlayIPArgsForCall) +} + +func (fake *DatabaseHandler) LastRenewedAtForUnderlayIPArgsForCall(i int) string { + fake.lastRenewedAtForUnderlayIPMutex.RLock() + defer fake.lastRenewedAtForUnderlayIPMutex.RUnlock() + return fake.lastRenewedAtForUnderlayIPArgsForCall[i].arg1 +} + +func (fake *DatabaseHandler) LastRenewedAtForUnderlayIPReturns(result1 int64, result2 error) { + fake.LastRenewedAtForUnderlayIPStub = nil + fake.lastRenewedAtForUnderlayIPReturns = struct { + result1 int64 + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) LastRenewedAtForUnderlayIPReturnsOnCall(i int, result1 int64, result2 error) { + fake.LastRenewedAtForUnderlayIPStub = nil + if fake.lastRenewedAtForUnderlayIPReturnsOnCall == nil { + fake.lastRenewedAtForUnderlayIPReturnsOnCall = make(map[int]struct { + result1 int64 + result2 error + }) + } + fake.lastRenewedAtForUnderlayIPReturnsOnCall[i] = struct { + result1 int64 + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) RenewLeaseForUnderlayIP(arg1 string) error { + fake.renewLeaseForUnderlayIPMutex.Lock() + ret, specificReturn := fake.renewLeaseForUnderlayIPReturnsOnCall[len(fake.renewLeaseForUnderlayIPArgsForCall)] + fake.renewLeaseForUnderlayIPArgsForCall = append(fake.renewLeaseForUnderlayIPArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("RenewLeaseForUnderlayIP", []interface{}{arg1}) + fake.renewLeaseForUnderlayIPMutex.Unlock() + if fake.RenewLeaseForUnderlayIPStub != nil { + return fake.RenewLeaseForUnderlayIPStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.renewLeaseForUnderlayIPReturns.result1 +} + +func (fake *DatabaseHandler) RenewLeaseForUnderlayIPCallCount() int { + fake.renewLeaseForUnderlayIPMutex.RLock() + defer fake.renewLeaseForUnderlayIPMutex.RUnlock() + return len(fake.renewLeaseForUnderlayIPArgsForCall) +} + +func (fake *DatabaseHandler) RenewLeaseForUnderlayIPArgsForCall(i int) string { + fake.renewLeaseForUnderlayIPMutex.RLock() + defer fake.renewLeaseForUnderlayIPMutex.RUnlock() + return fake.renewLeaseForUnderlayIPArgsForCall[i].arg1 +} + +func (fake *DatabaseHandler) RenewLeaseForUnderlayIPReturns(result1 error) { + fake.RenewLeaseForUnderlayIPStub = nil + fake.renewLeaseForUnderlayIPReturns = struct { + result1 error + }{result1} +} + +func (fake *DatabaseHandler) RenewLeaseForUnderlayIPReturnsOnCall(i int, result1 error) { + fake.RenewLeaseForUnderlayIPStub = nil + if fake.renewLeaseForUnderlayIPReturnsOnCall == nil { + fake.renewLeaseForUnderlayIPReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.renewLeaseForUnderlayIPReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *DatabaseHandler) All() ([]controller.Lease, error) { + fake.allMutex.Lock() + ret, specificReturn := fake.allReturnsOnCall[len(fake.allArgsForCall)] + fake.allArgsForCall = append(fake.allArgsForCall, struct{}{}) + fake.recordInvocation("All", []interface{}{}) + fake.allMutex.Unlock() + if fake.AllStub != nil { + return fake.AllStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.allReturns.result1, fake.allReturns.result2 +} + +func (fake *DatabaseHandler) AllCallCount() int { + fake.allMutex.RLock() + defer fake.allMutex.RUnlock() + return len(fake.allArgsForCall) +} + +func (fake *DatabaseHandler) AllReturns(result1 []controller.Lease, result2 error) { + fake.AllStub = nil + fake.allReturns = struct { + result1 []controller.Lease + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) AllReturnsOnCall(i int, result1 []controller.Lease, result2 error) { + fake.AllStub = nil + if fake.allReturnsOnCall == nil { + fake.allReturnsOnCall = make(map[int]struct { + result1 []controller.Lease + result2 error + }) + } + fake.allReturnsOnCall[i] = struct { + result1 []controller.Lease + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) AllBlockSubnets() ([]controller.Lease, error) { + fake.allBlockSubnetsMutex.Lock() + ret, specificReturn := fake.allBlockSubnetsReturnsOnCall[len(fake.allBlockSubnetsArgsForCall)] + fake.allBlockSubnetsArgsForCall = append(fake.allBlockSubnetsArgsForCall, struct{}{}) + fake.recordInvocation("AllBlockSubnets", []interface{}{}) + fake.allBlockSubnetsMutex.Unlock() + if fake.AllBlockSubnetsStub != nil { + return fake.AllBlockSubnetsStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.allBlockSubnetsReturns.result1, fake.allBlockSubnetsReturns.result2 +} + +func (fake *DatabaseHandler) AllBlockSubnetsCallCount() int { + fake.allBlockSubnetsMutex.RLock() + defer fake.allBlockSubnetsMutex.RUnlock() + return len(fake.allBlockSubnetsArgsForCall) +} + +func (fake *DatabaseHandler) AllBlockSubnetsReturns(result1 []controller.Lease, result2 error) { + fake.AllBlockSubnetsStub = nil + fake.allBlockSubnetsReturns = struct { + result1 []controller.Lease + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) AllBlockSubnetsReturnsOnCall(i int, result1 []controller.Lease, result2 error) { + fake.AllBlockSubnetsStub = nil + if fake.allBlockSubnetsReturnsOnCall == nil { + fake.allBlockSubnetsReturnsOnCall = make(map[int]struct { + result1 []controller.Lease + result2 error + }) + } + fake.allBlockSubnetsReturnsOnCall[i] = struct { + result1 []controller.Lease + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) AllSingleIPSubnets() ([]controller.Lease, error) { + fake.allSingleIPSubnetsMutex.Lock() + ret, specificReturn := fake.allSingleIPSubnetsReturnsOnCall[len(fake.allSingleIPSubnetsArgsForCall)] + fake.allSingleIPSubnetsArgsForCall = append(fake.allSingleIPSubnetsArgsForCall, struct{}{}) + fake.recordInvocation("AllSingleIPSubnets", []interface{}{}) + fake.allSingleIPSubnetsMutex.Unlock() + if fake.AllSingleIPSubnetsStub != nil { + return fake.AllSingleIPSubnetsStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.allSingleIPSubnetsReturns.result1, fake.allSingleIPSubnetsReturns.result2 +} + +func (fake *DatabaseHandler) AllSingleIPSubnetsCallCount() int { + fake.allSingleIPSubnetsMutex.RLock() + defer fake.allSingleIPSubnetsMutex.RUnlock() + return len(fake.allSingleIPSubnetsArgsForCall) +} + +func (fake *DatabaseHandler) AllSingleIPSubnetsReturns(result1 []controller.Lease, result2 error) { + fake.AllSingleIPSubnetsStub = nil + fake.allSingleIPSubnetsReturns = struct { + result1 []controller.Lease + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) AllSingleIPSubnetsReturnsOnCall(i int, result1 []controller.Lease, result2 error) { + fake.AllSingleIPSubnetsStub = nil + if fake.allSingleIPSubnetsReturnsOnCall == nil { + fake.allSingleIPSubnetsReturnsOnCall = make(map[int]struct { + result1 []controller.Lease + result2 error + }) + } + fake.allSingleIPSubnetsReturnsOnCall[i] = struct { + result1 []controller.Lease + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) AllActive(arg1 int) ([]controller.Lease, error) { + fake.allActiveMutex.Lock() + ret, specificReturn := fake.allActiveReturnsOnCall[len(fake.allActiveArgsForCall)] + fake.allActiveArgsForCall = append(fake.allActiveArgsForCall, struct { + arg1 int + }{arg1}) + fake.recordInvocation("AllActive", []interface{}{arg1}) + fake.allActiveMutex.Unlock() + if fake.AllActiveStub != nil { + return fake.AllActiveStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.allActiveReturns.result1, fake.allActiveReturns.result2 +} + +func (fake *DatabaseHandler) AllActiveCallCount() int { + fake.allActiveMutex.RLock() + defer fake.allActiveMutex.RUnlock() + return len(fake.allActiveArgsForCall) +} + +func (fake *DatabaseHandler) AllActiveArgsForCall(i int) int { + fake.allActiveMutex.RLock() + defer fake.allActiveMutex.RUnlock() + return fake.allActiveArgsForCall[i].arg1 +} + +func (fake *DatabaseHandler) AllActiveReturns(result1 []controller.Lease, result2 error) { + fake.AllActiveStub = nil + fake.allActiveReturns = struct { + result1 []controller.Lease + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) AllActiveReturnsOnCall(i int, result1 []controller.Lease, result2 error) { + fake.AllActiveStub = nil + if fake.allActiveReturnsOnCall == nil { + fake.allActiveReturnsOnCall = make(map[int]struct { + result1 []controller.Lease + result2 error + }) + } + fake.allActiveReturnsOnCall[i] = struct { + result1 []controller.Lease + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) OldestExpiredBlockSubnet(arg1 int) (*controller.Lease, error) { + fake.oldestExpiredBlockSubnetMutex.Lock() + ret, specificReturn := fake.oldestExpiredBlockSubnetReturnsOnCall[len(fake.oldestExpiredBlockSubnetArgsForCall)] + fake.oldestExpiredBlockSubnetArgsForCall = append(fake.oldestExpiredBlockSubnetArgsForCall, struct { + arg1 int + }{arg1}) + fake.recordInvocation("OldestExpiredBlockSubnet", []interface{}{arg1}) + fake.oldestExpiredBlockSubnetMutex.Unlock() + if fake.OldestExpiredBlockSubnetStub != nil { + return fake.OldestExpiredBlockSubnetStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.oldestExpiredBlockSubnetReturns.result1, fake.oldestExpiredBlockSubnetReturns.result2 +} + +func (fake *DatabaseHandler) OldestExpiredBlockSubnetCallCount() int { + fake.oldestExpiredBlockSubnetMutex.RLock() + defer fake.oldestExpiredBlockSubnetMutex.RUnlock() + return len(fake.oldestExpiredBlockSubnetArgsForCall) +} + +func (fake *DatabaseHandler) OldestExpiredBlockSubnetArgsForCall(i int) int { + fake.oldestExpiredBlockSubnetMutex.RLock() + defer fake.oldestExpiredBlockSubnetMutex.RUnlock() + return fake.oldestExpiredBlockSubnetArgsForCall[i].arg1 +} + +func (fake *DatabaseHandler) OldestExpiredBlockSubnetReturns(result1 *controller.Lease, result2 error) { + fake.OldestExpiredBlockSubnetStub = nil + fake.oldestExpiredBlockSubnetReturns = struct { + result1 *controller.Lease + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) OldestExpiredBlockSubnetReturnsOnCall(i int, result1 *controller.Lease, result2 error) { + fake.OldestExpiredBlockSubnetStub = nil + if fake.oldestExpiredBlockSubnetReturnsOnCall == nil { + fake.oldestExpiredBlockSubnetReturnsOnCall = make(map[int]struct { + result1 *controller.Lease + result2 error + }) + } + fake.oldestExpiredBlockSubnetReturnsOnCall[i] = struct { + result1 *controller.Lease + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) OldestExpiredSingleIP(arg1 int) (*controller.Lease, error) { + fake.oldestExpiredSingleIPMutex.Lock() + ret, specificReturn := fake.oldestExpiredSingleIPReturnsOnCall[len(fake.oldestExpiredSingleIPArgsForCall)] + fake.oldestExpiredSingleIPArgsForCall = append(fake.oldestExpiredSingleIPArgsForCall, struct { + arg1 int + }{arg1}) + fake.recordInvocation("OldestExpiredSingleIP", []interface{}{arg1}) + fake.oldestExpiredSingleIPMutex.Unlock() + if fake.OldestExpiredSingleIPStub != nil { + return fake.OldestExpiredSingleIPStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.oldestExpiredSingleIPReturns.result1, fake.oldestExpiredSingleIPReturns.result2 +} + +func (fake *DatabaseHandler) OldestExpiredSingleIPCallCount() int { + fake.oldestExpiredSingleIPMutex.RLock() + defer fake.oldestExpiredSingleIPMutex.RUnlock() + return len(fake.oldestExpiredSingleIPArgsForCall) +} + +func (fake *DatabaseHandler) OldestExpiredSingleIPArgsForCall(i int) int { + fake.oldestExpiredSingleIPMutex.RLock() + defer fake.oldestExpiredSingleIPMutex.RUnlock() + return fake.oldestExpiredSingleIPArgsForCall[i].arg1 +} + +func (fake *DatabaseHandler) OldestExpiredSingleIPReturns(result1 *controller.Lease, result2 error) { + fake.OldestExpiredSingleIPStub = nil + fake.oldestExpiredSingleIPReturns = struct { + result1 *controller.Lease + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) OldestExpiredSingleIPReturnsOnCall(i int, result1 *controller.Lease, result2 error) { + fake.OldestExpiredSingleIPStub = nil + if fake.oldestExpiredSingleIPReturnsOnCall == nil { + fake.oldestExpiredSingleIPReturnsOnCall = make(map[int]struct { + result1 *controller.Lease + result2 error + }) + } + fake.oldestExpiredSingleIPReturnsOnCall[i] = struct { + result1 *controller.Lease + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.addEntryMutex.RLock() + defer fake.addEntryMutex.RUnlock() + fake.deleteEntryMutex.RLock() + defer fake.deleteEntryMutex.RUnlock() + fake.leaseForUnderlayIPMutex.RLock() + defer fake.leaseForUnderlayIPMutex.RUnlock() + fake.lastRenewedAtForUnderlayIPMutex.RLock() + defer fake.lastRenewedAtForUnderlayIPMutex.RUnlock() + fake.renewLeaseForUnderlayIPMutex.RLock() + defer fake.renewLeaseForUnderlayIPMutex.RUnlock() + fake.allMutex.RLock() + defer fake.allMutex.RUnlock() + fake.allBlockSubnetsMutex.RLock() + defer fake.allBlockSubnetsMutex.RUnlock() + fake.allSingleIPSubnetsMutex.RLock() + defer fake.allSingleIPSubnetsMutex.RUnlock() + fake.allActiveMutex.RLock() + defer fake.allActiveMutex.RUnlock() + fake.oldestExpiredBlockSubnetMutex.RLock() + defer fake.oldestExpiredBlockSubnetMutex.RUnlock() + fake.oldestExpiredSingleIPMutex.RLock() + defer fake.oldestExpiredSingleIPMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *DatabaseHandler) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/controller/leaser/fakes/hardwareAddressGenerator.go b/src/code.cloudfoundry.org/silk/controller/leaser/fakes/hardwareAddressGenerator.go new file mode 100644 index 00000000..d21d818f --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/leaser/fakes/hardwareAddressGenerator.go @@ -0,0 +1,100 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "net" + "sync" +) + +type HardwareAddressGenerator struct { + GenerateForVTEPStub func(containerIP net.IP) (net.HardwareAddr, error) + generateForVTEPMutex sync.RWMutex + generateForVTEPArgsForCall []struct { + containerIP net.IP + } + generateForVTEPReturns struct { + result1 net.HardwareAddr + result2 error + } + generateForVTEPReturnsOnCall map[int]struct { + result1 net.HardwareAddr + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *HardwareAddressGenerator) GenerateForVTEP(containerIP net.IP) (net.HardwareAddr, error) { + fake.generateForVTEPMutex.Lock() + ret, specificReturn := fake.generateForVTEPReturnsOnCall[len(fake.generateForVTEPArgsForCall)] + fake.generateForVTEPArgsForCall = append(fake.generateForVTEPArgsForCall, struct { + containerIP net.IP + }{containerIP}) + fake.recordInvocation("GenerateForVTEP", []interface{}{containerIP}) + fake.generateForVTEPMutex.Unlock() + if fake.GenerateForVTEPStub != nil { + return fake.GenerateForVTEPStub(containerIP) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.generateForVTEPReturns.result1, fake.generateForVTEPReturns.result2 +} + +func (fake *HardwareAddressGenerator) GenerateForVTEPCallCount() int { + fake.generateForVTEPMutex.RLock() + defer fake.generateForVTEPMutex.RUnlock() + return len(fake.generateForVTEPArgsForCall) +} + +func (fake *HardwareAddressGenerator) GenerateForVTEPArgsForCall(i int) net.IP { + fake.generateForVTEPMutex.RLock() + defer fake.generateForVTEPMutex.RUnlock() + return fake.generateForVTEPArgsForCall[i].containerIP +} + +func (fake *HardwareAddressGenerator) GenerateForVTEPReturns(result1 net.HardwareAddr, result2 error) { + fake.GenerateForVTEPStub = nil + fake.generateForVTEPReturns = struct { + result1 net.HardwareAddr + result2 error + }{result1, result2} +} + +func (fake *HardwareAddressGenerator) GenerateForVTEPReturnsOnCall(i int, result1 net.HardwareAddr, result2 error) { + fake.GenerateForVTEPStub = nil + if fake.generateForVTEPReturnsOnCall == nil { + fake.generateForVTEPReturnsOnCall = make(map[int]struct { + result1 net.HardwareAddr + result2 error + }) + } + fake.generateForVTEPReturnsOnCall[i] = struct { + result1 net.HardwareAddr + result2 error + }{result1, result2} +} + +func (fake *HardwareAddressGenerator) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.generateForVTEPMutex.RLock() + defer fake.generateForVTEPMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *HardwareAddressGenerator) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/controller/leaser/fakes/lease_validator.go b/src/code.cloudfoundry.org/silk/controller/leaser/fakes/lease_validator.go new file mode 100644 index 00000000..8a6b0d17 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/leaser/fakes/lease_validator.go @@ -0,0 +1,96 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + + "code.cloudfoundry.org/silk/controller" +) + +type LeaseValidator struct { + ValidateStub func(controller.Lease) error + validateMutex sync.RWMutex + validateArgsForCall []struct { + arg1 controller.Lease + } + validateReturns struct { + result1 error + } + validateReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *LeaseValidator) Validate(arg1 controller.Lease) error { + fake.validateMutex.Lock() + ret, specificReturn := fake.validateReturnsOnCall[len(fake.validateArgsForCall)] + fake.validateArgsForCall = append(fake.validateArgsForCall, struct { + arg1 controller.Lease + }{arg1}) + fake.recordInvocation("Validate", []interface{}{arg1}) + fake.validateMutex.Unlock() + if fake.ValidateStub != nil { + return fake.ValidateStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.validateReturns.result1 +} + +func (fake *LeaseValidator) ValidateCallCount() int { + fake.validateMutex.RLock() + defer fake.validateMutex.RUnlock() + return len(fake.validateArgsForCall) +} + +func (fake *LeaseValidator) ValidateArgsForCall(i int) controller.Lease { + fake.validateMutex.RLock() + defer fake.validateMutex.RUnlock() + return fake.validateArgsForCall[i].arg1 +} + +func (fake *LeaseValidator) ValidateReturns(result1 error) { + fake.ValidateStub = nil + fake.validateReturns = struct { + result1 error + }{result1} +} + +func (fake *LeaseValidator) ValidateReturnsOnCall(i int, result1 error) { + fake.ValidateStub = nil + if fake.validateReturnsOnCall == nil { + fake.validateReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.validateReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *LeaseValidator) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.validateMutex.RLock() + defer fake.validateMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *LeaseValidator) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/leaser/hardware_address_generator.go b/src/code.cloudfoundry.org/silk/controller/leaser/hardware_address_generator.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/leaser/hardware_address_generator.go rename to src/code.cloudfoundry.org/silk/controller/leaser/hardware_address_generator.go diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/leaser/lease_controller.go b/src/code.cloudfoundry.org/silk/controller/leaser/lease_controller.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/leaser/lease_controller.go rename to src/code.cloudfoundry.org/silk/controller/leaser/lease_controller.go diff --git a/src/code.cloudfoundry.org/silk/controller/leaser/lease_controller_test.go b/src/code.cloudfoundry.org/silk/controller/leaser/lease_controller_test.go new file mode 100644 index 00000000..ee089f6a --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/leaser/lease_controller_test.go @@ -0,0 +1,593 @@ +package leaser_test + +import ( + "encoding/json" + "errors" + "fmt" + "net" + + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/lager/v3/lagertest" + + "code.cloudfoundry.org/silk/controller" + "code.cloudfoundry.org/silk/controller/database" + "code.cloudfoundry.org/silk/controller/leaser" + "code.cloudfoundry.org/silk/controller/leaser/fakes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("LeaseController", func() { + var ( + logger *lagertest.TestLogger + databaseHandler *fakes.DatabaseHandler + leaseController leaser.LeaseController + validator *fakes.LeaseValidator + cidrPool *fakes.CIDRPool + hardwareAddressGenerator *fakes.HardwareAddressGenerator + ) + BeforeEach(func() { + logger = lagertest.NewTestLogger("test") + databaseHandler = &fakes.DatabaseHandler{} + cidrPool = &fakes.CIDRPool{} + hardwareAddressGenerator = &fakes.HardwareAddressGenerator{} + validator = &fakes.LeaseValidator{} + leaseController = leaser.LeaseController{ + DatabaseHandler: databaseHandler, + HardwareAddressGenerator: hardwareAddressGenerator, + LeaseValidator: validator, + Logger: logger, + LeaseExpirationSeconds: 42, + } + hardwareAddressGenerator.GenerateForVTEPReturns( + net.HardwareAddr{0xee, 0xee, 0x0a, 0xff, 0x4c, 0x00}, nil, + ) + }) + + Describe("AcquireSubnetLease", func() { + BeforeEach(func() { + leaseController.AcquireSubnetLeaseAttempts = 10 + leaseController.CIDRPool = cidrPool + databaseHandler.AllBlockSubnetsReturns([]controller.Lease{ + {UnderlayIP: "10.244.11.22", OverlaySubnet: "10.255.33.0/24"}, + {UnderlayIP: "10.244.22.33", OverlaySubnet: "10.255.44.0/24"}, + }, nil) + databaseHandler.AllSingleIPSubnetsReturns([]controller.Lease{ + {UnderlayIP: "10.244.33.44", OverlaySubnet: "10.255.0.11/32"}, + {UnderlayIP: "10.244.44.55", OverlaySubnet: "10.255.0.12/32"}, + }, nil) + cidrPool.GetAvailableBlockReturns("10.255.76.0/24") + cidrPool.GetAvailableSingleIPReturns("10.255.0.13/32") + }) + + Context("when acquiring a single ip lease", func() { + It("acquires a lease successfully and logs the result", func() { + lease, err := leaseController.AcquireSubnetLease("10.244.55.66", true) + Expect(err).NotTo(HaveOccurred()) + Expect(lease.OverlaySubnet).To(Equal("10.255.0.13/32")) + }) + + Context("when retrieving all single ip subnets fails", func() { + It("returns an error", func() { + databaseHandler.AllSingleIPSubnetsReturns(nil, errors.New("guava")) + + _, err := leaseController.AcquireSubnetLease("10.244.5.6", true) + Expect(err).To(MatchError("getting all single ip subnets: guava")) + + Expect(databaseHandler.AllSingleIPSubnetsCallCount()).To(Equal(10)) + Expect(databaseHandler.AddEntryCallCount()).To(Equal(0)) + }) + }) + + Context("when no single ip subnets are free", func() { + BeforeEach(func() { + cidrPool.GetAvailableSingleIPReturns("") + }) + + Context("when there are no single ip expired leases", func() { + It("eventually returns an error after failing to find a free subnet", func() { + lease, err := leaseController.AcquireSubnetLease("10.244.5.6", true) + Expect(err).NotTo(HaveOccurred()) + Expect(lease).To(BeNil()) + + Expect(databaseHandler.AllSingleIPSubnetsCallCount()).To(Equal(10)) + Expect(databaseHandler.AddEntryCallCount()).To(Equal(0)) + + Expect(databaseHandler.OldestExpiredSingleIPCallCount()).To(Equal(10)) + Expect(databaseHandler.OldestExpiredSingleIPArgsForCall(0)).To(Equal(42)) + }) + }) + + Context("when there is a single ip expired lease", func() { + var expiredLease *controller.Lease + + BeforeEach(func() { + expiredLease = &controller.Lease{ + UnderlayIP: "10.244.5.60", + OverlaySubnet: "10.255.0.6/32", + OverlayHardwareAddr: "ee:ee:0a:ff:4c:00", + } + databaseHandler.OldestExpiredSingleIPReturns(expiredLease, nil) + }) + + It("deletes the expired lease and assigns that lease's subnet", func() { + lease, err := leaseController.AcquireSubnetLease("10.244.5.6", true) + Expect(err).NotTo(HaveOccurred()) + Expect(lease).To(Equal(&controller.Lease{ + UnderlayIP: "10.244.5.6", + OverlaySubnet: expiredLease.OverlaySubnet, + OverlayHardwareAddr: expiredLease.OverlayHardwareAddr, + })) + + Expect(databaseHandler.AllSingleIPSubnetsCallCount()).To(Equal(1)) + Expect(databaseHandler.AddEntryCallCount()).To(Equal(1)) + Expect(databaseHandler.DeleteEntryCallCount()).To(Equal(1)) + Expect(databaseHandler.DeleteEntryArgsForCall(0)).To(Equal(expiredLease.UnderlayIP)) + + Expect(databaseHandler.OldestExpiredSingleIPCallCount()).To(Equal(1)) + Expect(databaseHandler.OldestExpiredSingleIPArgsForCall(0)).To(Equal(42)) + }) + + Context("when getting the oldest expired lease returns an error", func() { + BeforeEach(func() { + databaseHandler.OldestExpiredSingleIPReturns(nil, errors.New("guava")) + }) + + It("returns an error", func() { + _, err := leaseController.AcquireSubnetLease("10.244.5.6", true) + Expect(err).To(MatchError("get oldest expired single ip: guava")) + }) + }) + + Context("when deleting the entry errors", func() { + BeforeEach(func() { + databaseHandler.DeleteEntryReturns(errors.New("guava")) + }) + + It("returns an error", func() { + _, err := leaseController.AcquireSubnetLease("10.244.5.6", true) + Expect(err).To(MatchError("delete expired subnet: guava")) + }) + }) + }) + }) + }) + + It("acquires a lease and logs the success", func() { + lease, err := leaseController.AcquireSubnetLease("10.244.5.6", false) + Expect(err).NotTo(HaveOccurred()) + Expect(lease.UnderlayIP).To(Equal("10.244.5.6")) + Expect(lease.OverlaySubnet).To(Equal("10.255.76.0/24")) + Expect(lease.OverlayHardwareAddr).To(Equal("ee:ee:0a:ff:4c:00")) + Expect(logger.Logs()[0].Message).To(Equal("test.lease-acquired")) + + loggedLease, err := json.Marshal(logger.Logs()[0].Data["lease"]) + Expect(err).NotTo(HaveOccurred()) + Expect(loggedLease).To(MatchJSON(`{"underlay_ip":"10.244.5.6","overlay_subnet":"10.255.76.0/24","overlay_hardware_addr":"ee:ee:0a:ff:4c:00"}`)) + + Expect(databaseHandler.AllBlockSubnetsCallCount()).To(Equal(1)) + Expect(cidrPool.GetAvailableBlockCallCount()).To(Equal(1)) + Expect(cidrPool.GetAvailableBlockArgsForCall(0)).To(Equal([]string{"10.255.33.0/24", "10.255.44.0/24"})) + Expect(databaseHandler.AddEntryCallCount()).To(Equal(1)) + + savedLease := databaseHandler.AddEntryArgsForCall(0) + Expect(savedLease.UnderlayIP).To(Equal("10.244.5.6")) + Expect(savedLease.OverlaySubnet).To(Equal("10.255.76.0/24")) + Expect(savedLease.OverlayHardwareAddr).To(Equal("ee:ee:0a:ff:4c:00")) + }) + + Context("when getting all taken subnets returns an error", func() { + It("returns an error", func() { + databaseHandler.AllBlockSubnetsReturns(nil, errors.New("guava")) + + _, err := leaseController.AcquireSubnetLease("10.244.5.6", false) + Expect(err).To(MatchError("getting all subnets: guava")) + + Expect(databaseHandler.AllBlockSubnetsCallCount()).To(Equal(10)) + Expect(databaseHandler.AddEntryCallCount()).To(Equal(0)) + }) + }) + + Context("when no subnets are free", func() { + BeforeEach(func() { + cidrPool.GetAvailableBlockReturns("") + }) + + Context("when there are no expired leases", func() { + It("eventually returns an error after failing to find a free subnet", func() { + lease, err := leaseController.AcquireSubnetLease("10.244.5.6", false) + Expect(err).NotTo(HaveOccurred()) + Expect(lease).To(BeNil()) + + Expect(databaseHandler.AllBlockSubnetsCallCount()).To(Equal(10)) + Expect(databaseHandler.AddEntryCallCount()).To(Equal(0)) + + Expect(databaseHandler.OldestExpiredBlockSubnetCallCount()).To(Equal(10)) + Expect(databaseHandler.OldestExpiredBlockSubnetArgsForCall(0)).To(Equal(42)) + }) + }) + + Context("when there is an expired lease", func() { + var expiredLease *controller.Lease + + BeforeEach(func() { + expiredLease = &controller.Lease{ + UnderlayIP: "10.244.5.60", + OverlaySubnet: "10.255.76.0/24", + OverlayHardwareAddr: "ee:ee:0a:ff:4c:00", + } + databaseHandler.OldestExpiredBlockSubnetReturns(expiredLease, nil) + }) + + It("Deletes the expired lease and assigns that lease's subnet", func() { + lease, err := leaseController.AcquireSubnetLease("10.244.5.6", false) + Expect(err).NotTo(HaveOccurred()) + Expect(lease).To(Equal(&controller.Lease{ + UnderlayIP: "10.244.5.6", + OverlaySubnet: expiredLease.OverlaySubnet, + OverlayHardwareAddr: expiredLease.OverlayHardwareAddr, + })) + + Expect(databaseHandler.AllBlockSubnetsCallCount()).To(Equal(1)) + Expect(databaseHandler.AddEntryCallCount()).To(Equal(1)) + Expect(databaseHandler.DeleteEntryCallCount()).To(Equal(1)) + Expect(databaseHandler.DeleteEntryArgsForCall(0)).To(Equal(expiredLease.UnderlayIP)) + + Expect(databaseHandler.OldestExpiredBlockSubnetCallCount()).To(Equal(1)) + Expect(databaseHandler.OldestExpiredBlockSubnetArgsForCall(0)).To(Equal(42)) + }) + + Context("when getting the oldest expired lease returns an error", func() { + BeforeEach(func() { + databaseHandler.OldestExpiredBlockSubnetReturns(nil, errors.New("guava")) + }) + It("returns an error", func() { + _, err := leaseController.AcquireSubnetLease("10.244.5.6", false) + Expect(err).To(MatchError("get oldest expired: guava")) + }) + }) + + Context("when deleting the entry errors", func() { + BeforeEach(func() { + databaseHandler.DeleteEntryReturns(errors.New("guava")) + }) + It("returns an error", func() { + _, err := leaseController.AcquireSubnetLease("10.244.5.6", false) + Expect(err).To(MatchError("delete expired subnet: guava")) + }) + }) + }) + }) + + Context("when the underlay ip is not an IPv4 addr", func() { + It("returns an error", func() { + _, err := leaseController.AcquireSubnetLease("banana", false) + Expect(err).To(MatchError("invalid ipv4 address: banana")) + }) + }) + + Context("when the subnet is an invalid CIDR", func() { + BeforeEach(func() { + cidrPool.GetAvailableBlockReturns("foo") + }) + It("eventually returns an error after failing to find a free subnet", func() { + _, err := leaseController.AcquireSubnetLease("10.244.5.6", false) + Expect(err).To(MatchError("parse subnet: invalid CIDR address: foo")) + + Expect(databaseHandler.AllBlockSubnetsCallCount()).To(Equal(10)) + Expect(databaseHandler.AddEntryCallCount()).To(Equal(0)) + }) + }) + + Context("when generating the hardware address fails", func() { + BeforeEach(func() { + hardwareAddressGenerator.GenerateForVTEPReturns(nil, errors.New("guava")) + }) + It("eventually returns an error after failing to find a free subnet", func() { + _, err := leaseController.AcquireSubnetLease("10.244.5.6", false) + Expect(err).To(MatchError("generate hardware address: guava")) + + Expect(databaseHandler.AllBlockSubnetsCallCount()).To(Equal(10)) + Expect(databaseHandler.AddEntryCallCount()).To(Equal(0)) + }) + }) + + Context("when adding the lease entry fails", func() { + It("returns an error", func() { + databaseHandler.AddEntryReturns(errors.New("guava")) + + _, err := leaseController.AcquireSubnetLease("10.244.5.6", false) + Expect(err).To(MatchError("adding lease entry: guava")) + + Expect(databaseHandler.AddEntryCallCount()).To(Equal(10)) + }) + }) + + Context("when a lease has already been assigned", func() { + var existingLease *controller.Lease + BeforeEach(func() { + existingLease = &controller.Lease{ + UnderlayIP: "10.244.5.6", + OverlaySubnet: "10.255.76.0/24", + OverlayHardwareAddr: "ee:ee:0a:ff:4c:00", + } + databaseHandler.LeaseForUnderlayIPReturns(existingLease, nil) + cidrPool.IsMemberReturns(true) + }) + + It("gets the previously assigned lease", func() { + lease, err := leaseController.AcquireSubnetLease("10.244.5.6", false) + Expect(err).NotTo(HaveOccurred()) + Expect(lease).To(Equal(existingLease)) + + Expect(logger.Logs()[0].Message).To(Equal("test.lease-renewed")) + loggedLease, err := json.Marshal(logger.Logs()[0].Data["lease"]) + Expect(err).NotTo(HaveOccurred()) + Expect(loggedLease).To(MatchJSON(`{"underlay_ip":"10.244.5.6","overlay_subnet":"10.255.76.0/24","overlay_hardware_addr":"ee:ee:0a:ff:4c:00"}`)) + + Expect(databaseHandler.AddEntryCallCount()).To(Equal(0)) + }) + }) + + Context("when a lease has already been assigned in a different network", func() { + var existingLease *controller.Lease + BeforeEach(func() { + existingLease = &controller.Lease{ + UnderlayIP: "10.244.5.6", + OverlaySubnet: "10.254.76.0/24", + OverlayHardwareAddr: "ee:ee:0a:fe:4c:00", + } + databaseHandler.LeaseForUnderlayIPReturns(existingLease, nil) + cidrPool.IsMemberReturns(false) + }) + + It("deletes the previously assigned lease and assigns a new one", func() { + lease, err := leaseController.AcquireSubnetLease("10.244.5.6", false) + Expect(err).NotTo(HaveOccurred()) + Expect(lease).NotTo(Equal(existingLease)) + + Expect(logger.Logs()[0].Message).To(Equal("test.lease-deleted")) + deletedLease, err := json.Marshal(logger.Logs()[0].Data["lease"]) + Expect(err).NotTo(HaveOccurred()) + Expect(deletedLease).To(MatchJSON(`{"underlay_ip":"10.244.5.6","overlay_subnet":"10.254.76.0/24","overlay_hardware_addr":"ee:ee:0a:fe:4c:00"}`)) + + Expect(databaseHandler.DeleteEntryCallCount()).To(Equal(1)) + Expect(databaseHandler.AddEntryCallCount()).To(Equal(1)) + }) + + Context("when deleting the existing entry fails", func() { + BeforeEach(func() { + databaseHandler.DeleteEntryReturns(fmt.Errorf("peanut")) + }) + It("returns an error", func() { + _, err := leaseController.AcquireSubnetLease("10.244.5.6", false) + Expect(err).To(MatchError("deleting lease for underlay ip 10.244.5.6: peanut")) + Expect(databaseHandler.AddEntryCallCount()).To(Equal(0)) + }) + }) + }) + + Context("when checking for an existing lease fails", func() { + BeforeEach(func() { + databaseHandler.LeaseForUnderlayIPReturns(nil, fmt.Errorf("fruit")) + }) + It("returns an error", func() { + _, err := leaseController.AcquireSubnetLease("10.244.5.6", false) + Expect(err).To(MatchError("getting lease for underlay ip: fruit")) + Expect(databaseHandler.AddEntryCallCount()).To(Equal(0)) + }) + }) + }) + + Describe("RenewSubnetLease", func() { + var leaseToRenew controller.Lease + var lastRenewedAt int64 + BeforeEach(func() { + leaseToRenew = controller.Lease{ + UnderlayIP: "10.244.11.22", + OverlaySubnet: "10.255.33.0/24", + OverlayHardwareAddr: "ee:ee:0a:ff:21:00", + } + databaseHandler.LeaseForUnderlayIPReturns(&leaseToRenew, nil) + lastRenewedAt = 42 + databaseHandler.LastRenewedAtForUnderlayIPReturns(lastRenewedAt, nil) + }) + + It("renews a lease and logs the success", func() { + err := leaseController.RenewSubnetLease(leaseToRenew) + Expect(err).NotTo(HaveOccurred()) + + Expect(databaseHandler.LeaseForUnderlayIPCallCount()).To(Equal(1)) + Expect(databaseHandler.LeaseForUnderlayIPArgsForCall(0)).To(Equal("10.244.11.22")) + Expect(databaseHandler.RenewLeaseForUnderlayIPCallCount()).To(Equal(1)) + Expect(databaseHandler.RenewLeaseForUnderlayIPArgsForCall(0)).To(Equal("10.244.11.22")) + Expect(databaseHandler.LastRenewedAtForUnderlayIPCallCount()).To(Equal(1)) + + Expect(logger.Logs()).To(HaveLen(1)) + Expect(logger.Logs()[0].Message).To(Equal("test.lease-renewed")) + Expect(logger.Logs()[0].LogLevel).To(Equal(lager.DEBUG)) + loggedLease, err := json.Marshal(logger.Logs()[0].Data["lease"]) + Expect(err).NotTo(HaveOccurred()) + Expect(loggedLease).To(MatchJSON(`{"underlay_ip":"10.244.11.22","overlay_subnet":"10.255.33.0/24","overlay_hardware_addr":"ee:ee:0a:ff:21:00"}`)) + Expect(int64(logger.Logs()[0].Data["last_renewed_at"].(float64))).To(Equal(lastRenewedAt)) + }) + + Context("when the existing lease does not equal the one we are renewing", func() { + BeforeEach(func() { + existingLease := &controller.Lease{ + UnderlayIP: leaseToRenew.UnderlayIP, + OverlaySubnet: "10.255.77.0/24", + OverlayHardwareAddr: leaseToRenew.OverlayHardwareAddr, + } + databaseHandler.LeaseForUnderlayIPReturns(existingLease, nil) + }) + It("returns a non-retriable error", func() { + err := leaseController.RenewSubnetLease(leaseToRenew) + Expect(err).To(HaveOccurred()) + Expect(err).To(BeAssignableToTypeOf(controller.NonRetriableError(""))) + Expect(err).To(MatchError("lease mismatch")) + }) + }) + + Context("when the existing lease does not exist", func() { + BeforeEach(func() { + databaseHandler.LeaseForUnderlayIPReturns(nil, nil) + }) + It("adds the entry and logs the success", func() { + err := leaseController.RenewSubnetLease(leaseToRenew) + Expect(err).NotTo(HaveOccurred()) + + Expect(databaseHandler.LeaseForUnderlayIPCallCount()).To(Equal(1)) + Expect(databaseHandler.LeaseForUnderlayIPArgsForCall(0)).To(Equal("10.244.11.22")) + + Expect(databaseHandler.AddEntryCallCount()).To(Equal(1)) + Expect(databaseHandler.AddEntryArgsForCall(0)).To(Equal(leaseToRenew)) + + Expect(databaseHandler.RenewLeaseForUnderlayIPCallCount()).To(Equal(1)) + Expect(databaseHandler.RenewLeaseForUnderlayIPArgsForCall(0)).To(Equal("10.244.11.22")) + Expect(databaseHandler.LastRenewedAtForUnderlayIPCallCount()).To(Equal(1)) + + Expect(logger.Logs()).To(HaveLen(1)) + Expect(logger.Logs()[0].Message).To(Equal("test.lease-renewed")) + Expect(logger.Logs()[0].LogLevel).To(Equal(lager.DEBUG)) + loggedLease, err := json.Marshal(logger.Logs()[0].Data["lease"]) + Expect(err).NotTo(HaveOccurred()) + Expect(loggedLease).To(MatchJSON(`{"underlay_ip":"10.244.11.22","overlay_subnet":"10.255.33.0/24","overlay_hardware_addr":"ee:ee:0a:ff:21:00"}`)) + Expect(int64(logger.Logs()[0].Data["last_renewed_at"].(float64))).To(Equal(lastRenewedAt)) + }) + + Context("when adding the entry fails", func() { + BeforeEach(func() { + databaseHandler.AddEntryReturns(errors.New("pineapple")) + }) + It("returns a non-retriable error", func() { + err := leaseController.RenewSubnetLease(leaseToRenew) + Expect(err).To(HaveOccurred()) + Expect(err).To(BeAssignableToTypeOf(controller.NonRetriableError(""))) + Expect(err).To(MatchError("pineapple")) + }) + }) + }) + + Context("when the lease is not valid", func() { + BeforeEach(func() { + validator.ValidateReturns(errors.New("banana")) + }) + It("returns a non-retriable error", func() { + err := leaseController.RenewSubnetLease(leaseToRenew) + Expect(err).To(HaveOccurred()) + Expect(err).To(BeAssignableToTypeOf(controller.NonRetriableError(""))) + Expect(err).To(MatchError("banana")) + }) + }) + + Context("when checking the lease for the underlay ip fails", func() { + BeforeEach(func() { + databaseHandler.LeaseForUnderlayIPReturns(nil, errors.New("banana")) + }) + It("returns an error", func() { + err := leaseController.RenewSubnetLease(leaseToRenew) + Expect(err).To(MatchError("getting lease for underlay ip: banana")) + }) + }) + + Context("when renewing the lease for the underlay ip fails", func() { + BeforeEach(func() { + databaseHandler.RenewLeaseForUnderlayIPReturns(errors.New("banana")) + }) + It("returns an error", func() { + err := leaseController.RenewSubnetLease(leaseToRenew) + Expect(err).To(MatchError("renewing lease for underlay ip: banana")) + }) + }) + + Context("when getting the last renewed at time fails", func() { + BeforeEach(func() { + databaseHandler.LastRenewedAtForUnderlayIPReturns(0, errors.New("banana")) + }) + It("returns an error", func() { + err := leaseController.RenewSubnetLease(leaseToRenew) + Expect(err).To(MatchError("getting last renewed at: banana")) + }) + }) + }) + + Describe("ReleaseSubnetLease", func() { + var ( + underlayIP string + ) + BeforeEach(func() { + underlayIP = "10.244.5.0" + }) + It("releases the lease", func() { + err := leaseController.ReleaseSubnetLease(underlayIP) + Expect(err).NotTo(HaveOccurred()) + + Expect(databaseHandler.DeleteEntryCallCount()).To(Equal(1)) + Expect(databaseHandler.DeleteEntryArgsForCall(0)).To(Equal(underlayIP)) + + Expect(logger.Logs()).To(HaveLen(1)) + Expect(logger.Logs()[0].Data["underlay_ip"]).To(Equal("10.244.5.0")) + Expect(logger.Logs()[0].Message).To(Equal("test.lease-released")) + }) + + Context("when the database returns RecordNotAffectedError", func() { + BeforeEach(func() { + databaseHandler.DeleteEntryReturns(database.RecordNotAffectedError) + }) + It("swallows the error and logs it at DEBUG level", func() { + err := leaseController.ReleaseSubnetLease(underlayIP) + Expect(err).NotTo(HaveOccurred()) + + Expect(logger.Logs()).To(HaveLen(1)) + Expect(logger.Logs()[0].Message).To(Equal("test.lease-not-found")) + Expect(logger.Logs()[0].Data).To(HaveKeyWithValue("underlay_ip", "10.244.5.0")) + Expect(logger.Logs()[0].LogLevel).To(Equal(lager.DEBUG)) + }) + }) + + Context("when the database returns some other error", func() { + BeforeEach(func() { + databaseHandler.DeleteEntryReturns(errors.New("banana")) + }) + It("wraps the error from the database handler", func() { + err := leaseController.ReleaseSubnetLease(underlayIP) + Expect(err).To(MatchError("release lease: banana")) + }) + }) + }) + + Describe("RoutableLeases", func() { + activeLeases := []controller.Lease{ + { + UnderlayIP: "10.244.5.9", + OverlaySubnet: "10.255.16.0/24", + }, + { + UnderlayIP: "10.244.22.33", + OverlaySubnet: "10.255.75.0/32", + }, + } + BeforeEach(func() { + databaseHandler.AllActiveReturns(activeLeases, nil) + }) + It("returns all the active subnet leases", func() { + leases, err := leaseController.RoutableLeases() + Expect(err).NotTo(HaveOccurred()) + Expect(databaseHandler.AllActiveCallCount()).To(Equal(1)) + Expect(databaseHandler.AllActiveArgsForCall(0)).To(Equal(42)) + Expect(leases).To(Equal(activeLeases)) + }) + + Context("when getting the leases fails", func() { + BeforeEach(func() { + databaseHandler.AllActiveReturns(nil, errors.New("cupcake")) + }) + It("wraps the error from the database handler", func() { + _, err := leaseController.RoutableLeases() + Expect(err).To(MatchError("getting all leases: cupcake")) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/controller/leaser/lease_suite_test.go b/src/code.cloudfoundry.org/silk/controller/leaser/lease_suite_test.go new file mode 100644 index 00000000..2aa04a81 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/leaser/lease_suite_test.go @@ -0,0 +1,13 @@ +package leaser_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" +) + +func TestLeaser(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Leaser Suite") +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/leaser/validator.go b/src/code.cloudfoundry.org/silk/controller/leaser/validator.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/leaser/validator.go rename to src/code.cloudfoundry.org/silk/controller/leaser/validator.go diff --git a/src/code.cloudfoundry.org/silk/controller/leaser/validator_test.go b/src/code.cloudfoundry.org/silk/controller/leaser/validator_test.go new file mode 100644 index 00000000..60452d67 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/leaser/validator_test.go @@ -0,0 +1,60 @@ +package leaser_test + +import ( + "code.cloudfoundry.org/silk/controller" + "code.cloudfoundry.org/silk/controller/leaser" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Validator", func() { + var ( + validator *leaser.LeaseValidator + lease controller.Lease + ) + + BeforeEach(func() { + lease = controller.Lease{ + UnderlayIP: "1.2.3.4", + OverlaySubnet: "5.4.3.2/24", + OverlayHardwareAddr: "aa:bb:cc:dd:ee:ff", + } + validator = &leaser.LeaseValidator{} + }) + + It("checks that the lease is valid", func() { + err := validator.Validate(lease) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("when the underlay ip is not a valid ip", func() { + BeforeEach(func() { + lease.UnderlayIP = "not-an-ip" + }) + It("returns an error", func() { + err := validator.Validate(lease) + Expect(err).To(MatchError("invalid underlay ip: not-an-ip")) + }) + }) + + Context("when the overlay subnet is invalid", func() { + BeforeEach(func() { + lease.OverlaySubnet = "not-a-subnet" + }) + It("returns an error", func() { + err := validator.Validate(lease) + Expect(err).To(MatchError("invalid CIDR address: not-a-subnet")) + }) + }) + + Context("when the hardware addr is invalid is invalid", func() { + BeforeEach(func() { + lease.OverlayHardwareAddr = "not-a-mac" + }) + It("returns an error", func() { + err := validator.Validate(lease) + Expect(err).To(MatchError(ContainSubstring("invalid MAC address"))) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/controller/server_metrics/fakes/cidrPool.go b/src/code.cloudfoundry.org/silk/controller/server_metrics/fakes/cidrPool.go new file mode 100644 index 00000000..73867f2d --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/server_metrics/fakes/cidrPool.go @@ -0,0 +1,84 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" +) + +type CIDRPool struct { + BlockPoolSizeStub func() int + blockPoolSizeMutex sync.RWMutex + blockPoolSizeArgsForCall []struct{} + blockPoolSizeReturns struct { + result1 int + } + blockPoolSizeReturnsOnCall map[int]struct { + result1 int + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *CIDRPool) BlockPoolSize() int { + fake.blockPoolSizeMutex.Lock() + ret, specificReturn := fake.blockPoolSizeReturnsOnCall[len(fake.blockPoolSizeArgsForCall)] + fake.blockPoolSizeArgsForCall = append(fake.blockPoolSizeArgsForCall, struct{}{}) + fake.recordInvocation("BlockPoolSize", []interface{}{}) + fake.blockPoolSizeMutex.Unlock() + if fake.BlockPoolSizeStub != nil { + return fake.BlockPoolSizeStub() + } + if specificReturn { + return ret.result1 + } + return fake.blockPoolSizeReturns.result1 +} + +func (fake *CIDRPool) BlockPoolSizeCallCount() int { + fake.blockPoolSizeMutex.RLock() + defer fake.blockPoolSizeMutex.RUnlock() + return len(fake.blockPoolSizeArgsForCall) +} + +func (fake *CIDRPool) BlockPoolSizeReturns(result1 int) { + fake.BlockPoolSizeStub = nil + fake.blockPoolSizeReturns = struct { + result1 int + }{result1} +} + +func (fake *CIDRPool) BlockPoolSizeReturnsOnCall(i int, result1 int) { + fake.BlockPoolSizeStub = nil + if fake.blockPoolSizeReturnsOnCall == nil { + fake.blockPoolSizeReturnsOnCall = make(map[int]struct { + result1 int + }) + } + fake.blockPoolSizeReturnsOnCall[i] = struct { + result1 int + }{result1} +} + +func (fake *CIDRPool) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.blockPoolSizeMutex.RLock() + defer fake.blockPoolSizeMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *CIDRPool) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/controller/server_metrics/fakes/databaseHandler.go b/src/code.cloudfoundry.org/silk/controller/server_metrics/fakes/databaseHandler.go new file mode 100644 index 00000000..7fef55f9 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/server_metrics/fakes/databaseHandler.go @@ -0,0 +1,157 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + + "code.cloudfoundry.org/silk/controller" +) + +type DatabaseHandler struct { + AllStub func() ([]controller.Lease, error) + allMutex sync.RWMutex + allArgsForCall []struct{} + allReturns struct { + result1 []controller.Lease + result2 error + } + allReturnsOnCall map[int]struct { + result1 []controller.Lease + result2 error + } + AllActiveStub func(int) ([]controller.Lease, error) + allActiveMutex sync.RWMutex + allActiveArgsForCall []struct { + arg1 int + } + allActiveReturns struct { + result1 []controller.Lease + result2 error + } + allActiveReturnsOnCall map[int]struct { + result1 []controller.Lease + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *DatabaseHandler) All() ([]controller.Lease, error) { + fake.allMutex.Lock() + ret, specificReturn := fake.allReturnsOnCall[len(fake.allArgsForCall)] + fake.allArgsForCall = append(fake.allArgsForCall, struct{}{}) + fake.recordInvocation("All", []interface{}{}) + fake.allMutex.Unlock() + if fake.AllStub != nil { + return fake.AllStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.allReturns.result1, fake.allReturns.result2 +} + +func (fake *DatabaseHandler) AllCallCount() int { + fake.allMutex.RLock() + defer fake.allMutex.RUnlock() + return len(fake.allArgsForCall) +} + +func (fake *DatabaseHandler) AllReturns(result1 []controller.Lease, result2 error) { + fake.AllStub = nil + fake.allReturns = struct { + result1 []controller.Lease + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) AllReturnsOnCall(i int, result1 []controller.Lease, result2 error) { + fake.AllStub = nil + if fake.allReturnsOnCall == nil { + fake.allReturnsOnCall = make(map[int]struct { + result1 []controller.Lease + result2 error + }) + } + fake.allReturnsOnCall[i] = struct { + result1 []controller.Lease + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) AllActive(arg1 int) ([]controller.Lease, error) { + fake.allActiveMutex.Lock() + ret, specificReturn := fake.allActiveReturnsOnCall[len(fake.allActiveArgsForCall)] + fake.allActiveArgsForCall = append(fake.allActiveArgsForCall, struct { + arg1 int + }{arg1}) + fake.recordInvocation("AllActive", []interface{}{arg1}) + fake.allActiveMutex.Unlock() + if fake.AllActiveStub != nil { + return fake.AllActiveStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.allActiveReturns.result1, fake.allActiveReturns.result2 +} + +func (fake *DatabaseHandler) AllActiveCallCount() int { + fake.allActiveMutex.RLock() + defer fake.allActiveMutex.RUnlock() + return len(fake.allActiveArgsForCall) +} + +func (fake *DatabaseHandler) AllActiveArgsForCall(i int) int { + fake.allActiveMutex.RLock() + defer fake.allActiveMutex.RUnlock() + return fake.allActiveArgsForCall[i].arg1 +} + +func (fake *DatabaseHandler) AllActiveReturns(result1 []controller.Lease, result2 error) { + fake.AllActiveStub = nil + fake.allActiveReturns = struct { + result1 []controller.Lease + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) AllActiveReturnsOnCall(i int, result1 []controller.Lease, result2 error) { + fake.AllActiveStub = nil + if fake.allActiveReturnsOnCall == nil { + fake.allActiveReturnsOnCall = make(map[int]struct { + result1 []controller.Lease + result2 error + }) + } + fake.allActiveReturnsOnCall[i] = struct { + result1 []controller.Lease + result2 error + }{result1, result2} +} + +func (fake *DatabaseHandler) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.allMutex.RLock() + defer fake.allMutex.RUnlock() + fake.allActiveMutex.RLock() + defer fake.allActiveMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *DatabaseHandler) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/server_metrics/server_metrics.go b/src/code.cloudfoundry.org/silk/controller/server_metrics/server_metrics.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/controller/server_metrics/server_metrics.go rename to src/code.cloudfoundry.org/silk/controller/server_metrics/server_metrics.go diff --git a/src/code.cloudfoundry.org/silk/controller/server_metrics/server_metrics_suite_test.go b/src/code.cloudfoundry.org/silk/controller/server_metrics/server_metrics_suite_test.go new file mode 100644 index 00000000..7d09f06e --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/server_metrics/server_metrics_suite_test.go @@ -0,0 +1,13 @@ +package server_metrics_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" +) + +func TestServerMetrics(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "ServerMetrics Suite") +} diff --git a/src/code.cloudfoundry.org/silk/controller/server_metrics/server_metrics_test.go b/src/code.cloudfoundry.org/silk/controller/server_metrics/server_metrics_test.go new file mode 100644 index 00000000..ec1d3421 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/controller/server_metrics/server_metrics_test.go @@ -0,0 +1,86 @@ +package server_metrics_test + +import ( + "code.cloudfoundry.org/silk/controller" + "code.cloudfoundry.org/silk/controller/server_metrics" + "code.cloudfoundry.org/silk/controller/server_metrics/fakes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ServerMetrics", func() { + var allLeases []controller.Lease + var fakeDatabaseHandler *fakes.DatabaseHandler + var fakeCIDRPool *fakes.CIDRPool + + BeforeEach(func() { + allLeases = []controller.Lease{ + { + UnderlayIP: "10.244.5.9", + OverlaySubnet: "10.255.16.0/24", + OverlayHardwareAddr: "ee:ee:0a:ff:10:00", + }, + { + UnderlayIP: "10.244.22.33", + OverlaySubnet: "10.255.75.0/32", + OverlayHardwareAddr: "ee:ee:0a:ff:4b:00", + }, + } + fakeDatabaseHandler = &fakes.DatabaseHandler{} + fakeDatabaseHandler.AllReturns(allLeases, nil) + fakeDatabaseHandler.AllActiveReturns([]controller.Lease{allLeases[0]}, nil) + + fakeCIDRPool = &fakes.CIDRPool{} + fakeCIDRPool.BlockPoolSizeReturns(100) + }) + + Describe("totalLeases", func() { + It("returns the total number of leases in the datastore", func() { + source := server_metrics.NewTotalLeasesSource(fakeDatabaseHandler) + + Expect(source.Name).To(Equal("totalLeases")) + Expect(source.Unit).To(Equal("")) + + value, err := source.Getter() + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeDatabaseHandler.AllCallCount()).To(Equal(1)) + Expect(value).To(Equal(2.0)) + }) + }) + + Describe("freeLeases", func() { + It("returns the total number of free leases", func() { + source := server_metrics.NewFreeLeasesSource(fakeDatabaseHandler, fakeCIDRPool) + + Expect(source.Name).To(Equal("freeLeases")) + Expect(source.Unit).To(Equal("")) + + value, err := source.Getter() + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeDatabaseHandler.AllCallCount()).To(Equal(1)) + Expect(fakeCIDRPool.BlockPoolSizeCallCount()).To(Equal(1)) + Expect(value).To(Equal(98.0)) + }) + }) + + Describe("staleLeases", func() { + It("returns the total number of stale leases in the datastore", func() { + source := server_metrics.NewStaleLeasesSource(fakeDatabaseHandler, 5) + + Expect(source.Name).To(Equal("staleLeases")) + Expect(source.Unit).To(Equal("")) + + value, err := source.Getter() + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeDatabaseHandler.AllCallCount()).To(Equal(1)) + Expect(fakeDatabaseHandler.AllActiveCallCount()).To(Equal(1)) + Expect(fakeDatabaseHandler.AllActiveArgsForCall(0)).To(Equal(5)) + Expect(value).To(Equal(1.0)) + }) + }) + +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/errors.go b/src/code.cloudfoundry.org/silk/daemon/errors.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/errors.go rename to src/code.cloudfoundry.org/silk/daemon/errors.go diff --git a/src/code.cloudfoundry.org/silk/daemon/integration/error_cases_test.go b/src/code.cloudfoundry.org/silk/daemon/integration/error_cases_test.go new file mode 100644 index 00000000..39746575 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/daemon/integration/error_cases_test.go @@ -0,0 +1,317 @@ +package integration_test + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" + "strconv" + "time" + + "code.cloudfoundry.org/silk/testsupport" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("error cases", func() { + var ( + configFilePath string + ) + + BeforeEach(func() { + configFilePath = writeConfigFile(daemonConf) + }) + + AfterEach(func() { + stopDaemon() + }) + + Context("when the path to the config is bad", func() { + It("exits with status 1", func() { + session = startDaemon("/some/bad/path") + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit(1)) + Expect(session.Err.Contents()).To(ContainSubstring("cfnetworking.silk-daemon error: load config file: reading file /some/bad/path")) + }) + }) + + Context("when the tls config is invalid", func() { + var configFilePath string + BeforeEach(func() { + clientConf := daemonConf + clientConf.ServerCACertFile = "/dev/null" + configFilePath = writeConfigFile(clientConf) + }) + + It("exits with status 1", func() { + session := startDaemon(configFilePath) + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit(1)) + Expect(session.Err.Contents()).To(ContainSubstring("create tls config:")) + }) + }) + + Context("when the contents of the config file cannot be unmarshaled", func() { + BeforeEach(func() { + Expect(ioutil.WriteFile(configFilePath, []byte("some-bad-contents"), os.ModePerm)).To(Succeed()) + }) + + It("exits with status 1", func() { + session = startDaemon(configFilePath) + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit(1)) + Expect(session.Err.Contents()).To(ContainSubstring("cfnetworking.silk-daemon error: load config file: unmarshaling contents")) + }) + }) + + Describe("failures to acquire a new lease", func() { + Context("when acquire returns a 500", func() { + BeforeEach(func() { + fakeServer.SetHandler("/leases/acquire", &testsupport.FakeHandler{ + ResponseCode: 500, + ResponseBody: struct{}{}, + }) + }) + + It("exits with status 1", func() { + session = startDaemon(configFilePath) + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit(1)) + Expect(string(session.Err.Contents())).To(ContainSubstring("potato-prefix.silk-daemon error: acquire subnet lease: http status 500")) + }) + }) + + Context("when the controller address is not reachable", func() { + BeforeEach(func() { + fakeServer.Stop() + }) + It("exits with status 1", func() { + session = startDaemon(configFilePath) + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit(1)) + Expect(string(session.Err.Contents())).To(MatchRegexp(`.*acquire subnet lease:.*dial tcp.*`)) + }) + }) + }) + + Context("when a local lease is discovered", func() { + BeforeEach(func() { + By("ensuring a local lease is already present") + startAndWaitForDaemon() // creates a new lease + stopDaemon() // stops daemon, but leaves local lease intact + }) + + Context("when the controller becomes unavailable for a long time", func() { + It("allows containers to be scheduled until the partition tolerance duration has elapsed", func() { + By("starting the daemon and waiting for it to become healthy") + startAndWaitForDaemon() + + By("stopping the controller") + fakeServer.Stop() + + partitionToleranceConfig := time.Duration(daemonConf.PartitionToleranceSeconds) * time.Second + const partitionToleranceEpsilon = 2 * time.Second // an error margin + By("verifying that the daemon remains alive during a partition shorter than the tolerance duration") + Consistently(func() error { + if exitCode := session.ExitCode(); exitCode != -1 { + return fmt.Errorf("expected daemon to be alive, but it exited with code %d", exitCode) + } + if err := doHealthCheckWithErr(); err != nil { + return err + } + return nil + }, partitionToleranceConfig-partitionToleranceEpsilon).Should(Succeed()) + + By("verifying that the daemon crashes once the partition lasts longer than the tolerance") + Eventually(session, partitionToleranceEpsilon*2).Should(gexec.Exit(1)) + }) + }) + + Context("when renewing the local lease fails due to a retriable error", func() { + BeforeEach(func() { + fakeServer.SetHandler("/leases/renew", &testsupport.FakeHandler{ + ResponseCode: 404, + ResponseBody: map[string]interface{}{}, + }) + }) + + Context("when reading the datastore fails", func() { + BeforeEach(func() { + daemonConf.Datastore = "/dev/urandom" + configFilePath := writeConfigFile(daemonConf) + startDaemon(configFilePath) + }) + + AfterEach(func() { + err := vtepFactory.DeleteVTEP(vtepName) + Expect(err).NotTo(HaveOccurred()) + }) + + It("exits with status 1", func() { + configFilePath := writeConfigFile(daemonConf) + startDaemon(configFilePath) + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit(1)) + Expect(string(session.Err.Contents())).To(ContainSubstring("potato-prefix.silk-daemon error: read datastore")) + }) + }) + + Context("when no containers are running", func() { + Context("when acquire returns a 500", func() { + BeforeEach(func() { + fakeServer.SetHandler("/leases/acquire", &testsupport.FakeHandler{ + ResponseCode: 500, + ResponseBody: struct{}{}, + }) + }) + + It("exits with status 1", func() { + configFilePath := writeConfigFile(daemonConf) + startDaemon(configFilePath) + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit(1)) + Expect(string(session.Err.Contents())).To(ContainSubstring("potato-prefix.silk-daemon error: acquire subnet lease: http status 500")) + }) + }) + }) + + Context("when containers are running", func() { + BeforeEach(func() { + err := ioutil.WriteFile(datastorePath, []byte(`{ + "some-handle": { + "handle": "some-handle", + "ip": "192.168.0.100", + "metadata": {} + } + }`), os.FileMode(0600)) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + err := vtepFactory.DeleteVTEP(vtepName) + Expect(err).NotTo(HaveOccurred()) + }) + + It("exits with status 1", func() { + configFilePath = writeConfigFile(daemonConf) + startDaemon(configFilePath) + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit(1)) + Expect(session.Out).To(gbytes.Say(`renew-lease.*"error":"http status 404: "`)) + Expect(string(session.Err.Contents())).To(ContainSubstring(`renew subnet lease with containers: 1`)) + }) + }) + }) + + Context("when renewing the local lease fails due to a non-retriable error", func() { + BeforeEach(func() { + fakeServer.SetHandler("/leases/renew", &testsupport.FakeHandler{ + ResponseCode: http.StatusConflict, + ResponseBody: map[string]interface{}{}, + }) + }) + + It("exits with status 1", func() { + configFilePath := writeConfigFile(daemonConf) + startDaemon(configFilePath) + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit(1)) + Expect(string(session.Err.Contents())).To(ContainSubstring(`This cell must be restarted (run "bosh restart "): fatal: renew lease: non-retriable:`)) + }) + }) + }) + + Context("when requests to the controller server time out", func() { + BeforeEach(func() { + mustSucceed("iptables", "-A", "INPUT", "-p", "tcp", "--dport", strconv.Itoa(serverListenPort), "-j", "DROP") + }) + + AfterEach(func() { + mustSucceed("iptables", "-D", "INPUT", "-p", "tcp", "--dport", strconv.Itoa(serverListenPort), "-j", "DROP") + }) + It("exits with status 1", func() { + daemonConf.ClientTimeoutSeconds = 1 + configFilePath := writeConfigFile(daemonConf) + startDaemon(configFilePath) + Eventually(session, 3*time.Second).Should(gexec.Exit(1)) + Expect(string(session.Err.Contents())).To(MatchRegexp(`potato-prefix.silk-daemon error: acquire subnet lease: http client do:.* \(Client.Timeout exceeded while awaiting headers\)`)) + }) + }) + + Context("when a local lease that is not part of the overlay network is discovered", func() { + BeforeEach(func() { + By("ensuring a local lease is already present") + startAndWaitForDaemon() // creates a new lease + stopDaemon() // stops daemon, but leaves local lease intact + }) + + Context("when containers are running", func() { + BeforeEach(func() { + err := ioutil.WriteFile(datastorePath, []byte(`{ + "some-handle": { + "handle": "some-handle", + "ip": "192.168.0.100", + "metadata": {} + } + }`), os.FileMode(0600)) + Expect(err).NotTo(HaveOccurred()) + + daemonConf.OverlayNetwork = "10.254.0.0/16" + configFilePath := writeConfigFile(daemonConf) + writeConfigFile(daemonConf) + startDaemon(configFilePath) + }) + + AfterEach(func() { + err := vtepFactory.DeleteVTEP(vtepName) + Expect(err).NotTo(HaveOccurred()) + }) + + It("exits with status 1", func() { + Eventually(session, 3*time.Second).Should(gexec.Exit(1)) + Expect(string(session.Err.Contents())).To(MatchRegexp(`potato-prefix.silk-daemon error: discovered lease is not in overlay network and has containers: 1`)) + }) + }) + + Context("when reading the datastore fails", func() { + BeforeEach(func() { + daemonConf.Datastore = "/dev/urandom" + daemonConf.OverlayNetwork = "10.254.0.0/16" + configFilePath := writeConfigFile(daemonConf) + startDaemon(configFilePath) + }) + + AfterEach(func() { + err := vtepFactory.DeleteVTEP(vtepName) + Expect(err).NotTo(HaveOccurred()) + }) + + It("exits with status 1", func() { + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit(1)) + Expect(string(session.Err.Contents())).To(ContainSubstring("read datastore")) + }) + }) + + Context("no containers are running but acquiring a new lease fails", func() { + BeforeEach(func() { + fakeServer.SetHandler("/leases/acquire", &testsupport.FakeHandler{ + ResponseCode: 500, + ResponseBody: struct{}{}, + }) + }) + + It("exits with status 1", func() { + daemonConf.OverlayNetwork = "10.254.0.0/16" + configFilePath := writeConfigFile(daemonConf) + startDaemon(configFilePath) + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit(1)) + Expect(string(session.Err.Contents())).To(ContainSubstring("acquire subnet lease: http status 500")) + }) + }) + + Context("when the vxlan interface does not exist", func() { + BeforeEach(func() { + daemonConf.VxlanInterfaceName = "non-existent-eth1" + configFilePath = writeConfigFile(daemonConf) + }) + It("exits with status 1", func() { + session := startDaemon(configFilePath) + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit(1)) + Expect(string(session.Err.Contents())).To(ContainSubstring(`potato-prefix.silk-daemon error: create vtep config: find device from name non-existent-eth1: route ip+net: no such network interface`)) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/daemon/integration/integration_suite_test.go b/src/code.cloudfoundry.org/silk/daemon/integration/integration_suite_test.go new file mode 100644 index 00000000..b653a81b --- /dev/null +++ b/src/code.cloudfoundry.org/silk/daemon/integration/integration_suite_test.go @@ -0,0 +1,75 @@ +package integration_test + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "math/rand" + "os" + + "code.cloudfoundry.org/cf-networking-helpers/testsupport" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" + + "testing" +) + +var ( + certDir string + paths testPaths +) + +type testPaths struct { + ServerCACertFile string + ClientCACertFile string + ServerCertFile string + ServerKeyFile string + ClientCertFile string + ClientKeyFile string + DaemonBin string +} + +func TestIntegration(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Daemon Integration Suite") +} + +var _ = SynchronizedBeforeSuite(func() []byte { + var err error + certDir, err = ioutil.TempDir("", "silk-certs") + Expect(err).NotTo(HaveOccurred()) + + certWriter, err := testsupport.NewCertWriter(certDir) + Expect(err).NotTo(HaveOccurred()) + + paths.ServerCACertFile, err = certWriter.WriteCA("server-ca") + Expect(err).NotTo(HaveOccurred()) + paths.ServerCertFile, paths.ServerKeyFile, err = certWriter.WriteAndSign("server", "server-ca") + Expect(err).NotTo(HaveOccurred()) + + paths.ClientCACertFile, err = certWriter.WriteCA("client-ca") + Expect(err).NotTo(HaveOccurred()) + paths.ClientCertFile, paths.ClientKeyFile, err = certWriter.WriteAndSign("client", "client-ca") + Expect(err).NotTo(HaveOccurred()) + + fmt.Fprintf(GinkgoWriter, "building binary...") + paths.DaemonBin, err = gexec.Build("code.cloudfoundry.org/silk/cmd/silk-daemon", "-race", "-buildvcs=false") + fmt.Fprintf(GinkgoWriter, "done") + Expect(err).NotTo(HaveOccurred()) + + data, err := json.Marshal(paths) + Expect(err).NotTo(HaveOccurred()) + + return data +}, func(data []byte) { + Expect(json.Unmarshal(data, &paths)).To(Succeed()) + + rand.Seed(GinkgoRandomSeed() + int64(GinkgoParallelProcess())) +}) + +var _ = SynchronizedAfterSuite(func() {}, func() { + gexec.CleanupBuildArtifacts() + os.Remove(certDir) +}) diff --git a/src/code.cloudfoundry.org/silk/daemon/integration/integration_test.go b/src/code.cloudfoundry.org/silk/daemon/integration/integration_test.go new file mode 100644 index 00000000..26063352 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/daemon/integration/integration_test.go @@ -0,0 +1,695 @@ +package integration_test + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "code.cloudfoundry.org/cf-networking-helpers/mutualtls" + "code.cloudfoundry.org/cf-networking-helpers/testsupport/metrics" + "code.cloudfoundry.org/cf-networking-helpers/testsupport/ports" + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/client/config" + "code.cloudfoundry.org/silk/controller" + "code.cloudfoundry.org/silk/daemon" + "code.cloudfoundry.org/silk/daemon/vtep" + "code.cloudfoundry.org/silk/lib/adapter" + "code.cloudfoundry.org/silk/testsupport" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gexec" + "github.com/onsi/gomega/types" + matchers "github.com/pivotal-cf-experimental/gomegamatchers" + "github.com/tedsuo/ifrit" + "github.com/vishvananda/netlink" +) + +const ( + DEFAULT_TIMEOUT = "5s" + localIP = "127.0.0.1" +) + +var ( + externalMTU int + daemonConf config.Config + daemonLease controller.Lease + fakeServer *testsupport.FakeController + serverListenPort int + serverListenAddr string + serverTLSConfig *tls.Config + session *gexec.Session + daemonHealthCheckURL string + daemonDebugServerPort int + datastorePath string + vtepFactory *vtep.Factory + vtepName string + vtepPort int + vni int + fakeMetron metrics.FakeMetron + overlaySubnet string + overlayVtepIP net.IP + remoteOverlaySubnet string + remoteOverlayVtepIP net.IP + remoteSingleIPSubnet string + remoteSingleIP net.IP +) + +var _ = BeforeEach(func() { + fakeMetron = metrics.NewFakeMetron() + + externalIface, err := locateInterface(net.ParseIP(localIP)) + Expect(err).NotTo(HaveOccurred()) + externalMTU = externalIface.MTU + + overlaySubnet = fmt.Sprintf("10.255.%d.0/24", GinkgoParallelProcess()+100) + overlayVtepIP, _, _ = net.ParseCIDR(overlaySubnet) + + remoteOverlaySubnet = fmt.Sprintf("10.255.%d.0/24", GinkgoParallelProcess()+2) + remoteOverlayVtepIP, _, _ = net.ParseCIDR(remoteOverlaySubnet) + + remoteSingleIPSubnet = fmt.Sprintf("10.255.0.%d/32", GinkgoParallelProcess()+20) + remoteSingleIP, _, _ = net.ParseCIDR(remoteSingleIPSubnet) + + daemonLease = controller.Lease{ + UnderlayIP: localIP, + OverlaySubnet: overlaySubnet, + OverlayHardwareAddr: "ee:ee:0a:ff:1e:00", + } + vni = GinkgoParallelProcess() + vtepName = fmt.Sprintf("silk-vtep-%d", GinkgoParallelProcess()) + daemonHealthCheckPort := ports.PickAPort() + daemonHealthCheckURL = fmt.Sprintf("http://127.0.0.1:%d/health", daemonHealthCheckPort) + daemonDebugServerPort = ports.PickAPort() + serverListenPort = ports.PickAPort() + vtepPort = ports.PickAPort() + serverListenAddr = fmt.Sprintf("127.0.0.1:%d", serverListenPort) + datastoreDir, err := ioutil.TempDir("", "") + Expect(err).NotTo(HaveOccurred()) + datastorePath = filepath.Join(datastoreDir, "container-metadata.json") + daemonConf = config.Config{ + UnderlayIP: localIP, + SubnetPrefixLength: 24, + OverlayNetwork: "10.255.0.0/16", + HealthCheckPort: uint16(daemonHealthCheckPort), + VTEPName: vtepName, + ConnectivityServerURL: fmt.Sprintf("https://%s", serverListenAddr), + ServerCACertFile: paths.ServerCACertFile, + ClientCertFile: paths.ClientCertFile, + ClientKeyFile: paths.ClientKeyFile, + VNI: vni, + PollInterval: 1, + DebugServerPort: daemonDebugServerPort, + Datastore: datastorePath, + PartitionToleranceSeconds: 10, + ClientTimeoutSeconds: 10, + MetronPort: fakeMetron.Port(), + VTEPPort: vtepPort, + LogPrefix: "potato-prefix", + } + + vtepFactory = &vtep.Factory{NetlinkAdapter: &adapter.NetlinkAdapter{}, Logger: lagertest.NewTestLogger("test")} + + serverTLSConfig, err = mutualtls.NewServerTLSConfig(paths.ServerCertFile, paths.ServerKeyFile, paths.ClientCACertFile) + Expect(err).NotTo(HaveOccurred()) + fakeServer = testsupport.StartServer(serverListenAddr, serverTLSConfig) + + acquireHandler := &testsupport.FakeHandler{ + ResponseCode: 200, + ResponseBody: &controller.Lease{ + UnderlayIP: localIP, + OverlaySubnet: overlaySubnet, + OverlayHardwareAddr: "ee:ee:0a:ff:1e:00", + }, + } + + leases := map[string][]controller.Lease{ + "leases": []controller.Lease{ + { + UnderlayIP: localIP, + OverlaySubnet: overlaySubnet, + OverlayHardwareAddr: "ee:ee:0a:ff:1e:00", + }, { + UnderlayIP: "172.17.0.5", + OverlaySubnet: remoteOverlaySubnet, + OverlayHardwareAddr: "ee:ee:0a:ff:28:00", + }, { + UnderlayIP: "172.17.0.6", + OverlaySubnet: remoteSingleIPSubnet, + OverlayHardwareAddr: "ee:ee:0a:ff:28:20", + }, + }, + } + indexHandler := &testsupport.FakeHandler{ + ResponseCode: 200, + ResponseBody: leases, + } + + fakeServer.SetHandler("/leases/acquire", acquireHandler) + fakeServer.SetHandler("/leases", indexHandler) +}) + +var _ = AfterEach(func() { + fakeServer.Stop() + vtepFactory.DeleteVTEP(vtepName) +}) + +var _ = Describe("Daemon Integration", func() { + BeforeEach(func() { + startAndWaitForDaemon() + }) + + AfterEach(func() { + stopDaemon() + }) + + withName := func(name string) types.GomegaMatcher { + return WithTransform(func(ev metrics.Event) string { + return ev.Name + }, Equal(name)) + } + + withValue := func(value interface{}) types.GomegaMatcher { + return WithTransform(func(ev metrics.Event) float64 { + return ev.Value + }, BeEquivalentTo(value)) + } + + hasMetricWithValue := func(name string, value interface{}) types.GomegaMatcher { + return SatisfyAll(withName(name), withValue(value)) + } + + It("syncs with the controller and updates the local networking stack", func() { + By("getting the device") + link, err := netlink.LinkByName(vtepName) + Expect(err).NotTo(HaveOccurred()) + vtep := link.(*netlink.Vxlan) + + By("asserting on the device properties") + Expect(vtep.Attrs().Flags & net.FlagUp).To(Equal(net.FlagUp)) + Expect(vtep.HardwareAddr.String()).To(Equal("ee:ee:0a:ff:1e:00")) + Expect(vtep.SrcAddr.String()).To(Equal(localIP)) + defaultDevice, err := locateInterface(net.ParseIP(localIP)) + Expect(err).NotTo(HaveOccurred()) + Expect(vtep.VtepDevIndex).To(Equal(defaultDevice.Index)) + Expect(vtep.VxlanId).To(Equal(vni)) + Expect(vtep.Port).To(Equal(vtepPort)) + Expect(vtep.Learning).To(Equal(false)) + Expect(vtep.GBP).To(BeTrue()) + + By("getting the addresses on the device") + addresses, err := netlink.AddrList(vtep, netlink.FAMILY_V4) + Expect(err).NotTo(HaveOccurred()) + Expect(addresses).To(HaveLen(1)) + Expect(addresses[0].IP.String()).To(Equal(overlayVtepIP.String())) + By("checking the daemon's healthcheck") + doHealthCheck() + + By("inspecting the daemon's log to see that it acquired a new lease") + Expect(session.Out).To(gbytes.Say(`potato-prefix\.silk-daemon.*acquired-lease.*overlay_subnet.*` + overlaySubnet + `.*overlay_hardware_addr.*ee:ee:0a:ff:1e:00`)) + + By("stopping the daemon") + stopDaemon() + + By("setting up renew handler") + renewHandler := &testsupport.FakeHandler{ + ResponseCode: 200, + ResponseBody: struct{}{}, + } + fakeServer.SetHandler("/leases/renew", renewHandler) + + By("restarting the daemon") + startAndWaitForDaemon() + + By("renewing its lease") + var renewRequest controller.Lease + Expect(json.Unmarshal(renewHandler.LastRequestBody, &renewRequest)).To(Succeed()) + Expect(renewRequest).To(Equal(daemonLease)) + + By("checking the daemon's healthcheck") + doHealthCheck() + + By("inspecting the daemon's log to see that it renewed a new lease") + Expect(session.Out).To(gbytes.Say(`renewed-lease.*overlay_subnet.*` + overlaySubnet + `.*overlay_hardware_addr.*ee:ee:0a:ff:1e:00`)) + + By("checking that a renew-success metric was emitted") + Eventually(fakeMetron.AllEvents, "5s").Should(ContainElement(withName("renewSuccess"))) + + By("modifying the renewHandler to respond with 404") + renewHandler = &testsupport.FakeHandler{ + ResponseCode: 404, + ResponseBody: struct{}{}, + } + fakeServer.SetHandler("/leases/renew", renewHandler) + + By("checking that a renew-failed metric was emitted") + Eventually(fakeMetron.AllEvents, "5s").Should(ContainElement(withName("renewFailure"))) + }) + + Context("when single ip only is true", func() { + BeforeEach(func() { + fakeServer.SetHandlerFunc("/leases/acquire", func(w http.ResponseWriter, req *http.Request) { + contents, err := ioutil.ReadAll(req.Body) + Expect(err).NotTo(HaveOccurred()) + + var acquireRequest controller.AcquireLeaseRequest + err = json.Unmarshal(contents, &acquireRequest) + Expect(err).NotTo(HaveOccurred()) + if acquireRequest.SingleOverlayIP { + contents, err := json.Marshal(controller.Lease{ + UnderlayIP: "10.244.64.65", + OverlaySubnet: "10.255.0.32/32", + OverlayHardwareAddr: "ee:ee:0a:ff:00:20", + }) + Expect(err).NotTo(HaveOccurred()) + w.WriteHeader(http.StatusOK) + w.Write(contents) + return + } + + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("{}")) + }) + + daemonConf.SingleIPOnly = true + stopDaemon() + startAndWaitForDaemon() + }) + + It("updates the local networking stack", func() { + link, err := netlink.LinkByName(vtepName) + Expect(err).NotTo(HaveOccurred()) + vtep := link.(*netlink.Vxlan) + Expect(vtep.HardwareAddr.String()).To(Equal("ee:ee:0a:ff:00:20")) + Expect(vtep.SrcAddr.String()).To(Equal(localIP)) + + addresses, err := netlink.AddrList(vtep, netlink.FAMILY_V4) + Expect(err).NotTo(HaveOccurred()) + Expect(addresses).To(HaveLen(1)) + Expect(addresses[0].IP.String()).To(Equal("10.255.0.32")) + }) + }) + + Context("when custom_underlay_interface_name is specified", func() { + var ( + dummyName string + dummyInterface *net.Interface + ) + BeforeEach(func() { + var err error + + dummyName = "eth1" + daemonConf.VxlanInterfaceName = dummyName + mustSucceed("ip", "link", "add", dummyName, "type", "dummy") + + dummyInterface, err = net.InterfaceByName("eth1") + Expect(err).NotTo(HaveOccurred()) + + stopDaemon() + }) + AfterEach(func() { + mustSucceed("ip", "link", "delete", dummyName) + }) + It("attaches the vtep device to the interface specified", func() { + startAndWaitForDaemon() + link, err := netlink.LinkByName(vtepName) + Expect(err).NotTo(HaveOccurred()) + + vtep := link.(*netlink.Vxlan) + Expect(vtep.VtepDevIndex).To(Equal(dummyInterface.Index)) + }) + }) + + It("emits an uptime metric", func() { + Eventually(fakeMetron.AllEvents, "5s").Should(ContainElement(withName("uptime"))) + }) + + Describe("polling", func() { + BeforeEach(func() { + By("set up renew handler") + handler := &testsupport.FakeHandler{ + ResponseCode: 200, + ResponseBody: struct{}{}, + } + fakeServer.SetHandler("/leases/renew", handler) + + By("turning on debug logging") + setLogLevel("DEBUG", daemonDebugServerPort) + }) + + It("polls to renew the lease and logs at debug level", func() { + By("checking that the lease renewal is logged") + Eventually(session.Out, 2).Should(gbytes.Say(fmt.Sprintf(`silk-daemon.renew-lease.*"lease".*overlay_subnet.*` + overlaySubnet + `.*overlay_hardware_addr.*ee:ee:0a:ff:1e:00`))) + + By("stopping the controller") + handler := &testsupport.FakeHandler{ + ResponseCode: 500, + ResponseBody: struct{}{}, + } + fakeServer.SetHandler("/leases/renew", handler) + + By("checking that the lease renewal failure is logged") + Eventually(session.Out, 2).Should(gbytes.Say(fmt.Sprintf(`silk-daemon.poll-cycle.*renew lease: http status 500`))) + + }) + + It("polls for other leases and logs at debug level", func() { + By("checking that the correct leases are logged") + Eventually(session.Out, 2).Should(gbytes.Say(`level.*debug.*silk-daemon.converge-leases`)) + Eventually(session.Out, 2).Should(gbytes.Say(fmt.Sprintf(`underlay_ip.*%s.*overlay_subnet.*`+overlaySubnet+`.*overlay_hardware_addr.*ee:ee:0a:ff:1e:00`, localIP))) + Eventually(session.Out, 2).Should(gbytes.Say(`underlay_ip.*172.17.0.5.*overlay_subnet.*` + remoteOverlaySubnet + `.*overlay_hardware_addr.*ee:ee:0a:ff:28:00`)) + + By("checking the arp fdb and routing are correct") + routes := mustSucceed("ip", "route", "list", "dev", vtepName) + routeFields := strings.Fields(routes) + Expect(routeFields).To(matchers.ContainSequence([]string{"10.255.0.0/16", "proto", "kernel", "scope", "link", "src", overlayVtepIP.String()})) + Expect(routeFields).To(matchers.ContainSequence([]string{remoteOverlaySubnet, "via", remoteOverlayVtepIP.String(), "src", overlayVtepIP.String()})) + Expect(routeFields).To(matchers.ContainSequence([]string{remoteSingleIP.String(), "via", remoteSingleIP.String(), "src", overlayVtepIP.String()})) + + arpEntries := mustSucceed("ip", "neigh", "list", "dev", vtepName) + Expect(arpEntries).To(ContainSubstring(remoteOverlayVtepIP.String() + " lladdr ee:ee:0a:ff:28:00 PERMANENT")) + + fdbEntries := mustSucceed("bridge", "fdb", "list", "dev", vtepName) + Expect(fdbEntries).To(ContainSubstring("ee:ee:0a:ff:28:00 dst 172.17.0.5 self permanent")) + + By("checking that it emits a metric for the number of leases it sees") + Eventually(fakeMetron.AllEvents, "5s").Should(ContainElement(hasMetricWithValue("numberLeases", 3))) + + By("removing the leases from the controller") + fakeServer.SetHandler("/leases", &testsupport.FakeHandler{ + ResponseCode: 200, + ResponseBody: map[string][]controller.Lease{"leases": []controller.Lease{}}}, + ) + + By("checking that the emitted number of leases has updated to zero") + Eventually(fakeMetron.AllEvents, "5s").Should(ContainElement(hasMetricWithValue("numberLeases", 0))) + + By("checking that no leases are logged") + Eventually(session.Out, 2).Should(gbytes.Say(fmt.Sprintf(`silk-daemon.converge-leases.*"leases":\[]`))) + }) + + Context("when cells with overlay subnets are brought down", func() { + It("polls and updates the leases accordingly", func() { + By("checking that the correct leases are logged") + Eventually(session.Out, 2).Should(gbytes.Say(`level.*debug.*silk-daemon.converge-leases`)) + Eventually(session.Out, 2).Should(gbytes.Say(fmt.Sprintf(`underlay_ip.*%s.*overlay_subnet.*`+overlaySubnet+`.*overlay_hardware_addr.*ee:ee:0a:ff:1e:00`, localIP))) + Eventually(session.Out, 2).Should(gbytes.Say(`underlay_ip.*172.17.0.5.*overlay_subnet.*` + remoteOverlaySubnet + `.*overlay_hardware_addr.*ee:ee:0a:ff:28:00`)) + + By("checking the arp fdb and routing are correct") + + Eventually(string(session.Out.Contents()), "5s").Should(ContainSubstring(`"level":"debug","source":"potato-prefix.silk-daemon","message":"potato-prefix.silk-daemon.converge-leases","data":{"leases":[{"underlay_ip":"127.0.0.1","overlay_subnet":"` + overlaySubnet + `","overlay_hardware_addr":"ee:ee:0a:ff:1e:00"},{"underlay_ip":"172.17.0.5","overlay_subnet":"` + remoteOverlaySubnet + `","overlay_hardware_addr":"ee:ee:0a:ff:28:00"},{"underlay_ip":"172.17.0.6","overlay_subnet":"` + remoteSingleIPSubnet + `","overlay_hardware_addr":"ee:ee:0a:ff:28:20"}]}}`)) + + routes := mustSucceed("ip", "route", "list", "dev", vtepName) + routeFields := strings.Fields(routes) + Expect(routeFields).To(matchers.ContainSequence([]string{"10.255.0.0/16", "proto", "kernel", "scope", "link", "src", overlayVtepIP.String()})) + Expect(routeFields).To(matchers.ContainSequence([]string{remoteOverlaySubnet, "via", remoteOverlayVtepIP.String(), "src", overlayVtepIP.String()})) + + arpEntries := mustSucceed("ip", "neigh", "list", "dev", vtepName) + Expect(arpEntries).To(ContainSubstring(remoteOverlayVtepIP.String() + " lladdr ee:ee:0a:ff:28:00 PERMANENT")) + + fdbEntries := mustSucceed("bridge", "fdb", "list", "dev", vtepName) + Expect(fdbEntries).To(ContainSubstring("ee:ee:0a:ff:28:00 dst 172.17.0.5 self permanent")) + + By("simulating a cell shutdown by removing a lease from the controller") + fakeServer.SetHandler("/leases", &testsupport.FakeHandler{ + ResponseCode: 200, + ResponseBody: map[string][]controller.Lease{"leases": []controller.Lease{ + { + UnderlayIP: localIP, + OverlaySubnet: overlaySubnet, + OverlayHardwareAddr: "ee:ee:0a:ff:1e:00", + }, + }}}, + ) + + By("checking that updated leases are logged") + Eventually(session.Out, 2).Should(gbytes.Say(fmt.Sprintf(`level.*debug.*silk-daemon.converge-leases`))) + Eventually(session.Out, 2).Should(gbytes.Say(fmt.Sprintf(`underlay_ip.*%s.*overlay_subnet.*`+overlaySubnet+`.*overlay_hardware_addr.*ee:ee:0a:ff:1e:00`, localIP))) + Eventually(session.Out, 2).ShouldNot(gbytes.Say(`underlay_ip.*172.17.0.5.*overlay_subnet.*` + remoteOverlaySubnet + `.*overlay_hardware_addr.*ee:ee:0a:ff:28:00`)) + + By("checking the arp fdb and routing are updated correctly") + routes = mustSucceed("ip", "route", "list", "dev", vtepName) + routeFields = strings.Fields(routes) + Expect(routeFields).To(matchers.ContainSequence([]string{"10.255.0.0/16", "proto", "kernel", "scope", "link", "src", overlayVtepIP.String()})) + Expect(routeFields).NotTo(matchers.ContainSequence([]string{remoteOverlaySubnet, "via", remoteOverlayVtepIP.String(), "src", overlayVtepIP.String()})) + + arpEntries = mustSucceed("ip", "neigh", "list", "dev", vtepName) + Expect(arpEntries).NotTo(ContainSubstring(remoteOverlayVtepIP.String() + " lladdr ee:ee:0a:ff:28:00 PERMANENT")) + + fdbEntries = mustSucceed("bridge", "fdb", "list", "dev", vtepName) + Expect(fdbEntries).NotTo(ContainSubstring("ee:ee:0a:ff:28:00 dst 172.17.0.5 self permanent")) + }) + }) + + Context("when the controller returns leases outside of my overlay network", func() { + BeforeEach(func() { + indexHandler := &testsupport.FakeHandler{ + ResponseCode: 200, + ResponseBody: map[string][]controller.Lease{ + "leases": []controller.Lease{ + { // in our overlay + UnderlayIP: localIP, + OverlaySubnet: overlaySubnet, + OverlayHardwareAddr: "ee:ee:0a:ff:1e:00", + }, + { // not in our overlay + UnderlayIP: "172.17.0.4", + OverlaySubnet: "10.123.40.0/24", + OverlayHardwareAddr: "ee:ee:0a:fe:28:00", + }, + { // in our overlay + UnderlayIP: "172.17.0.5", + OverlaySubnet: remoteOverlaySubnet, + OverlayHardwareAddr: "ee:ee:0a:ff:28:00", + }, + }, + }, + } + fakeServer.SetHandler("/leases", indexHandler) + }) + + It("only updates the leases inside the overlay network", func() { + By("logging the number of leases we skipped") + Eventually(session.Out, 2).Should(gbytes.Say(`level.*info.*silk-daemon.converger.*non-routable-lease-count.*1`)) + + By("checking that the correct leases are logged") + Eventually(session.Out, 2).Should(gbytes.Say(`level.*debug.*silk-daemon.converge-leases`)) + Eventually(session.Out, 2).Should(gbytes.Say(fmt.Sprintf(`underlay_ip.*%s.*overlay_subnet.*`+overlaySubnet+`.*overlay_hardware_addr.*ee:ee:0a:ff:1e:00`, localIP))) + Eventually(session.Out, 2).Should(gbytes.Say(`underlay_ip.*172.17.0.5.*overlay_subnet.*` + remoteOverlaySubnet + `.*overlay_hardware_addr.*ee:ee:0a:ff:28:00`)) + + Eventually(session.Out, "5s").Should(gbytes.Say(`level.*debug.*silk-daemon.converge-leases.*data":\{"leases":\[\{"underlay_ip":"127.0.0.1","overlay_subnet":"` + overlaySubnet + `","overlay_hardware_addr":"ee:ee:0a:ff:1e:00"\},\{"underlay_ip":"172.17.0.4","overlay_subnet":"10.123.40.0/24","overlay_hardware_addr":"ee:ee:0a:fe:28:00"\},\{"underlay_ip":"172.17.0.5","overlay_subnet":"` + remoteOverlaySubnet + `","overlay_hardware_addr":"ee:ee:0a:ff:28:00"`)) + + By("checking the arp fdb and routing are correct") + routes := mustSucceed("ip", "route", "list", "dev", vtepName) + routeFields := strings.Fields(routes) + Expect(routeFields).To(matchers.ContainSequence([]string{"10.255.0.0/16", "proto", "kernel", "scope", "link", "src", overlayVtepIP.String()})) + Expect(routeFields).To(matchers.ContainSequence([]string{remoteOverlaySubnet, "via", remoteOverlayVtepIP.String(), "src", overlayVtepIP.String()})) + + arpEntries := mustSucceed("ip", "neigh", "list", "dev", vtepName) + Expect(arpEntries).To(ContainSubstring(remoteOverlayVtepIP.String() + " lladdr ee:ee:0a:ff:28:00 PERMANENT")) + + fdbEntries := mustSucceed("bridge", "fdb", "list", "dev", vtepName) + Expect(fdbEntries).To(ContainSubstring("ee:ee:0a:ff:28:00 dst 172.17.0.5 self permanent")) + + By("checking that routes do not exist for the nonroutable lease") + Expect(routeFields).NotTo(matchers.ContainSequence([]string{"10.123.40.0/24", "via", "10.123.40.0", "src", overlayVtepIP.String()})) + Expect(arpEntries).NotTo(ContainSubstring("10.123.40.0 lladdr ee:ee:0a:fe:28:00 PERMANENT")) + Expect(fdbEntries).NotTo(ContainSubstring("ee:ee:0a:fe:28:00 dst 172.17.0.4 self permanent")) + }) + }) + }) + + Context("when a local lease is discovered but it cannot be renewed", func() { + BeforeEach(func() { + stopDaemon() + + fakeServer.SetHandler("/leases/renew", &testsupport.FakeHandler{ + ResponseCode: 404, + ResponseBody: map[string]interface{}{}, + }) + }) + + Context("when no containers are running", func() { + It("logs an error message, acquires a new lease and stays alive", func() { + startAndWaitForDaemon() + Expect(session.Out).To(gbytes.Say(`renew-lease.*"error":"http status 404: "`)) + Expect(session.Out).To(gbytes.Say(`acquired-lease.*`)) + Consistently(session, "4s").ShouldNot(gexec.Exit()) + }) + + Context("when renew returns a 500", func() { + BeforeEach(func() { + fakeServer.SetHandler("/leases/renew", &testsupport.FakeHandler{ + ResponseCode: 500, + ResponseBody: struct{}{}, + }) + }) + + It("logs the error message and stays alive", func() { + startAndWaitForDaemon() + Expect(session.Out).To(gbytes.Say(`renew-lease.*"error":"http status 500: "`)) + Consistently(session, "4s").ShouldNot(gexec.Exit()) + }) + }) + + Context("when renew returns a 409 Conflict", func() { + BeforeEach(func() { + fakeServer.SetHandler("/leases/renew", &testsupport.FakeHandler{ + ResponseCode: 409, + ResponseBody: map[string]string{"error": "lease mismatch"}, + }) + }) + + It("logs the error and dies", func() { + session = startDaemon(writeConfigFile(daemonConf)) + // startAndWaitForDaemon() + Eventually(session.Out).Should(gbytes.Say(`renew-lease.*"error":"non-retriable: lease mismatch"`)) + + Eventually(session, "10s").Should(gexec.Exit(1)) + }) + }) + }) + }) + + Context("when the discovered lease is not in the overlay network", func() { + BeforeEach(func() { + stopDaemon() + daemonConf.OverlayNetwork = "10.254.0.0/16" + }) + + Context("when no containers are running", func() { + It("logs an error message and acquires a new lease", func() { + startAndWaitForDaemon() + Expect(session.Out).To(gbytes.Say(`network-contains-lease.*"error":"discovered lease is not in overlay network"`)) + Expect(session.Out).To(gbytes.Say(`acquired-lease.*`)) + }) + }) + }) +}) + +func startAndWaitForDaemon() { + session = startDaemon(writeConfigFile(daemonConf)) + + By("waiting until the daemon is healthy before tests") + callHealthcheck := func() (int, error) { + resp, err := http.Get(daemonHealthCheckURL) + if resp == nil { + return -1, err + } + return resp.StatusCode, nil + } + Eventually(callHealthcheck, time.Minute, time.Second).Should(Equal(http.StatusOK)) +} + +func doHealthCheck() { + Expect(doHealthCheckWithErr()).To(Succeed()) +} + +func doHealthCheckWithErr() error { + resp, err := http.Get(daemonHealthCheckURL) + if err != nil { + return err + } + responseBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + var response daemon.NetworkInfo + err = json.Unmarshal(responseBytes, &response) + if err != nil { + return err + } + if response.OverlaySubnet != daemonLease.OverlaySubnet { + return fmt.Errorf("mismatched overlay subnet: %s vs %s", response.OverlaySubnet, daemonLease.OverlaySubnet) + } + const vxlanEncapOverhead = 50 // bytes + Expect(response.MTU).To(Equal(externalMTU - vxlanEncapOverhead)) + if response.MTU != externalMTU-vxlanEncapOverhead { + return fmt.Errorf("mismatched mtu: %d vs %d", response.MTU, externalMTU-vxlanEncapOverhead) + } + return nil +} + +func writeConfigFile(config config.Config) string { + configFile, err := ioutil.TempFile("", "test-config") + Expect(err).NotTo(HaveOccurred()) + + configBytes, err := json.Marshal(config) + Expect(err).NotTo(HaveOccurred()) + + err = ioutil.WriteFile(configFile.Name(), configBytes, os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + return configFile.Name() +} + +func startDaemon(configFilePath string) *gexec.Session { + startCmd := exec.Command(paths.DaemonBin, "--config", configFilePath) + s, err := gexec.Start(startCmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session = s + return s +} + +func stopDaemon() { + session.Interrupt() + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit()) +} + +func locateInterface(toFind net.IP) (net.Interface, error) { + ifaces, err := net.Interfaces() + if err != nil { + return net.Interface{}, err + } + for _, iface := range ifaces { + addrs, err := iface.Addrs() + if err != nil { + return net.Interface{}, err + } + + for _, addr := range addrs { + ip, _, err := net.ParseCIDR(addr.String()) + if err != nil { + return net.Interface{}, err + } + if ip.String() == toFind.String() { + return iface, nil + } + } + } + + return net.Interface{}, fmt.Errorf("no interface with address %s", toFind.String()) +} + +func mustSucceed(binary string, args ...string) string { + cmd := exec.Command(binary, args...) + sess, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + Eventually(sess, "10s").Should(gexec.Exit(0)) + return string(sess.Out.Contents()) +} + +func stopServer(server ifrit.Process) { + if server == nil { + return + } + server.Signal(os.Interrupt) + Eventually(server.Wait()).Should(Receive()) +} + +func setLogLevel(level string, port int) { + serverAddress := fmt.Sprintf("localhost:%d/log-level", port) + curlCmd := exec.Command("curl", serverAddress, "-X", "POST", "-d", level) + Expect(curlCmd.Start()).To(Succeed()) + Expect(curlCmd.Wait()).To(Succeed()) + return +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/network_info.go b/src/code.cloudfoundry.org/silk/daemon/network_info.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/network_info.go rename to src/code.cloudfoundry.org/silk/daemon/network_info.go diff --git a/src/code.cloudfoundry.org/silk/daemon/planner/fakes/controller_client.go b/src/code.cloudfoundry.org/silk/daemon/planner/fakes/controller_client.go new file mode 100644 index 00000000..0e07f4fb --- /dev/null +++ b/src/code.cloudfoundry.org/silk/daemon/planner/fakes/controller_client.go @@ -0,0 +1,148 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + "code.cloudfoundry.org/silk/controller" +) + +type ControllerClient struct { + GetActiveLeasesStub func() ([]controller.Lease, error) + getRoutableLeasesMutex sync.RWMutex + getRoutableLeasesArgsForCall []struct{} + getRoutableLeasesReturns struct { + result1 []controller.Lease + result2 error + } + getRoutableLeasesReturnsOnCall map[int]struct { + result1 []controller.Lease + result2 error + } + RenewSubnetLeaseStub func(controller.Lease) error + renewSubnetLeaseMutex sync.RWMutex + renewSubnetLeaseArgsForCall []struct { + arg1 controller.Lease + } + renewSubnetLeaseReturns struct { + result1 error + } + renewSubnetLeaseReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *ControllerClient) GetActiveLeases() ([]controller.Lease, error) { + fake.getRoutableLeasesMutex.Lock() + ret, specificReturn := fake.getRoutableLeasesReturnsOnCall[len(fake.getRoutableLeasesArgsForCall)] + fake.getRoutableLeasesArgsForCall = append(fake.getRoutableLeasesArgsForCall, struct{}{}) + fake.recordInvocation("GetActiveLeases", []interface{}{}) + fake.getRoutableLeasesMutex.Unlock() + if fake.GetActiveLeasesStub != nil { + return fake.GetActiveLeasesStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.getRoutableLeasesReturns.result1, fake.getRoutableLeasesReturns.result2 +} + +func (fake *ControllerClient) GetActiveLeasesCallCount() int { + fake.getRoutableLeasesMutex.RLock() + defer fake.getRoutableLeasesMutex.RUnlock() + return len(fake.getRoutableLeasesArgsForCall) +} + +func (fake *ControllerClient) GetActiveLeasesReturns(result1 []controller.Lease, result2 error) { + fake.GetActiveLeasesStub = nil + fake.getRoutableLeasesReturns = struct { + result1 []controller.Lease + result2 error + }{result1, result2} +} + +func (fake *ControllerClient) GetActiveLeasesReturnsOnCall(i int, result1 []controller.Lease, result2 error) { + fake.GetActiveLeasesStub = nil + if fake.getRoutableLeasesReturnsOnCall == nil { + fake.getRoutableLeasesReturnsOnCall = make(map[int]struct { + result1 []controller.Lease + result2 error + }) + } + fake.getRoutableLeasesReturnsOnCall[i] = struct { + result1 []controller.Lease + result2 error + }{result1, result2} +} + +func (fake *ControllerClient) RenewSubnetLease(arg1 controller.Lease) error { + fake.renewSubnetLeaseMutex.Lock() + ret, specificReturn := fake.renewSubnetLeaseReturnsOnCall[len(fake.renewSubnetLeaseArgsForCall)] + fake.renewSubnetLeaseArgsForCall = append(fake.renewSubnetLeaseArgsForCall, struct { + arg1 controller.Lease + }{arg1}) + fake.recordInvocation("RenewSubnetLease", []interface{}{arg1}) + fake.renewSubnetLeaseMutex.Unlock() + if fake.RenewSubnetLeaseStub != nil { + return fake.RenewSubnetLeaseStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.renewSubnetLeaseReturns.result1 +} + +func (fake *ControllerClient) RenewSubnetLeaseCallCount() int { + fake.renewSubnetLeaseMutex.RLock() + defer fake.renewSubnetLeaseMutex.RUnlock() + return len(fake.renewSubnetLeaseArgsForCall) +} + +func (fake *ControllerClient) RenewSubnetLeaseArgsForCall(i int) controller.Lease { + fake.renewSubnetLeaseMutex.RLock() + defer fake.renewSubnetLeaseMutex.RUnlock() + return fake.renewSubnetLeaseArgsForCall[i].arg1 +} + +func (fake *ControllerClient) RenewSubnetLeaseReturns(result1 error) { + fake.RenewSubnetLeaseStub = nil + fake.renewSubnetLeaseReturns = struct { + result1 error + }{result1} +} + +func (fake *ControllerClient) RenewSubnetLeaseReturnsOnCall(i int, result1 error) { + fake.RenewSubnetLeaseStub = nil + if fake.renewSubnetLeaseReturnsOnCall == nil { + fake.renewSubnetLeaseReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.renewSubnetLeaseReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *ControllerClient) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getRoutableLeasesMutex.RLock() + defer fake.getRoutableLeasesMutex.RUnlock() + fake.renewSubnetLeaseMutex.RLock() + defer fake.renewSubnetLeaseMutex.RUnlock() + return fake.invocations +} + +func (fake *ControllerClient) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/daemon/planner/fakes/converger.go b/src/code.cloudfoundry.org/silk/daemon/planner/fakes/converger.go new file mode 100644 index 00000000..b21d9c62 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/daemon/planner/fakes/converger.go @@ -0,0 +1,97 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + "code.cloudfoundry.org/silk/controller" +) + +type Converger struct { + ConvergeStub func([]controller.Lease) error + convergeMutex sync.RWMutex + convergeArgsForCall []struct { + arg1 []controller.Lease + } + convergeReturns struct { + result1 error + } + convergeReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *Converger) Converge(arg1 []controller.Lease) error { + var arg1Copy []controller.Lease + if arg1 != nil { + arg1Copy = make([]controller.Lease, len(arg1)) + copy(arg1Copy, arg1) + } + fake.convergeMutex.Lock() + ret, specificReturn := fake.convergeReturnsOnCall[len(fake.convergeArgsForCall)] + fake.convergeArgsForCall = append(fake.convergeArgsForCall, struct { + arg1 []controller.Lease + }{arg1Copy}) + fake.recordInvocation("Converge", []interface{}{arg1Copy}) + fake.convergeMutex.Unlock() + if fake.ConvergeStub != nil { + return fake.ConvergeStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.convergeReturns.result1 +} + +func (fake *Converger) ConvergeCallCount() int { + fake.convergeMutex.RLock() + defer fake.convergeMutex.RUnlock() + return len(fake.convergeArgsForCall) +} + +func (fake *Converger) ConvergeArgsForCall(i int) []controller.Lease { + fake.convergeMutex.RLock() + defer fake.convergeMutex.RUnlock() + return fake.convergeArgsForCall[i].arg1 +} + +func (fake *Converger) ConvergeReturns(result1 error) { + fake.ConvergeStub = nil + fake.convergeReturns = struct { + result1 error + }{result1} +} + +func (fake *Converger) ConvergeReturnsOnCall(i int, result1 error) { + fake.ConvergeStub = nil + if fake.convergeReturnsOnCall == nil { + fake.convergeReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.convergeReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *Converger) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.convergeMutex.RLock() + defer fake.convergeMutex.RUnlock() + return fake.invocations +} + +func (fake *Converger) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/daemon/planner/fakes/fatalErrorDetector.go b/src/code.cloudfoundry.org/silk/daemon/planner/fakes/fatalErrorDetector.go new file mode 100644 index 00000000..030a7d51 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/daemon/planner/fakes/fatalErrorDetector.go @@ -0,0 +1,115 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + "code.cloudfoundry.org/silk/daemon/planner" +) + +type FatalErrorDetector struct { + GotSuccessStub func() + gotSuccessMutex sync.RWMutex + gotSuccessArgsForCall []struct{} + IsFatalStub func(error) bool + isFatalMutex sync.RWMutex + isFatalArgsForCall []struct { + arg1 error + } + isFatalReturns struct { + result1 bool + } + isFatalReturnsOnCall map[int]struct { + result1 bool + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FatalErrorDetector) GotSuccess() { + fake.gotSuccessMutex.Lock() + fake.gotSuccessArgsForCall = append(fake.gotSuccessArgsForCall, struct{}{}) + fake.recordInvocation("GotSuccess", []interface{}{}) + fake.gotSuccessMutex.Unlock() + if fake.GotSuccessStub != nil { + fake.GotSuccessStub() + } +} + +func (fake *FatalErrorDetector) GotSuccessCallCount() int { + fake.gotSuccessMutex.RLock() + defer fake.gotSuccessMutex.RUnlock() + return len(fake.gotSuccessArgsForCall) +} + +func (fake *FatalErrorDetector) IsFatal(arg1 error) bool { + fake.isFatalMutex.Lock() + ret, specificReturn := fake.isFatalReturnsOnCall[len(fake.isFatalArgsForCall)] + fake.isFatalArgsForCall = append(fake.isFatalArgsForCall, struct { + arg1 error + }{arg1}) + fake.recordInvocation("IsFatal", []interface{}{arg1}) + fake.isFatalMutex.Unlock() + if fake.IsFatalStub != nil { + return fake.IsFatalStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.isFatalReturns.result1 +} + +func (fake *FatalErrorDetector) IsFatalCallCount() int { + fake.isFatalMutex.RLock() + defer fake.isFatalMutex.RUnlock() + return len(fake.isFatalArgsForCall) +} + +func (fake *FatalErrorDetector) IsFatalArgsForCall(i int) error { + fake.isFatalMutex.RLock() + defer fake.isFatalMutex.RUnlock() + return fake.isFatalArgsForCall[i].arg1 +} + +func (fake *FatalErrorDetector) IsFatalReturns(result1 bool) { + fake.IsFatalStub = nil + fake.isFatalReturns = struct { + result1 bool + }{result1} +} + +func (fake *FatalErrorDetector) IsFatalReturnsOnCall(i int, result1 bool) { + fake.IsFatalStub = nil + if fake.isFatalReturnsOnCall == nil { + fake.isFatalReturnsOnCall = make(map[int]struct { + result1 bool + }) + } + fake.isFatalReturnsOnCall[i] = struct { + result1 bool + }{result1} +} + +func (fake *FatalErrorDetector) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.gotSuccessMutex.RLock() + defer fake.gotSuccessMutex.RUnlock() + fake.isFatalMutex.RLock() + defer fake.isFatalMutex.RUnlock() + return fake.invocations +} + +func (fake *FatalErrorDetector) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ planner.FatalErrorDetector = new(FatalErrorDetector) diff --git a/src/code.cloudfoundry.org/silk/daemon/planner/fakes/metricSender.go b/src/code.cloudfoundry.org/silk/daemon/planner/fakes/metricSender.go new file mode 100644 index 00000000..2d9d52ae --- /dev/null +++ b/src/code.cloudfoundry.org/silk/daemon/planner/fakes/metricSender.go @@ -0,0 +1,95 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" +) + +type MetricSender struct { + SendValueStub func(name string, value float64, units string) + sendValueMutex sync.RWMutex + sendValueArgsForCall []struct { + name string + value float64 + units string + } + IncrementCounterStub func(name string) + incrementCounterMutex sync.RWMutex + incrementCounterArgsForCall []struct { + name string + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *MetricSender) SendValue(name string, value float64, units string) { + fake.sendValueMutex.Lock() + fake.sendValueArgsForCall = append(fake.sendValueArgsForCall, struct { + name string + value float64 + units string + }{name, value, units}) + fake.recordInvocation("SendValue", []interface{}{name, value, units}) + fake.sendValueMutex.Unlock() + if fake.SendValueStub != nil { + fake.SendValueStub(name, value, units) + } +} + +func (fake *MetricSender) SendValueCallCount() int { + fake.sendValueMutex.RLock() + defer fake.sendValueMutex.RUnlock() + return len(fake.sendValueArgsForCall) +} + +func (fake *MetricSender) SendValueArgsForCall(i int) (string, float64, string) { + fake.sendValueMutex.RLock() + defer fake.sendValueMutex.RUnlock() + return fake.sendValueArgsForCall[i].name, fake.sendValueArgsForCall[i].value, fake.sendValueArgsForCall[i].units +} + +func (fake *MetricSender) IncrementCounter(name string) { + fake.incrementCounterMutex.Lock() + fake.incrementCounterArgsForCall = append(fake.incrementCounterArgsForCall, struct { + name string + }{name}) + fake.recordInvocation("IncrementCounter", []interface{}{name}) + fake.incrementCounterMutex.Unlock() + if fake.IncrementCounterStub != nil { + fake.IncrementCounterStub(name) + } +} + +func (fake *MetricSender) IncrementCounterCallCount() int { + fake.incrementCounterMutex.RLock() + defer fake.incrementCounterMutex.RUnlock() + return len(fake.incrementCounterArgsForCall) +} + +func (fake *MetricSender) IncrementCounterArgsForCall(i int) string { + fake.incrementCounterMutex.RLock() + defer fake.incrementCounterMutex.RUnlock() + return fake.incrementCounterArgsForCall[i].name +} + +func (fake *MetricSender) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.sendValueMutex.RLock() + defer fake.sendValueMutex.RUnlock() + fake.incrementCounterMutex.RLock() + defer fake.incrementCounterMutex.RUnlock() + return fake.invocations +} + +func (fake *MetricSender) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/planner/fatal_error_detector.go b/src/code.cloudfoundry.org/silk/daemon/planner/fatal_error_detector.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/planner/fatal_error_detector.go rename to src/code.cloudfoundry.org/silk/daemon/planner/fatal_error_detector.go diff --git a/src/code.cloudfoundry.org/silk/daemon/planner/fatal_error_detector_test.go b/src/code.cloudfoundry.org/silk/daemon/planner/fatal_error_detector_test.go new file mode 100644 index 00000000..fddc9c44 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/daemon/planner/fatal_error_detector_test.go @@ -0,0 +1,66 @@ +package planner_test + +import ( + "fmt" + "time" + + "code.cloudfoundry.org/silk/controller" + "code.cloudfoundry.org/silk/daemon/planner" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("FatalErrorDetector", func() { + var fed planner.FatalErrorDetector + const graceDuration time.Duration = 10 * time.Millisecond + + BeforeEach(func() { + fed = planner.NewGracefulDetector(graceDuration) + }) + + Describe("IsFatal", func() { + Context("when given a non-retriable error", func() { + It("reports it as fatal", func() { + err := controller.NonRetriableError("guava") + Expect(fed.IsFatal(err)).To(BeTrue()) + }) + }) + Context("when given any other type of error", func() { + It("reports it non-fatal", func() { + err := fmt.Errorf("banana") + Expect(fed.IsFatal(err)).To(BeFalse()) + }) + }) + + Context("when GraceDuration has passed since the last GotSuccess", func() { + It("reports errors as fatal if GraceDuration has passed since the last New or GotSuccess", func() { + err := fmt.Errorf("banana") + + By("checking that the error is not fatal, for a while") + Consistently(func() bool { + return fed.IsFatal(err) + }, graceDuration-time.Millisecond).Should(BeFalse()) + + By("waiting for the grace duration") + time.Sleep(graceDuration) + + By("checking the error is fatal") + Expect(fed.IsFatal(err)).To(BeTrue()) + + By("reporting a success, which resets the timer") + fed.GotSuccess() + + By("checking that the error is not fatal, for a while") + Consistently(func() bool { + return fed.IsFatal(err) + }, graceDuration-time.Millisecond).Should(BeFalse()) + + By("waiting for the grace duration, again") + time.Sleep(graceDuration) + + By("checking the error is fatal, again") + Expect(fed.IsFatal(err)).To(BeTrue()) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/daemon/planner/planner_suite_test.go b/src/code.cloudfoundry.org/silk/daemon/planner/planner_suite_test.go new file mode 100644 index 00000000..8d19865b --- /dev/null +++ b/src/code.cloudfoundry.org/silk/daemon/planner/planner_suite_test.go @@ -0,0 +1,13 @@ +package planner_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" +) + +func TestPlanner(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Planner Suite") +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/planner/vxlan_planner.go b/src/code.cloudfoundry.org/silk/daemon/planner/vxlan_planner.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/planner/vxlan_planner.go rename to src/code.cloudfoundry.org/silk/daemon/planner/vxlan_planner.go diff --git a/src/code.cloudfoundry.org/silk/daemon/planner/vxlan_planner_test.go b/src/code.cloudfoundry.org/silk/daemon/planner/vxlan_planner_test.go new file mode 100644 index 00000000..544a0694 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/daemon/planner/vxlan_planner_test.go @@ -0,0 +1,232 @@ +package planner_test + +import ( + "errors" + + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/controller" + "code.cloudfoundry.org/silk/daemon" + "code.cloudfoundry.org/silk/daemon/planner" + "code.cloudfoundry.org/silk/daemon/planner/fakes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" +) + +var LogsWith = func(level lager.LogLevel, msg string) types.GomegaMatcher { + return And( + WithTransform(func(log lager.LogFormat) string { + return log.Message + }, Equal(msg)), + WithTransform(func(log lager.LogFormat) lager.LogLevel { + return log.LogLevel + }, Equal(level)), + ) +} + +var HaveLogData = func(nextMatcher types.GomegaMatcher) types.GomegaMatcher { + return WithTransform(func(log lager.LogFormat) lager.Data { + return log.Data + }, nextMatcher) +} + +var _ = Describe("VxlanPlanner", func() { + var ( + logger *lagertest.TestLogger + vxlanPlanner *planner.VXLANPlanner + controllerClient *fakes.ControllerClient + converger *fakes.Converger + errorDetector *fakes.FatalErrorDetector + metricSender *fakes.MetricSender + ) + + BeforeEach(func() { + logger = lagertest.NewTestLogger("test") + controllerClient = &fakes.ControllerClient{} + converger = &fakes.Converger{} + metricSender = &fakes.MetricSender{} + errorDetector = &fakes.FatalErrorDetector{} + vxlanPlanner = &planner.VXLANPlanner{ + Logger: logger, + ControllerClient: controllerClient, + Converger: converger, + Lease: controller.Lease{ + UnderlayIP: "172.244.17.0", + OverlaySubnet: "10.244.17.0/24", + OverlayHardwareAddr: "ee:ee:0a:f4:11:00", + }, + ErrorDetector: errorDetector, + MetricSender: metricSender, + } + }) + + Describe("DoCycle", func() { + var leases []controller.Lease + + BeforeEach(func() { + leases = []controller.Lease{controller.Lease{ + UnderlayIP: "172.244.15.0", + OverlaySubnet: "10.244.15.0/24", + OverlayHardwareAddr: "ee:ee:0a:f4:0f:00", + }, controller.Lease{ + UnderlayIP: "172.244.16.0", + OverlaySubnet: "10.244.16.0/24", + OverlayHardwareAddr: "ee:ee:0a:f4:10:00", + }} + controllerClient.GetActiveLeasesReturns(leases, nil) + }) + + It("calls the controller to renew its lease", func() { + err := vxlanPlanner.DoCycle() + Expect(err).NotTo(HaveOccurred()) + + Expect(controllerClient.RenewSubnetLeaseCallCount()).To(Equal(1)) + Expect(controllerClient.RenewSubnetLeaseArgsForCall(0)).To(Equal(controller.Lease{ + UnderlayIP: "172.244.17.0", + OverlaySubnet: "10.244.17.0/24", + OverlayHardwareAddr: "ee:ee:0a:f4:11:00", + })) + + By("checking that the lease was logged at debug level") + Expect(logger.Logs()).To(ContainElement(SatisfyAll( + LogsWith(lager.DEBUG, "test.renew-lease"), + HaveLogData(HaveKeyWithValue("lease", + SatisfyAll( + HaveKeyWithValue("underlay_ip", "172.244.17.0"), + HaveKeyWithValue("overlay_subnet", "10.244.17.0/24"), + HaveKeyWithValue("overlay_hardware_addr", "ee:ee:0a:f4:11:00"), + ), + )), + ))) + + By("informing the error detector of the successful renewal") + Expect(errorDetector.GotSuccessCallCount()).To(Equal(1)) + + Expect(metricSender.IncrementCounterCallCount()).To(Equal(2)) + name := metricSender.IncrementCounterArgsForCall(0) + Expect(name).To(Equal("renewSuccess")) + }) + + It("emits a metric with the number of leases received", func() { + err := vxlanPlanner.DoCycle() + Expect(err).NotTo(HaveOccurred()) + + Expect(metricSender.SendValueCallCount()).To(Equal(1)) + name, value, unit := metricSender.SendValueArgsForCall(0) + Expect(name).To(Equal("numberLeases")) + Expect(value).To(BeEquivalentTo(2)) + Expect(unit).To(Equal("")) + + leases = append(leases, controller.Lease{ + UnderlayIP: "172.244.17.0", + OverlaySubnet: "10.244.17.0/24", + OverlayHardwareAddr: "ee:ee:0a:f6:10:00", + }) + controllerClient.GetActiveLeasesReturns(leases, nil) + + err = vxlanPlanner.DoCycle() + name, value, unit = metricSender.SendValueArgsForCall(1) + Expect(name).To(Equal("numberLeases")) + Expect(value).To(BeEquivalentTo(3)) + Expect(unit).To(Equal("")) + + }) + + It("passes the received leases to the converger to update the networking stack", func() { + err := vxlanPlanner.DoCycle() + Expect(err).NotTo(HaveOccurred()) + + Expect(converger.ConvergeCallCount()).To(Equal(1)) + Expect(converger.ConvergeArgsForCall(0)).To(Equal(leases)) + + By("checking that the leases were logged at debug level") + Expect(logger.Logs()).To(ContainElement(SatisfyAll( + LogsWith(lager.DEBUG, "test.converge-leases"), + HaveLogData(HaveKeyWithValue("leases", ConsistOf( + SatisfyAll( + HaveKeyWithValue("underlay_ip", "172.244.15.0"), + HaveKeyWithValue("overlay_subnet", "10.244.15.0/24"), + HaveKeyWithValue("overlay_hardware_addr", "ee:ee:0a:f4:0f:00"), + ), + SatisfyAll( + HaveKeyWithValue("underlay_ip", "172.244.16.0"), + HaveKeyWithValue("overlay_subnet", "10.244.16.0/24"), + HaveKeyWithValue("overlay_hardware_addr", "ee:ee:0a:f4:10:00"), + ), + ))), + ))) + + By("checking that a metric was emitted for converge success") + Expect(metricSender.IncrementCounterCallCount()).To(Equal(2)) + Expect(metricSender.IncrementCounterArgsForCall(1)).To(Equal("convergeSuccess")) + }) + + Context("when renewing the subnet lease fails", func() { + Context("when the error is detected as non-fatal", func() { + BeforeEach(func() { + controllerClient.RenewSubnetLeaseReturns(errors.New("guava")) + errorDetector.IsFatalReturns(false) + }) + It("returns the error as non-fatal and emits a failure metric", func() { + err := vxlanPlanner.DoCycle() + Expect(err).To(MatchError("renew lease: guava")) + _, ok := err.(daemon.FatalError) + Expect(ok).NotTo(BeTrue()) + + Expect(errorDetector.IsFatalCallCount()).To(Equal(1)) + Expect(errorDetector.IsFatalArgsForCall(0)).To(Equal(errors.New("guava"))) + Expect(errorDetector.GotSuccessCallCount()).To(Equal(0)) + + Expect(metricSender.IncrementCounterCallCount()).To(Equal(1)) + Expect(metricSender.IncrementCounterArgsForCall(0)).To(Equal("renewFailure")) + }) + }) + + Context("when the error is detected as fatal", func() { + BeforeEach(func() { + controllerClient.RenewSubnetLeaseReturns(errors.New("guava")) + errorDetector.IsFatalReturns(true) + }) + It("returns the error as a fatal error and emits a failure metric", func() { + err := vxlanPlanner.DoCycle() + Expect(err).To(MatchError("fatal: renew lease: guava")) + _, ok := err.(daemon.FatalError) + Expect(ok).To(BeTrue()) + + Expect(errorDetector.IsFatalCallCount()).To(Equal(1)) + Expect(errorDetector.IsFatalArgsForCall(0)).To(Equal(errors.New("guava"))) + Expect(errorDetector.GotSuccessCallCount()).To(Equal(0)) + + Expect(metricSender.IncrementCounterCallCount()).To(Equal(1)) + Expect(metricSender.IncrementCounterArgsForCall(0)).To(Equal("renewFailure")) + }) + }) + }) + + Context("when getting the routable releases fails", func() { + BeforeEach(func() { + controllerClient.GetActiveLeasesReturns(nil, errors.New("guava")) + }) + It("returns the error", func() { + err := vxlanPlanner.DoCycle() + Expect(err).To(MatchError("get routable leases: guava")) + }) + }) + + Context("when the converger fails", func() { + BeforeEach(func() { + converger.ConvergeReturns(errors.New("banana")) + }) + It("returns an error", func() { + err := vxlanPlanner.DoCycle() + Expect(err).To(MatchError("converge leases: banana")) + + By("checking that a metric was emitted for converge failure") + Expect(metricSender.IncrementCounterCallCount()).To(Equal(2)) + Expect(metricSender.IncrementCounterArgsForCall(1)).To(Equal("convergeFailure")) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/poller/poller.go b/src/code.cloudfoundry.org/silk/daemon/poller/poller.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/poller/poller.go rename to src/code.cloudfoundry.org/silk/daemon/poller/poller.go diff --git a/src/code.cloudfoundry.org/silk/daemon/poller/poller_suite_test.go b/src/code.cloudfoundry.org/silk/daemon/poller/poller_suite_test.go new file mode 100644 index 00000000..01e0167e --- /dev/null +++ b/src/code.cloudfoundry.org/silk/daemon/poller/poller_suite_test.go @@ -0,0 +1,13 @@ +package poller_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" +) + +func TestPoller(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Poller Suite") +} diff --git a/src/code.cloudfoundry.org/silk/daemon/poller/poller_test.go b/src/code.cloudfoundry.org/silk/daemon/poller/poller_test.go new file mode 100644 index 00000000..deeb6b39 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/daemon/poller/poller_test.go @@ -0,0 +1,122 @@ +package poller_test + +import ( + "errors" + "os" + "sync/atomic" + "time" + + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/daemon" + "code.cloudfoundry.org/silk/daemon/poller" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" +) + +var _ = Describe("Poller", func() { + Describe("Run", func() { + var ( + logger *lagertest.TestLogger + p *poller.Poller + signals chan os.Signal + ready chan struct{} + + cycleCount uint64 + retChan chan error + ) + + BeforeEach(func() { + signals = make(chan os.Signal) + ready = make(chan struct{}) + + cycleCount = 0 + retChan = make(chan error) + + logger = lagertest.NewTestLogger("test") + + p = &poller.Poller{ + Logger: logger, + PollInterval: 1 * time.Second, + + SingleCycleFunc: func() error { + atomic.AddUint64(&cycleCount, 1) + return nil + }, + } + }) + + Context("when running", func() { + + It("calls the single cycle func on start", func() { + go func() { + retChan <- p.Run(signals, ready) + }() + + Eventually(ready).Should(BeClosed()) + Expect(atomic.LoadUint64(&cycleCount)).To(Equal(uint64(1))) + + signals <- os.Interrupt + Eventually(retChan).Should(Receive(nil)) + }) + + It("calls the single cycle func after the poll interval", func() { + go func() { + retChan <- p.Run(signals, ready) + }() + + Eventually(ready).Should(BeClosed()) + Eventually(func() uint64 { + return atomic.LoadUint64(&cycleCount) + }, 3*time.Second).Should(BeNumerically(">", 1)) + + Consistently(retChan).ShouldNot(Receive()) + + signals <- os.Interrupt + Eventually(retChan).Should(Receive(nil)) + }) + + }) + + Context("when the cycle func fails with a non-fatal error", func() { + BeforeEach(func() { + p.SingleCycleFunc = func() error { return errors.New("banana") } + }) + + It("logs the error and continues", func() { + go func() { + retChan <- p.Run(signals, ready) + }() + + Eventually(ready).Should(BeClosed()) + + Eventually(logger).Should(gbytes.Say("poll-cycle.*banana")) + + Consistently(retChan).ShouldNot(Receive()) + + signals <- os.Interrupt + Eventually(retChan).Should(Receive(nil)) + }) + }) + + Context("when the cycle func fails with a fatal error", func() { + BeforeEach(func() { + p.SingleCycleFunc = func() error { + return daemon.FatalError("banana") + } + }) + + It("logs the error and exits", func() { + go func() { + retChan <- p.Run(signals, ready) + }() + + Eventually(ready).Should(BeClosed()) + Eventually(logger).Should(gbytes.Say("poll-cycle.*banana")) + Eventually(retChan).Should(Receive(MatchError( + "This cell must be restarted (run \"bosh restart \"): fatal: banana", + ))) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/vtep/config_creator.go b/src/code.cloudfoundry.org/silk/daemon/vtep/config_creator.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/vtep/config_creator.go rename to src/code.cloudfoundry.org/silk/daemon/vtep/config_creator.go diff --git a/src/code.cloudfoundry.org/silk/daemon/vtep/config_creator_test.go b/src/code.cloudfoundry.org/silk/daemon/vtep/config_creator_test.go new file mode 100644 index 00000000..b0ed8c4a --- /dev/null +++ b/src/code.cloudfoundry.org/silk/daemon/vtep/config_creator_test.go @@ -0,0 +1,218 @@ +package vtep_test + +import ( + "errors" + "net" + + clientConfig "code.cloudfoundry.org/silk/client/config" + "code.cloudfoundry.org/silk/controller" + "code.cloudfoundry.org/silk/daemon/vtep" + "code.cloudfoundry.org/silk/daemon/vtep/fakes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ConfigCreator", func() { + Describe("Create", func() { + var ( + creator *vtep.ConfigCreator + fakeNetAdapter *fakes.NetAdapter + clientConf clientConfig.Config + lease controller.Lease + ) + BeforeEach(func() { + fakeNetAdapter = &fakes.NetAdapter{} + creator = &vtep.ConfigCreator{ + NetAdapter: fakeNetAdapter, + } + clientConf = clientConfig.Config{ + UnderlayIP: "172.255.30.2", + SubnetPrefixLength: 24, + VTEPName: "some-vtep-name", + VNI: 99, + OverlayNetwork: "10.255.0.0/16", + VTEPPort: 12225, + } + lease = controller.Lease{ + UnderlayIP: "172.255.30.02", + OverlaySubnet: "10.255.30.0/24", + OverlayHardwareAddr: "ee:ee:0a:ff:1e:00", + } + + fakeNetAdapter.InterfacesReturns([]net.Interface{net.Interface{ + Index: 42, + }}, nil) + fakeNetAdapter.InterfaceAddrsReturns([]net.Addr{ + &net.IPNet{ + IP: net.IP{172, 255, 30, 2}, + Mask: net.IPMask{255, 255, 255, 255}, + }, + }, nil) + }) + + It("returns a Config", func() { + conf, err := creator.Create(clientConf, lease) + Expect(err).NotTo(HaveOccurred()) + Expect(conf.VTEPName).To(Equal("some-vtep-name")) + Expect(conf.UnderlayInterface).To(Equal(net.Interface{Index: 42})) + Expect(conf.UnderlayIP.String()).To(Equal("172.255.30.2")) + Expect(conf.OverlayIP.String()).To(Equal("10.255.30.0")) + Expect(conf.OverlayHardwareAddr).To(Equal(net.HardwareAddr{0xee, 0xee, 0x0a, 0xff, 0x1e, 0x00})) + Expect(conf.VNI).To(Equal(99)) + Expect(conf.OverlayNetworkPrefixLength).To(Equal(16)) + Expect(conf.VTEPPort).To(Equal(12225)) + + Expect(fakeNetAdapter.InterfacesCallCount()).To(Equal(1)) + Expect(fakeNetAdapter.InterfaceAddrsCallCount()).To(Equal(1)) + Expect(fakeNetAdapter.InterfaceAddrsArgsForCall(0)).To(Equal(net.Interface{Index: 42})) + Expect(fakeNetAdapter.InterfaceByNameCallCount()).To(Equal(0)) + }) + + Context("when VxlanInterfaceName is set", func() { + BeforeEach(func() { + clientConf.VxlanInterfaceName = "eth1" + fakeNetAdapter.InterfaceByNameReturns(&net.Interface{ + Index: 38, + }, nil) + }) + It("uses the underlay interface name in the config", func() { + conf, err := creator.Create(clientConf, lease) + Expect(err).NotTo(HaveOccurred()) + Expect(conf.UnderlayInterface).To(Equal(net.Interface{Index: 38})) + + Expect(fakeNetAdapter.InterfacesCallCount()).To(Equal(0)) + Expect(fakeNetAdapter.InterfaceByNameCallCount()).To(Equal(1)) + Expect(fakeNetAdapter.InterfaceByNameArgsForCall(0)).To(Equal("eth1")) + }) + Context("when the VxlanInterfaceName does not exist", func() { + BeforeEach(func() { + fakeNetAdapter.InterfaceByNameReturns(nil, errors.New("banana")) + }) + It("returns an error", func() { + _, err := creator.Create(clientConf, lease) + Expect(err).To(MatchError("find device from name eth1: banana")) + }) + }) + }) + + Context("when the overlay network prefix length is greater than or equal to the subnet prefix length", func() { + BeforeEach(func() { + clientConf.OverlayNetwork = "10.255.0.0/30" + }) + It("returns an error", func() { + _, err := creator.Create(clientConf, lease) + Expect(err).To(MatchError("overlay prefix 30 must be smaller than subnet prefix 24")) + }) + }) + + Context("when the overlay network is not set", func() { + BeforeEach(func() { + clientConf.OverlayNetwork = "" + }) + It("returns an error", func() { + _, err := creator.Create(clientConf, lease) + Expect(err).To(MatchError("determine overlay network: invalid CIDR address: ")) + }) + }) + + Context("when the vtep name is empty", func() { + BeforeEach(func() { + clientConf.VTEPName = "" + }) + It("returns a sensible error", func() { + _, err := creator.Create(clientConf, lease) + Expect(err).To(MatchError("empty vtep name")) + }) + }) + + Context("when the vtep port is less than 1", func() { + BeforeEach(func() { + clientConf.VTEPPort = 0 + }) + + It("returns a sensible error", func() { + _, err := creator.Create(clientConf, lease) + Expect(err).To(MatchError("vtep port must be greater than 0")) + }) + }) + + Context("when parsing the underlay ip returns nil", func() { + BeforeEach(func() { + clientConf.UnderlayIP = "some-invalid" + }) + It("returns a sensible error", func() { + _, err := creator.Create(clientConf, lease) + Expect(err).To(MatchError("parse underlay ip: some-invalid")) + }) + }) + + Context("when parsing the lease subnet returns nil", func() { + BeforeEach(func() { + lease.OverlaySubnet = "foo" + }) + It("returns a sensible error", func() { + _, err := creator.Create(clientConf, lease) + Expect(err).To(MatchError("determine vtep overlay ip: invalid CIDR address: foo")) + }) + }) + + Context("when the interface cannot be found", func() { + BeforeEach(func() { + fakeNetAdapter.InterfacesReturns(nil, errors.New("pomelo")) + }) + It("returns a sensible error", func() { + _, err := creator.Create(clientConf, lease) + Expect(err).To(MatchError("find device from ip 172.255.30.2: find interfaces: pomelo")) + }) + }) + + Context("when the getting the addresses of the interface errors", func() { + BeforeEach(func() { + fakeNetAdapter.InterfaceAddrsReturns(nil, errors.New("grape")) + }) + It("returns a sensible error", func() { + _, err := creator.Create(clientConf, lease) + Expect(err).To(MatchError("find device from ip 172.255.30.2: get addresses: grape")) + }) + }) + + Context("when parsing the CIDR of the interface fails", func() { + BeforeEach(func() { + fakeNetAdapter.InterfaceAddrsReturns([]net.Addr{ + &net.IPNet{ + IP: net.IP{173, 255, 44, 4}, + }, + }, nil) + }) + It("returns a sensible error", func() { + _, err := creator.Create(clientConf, lease) + Expect(err).To(MatchError("find device from ip 172.255.30.2: parse address: invalid CIDR address: ")) + }) + }) + + Context("when there are no interfaces with the given ip address", func() { + BeforeEach(func() { + fakeNetAdapter.InterfaceAddrsReturns([]net.Addr{ + &net.IPNet{ + IP: net.IP{173, 255, 44, 4}, + Mask: net.IPMask{255, 255, 255, 255}, + }, + }, nil) + }) + It("returns a sensible error", func() { + _, err := creator.Create(clientConf, lease) + Expect(err).To(MatchError("find device from ip 172.255.30.2: no interface with address 172.255.30.2")) + }) + }) + + Context("when parsing the hardware addr fails", func() { + BeforeEach(func() { + lease.OverlayHardwareAddr = "foo" + }) + It("returns a sensible error", func() { + _, err := creator.Create(clientConf, lease) + Expect(err).To(MatchError(ContainSubstring("parsing hardware address:"))) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/vtep/converger.go b/src/code.cloudfoundry.org/silk/daemon/vtep/converger.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/vtep/converger.go rename to src/code.cloudfoundry.org/silk/daemon/vtep/converger.go diff --git a/src/code.cloudfoundry.org/silk/daemon/vtep/converger_test.go b/src/code.cloudfoundry.org/silk/daemon/vtep/converger_test.go new file mode 100644 index 00000000..084d01ed --- /dev/null +++ b/src/code.cloudfoundry.org/silk/daemon/vtep/converger_test.go @@ -0,0 +1,439 @@ +package vtep_test + +import ( + "errors" + "net" + "syscall" + + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/controller" + "code.cloudfoundry.org/silk/daemon/vtep" + "code.cloudfoundry.org/silk/daemon/vtep/fakes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/vishvananda/netlink" +) + +var _ = Describe("Converger", func() { + var ( + fakeNetlink *fakes.NetlinkAdapter + converger *vtep.Converger + leases []controller.Lease + overlayNet *net.IPNet + logger *lagertest.TestLogger + localMac net.HardwareAddr + remoteMac net.HardwareAddr + ) + + Describe("Converge", func() { + BeforeEach(func() { + fakeNetlink = &fakes.NetlinkAdapter{} + _, localSubnet, _ := net.ParseCIDR("10.255.32.0/24") + _, overlayNet, _ = net.ParseCIDR("10.255.0.0/16") + logger = lagertest.NewTestLogger("test") + localVTEP := net.Interface{ + Index: 42, + Name: "silk-vtep", + } + converger = &vtep.Converger{ + OverlayNetwork: overlayNet, + LocalSubnet: localSubnet, + LocalVTEP: localVTEP, + NetlinkAdapter: fakeNetlink, + Logger: logger, + } + localMac, _ = net.ParseMAC("ee:ee:aa:bb:cc:dd") + remoteMac, _ = net.ParseMAC("ee:ee:aa:aa:aa:ff") + leases = []controller.Lease{ + controller.Lease{ + UnderlayIP: "10.10.0.4", + OverlaySubnet: "10.255.32.0/24", + OverlayHardwareAddr: localMac.String(), + }, + controller.Lease{ + UnderlayIP: "10.10.0.5", + OverlaySubnet: "10.255.19.0/24", + OverlayHardwareAddr: remoteMac.String(), + }, + } + }) + + It("adds routing rule for each remote lease", func() { + err := converger.Converge(leases) + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeNetlink.RouteReplaceCallCount()).To(Equal(1)) + addedRoute := fakeNetlink.RouteReplaceArgsForCall(0) + destGW, destNet, _ := net.ParseCIDR("10.255.19.0/24") + Expect(addedRoute).To(Equal(&netlink.Route{ + LinkIndex: 42, + Scope: netlink.SCOPE_UNIVERSE, + Dst: destNet, + Gw: destGW, + Src: net.ParseIP("10.255.32.0").To4(), + })) + }) + + It("adds an ARP and FDB rule for each remote lease", func() { + err := converger.Converge(leases) + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeNetlink.NeighSetCallCount()).To(Equal(2)) + neighs := []*netlink.Neigh{ + fakeNetlink.NeighSetArgsForCall(0), + fakeNetlink.NeighSetArgsForCall(1), + } + Expect(neighs).To(ConsistOf( + &netlink.Neigh{ + LinkIndex: 42, + State: netlink.NUD_PERMANENT, + Type: syscall.RTN_UNICAST, + IP: net.ParseIP("10.255.19.0"), + HardwareAddr: remoteMac, + }, + &netlink.Neigh{ + LinkIndex: 42, + State: netlink.NUD_PERMANENT, + Family: syscall.AF_BRIDGE, + Flags: netlink.NTF_SELF, + IP: net.ParseIP("10.10.0.5"), + HardwareAddr: remoteMac, + }, + )) + }) + + It("does not log anything about non-routable leases", func() { + err := converger.Converge(leases) + Expect(err).NotTo(HaveOccurred()) + + Expect(logger.Logs()).To(HaveLen(0)) + }) + + Context("when the a remote lease is removed", func() { + var ( + deletedNeighs []netlink.Neigh + oldDestMac net.HardwareAddr + ) + BeforeEach(func() { + destGW, destNet, _ := net.ParseCIDR("10.255.19.0/24") + + oldDestMac, _ = net.ParseMAC("ee:ee:0a:ff:14:00") + oldDestGW, oldDestNet, _ := net.ParseCIDR("10.255.20.0/24") + + fakeNetlink.FDBListReturns([]netlink.Neigh{ + netlink.Neigh{ + LinkIndex: 42, + State: netlink.NUD_PERMANENT, + Family: syscall.AF_BRIDGE, + Flags: netlink.NTF_SELF, + IP: net.ParseIP("10.10.0.5"), + HardwareAddr: remoteMac, + }, + netlink.Neigh{ + LinkIndex: 42, + State: netlink.NUD_PERMANENT, + Family: syscall.AF_BRIDGE, + Flags: netlink.NTF_SELF, + IP: net.ParseIP("10.10.0.6"), + HardwareAddr: oldDestMac, + }, + }, nil) + + fakeNetlink.ARPListReturns([]netlink.Neigh{ + netlink.Neigh{ + LinkIndex: 42, + State: netlink.NUD_PERMANENT, + Type: syscall.RTN_UNICAST, + IP: net.ParseIP("10.255.19.0"), + HardwareAddr: remoteMac, + }, + netlink.Neigh{ + LinkIndex: 42, + State: netlink.NUD_PERMANENT, + Type: syscall.RTN_UNICAST, + IP: net.ParseIP("10.255.20.0"), + HardwareAddr: oldDestMac, + }, + }, nil) + + fakeNetlink.RouteListReturns([]netlink.Route{ + netlink.Route{ + LinkIndex: 42, + Scope: netlink.SCOPE_UNIVERSE, + Dst: destNet, + Gw: destGW, + Src: net.ParseIP("10.255.32.0").To4(), + }, + netlink.Route{ + LinkIndex: 42, + Scope: netlink.SCOPE_UNIVERSE, + Dst: overlayNet, + Gw: nil, + Src: net.ParseIP("10.255.32.0").To4(), + }, + netlink.Route{ + LinkIndex: 42, + Scope: netlink.SCOPE_UNIVERSE, + Dst: oldDestNet, + Gw: oldDestGW, + Src: net.ParseIP("10.255.48.0").To4(), + }, + }, nil) + + deletedNeighs = []netlink.Neigh{} + fakeNetlink.NeighDelStub = func(neigh *netlink.Neigh) error { + deletedNeighs = append(deletedNeighs, *neigh) + return nil + } + }) + + It("deletes the routing, ARP, and FDB rules related to the removed lease", func() { + err := converger.Converge(leases) + Expect(err).NotTo(HaveOccurred()) + + By("checking that the route is deleted") + Expect(fakeNetlink.RouteDelCallCount()).To(Equal(1)) + deletedRoute := fakeNetlink.RouteDelArgsForCall(0) + destGW, destNet, _ := net.ParseCIDR("10.255.20.0/24") + Expect(deletedRoute).To(Equal(&netlink.Route{ + LinkIndex: 42, + Scope: netlink.SCOPE_UNIVERSE, + Dst: destNet, + Gw: destGW, + Src: net.ParseIP("10.255.48.0").To4(), + })) + + By("checking that the ARP and FDB rules is deleted") + Expect(fakeNetlink.NeighDelCallCount()).To(Equal(2)) + + Expect(deletedNeighs).To(ConsistOf( + // ARP + netlink.Neigh{ + LinkIndex: 42, + State: netlink.NUD_PERMANENT, + Type: syscall.RTN_UNICAST, + IP: net.ParseIP("10.255.20.0"), + HardwareAddr: oldDestMac, + }, + // FDB + netlink.Neigh{ + LinkIndex: 42, + State: netlink.NUD_PERMANENT, + Family: syscall.AF_BRIDGE, + Flags: netlink.NTF_SELF, + IP: net.ParseIP("10.10.0.6"), + HardwareAddr: oldDestMac, + }, + )) + }) + + }) + + Context("when there are other routing rules", func() { + BeforeEach(func() { + fakeNetlink.RouteListReturns([]netlink.Route{ + netlink.Route{ + LinkIndex: 42, + Scope: netlink.SCOPE_UNIVERSE, + Dst: overlayNet, + Gw: nil, + Src: net.ParseIP("10.255.32.0").To4(), + }, + }, nil) + }) + + It("does not delete rules it did not create", func() { + err := converger.Converge(leases) + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeNetlink.RouteDelCallCount()).To(Equal(0)) + }) + }) + + Context("when the link cannot be found", func() { + BeforeEach(func() { + fakeNetlink.LinkByIndexReturns(nil, errors.New("passionfruit")) + }) + + It("breaks early and returns a meaningful error", func() { + err := converger.Converge(leases) + Expect(err).To(MatchError("link by index: passionfruit")) + }) + }) + + Context("when previous routes cannot be found", func() { + BeforeEach(func() { + fakeNetlink.RouteListReturns(nil, errors.New("peach")) + }) + + It("breaks early and returns a meaningful error", func() { + err := converger.Converge(leases) + Expect(err).To(MatchError("list routes: peach")) + }) + }) + + Context("when previous fdb entries cannot be found", func() { + BeforeEach(func() { + fakeNetlink.FDBListReturns(nil, errors.New("kiwi")) + }) + + It("breaks early and returns a meaningful error", func() { + err := converger.Converge(leases) + Expect(err).To(MatchError("list fdb: kiwi")) + }) + }) + + Context("when previous arp entries cannot be found", func() { + BeforeEach(func() { + fakeNetlink.ARPListReturns(nil, errors.New("lychee")) + }) + + It("breaks early and returns a meaningful error", func() { + err := converger.Converge(leases) + Expect(err).To(MatchError("list arp: lychee")) + }) + }) + + Context("when the lease subnet is malformed", func() { + BeforeEach(func() { + leases[1].OverlaySubnet = "banana" + }) + It("breaks early and returns a meaningful error", func() { + err := converger.Converge(leases) + Expect(err).To(MatchError("parse lease: invalid CIDR address: banana")) + }) + }) + + Context("when the underlay IP is malformed", func() { + BeforeEach(func() { + leases[1].UnderlayIP = "kumquat" + }) + It("breaks early and returns a meaningful error", func() { + err := converger.Converge(leases) + Expect(err).To(MatchError("invalid underlay ip: kumquat")) + }) + }) + + Context("when adding the route fails", func() { + BeforeEach(func() { + fakeNetlink.RouteReplaceReturns(errors.New("apricot")) + }) + It("returns a meaningful error", func() { + err := converger.Converge(leases) + Expect(err).To(MatchError("add route: apricot")) + }) + }) + + Context("when adding a neigh fails", func() { + BeforeEach(func() { + fakeNetlink.NeighSetReturns(errors.New("pear")) + }) + It("returns a meaningful error", func() { + err := converger.Converge(leases) + Expect(err).To(MatchError("set neigh: pear")) + }) + }) + + Context("when deleting the route fails", func() { + BeforeEach(func() { + fakeNetlink.RouteDelReturns(errors.New("durian")) + + destGW, destNet, _ := net.ParseCIDR("10.255.19.0/24") + fakeNetlink.RouteListReturns([]netlink.Route{ + netlink.Route{ + LinkIndex: 42, + Scope: netlink.SCOPE_UNIVERSE, + Dst: destNet, + Gw: destGW, + Src: net.ParseIP("10.255.32.0").To4(), + }, + }, nil) + }) + It("returns a meaningful error", func() { + err := converger.Converge([]controller.Lease{}) + Expect(err).To(MatchError("del route: durian")) + }) + }) + + Context("when deleting a neigh fails", func() { + BeforeEach(func() { + fakeNetlink.NeighDelReturns(errors.New("mango")) + + fakeNetlink.ARPListReturns([]netlink.Neigh{ + netlink.Neigh{ + LinkIndex: 42, + State: netlink.NUD_PERMANENT, + Type: syscall.RTN_UNICAST, + IP: net.ParseIP("10.255.19.0"), + HardwareAddr: remoteMac, + }, + }, nil) + }) + It("returns a meaningful error", func() { + err := converger.Converge([]controller.Lease{}) + Expect(err).To(MatchError("del neigh with ip/hwaddr 10.255.19.0 ee:ee:aa:aa:aa:ff: mango")) + }) + }) + + Context("when there are remote leases that are not in the overlay network", func() { + BeforeEach(func() { + leases = []controller.Lease{ + { // local, skipped + UnderlayIP: "10.10.0.2", + OverlaySubnet: "10.255.32.0/24", + OverlayHardwareAddr: "aa:aa:00:00:00:00", + }, + { // not in overlay, skipped + UnderlayIP: "10.10.0.3", + OverlaySubnet: "10.254.11.0/24", + OverlayHardwareAddr: "aa:aa:00:00:00:01", + }, + { // not in overlay, skipped + UnderlayIP: "10.10.0.4", + OverlaySubnet: "10.254.12.0/24", + OverlayHardwareAddr: "aa:aa:00:00:00:02", + }, + { // in overlay + UnderlayIP: "10.10.0.5", + OverlaySubnet: "10.255.19.0/24", + OverlayHardwareAddr: "aa:aa:00:00:00:03", + }, + } + }) + It("does not touch them and adds only the leases in the overlay network", func() { + err := converger.Converge(leases) + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeNetlink.RouteReplaceCallCount()).To(Equal(1)) + addedRoute := fakeNetlink.RouteReplaceArgsForCall(0) + Expect(addedRoute.Dst.IP).To(Equal(net.ParseIP("10.255.19.0").To4())) + Expect(fakeNetlink.NeighSetCallCount()).To(Equal(2)) + addedARP := fakeNetlink.NeighSetArgsForCall(0) + Expect(addedARP.IP).To(Equal(net.ParseIP("10.255.19.0"))) + addedFDB := fakeNetlink.NeighSetArgsForCall(1) + Expect(addedFDB.IP).To(Equal(net.ParseIP("10.10.0.5"))) + + Expect(logger.Logs()).To(HaveLen(1)) + Expect(logger.Logs()[0].LogLevel).To(Equal(lager.INFO)) + Expect(logger.Logs()[0].ToJSON()).To(MatchRegexp("converger.*non-routable-lease-count.*2")) + }) + }) + + Context("when a lease has an invalid MAC", func() { + BeforeEach(func() { + leases = []controller.Lease{ + { + UnderlayIP: "10.10.0.5", + OverlaySubnet: "10.255.19.0/24", + OverlayHardwareAddr: "banana", + }, + } + }) + It("returns an error", func() { + err := converger.Converge(leases) + Expect(err).To(MatchError("invalid hardware addr: banana")) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/vtep/factory.go b/src/code.cloudfoundry.org/silk/daemon/vtep/factory.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/daemon/vtep/factory.go rename to src/code.cloudfoundry.org/silk/daemon/vtep/factory.go diff --git a/src/code.cloudfoundry.org/silk/daemon/vtep/factory_test.go b/src/code.cloudfoundry.org/silk/daemon/vtep/factory_test.go new file mode 100644 index 00000000..87e532f5 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/daemon/vtep/factory_test.go @@ -0,0 +1,238 @@ +package vtep_test + +import ( + "errors" + "net" + + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/daemon/vtep" + "code.cloudfoundry.org/silk/daemon/vtep/fakes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/vishvananda/netlink" +) + +var _ = Describe("Factory", func() { + var ( + fakeNetlinkAdapter *fakes.NetlinkAdapter + factory *vtep.Factory + vtepConfig *vtep.Config + fakeLogger *lagertest.TestLogger + overlayMAC net.HardwareAddr + ) + + BeforeEach(func() { + fakeNetlinkAdapter = &fakes.NetlinkAdapter{} + fakeLogger = lagertest.NewTestLogger("test") + factory = &vtep.Factory{ + NetlinkAdapter: fakeNetlinkAdapter, + Logger: fakeLogger, + } + + overlayMAC = net.HardwareAddr{0xee, 0xee, 0x0a, 0xff, 0x20, 0x00} + + underlayInterface := net.Interface{ + Index: 4, + MTU: 1450, + Name: "eth4", + HardwareAddr: net.HardwareAddr{0xbb, 0xbb, 0x00, 0x00, 0x12, 0x34}, + Flags: net.FlagUp | net.FlagMulticast, + } + vtepConfig = &vtep.Config{ + VTEPName: "some-device", + UnderlayInterface: underlayInterface, + UnderlayIP: net.IP{172, 255, 0, 0}, + OverlayIP: net.IP{10, 255, 32, 0}, + OverlayHardwareAddr: overlayMAC, + VNI: 99, + OverlayNetworkPrefixLength: 10, + VTEPPort: 4913, + } + }) + + Describe("CreateVTEP", func() { + It("creates the link, with the HW address", func() { + err := factory.CreateVTEP(vtepConfig) + Expect(err).NotTo(HaveOccurred()) + + expectedLink := &netlink.Vxlan{ + LinkAttrs: netlink.LinkAttrs{ + Name: "some-device", + HardwareAddr: overlayMAC, + }, + VxlanId: 99, + SrcAddr: net.IP{172, 255, 0, 0}, + GBP: true, + Port: 4913, + VtepDevIndex: 4, + } + + Expect(fakeNetlinkAdapter.LinkAddCallCount()).To(Equal(1)) + Expect(fakeNetlinkAdapter.LinkAddArgsForCall(0)).To(Equal(expectedLink)) + + Expect(fakeNetlinkAdapter.LinkSetUpCallCount()).To(Equal(1)) + Expect(fakeNetlinkAdapter.LinkSetUpArgsForCall(0)).To(Equal(expectedLink)) + + Expect(fakeNetlinkAdapter.LinkSetHardwareAddrCallCount()).To(Equal(0)) + + Expect(fakeNetlinkAdapter.AddrAddScopeLinkCallCount()).To(Equal(1)) + link, addr := fakeNetlinkAdapter.AddrAddScopeLinkArgsForCall(0) + Expect(link).To(Equal(expectedLink)) + Expect(addr).To(Equal(&netlink.Addr{ + IPNet: &net.IPNet{ + IP: net.IP{10, 255, 32, 0}, + Mask: net.IPMask{0xff, 0xc0, 0x00, 0x00}, + }, + })) + }) + + Context("when adding the link fails", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkAddReturns(errors.New("potato")) + }) + It("wraps and returns the error", func() { + err := factory.CreateVTEP(vtepConfig) + Expect(err).To(MatchError("create link some-device: potato")) + }) + }) + + Context("when setting the link up fails", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkSetUpReturns(errors.New("potato")) + }) + It("wraps and returns the error", func() { + err := factory.CreateVTEP(vtepConfig) + Expect(err).To(MatchError("up link: potato")) + }) + }) + + Context("when adding the overlay address", func() { + BeforeEach(func() { + fakeNetlinkAdapter.AddrAddScopeLinkReturns(errors.New("potato")) + }) + It("wraps and returns the error", func() { + err := factory.CreateVTEP(vtepConfig) + Expect(err).To(MatchError("add address: potato")) + }) + }) + }) + + Describe("GetVTEPState", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkByNameReturns(&netlink.Vxlan{ + LinkAttrs: netlink.LinkAttrs{ + Name: "some-device", + MTU: 1400, + HardwareAddr: net.HardwareAddr{0xee, 0xee, 0x0a, 0xff, 0x42, 0x00}, + }, + }, nil) + fakeNetlinkAdapter.AddrListReturns([]netlink.Addr{ + netlink.Addr{ + IPNet: &net.IPNet{ + IP: net.IP{10, 255, 32, 0}, + Mask: net.IPMask{0xff, 0xff, 0xff, 0xff}, + }, + }, + }, nil) + }) + It("returns the overlay address, hardware addr, and MTU", func() { + hwAddr, ip, mtu, err := factory.GetVTEPState(vtepConfig.VTEPName) + Expect(err).NotTo(HaveOccurred()) + Expect(ip).To(Equal(net.IP{10, 255, 32, 0})) + Expect(hwAddr).To(Equal(net.HardwareAddr{0xee, 0xee, 0x0a, 0xff, 0x42, 0x00})) + Expect(mtu).To(Equal(1400)) + + Expect(fakeNetlinkAdapter.LinkByNameCallCount()).To(Equal(1)) + Expect(fakeNetlinkAdapter.LinkByNameArgsForCall(0)).To(Equal(vtepConfig.VTEPName)) + + Expect(fakeNetlinkAdapter.AddrListCallCount()).To(Equal(1)) + link, family := (fakeNetlinkAdapter.AddrListArgsForCall(0)) + Expect(link).To(Equal(&netlink.Vxlan{ + LinkAttrs: netlink.LinkAttrs{ + Name: "some-device", + HardwareAddr: net.HardwareAddr{0xee, 0xee, 0x0a, 0xff, 0x42, 0x00}, + MTU: 1400, + }, + })) + Expect(family).To(Equal(netlink.FAMILY_V4)) + }) + + Context("when finding the link errors", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkByNameReturns(nil, errors.New("potato")) + }) + It("returns an error", func() { + _, _, _, err := factory.GetVTEPState(vtepConfig.VTEPName) + Expect(err).To(MatchError("find link: potato")) + }) + }) + + Context("when listing the addresses fails", func() { + BeforeEach(func() { + fakeNetlinkAdapter.AddrListReturns(nil, errors.New("potato")) + }) + It("returns an error", func() { + _, _, _, err := factory.GetVTEPState(vtepConfig.VTEPName) + Expect(err).To(MatchError("list addresses: potato")) + }) + }) + + Context("when there are no addresses", func() { + BeforeEach(func() { + fakeNetlinkAdapter.AddrListReturns(nil, nil) + }) + It("returns an error", func() { + _, _, _, err := factory.GetVTEPState(vtepConfig.VTEPName) + Expect(err).To(MatchError("no addresses")) + }) + }) + }) + + Describe("DeleteVTEP", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkByNameReturns(&netlink.Vxlan{ + LinkAttrs: netlink.LinkAttrs{ + Name: "some-device", + HardwareAddr: net.HardwareAddr{0xee, 0xee, 0x0a, 0xff, 0x42, 0x00}, + }, + }, nil) + }) + + It("deletes the vtep", func() { + err := factory.DeleteVTEP(vtepConfig.VTEPName) + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeNetlinkAdapter.LinkByNameCallCount()).To(Equal(1)) + Expect(fakeNetlinkAdapter.LinkByNameArgsForCall(0)).To(Equal(vtepConfig.VTEPName)) + + Expect(fakeNetlinkAdapter.LinkDelCallCount()).To(Equal(1)) + Expect(fakeNetlinkAdapter.LinkDelArgsForCall(0)).To(Equal(&netlink.Vxlan{ + LinkAttrs: netlink.LinkAttrs{ + Name: "some-device", + HardwareAddr: net.HardwareAddr{0xee, 0xee, 0x0a, 0xff, 0x42, 0x00}, + }, + })) + }) + + Context("when the link cannot be found", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkByNameReturns(nil, errors.New("banana")) + }) + It("returns an error", func() { + err := factory.DeleteVTEP(vtepConfig.VTEPName) + Expect(err).To(MatchError("find link some-device: banana")) + }) + }) + + Context("when the link cannot be deleted", func() { + BeforeEach(func() { + fakeNetlinkAdapter.LinkDelReturns(errors.New("banana")) + }) + It("returns an error", func() { + err := factory.DeleteVTEP(vtepConfig.VTEPName) + Expect(err).To(MatchError("delete link some-device: banana")) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/daemon/vtep/fakes/netAdapter.go b/src/code.cloudfoundry.org/silk/daemon/vtep/fakes/netAdapter.go new file mode 100644 index 00000000..6c28ea9b --- /dev/null +++ b/src/code.cloudfoundry.org/silk/daemon/vtep/fakes/netAdapter.go @@ -0,0 +1,222 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "net" + "sync" +) + +type NetAdapter struct { + InterfacesStub func() ([]net.Interface, error) + interfacesMutex sync.RWMutex + interfacesArgsForCall []struct{} + interfacesReturns struct { + result1 []net.Interface + result2 error + } + interfacesReturnsOnCall map[int]struct { + result1 []net.Interface + result2 error + } + InterfaceAddrsStub func(net.Interface) ([]net.Addr, error) + interfaceAddrsMutex sync.RWMutex + interfaceAddrsArgsForCall []struct { + arg1 net.Interface + } + interfaceAddrsReturns struct { + result1 []net.Addr + result2 error + } + interfaceAddrsReturnsOnCall map[int]struct { + result1 []net.Addr + result2 error + } + InterfaceByNameStub func(name string) (*net.Interface, error) + interfaceByNameMutex sync.RWMutex + interfaceByNameArgsForCall []struct { + name string + } + interfaceByNameReturns struct { + result1 *net.Interface + result2 error + } + interfaceByNameReturnsOnCall map[int]struct { + result1 *net.Interface + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *NetAdapter) Interfaces() ([]net.Interface, error) { + fake.interfacesMutex.Lock() + ret, specificReturn := fake.interfacesReturnsOnCall[len(fake.interfacesArgsForCall)] + fake.interfacesArgsForCall = append(fake.interfacesArgsForCall, struct{}{}) + fake.recordInvocation("Interfaces", []interface{}{}) + fake.interfacesMutex.Unlock() + if fake.InterfacesStub != nil { + return fake.InterfacesStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.interfacesReturns.result1, fake.interfacesReturns.result2 +} + +func (fake *NetAdapter) InterfacesCallCount() int { + fake.interfacesMutex.RLock() + defer fake.interfacesMutex.RUnlock() + return len(fake.interfacesArgsForCall) +} + +func (fake *NetAdapter) InterfacesReturns(result1 []net.Interface, result2 error) { + fake.InterfacesStub = nil + fake.interfacesReturns = struct { + result1 []net.Interface + result2 error + }{result1, result2} +} + +func (fake *NetAdapter) InterfacesReturnsOnCall(i int, result1 []net.Interface, result2 error) { + fake.InterfacesStub = nil + if fake.interfacesReturnsOnCall == nil { + fake.interfacesReturnsOnCall = make(map[int]struct { + result1 []net.Interface + result2 error + }) + } + fake.interfacesReturnsOnCall[i] = struct { + result1 []net.Interface + result2 error + }{result1, result2} +} + +func (fake *NetAdapter) InterfaceAddrs(arg1 net.Interface) ([]net.Addr, error) { + fake.interfaceAddrsMutex.Lock() + ret, specificReturn := fake.interfaceAddrsReturnsOnCall[len(fake.interfaceAddrsArgsForCall)] + fake.interfaceAddrsArgsForCall = append(fake.interfaceAddrsArgsForCall, struct { + arg1 net.Interface + }{arg1}) + fake.recordInvocation("InterfaceAddrs", []interface{}{arg1}) + fake.interfaceAddrsMutex.Unlock() + if fake.InterfaceAddrsStub != nil { + return fake.InterfaceAddrsStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.interfaceAddrsReturns.result1, fake.interfaceAddrsReturns.result2 +} + +func (fake *NetAdapter) InterfaceAddrsCallCount() int { + fake.interfaceAddrsMutex.RLock() + defer fake.interfaceAddrsMutex.RUnlock() + return len(fake.interfaceAddrsArgsForCall) +} + +func (fake *NetAdapter) InterfaceAddrsArgsForCall(i int) net.Interface { + fake.interfaceAddrsMutex.RLock() + defer fake.interfaceAddrsMutex.RUnlock() + return fake.interfaceAddrsArgsForCall[i].arg1 +} + +func (fake *NetAdapter) InterfaceAddrsReturns(result1 []net.Addr, result2 error) { + fake.InterfaceAddrsStub = nil + fake.interfaceAddrsReturns = struct { + result1 []net.Addr + result2 error + }{result1, result2} +} + +func (fake *NetAdapter) InterfaceAddrsReturnsOnCall(i int, result1 []net.Addr, result2 error) { + fake.InterfaceAddrsStub = nil + if fake.interfaceAddrsReturnsOnCall == nil { + fake.interfaceAddrsReturnsOnCall = make(map[int]struct { + result1 []net.Addr + result2 error + }) + } + fake.interfaceAddrsReturnsOnCall[i] = struct { + result1 []net.Addr + result2 error + }{result1, result2} +} + +func (fake *NetAdapter) InterfaceByName(name string) (*net.Interface, error) { + fake.interfaceByNameMutex.Lock() + ret, specificReturn := fake.interfaceByNameReturnsOnCall[len(fake.interfaceByNameArgsForCall)] + fake.interfaceByNameArgsForCall = append(fake.interfaceByNameArgsForCall, struct { + name string + }{name}) + fake.recordInvocation("InterfaceByName", []interface{}{name}) + fake.interfaceByNameMutex.Unlock() + if fake.InterfaceByNameStub != nil { + return fake.InterfaceByNameStub(name) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.interfaceByNameReturns.result1, fake.interfaceByNameReturns.result2 +} + +func (fake *NetAdapter) InterfaceByNameCallCount() int { + fake.interfaceByNameMutex.RLock() + defer fake.interfaceByNameMutex.RUnlock() + return len(fake.interfaceByNameArgsForCall) +} + +func (fake *NetAdapter) InterfaceByNameArgsForCall(i int) string { + fake.interfaceByNameMutex.RLock() + defer fake.interfaceByNameMutex.RUnlock() + return fake.interfaceByNameArgsForCall[i].name +} + +func (fake *NetAdapter) InterfaceByNameReturns(result1 *net.Interface, result2 error) { + fake.InterfaceByNameStub = nil + fake.interfaceByNameReturns = struct { + result1 *net.Interface + result2 error + }{result1, result2} +} + +func (fake *NetAdapter) InterfaceByNameReturnsOnCall(i int, result1 *net.Interface, result2 error) { + fake.InterfaceByNameStub = nil + if fake.interfaceByNameReturnsOnCall == nil { + fake.interfaceByNameReturnsOnCall = make(map[int]struct { + result1 *net.Interface + result2 error + }) + } + fake.interfaceByNameReturnsOnCall[i] = struct { + result1 *net.Interface + result2 error + }{result1, result2} +} + +func (fake *NetAdapter) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.interfacesMutex.RLock() + defer fake.interfacesMutex.RUnlock() + fake.interfaceAddrsMutex.RLock() + defer fake.interfaceAddrsMutex.RUnlock() + fake.interfaceByNameMutex.RLock() + defer fake.interfaceByNameMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *NetAdapter) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/daemon/vtep/fakes/netlinkAdapter.go b/src/code.cloudfoundry.org/silk/daemon/vtep/fakes/netlinkAdapter.go new file mode 100644 index 00000000..16188ef8 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/daemon/vtep/fakes/netlinkAdapter.go @@ -0,0 +1,1050 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "net" + "sync" + + "github.com/vishvananda/netlink" +) + +type NetlinkAdapter struct { + LinkSetUpStub func(netlink.Link) error + linkSetUpMutex sync.RWMutex + linkSetUpArgsForCall []struct { + arg1 netlink.Link + } + linkSetUpReturns struct { + result1 error + } + linkSetUpReturnsOnCall map[int]struct { + result1 error + } + LinkAddStub func(netlink.Link) error + linkAddMutex sync.RWMutex + linkAddArgsForCall []struct { + arg1 netlink.Link + } + linkAddReturns struct { + result1 error + } + linkAddReturnsOnCall map[int]struct { + result1 error + } + LinkByNameStub func(string) (netlink.Link, error) + linkByNameMutex sync.RWMutex + linkByNameArgsForCall []struct { + arg1 string + } + linkByNameReturns struct { + result1 netlink.Link + result2 error + } + linkByNameReturnsOnCall map[int]struct { + result1 netlink.Link + result2 error + } + LinkByIndexStub func(int) (netlink.Link, error) + linkByIndexMutex sync.RWMutex + linkByIndexArgsForCall []struct { + arg1 int + } + linkByIndexReturns struct { + result1 netlink.Link + result2 error + } + linkByIndexReturnsOnCall map[int]struct { + result1 netlink.Link + result2 error + } + LinkSetHardwareAddrStub func(netlink.Link, net.HardwareAddr) error + linkSetHardwareAddrMutex sync.RWMutex + linkSetHardwareAddrArgsForCall []struct { + arg1 netlink.Link + arg2 net.HardwareAddr + } + linkSetHardwareAddrReturns struct { + result1 error + } + linkSetHardwareAddrReturnsOnCall map[int]struct { + result1 error + } + AddrAddScopeLinkStub func(link netlink.Link, addr *netlink.Addr) error + addrAddScopeLinkMutex sync.RWMutex + addrAddScopeLinkArgsForCall []struct { + link netlink.Link + addr *netlink.Addr + } + addrAddScopeLinkReturns struct { + result1 error + } + addrAddScopeLinkReturnsOnCall map[int]struct { + result1 error + } + AddrListStub func(link netlink.Link, family int) ([]netlink.Addr, error) + addrListMutex sync.RWMutex + addrListArgsForCall []struct { + link netlink.Link + family int + } + addrListReturns struct { + result1 []netlink.Addr + result2 error + } + addrListReturnsOnCall map[int]struct { + result1 []netlink.Addr + result2 error + } + RouteAddStub func(*netlink.Route) error + routeAddMutex sync.RWMutex + routeAddArgsForCall []struct { + arg1 *netlink.Route + } + routeAddReturns struct { + result1 error + } + routeAddReturnsOnCall map[int]struct { + result1 error + } + RouteReplaceStub func(*netlink.Route) error + routeReplaceMutex sync.RWMutex + routeReplaceArgsForCall []struct { + arg1 *netlink.Route + } + routeReplaceReturns struct { + result1 error + } + routeReplaceReturnsOnCall map[int]struct { + result1 error + } + RouteListStub func(netlink.Link, int) ([]netlink.Route, error) + routeListMutex sync.RWMutex + routeListArgsForCall []struct { + arg1 netlink.Link + arg2 int + } + routeListReturns struct { + result1 []netlink.Route + result2 error + } + routeListReturnsOnCall map[int]struct { + result1 []netlink.Route + result2 error + } + RouteDelStub func(*netlink.Route) error + routeDelMutex sync.RWMutex + routeDelArgsForCall []struct { + arg1 *netlink.Route + } + routeDelReturns struct { + result1 error + } + routeDelReturnsOnCall map[int]struct { + result1 error + } + LinkDelStub func(netlink.Link) error + linkDelMutex sync.RWMutex + linkDelArgsForCall []struct { + arg1 netlink.Link + } + linkDelReturns struct { + result1 error + } + linkDelReturnsOnCall map[int]struct { + result1 error + } + NeighSetStub func(*netlink.Neigh) error + neighSetMutex sync.RWMutex + neighSetArgsForCall []struct { + arg1 *netlink.Neigh + } + neighSetReturns struct { + result1 error + } + neighSetReturnsOnCall map[int]struct { + result1 error + } + ARPListStub func(index int) ([]netlink.Neigh, error) + aRPListMutex sync.RWMutex + aRPListArgsForCall []struct { + index int + } + aRPListReturns struct { + result1 []netlink.Neigh + result2 error + } + aRPListReturnsOnCall map[int]struct { + result1 []netlink.Neigh + result2 error + } + FDBListStub func(index int) ([]netlink.Neigh, error) + fDBListMutex sync.RWMutex + fDBListArgsForCall []struct { + index int + } + fDBListReturns struct { + result1 []netlink.Neigh + result2 error + } + fDBListReturnsOnCall map[int]struct { + result1 []netlink.Neigh + result2 error + } + NeighDelStub func(*netlink.Neigh) error + neighDelMutex sync.RWMutex + neighDelArgsForCall []struct { + arg1 *netlink.Neigh + } + neighDelReturns struct { + result1 error + } + neighDelReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *NetlinkAdapter) LinkSetUp(arg1 netlink.Link) error { + fake.linkSetUpMutex.Lock() + ret, specificReturn := fake.linkSetUpReturnsOnCall[len(fake.linkSetUpArgsForCall)] + fake.linkSetUpArgsForCall = append(fake.linkSetUpArgsForCall, struct { + arg1 netlink.Link + }{arg1}) + fake.recordInvocation("LinkSetUp", []interface{}{arg1}) + fake.linkSetUpMutex.Unlock() + if fake.LinkSetUpStub != nil { + return fake.LinkSetUpStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.linkSetUpReturns.result1 +} + +func (fake *NetlinkAdapter) LinkSetUpCallCount() int { + fake.linkSetUpMutex.RLock() + defer fake.linkSetUpMutex.RUnlock() + return len(fake.linkSetUpArgsForCall) +} + +func (fake *NetlinkAdapter) LinkSetUpArgsForCall(i int) netlink.Link { + fake.linkSetUpMutex.RLock() + defer fake.linkSetUpMutex.RUnlock() + return fake.linkSetUpArgsForCall[i].arg1 +} + +func (fake *NetlinkAdapter) LinkSetUpReturns(result1 error) { + fake.LinkSetUpStub = nil + fake.linkSetUpReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkSetUpReturnsOnCall(i int, result1 error) { + fake.LinkSetUpStub = nil + if fake.linkSetUpReturnsOnCall == nil { + fake.linkSetUpReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.linkSetUpReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkAdd(arg1 netlink.Link) error { + fake.linkAddMutex.Lock() + ret, specificReturn := fake.linkAddReturnsOnCall[len(fake.linkAddArgsForCall)] + fake.linkAddArgsForCall = append(fake.linkAddArgsForCall, struct { + arg1 netlink.Link + }{arg1}) + fake.recordInvocation("LinkAdd", []interface{}{arg1}) + fake.linkAddMutex.Unlock() + if fake.LinkAddStub != nil { + return fake.LinkAddStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.linkAddReturns.result1 +} + +func (fake *NetlinkAdapter) LinkAddCallCount() int { + fake.linkAddMutex.RLock() + defer fake.linkAddMutex.RUnlock() + return len(fake.linkAddArgsForCall) +} + +func (fake *NetlinkAdapter) LinkAddArgsForCall(i int) netlink.Link { + fake.linkAddMutex.RLock() + defer fake.linkAddMutex.RUnlock() + return fake.linkAddArgsForCall[i].arg1 +} + +func (fake *NetlinkAdapter) LinkAddReturns(result1 error) { + fake.LinkAddStub = nil + fake.linkAddReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkAddReturnsOnCall(i int, result1 error) { + fake.LinkAddStub = nil + if fake.linkAddReturnsOnCall == nil { + fake.linkAddReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.linkAddReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkByName(arg1 string) (netlink.Link, error) { + fake.linkByNameMutex.Lock() + ret, specificReturn := fake.linkByNameReturnsOnCall[len(fake.linkByNameArgsForCall)] + fake.linkByNameArgsForCall = append(fake.linkByNameArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("LinkByName", []interface{}{arg1}) + fake.linkByNameMutex.Unlock() + if fake.LinkByNameStub != nil { + return fake.LinkByNameStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.linkByNameReturns.result1, fake.linkByNameReturns.result2 +} + +func (fake *NetlinkAdapter) LinkByNameCallCount() int { + fake.linkByNameMutex.RLock() + defer fake.linkByNameMutex.RUnlock() + return len(fake.linkByNameArgsForCall) +} + +func (fake *NetlinkAdapter) LinkByNameArgsForCall(i int) string { + fake.linkByNameMutex.RLock() + defer fake.linkByNameMutex.RUnlock() + return fake.linkByNameArgsForCall[i].arg1 +} + +func (fake *NetlinkAdapter) LinkByNameReturns(result1 netlink.Link, result2 error) { + fake.LinkByNameStub = nil + fake.linkByNameReturns = struct { + result1 netlink.Link + result2 error + }{result1, result2} +} + +func (fake *NetlinkAdapter) LinkByNameReturnsOnCall(i int, result1 netlink.Link, result2 error) { + fake.LinkByNameStub = nil + if fake.linkByNameReturnsOnCall == nil { + fake.linkByNameReturnsOnCall = make(map[int]struct { + result1 netlink.Link + result2 error + }) + } + fake.linkByNameReturnsOnCall[i] = struct { + result1 netlink.Link + result2 error + }{result1, result2} +} + +func (fake *NetlinkAdapter) LinkByIndex(arg1 int) (netlink.Link, error) { + fake.linkByIndexMutex.Lock() + ret, specificReturn := fake.linkByIndexReturnsOnCall[len(fake.linkByIndexArgsForCall)] + fake.linkByIndexArgsForCall = append(fake.linkByIndexArgsForCall, struct { + arg1 int + }{arg1}) + fake.recordInvocation("LinkByIndex", []interface{}{arg1}) + fake.linkByIndexMutex.Unlock() + if fake.LinkByIndexStub != nil { + return fake.LinkByIndexStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.linkByIndexReturns.result1, fake.linkByIndexReturns.result2 +} + +func (fake *NetlinkAdapter) LinkByIndexCallCount() int { + fake.linkByIndexMutex.RLock() + defer fake.linkByIndexMutex.RUnlock() + return len(fake.linkByIndexArgsForCall) +} + +func (fake *NetlinkAdapter) LinkByIndexArgsForCall(i int) int { + fake.linkByIndexMutex.RLock() + defer fake.linkByIndexMutex.RUnlock() + return fake.linkByIndexArgsForCall[i].arg1 +} + +func (fake *NetlinkAdapter) LinkByIndexReturns(result1 netlink.Link, result2 error) { + fake.LinkByIndexStub = nil + fake.linkByIndexReturns = struct { + result1 netlink.Link + result2 error + }{result1, result2} +} + +func (fake *NetlinkAdapter) LinkByIndexReturnsOnCall(i int, result1 netlink.Link, result2 error) { + fake.LinkByIndexStub = nil + if fake.linkByIndexReturnsOnCall == nil { + fake.linkByIndexReturnsOnCall = make(map[int]struct { + result1 netlink.Link + result2 error + }) + } + fake.linkByIndexReturnsOnCall[i] = struct { + result1 netlink.Link + result2 error + }{result1, result2} +} + +func (fake *NetlinkAdapter) LinkSetHardwareAddr(arg1 netlink.Link, arg2 net.HardwareAddr) error { + fake.linkSetHardwareAddrMutex.Lock() + ret, specificReturn := fake.linkSetHardwareAddrReturnsOnCall[len(fake.linkSetHardwareAddrArgsForCall)] + fake.linkSetHardwareAddrArgsForCall = append(fake.linkSetHardwareAddrArgsForCall, struct { + arg1 netlink.Link + arg2 net.HardwareAddr + }{arg1, arg2}) + fake.recordInvocation("LinkSetHardwareAddr", []interface{}{arg1, arg2}) + fake.linkSetHardwareAddrMutex.Unlock() + if fake.LinkSetHardwareAddrStub != nil { + return fake.LinkSetHardwareAddrStub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fake.linkSetHardwareAddrReturns.result1 +} + +func (fake *NetlinkAdapter) LinkSetHardwareAddrCallCount() int { + fake.linkSetHardwareAddrMutex.RLock() + defer fake.linkSetHardwareAddrMutex.RUnlock() + return len(fake.linkSetHardwareAddrArgsForCall) +} + +func (fake *NetlinkAdapter) LinkSetHardwareAddrArgsForCall(i int) (netlink.Link, net.HardwareAddr) { + fake.linkSetHardwareAddrMutex.RLock() + defer fake.linkSetHardwareAddrMutex.RUnlock() + return fake.linkSetHardwareAddrArgsForCall[i].arg1, fake.linkSetHardwareAddrArgsForCall[i].arg2 +} + +func (fake *NetlinkAdapter) LinkSetHardwareAddrReturns(result1 error) { + fake.LinkSetHardwareAddrStub = nil + fake.linkSetHardwareAddrReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkSetHardwareAddrReturnsOnCall(i int, result1 error) { + fake.LinkSetHardwareAddrStub = nil + if fake.linkSetHardwareAddrReturnsOnCall == nil { + fake.linkSetHardwareAddrReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.linkSetHardwareAddrReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) AddrAddScopeLink(link netlink.Link, addr *netlink.Addr) error { + fake.addrAddScopeLinkMutex.Lock() + ret, specificReturn := fake.addrAddScopeLinkReturnsOnCall[len(fake.addrAddScopeLinkArgsForCall)] + fake.addrAddScopeLinkArgsForCall = append(fake.addrAddScopeLinkArgsForCall, struct { + link netlink.Link + addr *netlink.Addr + }{link, addr}) + fake.recordInvocation("AddrAddScopeLink", []interface{}{link, addr}) + fake.addrAddScopeLinkMutex.Unlock() + if fake.AddrAddScopeLinkStub != nil { + return fake.AddrAddScopeLinkStub(link, addr) + } + if specificReturn { + return ret.result1 + } + return fake.addrAddScopeLinkReturns.result1 +} + +func (fake *NetlinkAdapter) AddrAddScopeLinkCallCount() int { + fake.addrAddScopeLinkMutex.RLock() + defer fake.addrAddScopeLinkMutex.RUnlock() + return len(fake.addrAddScopeLinkArgsForCall) +} + +func (fake *NetlinkAdapter) AddrAddScopeLinkArgsForCall(i int) (netlink.Link, *netlink.Addr) { + fake.addrAddScopeLinkMutex.RLock() + defer fake.addrAddScopeLinkMutex.RUnlock() + return fake.addrAddScopeLinkArgsForCall[i].link, fake.addrAddScopeLinkArgsForCall[i].addr +} + +func (fake *NetlinkAdapter) AddrAddScopeLinkReturns(result1 error) { + fake.AddrAddScopeLinkStub = nil + fake.addrAddScopeLinkReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) AddrAddScopeLinkReturnsOnCall(i int, result1 error) { + fake.AddrAddScopeLinkStub = nil + if fake.addrAddScopeLinkReturnsOnCall == nil { + fake.addrAddScopeLinkReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.addrAddScopeLinkReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) AddrList(link netlink.Link, family int) ([]netlink.Addr, error) { + fake.addrListMutex.Lock() + ret, specificReturn := fake.addrListReturnsOnCall[len(fake.addrListArgsForCall)] + fake.addrListArgsForCall = append(fake.addrListArgsForCall, struct { + link netlink.Link + family int + }{link, family}) + fake.recordInvocation("AddrList", []interface{}{link, family}) + fake.addrListMutex.Unlock() + if fake.AddrListStub != nil { + return fake.AddrListStub(link, family) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.addrListReturns.result1, fake.addrListReturns.result2 +} + +func (fake *NetlinkAdapter) AddrListCallCount() int { + fake.addrListMutex.RLock() + defer fake.addrListMutex.RUnlock() + return len(fake.addrListArgsForCall) +} + +func (fake *NetlinkAdapter) AddrListArgsForCall(i int) (netlink.Link, int) { + fake.addrListMutex.RLock() + defer fake.addrListMutex.RUnlock() + return fake.addrListArgsForCall[i].link, fake.addrListArgsForCall[i].family +} + +func (fake *NetlinkAdapter) AddrListReturns(result1 []netlink.Addr, result2 error) { + fake.AddrListStub = nil + fake.addrListReturns = struct { + result1 []netlink.Addr + result2 error + }{result1, result2} +} + +func (fake *NetlinkAdapter) AddrListReturnsOnCall(i int, result1 []netlink.Addr, result2 error) { + fake.AddrListStub = nil + if fake.addrListReturnsOnCall == nil { + fake.addrListReturnsOnCall = make(map[int]struct { + result1 []netlink.Addr + result2 error + }) + } + fake.addrListReturnsOnCall[i] = struct { + result1 []netlink.Addr + result2 error + }{result1, result2} +} + +func (fake *NetlinkAdapter) RouteAdd(arg1 *netlink.Route) error { + fake.routeAddMutex.Lock() + ret, specificReturn := fake.routeAddReturnsOnCall[len(fake.routeAddArgsForCall)] + fake.routeAddArgsForCall = append(fake.routeAddArgsForCall, struct { + arg1 *netlink.Route + }{arg1}) + fake.recordInvocation("RouteAdd", []interface{}{arg1}) + fake.routeAddMutex.Unlock() + if fake.RouteAddStub != nil { + return fake.RouteAddStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.routeAddReturns.result1 +} + +func (fake *NetlinkAdapter) RouteAddCallCount() int { + fake.routeAddMutex.RLock() + defer fake.routeAddMutex.RUnlock() + return len(fake.routeAddArgsForCall) +} + +func (fake *NetlinkAdapter) RouteAddArgsForCall(i int) *netlink.Route { + fake.routeAddMutex.RLock() + defer fake.routeAddMutex.RUnlock() + return fake.routeAddArgsForCall[i].arg1 +} + +func (fake *NetlinkAdapter) RouteAddReturns(result1 error) { + fake.RouteAddStub = nil + fake.routeAddReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) RouteAddReturnsOnCall(i int, result1 error) { + fake.RouteAddStub = nil + if fake.routeAddReturnsOnCall == nil { + fake.routeAddReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.routeAddReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) RouteReplace(arg1 *netlink.Route) error { + fake.routeReplaceMutex.Lock() + ret, specificReturn := fake.routeReplaceReturnsOnCall[len(fake.routeReplaceArgsForCall)] + fake.routeReplaceArgsForCall = append(fake.routeReplaceArgsForCall, struct { + arg1 *netlink.Route + }{arg1}) + fake.recordInvocation("RouteReplace", []interface{}{arg1}) + fake.routeReplaceMutex.Unlock() + if fake.RouteReplaceStub != nil { + return fake.RouteReplaceStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.routeReplaceReturns.result1 +} + +func (fake *NetlinkAdapter) RouteReplaceCallCount() int { + fake.routeReplaceMutex.RLock() + defer fake.routeReplaceMutex.RUnlock() + return len(fake.routeReplaceArgsForCall) +} + +func (fake *NetlinkAdapter) RouteReplaceArgsForCall(i int) *netlink.Route { + fake.routeReplaceMutex.RLock() + defer fake.routeReplaceMutex.RUnlock() + return fake.routeReplaceArgsForCall[i].arg1 +} + +func (fake *NetlinkAdapter) RouteReplaceReturns(result1 error) { + fake.RouteReplaceStub = nil + fake.routeReplaceReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) RouteReplaceReturnsOnCall(i int, result1 error) { + fake.RouteReplaceStub = nil + if fake.routeReplaceReturnsOnCall == nil { + fake.routeReplaceReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.routeReplaceReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) RouteList(arg1 netlink.Link, arg2 int) ([]netlink.Route, error) { + fake.routeListMutex.Lock() + ret, specificReturn := fake.routeListReturnsOnCall[len(fake.routeListArgsForCall)] + fake.routeListArgsForCall = append(fake.routeListArgsForCall, struct { + arg1 netlink.Link + arg2 int + }{arg1, arg2}) + fake.recordInvocation("RouteList", []interface{}{arg1, arg2}) + fake.routeListMutex.Unlock() + if fake.RouteListStub != nil { + return fake.RouteListStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.routeListReturns.result1, fake.routeListReturns.result2 +} + +func (fake *NetlinkAdapter) RouteListCallCount() int { + fake.routeListMutex.RLock() + defer fake.routeListMutex.RUnlock() + return len(fake.routeListArgsForCall) +} + +func (fake *NetlinkAdapter) RouteListArgsForCall(i int) (netlink.Link, int) { + fake.routeListMutex.RLock() + defer fake.routeListMutex.RUnlock() + return fake.routeListArgsForCall[i].arg1, fake.routeListArgsForCall[i].arg2 +} + +func (fake *NetlinkAdapter) RouteListReturns(result1 []netlink.Route, result2 error) { + fake.RouteListStub = nil + fake.routeListReturns = struct { + result1 []netlink.Route + result2 error + }{result1, result2} +} + +func (fake *NetlinkAdapter) RouteListReturnsOnCall(i int, result1 []netlink.Route, result2 error) { + fake.RouteListStub = nil + if fake.routeListReturnsOnCall == nil { + fake.routeListReturnsOnCall = make(map[int]struct { + result1 []netlink.Route + result2 error + }) + } + fake.routeListReturnsOnCall[i] = struct { + result1 []netlink.Route + result2 error + }{result1, result2} +} + +func (fake *NetlinkAdapter) RouteDel(arg1 *netlink.Route) error { + fake.routeDelMutex.Lock() + ret, specificReturn := fake.routeDelReturnsOnCall[len(fake.routeDelArgsForCall)] + fake.routeDelArgsForCall = append(fake.routeDelArgsForCall, struct { + arg1 *netlink.Route + }{arg1}) + fake.recordInvocation("RouteDel", []interface{}{arg1}) + fake.routeDelMutex.Unlock() + if fake.RouteDelStub != nil { + return fake.RouteDelStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.routeDelReturns.result1 +} + +func (fake *NetlinkAdapter) RouteDelCallCount() int { + fake.routeDelMutex.RLock() + defer fake.routeDelMutex.RUnlock() + return len(fake.routeDelArgsForCall) +} + +func (fake *NetlinkAdapter) RouteDelArgsForCall(i int) *netlink.Route { + fake.routeDelMutex.RLock() + defer fake.routeDelMutex.RUnlock() + return fake.routeDelArgsForCall[i].arg1 +} + +func (fake *NetlinkAdapter) RouteDelReturns(result1 error) { + fake.RouteDelStub = nil + fake.routeDelReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) RouteDelReturnsOnCall(i int, result1 error) { + fake.RouteDelStub = nil + if fake.routeDelReturnsOnCall == nil { + fake.routeDelReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.routeDelReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkDel(arg1 netlink.Link) error { + fake.linkDelMutex.Lock() + ret, specificReturn := fake.linkDelReturnsOnCall[len(fake.linkDelArgsForCall)] + fake.linkDelArgsForCall = append(fake.linkDelArgsForCall, struct { + arg1 netlink.Link + }{arg1}) + fake.recordInvocation("LinkDel", []interface{}{arg1}) + fake.linkDelMutex.Unlock() + if fake.LinkDelStub != nil { + return fake.LinkDelStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.linkDelReturns.result1 +} + +func (fake *NetlinkAdapter) LinkDelCallCount() int { + fake.linkDelMutex.RLock() + defer fake.linkDelMutex.RUnlock() + return len(fake.linkDelArgsForCall) +} + +func (fake *NetlinkAdapter) LinkDelArgsForCall(i int) netlink.Link { + fake.linkDelMutex.RLock() + defer fake.linkDelMutex.RUnlock() + return fake.linkDelArgsForCall[i].arg1 +} + +func (fake *NetlinkAdapter) LinkDelReturns(result1 error) { + fake.LinkDelStub = nil + fake.linkDelReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) LinkDelReturnsOnCall(i int, result1 error) { + fake.LinkDelStub = nil + if fake.linkDelReturnsOnCall == nil { + fake.linkDelReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.linkDelReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) NeighSet(arg1 *netlink.Neigh) error { + fake.neighSetMutex.Lock() + ret, specificReturn := fake.neighSetReturnsOnCall[len(fake.neighSetArgsForCall)] + fake.neighSetArgsForCall = append(fake.neighSetArgsForCall, struct { + arg1 *netlink.Neigh + }{arg1}) + fake.recordInvocation("NeighSet", []interface{}{arg1}) + fake.neighSetMutex.Unlock() + if fake.NeighSetStub != nil { + return fake.NeighSetStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.neighSetReturns.result1 +} + +func (fake *NetlinkAdapter) NeighSetCallCount() int { + fake.neighSetMutex.RLock() + defer fake.neighSetMutex.RUnlock() + return len(fake.neighSetArgsForCall) +} + +func (fake *NetlinkAdapter) NeighSetArgsForCall(i int) *netlink.Neigh { + fake.neighSetMutex.RLock() + defer fake.neighSetMutex.RUnlock() + return fake.neighSetArgsForCall[i].arg1 +} + +func (fake *NetlinkAdapter) NeighSetReturns(result1 error) { + fake.NeighSetStub = nil + fake.neighSetReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) NeighSetReturnsOnCall(i int, result1 error) { + fake.NeighSetStub = nil + if fake.neighSetReturnsOnCall == nil { + fake.neighSetReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.neighSetReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) ARPList(index int) ([]netlink.Neigh, error) { + fake.aRPListMutex.Lock() + ret, specificReturn := fake.aRPListReturnsOnCall[len(fake.aRPListArgsForCall)] + fake.aRPListArgsForCall = append(fake.aRPListArgsForCall, struct { + index int + }{index}) + fake.recordInvocation("ARPList", []interface{}{index}) + fake.aRPListMutex.Unlock() + if fake.ARPListStub != nil { + return fake.ARPListStub(index) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.aRPListReturns.result1, fake.aRPListReturns.result2 +} + +func (fake *NetlinkAdapter) ARPListCallCount() int { + fake.aRPListMutex.RLock() + defer fake.aRPListMutex.RUnlock() + return len(fake.aRPListArgsForCall) +} + +func (fake *NetlinkAdapter) ARPListArgsForCall(i int) int { + fake.aRPListMutex.RLock() + defer fake.aRPListMutex.RUnlock() + return fake.aRPListArgsForCall[i].index +} + +func (fake *NetlinkAdapter) ARPListReturns(result1 []netlink.Neigh, result2 error) { + fake.ARPListStub = nil + fake.aRPListReturns = struct { + result1 []netlink.Neigh + result2 error + }{result1, result2} +} + +func (fake *NetlinkAdapter) ARPListReturnsOnCall(i int, result1 []netlink.Neigh, result2 error) { + fake.ARPListStub = nil + if fake.aRPListReturnsOnCall == nil { + fake.aRPListReturnsOnCall = make(map[int]struct { + result1 []netlink.Neigh + result2 error + }) + } + fake.aRPListReturnsOnCall[i] = struct { + result1 []netlink.Neigh + result2 error + }{result1, result2} +} + +func (fake *NetlinkAdapter) FDBList(index int) ([]netlink.Neigh, error) { + fake.fDBListMutex.Lock() + ret, specificReturn := fake.fDBListReturnsOnCall[len(fake.fDBListArgsForCall)] + fake.fDBListArgsForCall = append(fake.fDBListArgsForCall, struct { + index int + }{index}) + fake.recordInvocation("FDBList", []interface{}{index}) + fake.fDBListMutex.Unlock() + if fake.FDBListStub != nil { + return fake.FDBListStub(index) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.fDBListReturns.result1, fake.fDBListReturns.result2 +} + +func (fake *NetlinkAdapter) FDBListCallCount() int { + fake.fDBListMutex.RLock() + defer fake.fDBListMutex.RUnlock() + return len(fake.fDBListArgsForCall) +} + +func (fake *NetlinkAdapter) FDBListArgsForCall(i int) int { + fake.fDBListMutex.RLock() + defer fake.fDBListMutex.RUnlock() + return fake.fDBListArgsForCall[i].index +} + +func (fake *NetlinkAdapter) FDBListReturns(result1 []netlink.Neigh, result2 error) { + fake.FDBListStub = nil + fake.fDBListReturns = struct { + result1 []netlink.Neigh + result2 error + }{result1, result2} +} + +func (fake *NetlinkAdapter) FDBListReturnsOnCall(i int, result1 []netlink.Neigh, result2 error) { + fake.FDBListStub = nil + if fake.fDBListReturnsOnCall == nil { + fake.fDBListReturnsOnCall = make(map[int]struct { + result1 []netlink.Neigh + result2 error + }) + } + fake.fDBListReturnsOnCall[i] = struct { + result1 []netlink.Neigh + result2 error + }{result1, result2} +} + +func (fake *NetlinkAdapter) NeighDel(arg1 *netlink.Neigh) error { + fake.neighDelMutex.Lock() + ret, specificReturn := fake.neighDelReturnsOnCall[len(fake.neighDelArgsForCall)] + fake.neighDelArgsForCall = append(fake.neighDelArgsForCall, struct { + arg1 *netlink.Neigh + }{arg1}) + fake.recordInvocation("NeighDel", []interface{}{arg1}) + fake.neighDelMutex.Unlock() + if fake.NeighDelStub != nil { + return fake.NeighDelStub(arg1) + } + if specificReturn { + return ret.result1 + } + return fake.neighDelReturns.result1 +} + +func (fake *NetlinkAdapter) NeighDelCallCount() int { + fake.neighDelMutex.RLock() + defer fake.neighDelMutex.RUnlock() + return len(fake.neighDelArgsForCall) +} + +func (fake *NetlinkAdapter) NeighDelArgsForCall(i int) *netlink.Neigh { + fake.neighDelMutex.RLock() + defer fake.neighDelMutex.RUnlock() + return fake.neighDelArgsForCall[i].arg1 +} + +func (fake *NetlinkAdapter) NeighDelReturns(result1 error) { + fake.NeighDelStub = nil + fake.neighDelReturns = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) NeighDelReturnsOnCall(i int, result1 error) { + fake.NeighDelStub = nil + if fake.neighDelReturnsOnCall == nil { + fake.neighDelReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.neighDelReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *NetlinkAdapter) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.linkSetUpMutex.RLock() + defer fake.linkSetUpMutex.RUnlock() + fake.linkAddMutex.RLock() + defer fake.linkAddMutex.RUnlock() + fake.linkByNameMutex.RLock() + defer fake.linkByNameMutex.RUnlock() + fake.linkByIndexMutex.RLock() + defer fake.linkByIndexMutex.RUnlock() + fake.linkSetHardwareAddrMutex.RLock() + defer fake.linkSetHardwareAddrMutex.RUnlock() + fake.addrAddScopeLinkMutex.RLock() + defer fake.addrAddScopeLinkMutex.RUnlock() + fake.addrListMutex.RLock() + defer fake.addrListMutex.RUnlock() + fake.routeAddMutex.RLock() + defer fake.routeAddMutex.RUnlock() + fake.routeReplaceMutex.RLock() + defer fake.routeReplaceMutex.RUnlock() + fake.routeListMutex.RLock() + defer fake.routeListMutex.RUnlock() + fake.routeDelMutex.RLock() + defer fake.routeDelMutex.RUnlock() + fake.linkDelMutex.RLock() + defer fake.linkDelMutex.RUnlock() + fake.neighSetMutex.RLock() + defer fake.neighSetMutex.RUnlock() + fake.aRPListMutex.RLock() + defer fake.aRPListMutex.RUnlock() + fake.fDBListMutex.RLock() + defer fake.fDBListMutex.RUnlock() + fake.neighDelMutex.RLock() + defer fake.neighDelMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *NetlinkAdapter) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/silk/daemon/vtep/vtep_suite_test.go b/src/code.cloudfoundry.org/silk/daemon/vtep/vtep_suite_test.go new file mode 100644 index 00000000..3613dc6d --- /dev/null +++ b/src/code.cloudfoundry.org/silk/daemon/vtep/vtep_suite_test.go @@ -0,0 +1,13 @@ +package vtep_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "VTEP Suite") +} diff --git a/src/code.cloudfoundry.org/silk/data-plane.png b/src/code.cloudfoundry.org/silk/data-plane.png new file mode 100644 index 0000000000000000000000000000000000000000..b4fa8d5294982be72bfed12ff750793eeeb8a705 GIT binary patch literal 147676 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 IckAJQ0pM&=J^%m! literal 0 HcmV?d00001 diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/lib/adapter/net_adapter.go b/src/code.cloudfoundry.org/silk/lib/adapter/net_adapter.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/lib/adapter/net_adapter.go rename to src/code.cloudfoundry.org/silk/lib/adapter/net_adapter.go diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/lib/adapter/netlink_adapter.go b/src/code.cloudfoundry.org/silk/lib/adapter/netlink_adapter.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/lib/adapter/netlink_adapter.go rename to src/code.cloudfoundry.org/silk/lib/adapter/netlink_adapter.go diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/lib/datastore/datastore.go b/src/code.cloudfoundry.org/silk/lib/datastore/datastore.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/lib/datastore/datastore.go rename to src/code.cloudfoundry.org/silk/lib/datastore/datastore.go diff --git a/src/code.cloudfoundry.org/silk/lib/datastore/datastore_integration_test.go b/src/code.cloudfoundry.org/silk/lib/datastore/datastore_integration_test.go new file mode 100644 index 00000000..03d3ca08 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/lib/datastore/datastore_integration_test.go @@ -0,0 +1,210 @@ +package datastore_test + +import ( + "fmt" + "io/ioutil" + "os" + "sync/atomic" + + "code.cloudfoundry.org/cf-networking-helpers/testsupport" + "code.cloudfoundry.org/filelock" + "code.cloudfoundry.org/silk/lib/datastore" + "code.cloudfoundry.org/silk/lib/serial" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Datastore Lifecycle", func() { + var ( + handle string + ip string + store *datastore.Store + metadata map[string]interface{} + filepath string + ) + + BeforeEach(func() { + handle = "some-handle" + ip = "192.168.0.100" + metadata = map[string]interface{}{ + "AppID": "some-appid", + "OrgID": "some-orgid", + "PolicyGroupID": "some-policygroupid", + "SpaceID": "some-spaceid", + "randomKey": "randomValue", + } + + file, err := ioutil.TempFile("", "") + Expect(err).NotTo(HaveOccurred()) + filepath = file.Name() + + serializer := &serial.Serial{} + + store = &datastore.Store{ + Serializer: serializer, + LockerNew: filelock.NewLocker, + } + }) + + AfterEach(func() { + os.Remove(filepath) + }) + + Context("when empty", func() { + It("returns an empty map", func() { + data, err := store.ReadAll(filepath) + Expect(err).NotTo(HaveOccurred()) + Expect(len(data)).To(Equal(0)) + }) + }) + + Context("when adding", func() { + It("can add entry to datastore", func() { + By("adding an entry to store") + err := store.Add(filepath, handle, ip, metadata) + Expect(err).NotTo(HaveOccurred()) + + By("verify entry is in store") + data, err := store.ReadAll(filepath) + Expect(err).NotTo(HaveOccurred()) + Expect(data).Should(HaveKey(handle)) + + Expect(data[handle].IP).To(Equal(ip)) + for k, v := range metadata { + Expect(data[handle].Metadata).Should(HaveKeyWithValue(k, v)) + } + }) + + It("can add multiple entries to datastore", func() { + total := 250 + By("adding an entries to store") + for i := 0; i < total; i++ { + id := fmt.Sprintf("%s-%d", handle, i) + err := store.Add(filepath, id, ip, metadata) + Expect(err).NotTo(HaveOccurred()) + } + + By("verify entries are in store") + data, err := store.ReadAll(filepath) + Expect(err).NotTo(HaveOccurred()) + Expect(data).Should(HaveLen(total)) + }) + }) + + Context("when removing", func() { + It("can add entry and remove an entry from datastore", func() { + By("adding an entry to store") + err := store.Add(filepath, handle, ip, metadata) + Expect(err).NotTo(HaveOccurred()) + + By("verify entry is in store") + data, err := store.ReadAll(filepath) + Expect(err).NotTo(HaveOccurred()) + Expect(data).Should(HaveLen(1)) + + By("removing entry from store") + deleted, err := store.Delete(filepath, handle) + Expect(err).NotTo(HaveOccurred()) + Expect(deleted.Handle).To(Equal(handle)) + Expect(deleted.IP).To(Equal(ip)) + Expect(deleted.Metadata).To(Equal(metadata)) + + By("verify entry no longer in store") + data, err = store.ReadAll(filepath) + Expect(err).NotTo(HaveOccurred()) + Expect(data).Should(BeEmpty()) + + By("checking delete is idempotent") + _, err = store.Delete(filepath, handle) + Expect(err).NotTo(HaveOccurred()) + }) + + It("can remove multiple entries to datastore", func() { + total := 250 + By("adding an entries to store") + for i := 0; i < total; i++ { + id := fmt.Sprintf("%s-%d", handle, i) + err := store.Add(filepath, id, ip, metadata) + Expect(err).NotTo(HaveOccurred()) + } + + By("verify entries are in store") + data, err := store.ReadAll(filepath) + Expect(err).NotTo(HaveOccurred()) + Expect(data).Should(HaveLen(total)) + + By("removing entries from store") + for i := 0; i < total; i++ { + id := fmt.Sprintf("%s-%d", handle, i) + deleted, err := store.Delete(filepath, id) + Expect(deleted.Handle).To(Equal(id)) + Expect(err).NotTo(HaveOccurred()) + } + + By("verify store is empty") + data, err = store.ReadAll(filepath) + Expect(err).NotTo(HaveOccurred()) + Expect(data).Should(BeEmpty()) + }) + }) + + Context("when adding and deleting concurrently", func() { + It("remains consistent", func() { + + containerHandles := []interface{}{} + total := 250 + for i := 0; i < total; i++ { + id := fmt.Sprintf("%s-%d", handle, i) + containerHandles = append(containerHandles, id) + } + + parallelRunner := &testsupport.ParallelRunner{ + NumWorkers: 50, + } + toDelete := make(chan (interface{}), total) + toRead := make(chan (interface{}), total) + + go func() { + parallelRunner.RunOnSlice(containerHandles, func(containerHandle interface{}) { + p := containerHandle.(string) + func(id string) { + err := store.Add(filepath, id, ip, metadata) + Expect(err).NotTo(HaveOccurred()) + }(p) + toRead <- p + }) + close(toRead) + }() + + go func() { + parallelRunner.RunOnChannel(toRead, func(containerHandle interface{}) { + p := containerHandle.(string) + func(id string) { + contents, err := store.ReadAll(filepath) + Expect(err).NotTo(HaveOccurred()) + Expect(contents).To(HaveKey(p)) + }(p) + toDelete <- p + }) + close(toDelete) + }() + + var nDeleted int32 + parallelRunner.RunOnChannel(toDelete, func(containerHandle interface{}) { + p := containerHandle.(string) + func(id string) { + _, err := store.Delete(filepath, id) + Expect(err).NotTo(HaveOccurred()) + }(p) + atomic.AddInt32(&nDeleted, 1) + }) + Expect(nDeleted).To(Equal(int32(total))) + + By("adding an entries to store") + data, err := store.ReadAll(filepath) + Expect(err).NotTo(HaveOccurred()) + Expect(data).Should(HaveLen(0)) + + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/lib/datastore/datastore_suite_test.go b/src/code.cloudfoundry.org/silk/lib/datastore/datastore_suite_test.go new file mode 100644 index 00000000..6a1342fd --- /dev/null +++ b/src/code.cloudfoundry.org/silk/lib/datastore/datastore_suite_test.go @@ -0,0 +1,13 @@ +package datastore_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestDatastore(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Datastore Suite") +} diff --git a/src/code.cloudfoundry.org/silk/lib/datastore/datastore_test.go b/src/code.cloudfoundry.org/silk/lib/datastore/datastore_test.go new file mode 100644 index 00000000..5cc6ae9a --- /dev/null +++ b/src/code.cloudfoundry.org/silk/lib/datastore/datastore_test.go @@ -0,0 +1,234 @@ +package datastore_test + +import ( + "errors" + "os" + + "code.cloudfoundry.org/filelock" + "code.cloudfoundry.org/silk/lib/datastore" + + libfakes "code.cloudfoundry.org/silk/lib/fakes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Datastore", func() { + var ( + handle string + ip string + store *datastore.Store + metadata map[string]interface{} + + serializer *libfakes.Serializer + locker *libfakes.FileLocker + lockerNewCallCount int + lockerNewFilePath string + + lockedFile *os.File + filePath string + ) + + BeforeEach(func() { + handle = "some-handle" + ip = "192.168.0.100" + filePath = "file" + locker = &libfakes.FileLocker{} + serializer = &libfakes.Serializer{} + metadata = map[string]interface{}{ + "AppID": "some-appid", + "OrgID": "some-orgid", + "PolicyGroupID": "some-policygroupid", + "SpaceID": "some-spaceid", + "randomKey": "randomValue", + } + + store = &datastore.Store{ + Serializer: serializer, + LockerNew: func(filePath string) filelock.FileLocker { + lockerNewCallCount++ + lockerNewFilePath = filePath + return locker + }, + } + + lockedFile = &os.File{} + locker.OpenReturns(lockedFile, nil) + + lockerNewCallCount = 0 + }) + + Context("when adding an entry to store", func() { + It("deserializes the data from the file", func() { + err := store.Add(filePath, handle, ip, metadata) + Expect(err).NotTo(HaveOccurred()) + + Expect(lockerNewCallCount).To(Equal(1)) + Expect(lockerNewFilePath).To(Equal(filePath)) + Expect(locker.OpenCallCount()).To(Equal(1)) + Expect(serializer.DecodeAllCallCount()).To(Equal(1)) + Expect(serializer.EncodeAndOverwriteCallCount()).To(Equal(1)) + + file, _ := serializer.DecodeAllArgsForCall(0) + Expect(file).To(Equal(lockedFile)) + + _, actual := serializer.EncodeAndOverwriteArgsForCall(0) + expected := map[string]datastore.Container{ + handle: datastore.Container{ + Handle: handle, + IP: ip, + Metadata: metadata, + }, + } + Expect(actual).To(Equal(expected)) + }) + + Context("when handle is not valid", func() { + It("wraps and returns the error", func() { + err := store.Add(filePath, "", ip, metadata) + Expect(err).To(MatchError("invalid handle")) + }) + }) + + Context("when input IP is not valid", func() { + It("wraps and returns the error", func() { + err := store.Add(filePath, handle, "invalid-ip", metadata) + Expect(err).To(MatchError("invalid ip: invalid-ip")) + }) + }) + + Context("when file locker fails to open", func() { + BeforeEach(func() { + locker.OpenReturns(nil, errors.New("potato")) + }) + It("wraps and returns the error", func() { + err := store.Add(filePath, handle, ip, metadata) + Expect(err).To(MatchError("open lock: potato")) + }) + }) + + Context("when serializer fails to decode", func() { + BeforeEach(func() { + serializer.DecodeAllReturns(errors.New("potato")) + }) + It("wraps and returns the error", func() { + err := store.Add(filePath, handle, ip, metadata) + Expect(err).To(MatchError("decoding file: potato")) + }) + }) + + Context("when serializer fails to encode", func() { + BeforeEach(func() { + serializer.EncodeAndOverwriteReturns(errors.New("potato")) + }) + It("wraps and returns the error", func() { + err := store.Add(filePath, handle, ip, metadata) + Expect(err).To(MatchError("encode and overwrite: potato")) + }) + }) + + }) + + Context("when deleting an entry from store", func() { + + It("deserializes the data from the file", func() { + _, err := store.Delete(filePath, handle) + Expect(err).NotTo(HaveOccurred()) + + Expect(lockerNewCallCount).To(Equal(1)) + Expect(lockerNewFilePath).To(Equal(filePath)) + Expect(locker.OpenCallCount()).To(Equal(1)) + Expect(serializer.DecodeAllCallCount()).To(Equal(1)) + Expect(serializer.EncodeAndOverwriteCallCount()).To(Equal(1)) + + file, _ := serializer.DecodeAllArgsForCall(0) + Expect(file).To(Equal(lockedFile)) + + _, actual := serializer.EncodeAndOverwriteArgsForCall(0) + Expect(actual).ToNot(HaveKey(handle)) + }) + + It("is idempotent", func() { + _, err := store.Delete(filePath, handle) + Expect(err).NotTo(HaveOccurred()) + + _, err = store.Delete(filePath, handle) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("when handle is not valid", func() { + It("wraps and returns the error", func() { + _, err := store.Delete(filePath, "") + Expect(err).To(MatchError("invalid handle")) + }) + }) + + Context("when file locker fails to open", func() { + BeforeEach(func() { + locker.OpenReturns(nil, errors.New("potato")) + }) + It("wraps and returns the error", func() { + _, err := store.Delete(filePath, handle) + Expect(err).To(MatchError("open lock: potato")) + }) + }) + + Context("when serializer fails to decode", func() { + BeforeEach(func() { + serializer.DecodeAllReturns(errors.New("potato")) + }) + It("wraps and returns the error", func() { + _, err := store.Delete(filePath, handle) + Expect(err).To(MatchError("decoding file: potato")) + }) + }) + + Context("when serializer fails to encode", func() { + BeforeEach(func() { + serializer.EncodeAndOverwriteReturns(errors.New("potato")) + }) + It("wraps and returns the error", func() { + _, err := store.Delete(filePath, handle) + Expect(err).To(MatchError("encode and overwrite: potato")) + }) + }) + + }) + + Context("when reading from datastore", func() { + It("deserializes the data from the file", func() { + data, err := store.ReadAll(filePath) + Expect(err).NotTo(HaveOccurred()) + Expect(data).NotTo(BeNil()) + + Expect(lockerNewCallCount).To(Equal(1)) + Expect(lockerNewFilePath).To(Equal(filePath)) + Expect(locker.OpenCallCount()).To(Equal(1)) + Expect(serializer.DecodeAllCallCount()).To(Equal(1)) + Expect(serializer.EncodeAndOverwriteCallCount()).To(Equal(0)) + + file, _ := serializer.DecodeAllArgsForCall(0) + Expect(file).To(Equal(lockedFile)) + }) + + Context("when file locker fails to open", func() { + BeforeEach(func() { + locker.OpenReturns(nil, errors.New("potato")) + }) + It("wraps and returns the error", func() { + _, err := store.ReadAll(filePath) + Expect(err).To(MatchError("open lock: potato")) + }) + }) + + Context("when serializer fails to decode", func() { + BeforeEach(func() { + serializer.DecodeAllReturns(errors.New("potato")) + }) + It("wraps and returns the error", func() { + _, err := store.ReadAll(filePath) + Expect(err).To(MatchError("decoding file: potato")) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/lib/fakes/datastore.go b/src/code.cloudfoundry.org/silk/lib/fakes/datastore.go new file mode 100644 index 00000000..3cf5e747 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/lib/fakes/datastore.go @@ -0,0 +1,224 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + + "code.cloudfoundry.org/silk/lib/datastore" +) + +type Datastore struct { + AddStub func(handle, ip string, metadata map[string]interface{}) error + addMutex sync.RWMutex + addArgsForCall []struct { + handle string + ip string + metadata map[string]interface{} + } + addReturns struct { + result1 error + } + addReturnsOnCall map[int]struct { + result1 error + } + DeleteStub func(handle string) (datastore.Container, error) + deleteMutex sync.RWMutex + deleteArgsForCall []struct { + handle string + } + deleteReturns struct { + result1 datastore.Container + result2 error + } + deleteReturnsOnCall map[int]struct { + result1 datastore.Container + result2 error + } + ReadAllStub func() (map[string]datastore.Container, error) + readAllMutex sync.RWMutex + readAllArgsForCall []struct{} + readAllReturns struct { + result1 map[string]datastore.Container + result2 error + } + readAllReturnsOnCall map[int]struct { + result1 map[string]datastore.Container + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *Datastore) Add(handle string, ip string, metadata map[string]interface{}) error { + fake.addMutex.Lock() + ret, specificReturn := fake.addReturnsOnCall[len(fake.addArgsForCall)] + fake.addArgsForCall = append(fake.addArgsForCall, struct { + handle string + ip string + metadata map[string]interface{} + }{handle, ip, metadata}) + fake.recordInvocation("Add", []interface{}{handle, ip, metadata}) + fake.addMutex.Unlock() + if fake.AddStub != nil { + return fake.AddStub(handle, ip, metadata) + } + if specificReturn { + return ret.result1 + } + return fake.addReturns.result1 +} + +func (fake *Datastore) AddCallCount() int { + fake.addMutex.RLock() + defer fake.addMutex.RUnlock() + return len(fake.addArgsForCall) +} + +func (fake *Datastore) AddArgsForCall(i int) (string, string, map[string]interface{}) { + fake.addMutex.RLock() + defer fake.addMutex.RUnlock() + return fake.addArgsForCall[i].handle, fake.addArgsForCall[i].ip, fake.addArgsForCall[i].metadata +} + +func (fake *Datastore) AddReturns(result1 error) { + fake.AddStub = nil + fake.addReturns = struct { + result1 error + }{result1} +} + +func (fake *Datastore) AddReturnsOnCall(i int, result1 error) { + fake.AddStub = nil + if fake.addReturnsOnCall == nil { + fake.addReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.addReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *Datastore) Delete(handle string) (datastore.Container, error) { + fake.deleteMutex.Lock() + ret, specificReturn := fake.deleteReturnsOnCall[len(fake.deleteArgsForCall)] + fake.deleteArgsForCall = append(fake.deleteArgsForCall, struct { + handle string + }{handle}) + fake.recordInvocation("Delete", []interface{}{handle}) + fake.deleteMutex.Unlock() + if fake.DeleteStub != nil { + return fake.DeleteStub(handle) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.deleteReturns.result1, fake.deleteReturns.result2 +} + +func (fake *Datastore) DeleteCallCount() int { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return len(fake.deleteArgsForCall) +} + +func (fake *Datastore) DeleteArgsForCall(i int) string { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return fake.deleteArgsForCall[i].handle +} + +func (fake *Datastore) DeleteReturns(result1 datastore.Container, result2 error) { + fake.DeleteStub = nil + fake.deleteReturns = struct { + result1 datastore.Container + result2 error + }{result1, result2} +} + +func (fake *Datastore) DeleteReturnsOnCall(i int, result1 datastore.Container, result2 error) { + fake.DeleteStub = nil + if fake.deleteReturnsOnCall == nil { + fake.deleteReturnsOnCall = make(map[int]struct { + result1 datastore.Container + result2 error + }) + } + fake.deleteReturnsOnCall[i] = struct { + result1 datastore.Container + result2 error + }{result1, result2} +} + +func (fake *Datastore) ReadAll() (map[string]datastore.Container, error) { + fake.readAllMutex.Lock() + ret, specificReturn := fake.readAllReturnsOnCall[len(fake.readAllArgsForCall)] + fake.readAllArgsForCall = append(fake.readAllArgsForCall, struct{}{}) + fake.recordInvocation("ReadAll", []interface{}{}) + fake.readAllMutex.Unlock() + if fake.ReadAllStub != nil { + return fake.ReadAllStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.readAllReturns.result1, fake.readAllReturns.result2 +} + +func (fake *Datastore) ReadAllCallCount() int { + fake.readAllMutex.RLock() + defer fake.readAllMutex.RUnlock() + return len(fake.readAllArgsForCall) +} + +func (fake *Datastore) ReadAllReturns(result1 map[string]datastore.Container, result2 error) { + fake.ReadAllStub = nil + fake.readAllReturns = struct { + result1 map[string]datastore.Container + result2 error + }{result1, result2} +} + +func (fake *Datastore) ReadAllReturnsOnCall(i int, result1 map[string]datastore.Container, result2 error) { + fake.ReadAllStub = nil + if fake.readAllReturnsOnCall == nil { + fake.readAllReturnsOnCall = make(map[int]struct { + result1 map[string]datastore.Container + result2 error + }) + } + fake.readAllReturnsOnCall[i] = struct { + result1 map[string]datastore.Container + result2 error + }{result1, result2} +} + +func (fake *Datastore) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.addMutex.RLock() + defer fake.addMutex.RUnlock() + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + fake.readAllMutex.RLock() + defer fake.readAllMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *Datastore) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ datastore.Datastore = new(Datastore) diff --git a/src/code.cloudfoundry.org/silk/lib/fakes/file_locker.go b/src/code.cloudfoundry.org/silk/lib/fakes/file_locker.go new file mode 100644 index 00000000..9d6e7c44 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/lib/fakes/file_locker.go @@ -0,0 +1,94 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + + "code.cloudfoundry.org/filelock" + "code.cloudfoundry.org/silk/lib/datastore" +) + +type FileLocker struct { + OpenStub func() (filelock.LockedFile, error) + openMutex sync.RWMutex + openArgsForCall []struct{} + openReturns struct { + result1 filelock.LockedFile + result2 error + } + openReturnsOnCall map[int]struct { + result1 filelock.LockedFile + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FileLocker) Open() (filelock.LockedFile, error) { + fake.openMutex.Lock() + ret, specificReturn := fake.openReturnsOnCall[len(fake.openArgsForCall)] + fake.openArgsForCall = append(fake.openArgsForCall, struct{}{}) + fake.recordInvocation("Open", []interface{}{}) + fake.openMutex.Unlock() + if fake.OpenStub != nil { + return fake.OpenStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.openReturns.result1, fake.openReturns.result2 +} + +func (fake *FileLocker) OpenCallCount() int { + fake.openMutex.RLock() + defer fake.openMutex.RUnlock() + return len(fake.openArgsForCall) +} + +func (fake *FileLocker) OpenReturns(result1 filelock.LockedFile, result2 error) { + fake.OpenStub = nil + fake.openReturns = struct { + result1 filelock.LockedFile + result2 error + }{result1, result2} +} + +func (fake *FileLocker) OpenReturnsOnCall(i int, result1 filelock.LockedFile, result2 error) { + fake.OpenStub = nil + if fake.openReturnsOnCall == nil { + fake.openReturnsOnCall = make(map[int]struct { + result1 filelock.LockedFile + result2 error + }) + } + fake.openReturnsOnCall[i] = struct { + result1 filelock.LockedFile + result2 error + }{result1, result2} +} + +func (fake *FileLocker) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.openMutex.RLock() + defer fake.openMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FileLocker) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ datastore.FileLocker = new(FileLocker) diff --git a/src/code.cloudfoundry.org/silk/lib/fakes/overwriteable_file.go b/src/code.cloudfoundry.org/silk/lib/fakes/overwriteable_file.go new file mode 100644 index 00000000..58bbd00c --- /dev/null +++ b/src/code.cloudfoundry.org/silk/lib/fakes/overwriteable_file.go @@ -0,0 +1,304 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + "code.cloudfoundry.org/silk/lib/serial" +) + +type OverwriteableFile struct { + ReadStub func(p []byte) (n int, err error) + readMutex sync.RWMutex + readArgsForCall []struct { + p []byte + } + readReturns struct { + result1 int + result2 error + } + readReturnsOnCall map[int]struct { + result1 int + result2 error + } + WriteStub func(p []byte) (n int, err error) + writeMutex sync.RWMutex + writeArgsForCall []struct { + p []byte + } + writeReturns struct { + result1 int + result2 error + } + writeReturnsOnCall map[int]struct { + result1 int + result2 error + } + SeekStub func(offset int64, whence int) (int64, error) + seekMutex sync.RWMutex + seekArgsForCall []struct { + offset int64 + whence int + } + seekReturns struct { + result1 int64 + result2 error + } + seekReturnsOnCall map[int]struct { + result1 int64 + result2 error + } + TruncateStub func(size int64) error + truncateMutex sync.RWMutex + truncateArgsForCall []struct { + size int64 + } + truncateReturns struct { + result1 error + } + truncateReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *OverwriteableFile) Read(p []byte) (n int, err error) { + var pCopy []byte + if p != nil { + pCopy = make([]byte, len(p)) + copy(pCopy, p) + } + fake.readMutex.Lock() + ret, specificReturn := fake.readReturnsOnCall[len(fake.readArgsForCall)] + fake.readArgsForCall = append(fake.readArgsForCall, struct { + p []byte + }{pCopy}) + fake.recordInvocation("Read", []interface{}{pCopy}) + fake.readMutex.Unlock() + if fake.ReadStub != nil { + return fake.ReadStub(p) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.readReturns.result1, fake.readReturns.result2 +} + +func (fake *OverwriteableFile) ReadCallCount() int { + fake.readMutex.RLock() + defer fake.readMutex.RUnlock() + return len(fake.readArgsForCall) +} + +func (fake *OverwriteableFile) ReadArgsForCall(i int) []byte { + fake.readMutex.RLock() + defer fake.readMutex.RUnlock() + return fake.readArgsForCall[i].p +} + +func (fake *OverwriteableFile) ReadReturns(result1 int, result2 error) { + fake.ReadStub = nil + fake.readReturns = struct { + result1 int + result2 error + }{result1, result2} +} + +func (fake *OverwriteableFile) ReadReturnsOnCall(i int, result1 int, result2 error) { + fake.ReadStub = nil + if fake.readReturnsOnCall == nil { + fake.readReturnsOnCall = make(map[int]struct { + result1 int + result2 error + }) + } + fake.readReturnsOnCall[i] = struct { + result1 int + result2 error + }{result1, result2} +} + +func (fake *OverwriteableFile) Write(p []byte) (n int, err error) { + var pCopy []byte + if p != nil { + pCopy = make([]byte, len(p)) + copy(pCopy, p) + } + fake.writeMutex.Lock() + ret, specificReturn := fake.writeReturnsOnCall[len(fake.writeArgsForCall)] + fake.writeArgsForCall = append(fake.writeArgsForCall, struct { + p []byte + }{pCopy}) + fake.recordInvocation("Write", []interface{}{pCopy}) + fake.writeMutex.Unlock() + if fake.WriteStub != nil { + return fake.WriteStub(p) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.writeReturns.result1, fake.writeReturns.result2 +} + +func (fake *OverwriteableFile) WriteCallCount() int { + fake.writeMutex.RLock() + defer fake.writeMutex.RUnlock() + return len(fake.writeArgsForCall) +} + +func (fake *OverwriteableFile) WriteArgsForCall(i int) []byte { + fake.writeMutex.RLock() + defer fake.writeMutex.RUnlock() + return fake.writeArgsForCall[i].p +} + +func (fake *OverwriteableFile) WriteReturns(result1 int, result2 error) { + fake.WriteStub = nil + fake.writeReturns = struct { + result1 int + result2 error + }{result1, result2} +} + +func (fake *OverwriteableFile) WriteReturnsOnCall(i int, result1 int, result2 error) { + fake.WriteStub = nil + if fake.writeReturnsOnCall == nil { + fake.writeReturnsOnCall = make(map[int]struct { + result1 int + result2 error + }) + } + fake.writeReturnsOnCall[i] = struct { + result1 int + result2 error + }{result1, result2} +} + +func (fake *OverwriteableFile) Seek(offset int64, whence int) (int64, error) { + fake.seekMutex.Lock() + ret, specificReturn := fake.seekReturnsOnCall[len(fake.seekArgsForCall)] + fake.seekArgsForCall = append(fake.seekArgsForCall, struct { + offset int64 + whence int + }{offset, whence}) + fake.recordInvocation("Seek", []interface{}{offset, whence}) + fake.seekMutex.Unlock() + if fake.SeekStub != nil { + return fake.SeekStub(offset, whence) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fake.seekReturns.result1, fake.seekReturns.result2 +} + +func (fake *OverwriteableFile) SeekCallCount() int { + fake.seekMutex.RLock() + defer fake.seekMutex.RUnlock() + return len(fake.seekArgsForCall) +} + +func (fake *OverwriteableFile) SeekArgsForCall(i int) (int64, int) { + fake.seekMutex.RLock() + defer fake.seekMutex.RUnlock() + return fake.seekArgsForCall[i].offset, fake.seekArgsForCall[i].whence +} + +func (fake *OverwriteableFile) SeekReturns(result1 int64, result2 error) { + fake.SeekStub = nil + fake.seekReturns = struct { + result1 int64 + result2 error + }{result1, result2} +} + +func (fake *OverwriteableFile) SeekReturnsOnCall(i int, result1 int64, result2 error) { + fake.SeekStub = nil + if fake.seekReturnsOnCall == nil { + fake.seekReturnsOnCall = make(map[int]struct { + result1 int64 + result2 error + }) + } + fake.seekReturnsOnCall[i] = struct { + result1 int64 + result2 error + }{result1, result2} +} + +func (fake *OverwriteableFile) Truncate(size int64) error { + fake.truncateMutex.Lock() + ret, specificReturn := fake.truncateReturnsOnCall[len(fake.truncateArgsForCall)] + fake.truncateArgsForCall = append(fake.truncateArgsForCall, struct { + size int64 + }{size}) + fake.recordInvocation("Truncate", []interface{}{size}) + fake.truncateMutex.Unlock() + if fake.TruncateStub != nil { + return fake.TruncateStub(size) + } + if specificReturn { + return ret.result1 + } + return fake.truncateReturns.result1 +} + +func (fake *OverwriteableFile) TruncateCallCount() int { + fake.truncateMutex.RLock() + defer fake.truncateMutex.RUnlock() + return len(fake.truncateArgsForCall) +} + +func (fake *OverwriteableFile) TruncateArgsForCall(i int) int64 { + fake.truncateMutex.RLock() + defer fake.truncateMutex.RUnlock() + return fake.truncateArgsForCall[i].size +} + +func (fake *OverwriteableFile) TruncateReturns(result1 error) { + fake.TruncateStub = nil + fake.truncateReturns = struct { + result1 error + }{result1} +} + +func (fake *OverwriteableFile) TruncateReturnsOnCall(i int, result1 error) { + fake.TruncateStub = nil + if fake.truncateReturnsOnCall == nil { + fake.truncateReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.truncateReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *OverwriteableFile) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.readMutex.RLock() + defer fake.readMutex.RUnlock() + fake.writeMutex.RLock() + defer fake.writeMutex.RUnlock() + fake.seekMutex.RLock() + defer fake.seekMutex.RUnlock() + fake.truncateMutex.RLock() + defer fake.truncateMutex.RUnlock() + return fake.invocations +} + +func (fake *OverwriteableFile) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ serial.OverwriteableFile = new(OverwriteableFile) diff --git a/src/code.cloudfoundry.org/silk/lib/fakes/serializer.go b/src/code.cloudfoundry.org/silk/lib/fakes/serializer.go new file mode 100644 index 00000000..9a9999d4 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/lib/fakes/serializer.go @@ -0,0 +1,160 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "io" + "sync" + + "code.cloudfoundry.org/silk/lib/serial" +) + +type Serializer struct { + DecodeAllStub func(file io.ReadSeeker, outData interface{}) error + decodeAllMutex sync.RWMutex + decodeAllArgsForCall []struct { + file io.ReadSeeker + outData interface{} + } + decodeAllReturns struct { + result1 error + } + decodeAllReturnsOnCall map[int]struct { + result1 error + } + EncodeAndOverwriteStub func(file serial.OverwriteableFile, outData interface{}) error + encodeAndOverwriteMutex sync.RWMutex + encodeAndOverwriteArgsForCall []struct { + file serial.OverwriteableFile + outData interface{} + } + encodeAndOverwriteReturns struct { + result1 error + } + encodeAndOverwriteReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *Serializer) DecodeAll(file io.ReadSeeker, outData interface{}) error { + fake.decodeAllMutex.Lock() + ret, specificReturn := fake.decodeAllReturnsOnCall[len(fake.decodeAllArgsForCall)] + fake.decodeAllArgsForCall = append(fake.decodeAllArgsForCall, struct { + file io.ReadSeeker + outData interface{} + }{file, outData}) + fake.recordInvocation("DecodeAll", []interface{}{file, outData}) + fake.decodeAllMutex.Unlock() + if fake.DecodeAllStub != nil { + return fake.DecodeAllStub(file, outData) + } + if specificReturn { + return ret.result1 + } + return fake.decodeAllReturns.result1 +} + +func (fake *Serializer) DecodeAllCallCount() int { + fake.decodeAllMutex.RLock() + defer fake.decodeAllMutex.RUnlock() + return len(fake.decodeAllArgsForCall) +} + +func (fake *Serializer) DecodeAllArgsForCall(i int) (io.ReadSeeker, interface{}) { + fake.decodeAllMutex.RLock() + defer fake.decodeAllMutex.RUnlock() + return fake.decodeAllArgsForCall[i].file, fake.decodeAllArgsForCall[i].outData +} + +func (fake *Serializer) DecodeAllReturns(result1 error) { + fake.DecodeAllStub = nil + fake.decodeAllReturns = struct { + result1 error + }{result1} +} + +func (fake *Serializer) DecodeAllReturnsOnCall(i int, result1 error) { + fake.DecodeAllStub = nil + if fake.decodeAllReturnsOnCall == nil { + fake.decodeAllReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.decodeAllReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *Serializer) EncodeAndOverwrite(file serial.OverwriteableFile, outData interface{}) error { + fake.encodeAndOverwriteMutex.Lock() + ret, specificReturn := fake.encodeAndOverwriteReturnsOnCall[len(fake.encodeAndOverwriteArgsForCall)] + fake.encodeAndOverwriteArgsForCall = append(fake.encodeAndOverwriteArgsForCall, struct { + file serial.OverwriteableFile + outData interface{} + }{file, outData}) + fake.recordInvocation("EncodeAndOverwrite", []interface{}{file, outData}) + fake.encodeAndOverwriteMutex.Unlock() + if fake.EncodeAndOverwriteStub != nil { + return fake.EncodeAndOverwriteStub(file, outData) + } + if specificReturn { + return ret.result1 + } + return fake.encodeAndOverwriteReturns.result1 +} + +func (fake *Serializer) EncodeAndOverwriteCallCount() int { + fake.encodeAndOverwriteMutex.RLock() + defer fake.encodeAndOverwriteMutex.RUnlock() + return len(fake.encodeAndOverwriteArgsForCall) +} + +func (fake *Serializer) EncodeAndOverwriteArgsForCall(i int) (serial.OverwriteableFile, interface{}) { + fake.encodeAndOverwriteMutex.RLock() + defer fake.encodeAndOverwriteMutex.RUnlock() + return fake.encodeAndOverwriteArgsForCall[i].file, fake.encodeAndOverwriteArgsForCall[i].outData +} + +func (fake *Serializer) EncodeAndOverwriteReturns(result1 error) { + fake.EncodeAndOverwriteStub = nil + fake.encodeAndOverwriteReturns = struct { + result1 error + }{result1} +} + +func (fake *Serializer) EncodeAndOverwriteReturnsOnCall(i int, result1 error) { + fake.EncodeAndOverwriteStub = nil + if fake.encodeAndOverwriteReturnsOnCall == nil { + fake.encodeAndOverwriteReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.encodeAndOverwriteReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *Serializer) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.decodeAllMutex.RLock() + defer fake.decodeAllMutex.RUnlock() + fake.encodeAndOverwriteMutex.RLock() + defer fake.encodeAndOverwriteMutex.RUnlock() + return fake.invocations +} + +func (fake *Serializer) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ serial.Serializer = new(Serializer) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/lib/hwaddr/hwaddr.go b/src/code.cloudfoundry.org/silk/lib/hwaddr/hwaddr.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/lib/hwaddr/hwaddr.go rename to src/code.cloudfoundry.org/silk/lib/hwaddr/hwaddr.go diff --git a/src/code.cloudfoundry.org/silk/lib/hwaddr/hwaddr_suite_test.go b/src/code.cloudfoundry.org/silk/lib/hwaddr/hwaddr_suite_test.go new file mode 100644 index 00000000..1558e655 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/lib/hwaddr/hwaddr_suite_test.go @@ -0,0 +1,13 @@ +package hwaddr_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" +) + +func TestSerializer(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Hwaddr Suite") +} diff --git a/src/code.cloudfoundry.org/silk/lib/hwaddr/hwaddr_test.go b/src/code.cloudfoundry.org/silk/lib/hwaddr/hwaddr_test.go new file mode 100644 index 00000000..ef3310ee --- /dev/null +++ b/src/code.cloudfoundry.org/silk/lib/hwaddr/hwaddr_test.go @@ -0,0 +1,41 @@ +package hwaddr_test + +import ( + "fmt" + "net" + + "code.cloudfoundry.org/silk/lib/hwaddr" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Hwaddr", func() { + + Describe("GenerateHardwareAddr4", func() { + validPrefix := []byte{0xaa, 0xbb} + ipV4Addr := net.ParseIP("192.168.1.1") + ipV6Addr := net.ParseIP("2001:db8::68") + Context("when the provided IP isn't ipv4", func() { + It("returns an error", func() { + _, err := hwaddr.GenerateHardwareAddr4(ipV6Addr, validPrefix) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(fmt.Errorf("2001:db8::68 is not an IPv4 address"))) + }) + }) + DescribeTable("when the provided prefix isn't 2 bytes", func(prefix []byte) { + _, err := hwaddr.GenerateHardwareAddr4(ipV4Addr, prefix) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(fmt.Errorf("Prefix length should be 2 bytes, but received %d bytes", len(prefix)))) + }, + Entry("empty prefix", []byte{}), + Entry("< 8 bytes", []byte{0xaa}), + Entry("> 8 bytes", []byte{0xaa, 0xbb, 0xcc}), + ) + It("returns a MAC addr with the given prefix, based on the provided IP", func() { + addr, err := hwaddr.GenerateHardwareAddr4(ipV4Addr, validPrefix) + Expect(err).ToNot(HaveOccurred()) + // IP variables are []byte types, with len 16. IPv4 addrs only use the last 4 bytes for addr info + Expect(addr.String()).To(Equal(fmt.Sprintf("aa:bb:%02x:%02x:%02x:%02x", ipV4Addr[12], ipV4Addr[13], ipV4Addr[14], ipV4Addr[15]))) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/lib/serial/file_serializer.go b/src/code.cloudfoundry.org/silk/lib/serial/file_serializer.go similarity index 100% rename from src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/silk/lib/serial/file_serializer.go rename to src/code.cloudfoundry.org/silk/lib/serial/file_serializer.go diff --git a/src/code.cloudfoundry.org/silk/lib/serial/file_serializer_test.go b/src/code.cloudfoundry.org/silk/lib/serial/file_serializer_test.go new file mode 100644 index 00000000..d4783dab --- /dev/null +++ b/src/code.cloudfoundry.org/silk/lib/serial/file_serializer_test.go @@ -0,0 +1,140 @@ +package serial_test + +import ( + "errors" + "io/ioutil" + "os" + "strings" + + "code.cloudfoundry.org/silk/lib/fakes" + "code.cloudfoundry.org/silk/lib/serial" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("FileSerializer", func() { + var serializer *serial.Serial + BeforeEach(func() { + serializer = &serial.Serial{} + }) + + Describe("DecodeAll", func() { + var file *strings.Reader + var outData struct{ Some string } + + BeforeEach(func() { + file = strings.NewReader(`{ "some": "data" }`) + }) + + It("decodes the file as JSON", func() { + Expect(serializer.DecodeAll(file, &outData)).To(Succeed()) + Expect(outData.Some).To(Equal("data")) + }) + + Context("when the read cursor is not at the start of the file", func() { + BeforeEach(func() { + file.ReadByte() + }) + + It("still decodes the entire file contents", func() { + Expect(serializer.DecodeAll(file, &outData)).To(Succeed()) + Expect(outData.Some).To(Equal("data")) + }) + }) + + Context("when the file is empty", func() { + BeforeEach(func() { + file = strings.NewReader("") + }) + It("succeeds", func() { + Expect(serializer.DecodeAll(file, &outData)).To(Succeed()) + }) + }) + + Context("when seek fails", func() { + var file *fakes.OverwriteableFile + BeforeEach(func() { + file = &fakes.OverwriteableFile{} + file.SeekReturns(0, errors.New("banana")) + }) + It("returns the error", func() { + err := serializer.DecodeAll(file, &outData) + Expect(err).To(MatchError("banana")) + }) + }) + + Context("when the json decode fails", func() { + BeforeEach(func() { + file = strings.NewReader("{{{") + }) + It("returns the error", func() { + err := serializer.DecodeAll(file, &outData) + Expect(err).To(MatchError(ContainSubstring("invalid character"))) + }) + }) + }) + + Describe("EncodeAndOverwrite", func() { + var file *os.File + + BeforeEach(func() { + var err error + file, err = ioutil.TempFile("", "some-file.json") + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + Expect(file.Close()).To(Succeed()) + Expect(os.RemoveAll(file.Name())).To(Succeed()) + }) + + It("encodes the data", func() { + outData := map[string]string{"some": "data"} + Expect(serializer.EncodeAndOverwrite(file, outData)).To(Succeed()) + + fileBytes, err := ioutil.ReadFile(file.Name()) + Expect(err).NotTo(HaveOccurred()) + Expect(fileBytes).To(MatchJSON(`{"some":"data"}`)) + }) + + Context("when there is already data in the file", func() { + BeforeEach(func() { + file.WriteString("some old data much longer than the new data") + }) + + It("overwrites the old data with the new data", func() { + outData := map[string]string{"some": "new data"} + Expect(serializer.EncodeAndOverwrite(file, outData)).To(Succeed()) + + fileBytes, err := ioutil.ReadFile(file.Name()) + Expect(err).NotTo(HaveOccurred()) + Expect(fileBytes).To(MatchJSON(`{"some":"new data"}`)) + }) + }) + + Context("when file seek fails", func() { + var file *fakes.OverwriteableFile + BeforeEach(func() { + file = &fakes.OverwriteableFile{} + file.SeekReturns(0, errors.New("banana")) + }) + It("returns the error", func() { + outData := map[string]string{"some": "data"} + Expect(serializer.EncodeAndOverwrite(file, outData)).To(MatchError("banana")) + }) + }) + + Context("when file truncate fails", func() { + var file *fakes.OverwriteableFile + BeforeEach(func() { + file = &fakes.OverwriteableFile{} + file.TruncateReturns(errors.New("banana")) + }) + It("returns the error", func() { + outData := map[string]string{"some": "data"} + Expect(serializer.EncodeAndOverwrite(file, outData)).To(MatchError("banana")) + }) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/lib/serial/serializer_suite_test.go b/src/code.cloudfoundry.org/silk/lib/serial/serializer_suite_test.go new file mode 100644 index 00000000..4e0e491d --- /dev/null +++ b/src/code.cloudfoundry.org/silk/lib/serial/serializer_suite_test.go @@ -0,0 +1,13 @@ +package serial_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" +) + +func TestSerializer(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Serializer Suite") +} diff --git a/src/code.cloudfoundry.org/silk/teardown/integration/error_cases_test.go b/src/code.cloudfoundry.org/silk/teardown/integration/error_cases_test.go new file mode 100644 index 00000000..b5eb6391 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/teardown/integration/error_cases_test.go @@ -0,0 +1,121 @@ +package integration_test + +import ( + "io/ioutil" + "os" + + "code.cloudfoundry.org/silk/testsupport" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("error cases", func() { + var configFilePath string + + BeforeEach(func() { + configFilePath = writeConfigFile(clientConf) + }) + + AfterEach(func() { + os.Remove(configFilePath) + }) + + Context("when the path to the config is bad", func() { + It("exits with non-zero status code", func() { + session := runTeardown("/some/bad/path") + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit()) + Expect(session.ExitCode).NotTo(Equal(0)) + Expect(string(session.Err.Contents())).To(ContainSubstring("load config file: reading file /some/bad/path")) + }) + }) + + Context("when the contents of the config file cannot be unmarshaled", func() { + BeforeEach(func() { + Expect(ioutil.WriteFile(configFilePath, []byte("some-bad-contents"), os.ModePerm)).To(Succeed()) + }) + + It("exits with non-zero status code", func() { + session := runTeardown(configFilePath) + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit()) + Expect(session.ExitCode).NotTo(Equal(0)) + Expect(session.Err.Contents()).To(ContainSubstring("load config file: unmarshaling contents")) + }) + }) + + Context("when the tls config is invalid", func() { + BeforeEach(func() { + os.Remove(configFilePath) + clientConf.ServerCACertFile = "/dev/null" + configFilePath = writeConfigFile(clientConf) + }) + + It("exits with non-zero status code", func() { + session := runTeardown(configFilePath) + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit()) + Expect(session.ExitCode).NotTo(Equal(0)) + Expect(session.Err.Contents()).To(ContainSubstring("create tls config:")) + }) + }) + + Context("when the controller address is not reachable", func() { + BeforeEach(func() { + fakeServer.Stop() + }) + + It("logs the error and exits with non-zero status code, but still deletes the VTEP", func() { + session := runTeardown(configFilePath) + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit()) + Expect(session.ExitCode).NotTo(Equal(0)) + Expect(string(session.Err.Contents())).To(MatchRegexp(`.*release.*dial tcp.*`)) + + _, _, _, err := vtepFactory.GetVTEPState(clientConf.VTEPName) + Expect(err).To(MatchError("find link: Link not found")) + }) + }) + + Context("when the controller is reachable but returns a 500", func() { + BeforeEach(func() { + fakeServer.SetHandler("/leases/release", &testsupport.FakeHandler{ + ResponseCode: 500, + ResponseBody: map[string]string{"error": "potato"}, + }) + }) + + It("logs the error and exits with non-zero status", func() { + session := runTeardown(configFilePath) + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit()) + Expect(session.ExitCode).NotTo(Equal(0)) + Expect(string(session.Err.Contents())).To(ContainSubstring("release subnet lease: http status 500: potato")) + }) + }) + + Context("when the vtep does not exist", func() { + BeforeEach(func() { + removeVTEP() + }) + + It("exits with non-zero status code", func() { + session := runTeardown(configFilePath) + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit()) + Expect(session.ExitCode).NotTo(Equal(0)) + Expect(string(session.Err.Contents())).To(MatchRegexp("delete vtep: find link.*Link not found")) + }) + }) + + Context("when the controller is unavailable and the vtep is missing", func() { + BeforeEach(func() { + removeVTEP() + fakeServer.Stop() + }) + + It("logs both errors", func() { + session := runTeardown(configFilePath) + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit()) + Expect(session.ExitCode).NotTo(Equal(0)) + Expect(string(session.Err.Contents())).To(MatchRegexp("release subnet lease.*dial tcp")) + Expect(string(session.Err.Contents())).To(MatchRegexp("delete vtep: find link.*Link not found")) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/silk/teardown/integration/integration_suite_test.go b/src/code.cloudfoundry.org/silk/teardown/integration/integration_suite_test.go new file mode 100644 index 00000000..6fe5eeea --- /dev/null +++ b/src/code.cloudfoundry.org/silk/teardown/integration/integration_suite_test.go @@ -0,0 +1,74 @@ +package integration_test + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "math/rand" + "os" + + "code.cloudfoundry.org/cf-networking-helpers/testsupport" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" + + "testing" +) + +var ( + paths testPaths +) + +type testPaths struct { + CertDir string + ServerCACertFile string + ClientCACertFile string + ServerCertFile string + ServerKeyFile string + ClientCertFile string + ClientKeyFile string + TeardownBin string +} + +func TestIntegration(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Teardown Integration Suite") +} + +var _ = SynchronizedBeforeSuite(func() []byte { + var err error + paths.CertDir, err = ioutil.TempDir("", "silk-certs") + Expect(err).NotTo(HaveOccurred()) + + certWriter, err := testsupport.NewCertWriter(paths.CertDir) + Expect(err).NotTo(HaveOccurred()) + + paths.ServerCACertFile, err = certWriter.WriteCA("server-ca") + Expect(err).NotTo(HaveOccurred()) + paths.ServerCertFile, paths.ServerKeyFile, err = certWriter.WriteAndSign("server", "server-ca") + Expect(err).NotTo(HaveOccurred()) + + paths.ClientCACertFile, err = certWriter.WriteCA("client-ca") + Expect(err).NotTo(HaveOccurred()) + paths.ClientCertFile, paths.ClientKeyFile, err = certWriter.WriteAndSign("client", "client-ca") + Expect(err).NotTo(HaveOccurred()) + + fmt.Fprintf(GinkgoWriter, "building binary...") + paths.TeardownBin, err = gexec.Build("code.cloudfoundry.org/silk/cmd/silk-teardown", "-race", "-buildvcs=false") + fmt.Fprintf(GinkgoWriter, "done") + Expect(err).NotTo(HaveOccurred()) + + data, err := json.Marshal(paths) + Expect(err).NotTo(HaveOccurred()) + + return data +}, func(data []byte) { + Expect(json.Unmarshal(data, &paths)).To(Succeed()) + + rand.Seed(GinkgoRandomSeed() + int64(GinkgoParallelProcess())) +}) + +var _ = SynchronizedAfterSuite(func() {}, func() { + gexec.CleanupBuildArtifacts() + os.Remove(paths.CertDir) +}) diff --git a/src/code.cloudfoundry.org/silk/teardown/integration/integration_test.go b/src/code.cloudfoundry.org/silk/teardown/integration/integration_test.go new file mode 100644 index 00000000..59a5af9a --- /dev/null +++ b/src/code.cloudfoundry.org/silk/teardown/integration/integration_test.go @@ -0,0 +1,133 @@ +package integration_test + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net" + "os" + "os/exec" + + "code.cloudfoundry.org/cf-networking-helpers/mutualtls" + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/silk/client/config" + "code.cloudfoundry.org/silk/controller" + "code.cloudfoundry.org/silk/daemon/vtep" + "code.cloudfoundry.org/silk/lib/adapter" + "code.cloudfoundry.org/silk/testsupport" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var ( + DEFAULT_TIMEOUT = "5s" + + clientConf config.Config + fakeServer *testsupport.FakeController + serverListenAddr string + + vtepConfig *vtep.Config + vtepFactory *vtep.Factory + fakeHandler *testsupport.FakeHandler +) + +var _ = BeforeEach(func() { + localIP := "127.0.0.1" + vtepConfig = &vtep.Config{ + VTEPName: fmt.Sprintf("t-v-%d", GinkgoParallelProcess()), + UnderlayIP: net.ParseIP(localIP), + OverlayIP: net.IP{10, 255, byte(GinkgoParallelProcess()), 0}, + OverlayHardwareAddr: net.HardwareAddr{0xee, 0xee, 0x0a, 0xff, byte(GinkgoParallelProcess()), 0x00}, + VNI: GinkgoParallelProcess(), + } + + serverListenAddr = fmt.Sprintf("127.0.0.1:%d", 40000+GinkgoParallelProcess()) + datastoreFile, _ := ioutil.TempFile("", "-datastore") + datastoreFile.Close() + clientConf = config.Config{ + UnderlayIP: localIP, + SubnetPrefixLength: 24, + OverlayNetwork: "10.255.0.0/16", // unused by teardown, but config requires it + HealthCheckPort: 4000, + VTEPName: vtepConfig.VTEPName, + ConnectivityServerURL: fmt.Sprintf("https://%s", serverListenAddr), + ServerCACertFile: paths.ServerCACertFile, + ClientCertFile: paths.ClientCertFile, + ClientKeyFile: paths.ClientKeyFile, + VNI: GinkgoParallelProcess(), + PollInterval: 5, // unused by teardown + DebugServerPort: GinkgoParallelProcess(), // unused by teardown + Datastore: datastoreFile.Name(), + PartitionToleranceSeconds: 60, // unused by teardown + ClientTimeoutSeconds: 5, // unused by teardown + MetronPort: 1234, // unused by teardown + VTEPPort: 12345, // unused by teardown + LogPrefix: "potato-prefix", + } + + serverTLSConfig, err := mutualtls.NewServerTLSConfig(paths.ServerCertFile, paths.ServerKeyFile, paths.ClientCACertFile) + Expect(err).NotTo(HaveOccurred()) + fakeServer = testsupport.StartServer(serverListenAddr, serverTLSConfig) + + vtepFactory = &vtep.Factory{NetlinkAdapter: &adapter.NetlinkAdapter{}, Logger: lagertest.NewTestLogger("test")} + + Expect(vtepFactory.CreateVTEP(vtepConfig)).To(Succeed()) + + fakeHandler = &testsupport.FakeHandler{ + ResponseCode: 200, + ResponseBody: struct{}{}, + } + fakeServer.SetHandler("/leases/release", fakeHandler) +}) + +var _ = AfterEach(func() { + fakeServer.Stop() + removeVTEP() +}) + +var _ = Describe("Teardown", func() { + It("releases the lease and destroys the VTEP", func() { + By("running teardown") + session := runTeardown(writeConfigFile(clientConf)) + Expect(session).To(gexec.Exit(0)) + + By("verifying that the controller was called") + var lastRequest controller.ReleaseLeaseRequest + Expect(json.Unmarshal(fakeHandler.LastRequestBody, &lastRequest)).To(Succeed()) + Expect(lastRequest).To(Equal(controller.ReleaseLeaseRequest{ + UnderlayIP: vtepConfig.UnderlayIP.String(), + })) + + By("verifying that the vtep is no longer present") + _, _, _, err := vtepFactory.GetVTEPState(clientConf.VTEPName) + Expect(err).To(MatchError("find link: Link not found")) + + Expect(session.Out.Contents()).To(ContainSubstring("potato-prefix.silk-teardown.complete")) + }) +}) + +func removeVTEP() { + exec.Command("ip", "link", "del", vtepConfig.VTEPName).Run() +} + +func writeConfigFile(config config.Config) string { + configFile, err := ioutil.TempFile("", "test-config") + Expect(err).NotTo(HaveOccurred()) + + configBytes, err := json.Marshal(config) + Expect(err).NotTo(HaveOccurred()) + + err = ioutil.WriteFile(configFile.Name(), configBytes, os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + return configFile.Name() +} + +func runTeardown(configFilePath string) *gexec.Session { + startCmd := exec.Command(paths.TeardownBin, "--config", configFilePath) + session, err := gexec.Start(startCmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + Eventually(session, DEFAULT_TIMEOUT).Should(gexec.Exit()) + return session +} diff --git a/src/code.cloudfoundry.org/silk/testsupport/fakecontroller.go b/src/code.cloudfoundry.org/silk/testsupport/fakecontroller.go new file mode 100644 index 00000000..cfaa0ae0 --- /dev/null +++ b/src/code.cloudfoundry.org/silk/testsupport/fakecontroller.go @@ -0,0 +1,106 @@ +package testsupport + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "sync" + "time" + + "github.com/tedsuo/ifrit" + "github.com/tedsuo/ifrit/grouper" + "github.com/tedsuo/ifrit/http_server" + "github.com/tedsuo/ifrit/sigmon" + + . "github.com/onsi/gomega" +) + +type FakeController struct { + ifrit.Process + handlerLock sync.Mutex + handlers map[string]*FakeHandler + handlerFuncs map[string]FakeHandlerFunc +} + +type FakeHandlerFunc func(w http.ResponseWriter, r *http.Request) + +type FakeHandler struct { + LastRequestBody []byte + ResponseCode int + ResponseBody interface{} +} + +func (f *FakeController) ServeHTTP(w http.ResponseWriter, r *http.Request) { + f.handlerLock.Lock() + defer f.handlerLock.Unlock() + fakeHandlerFunc, ok := f.handlerFuncs[r.URL.Path] + if ok { + fakeHandlerFunc(w, r) + return + } + + var fakeHandler *FakeHandler + for route, h := range f.handlers { + if r.URL.Path == route { + fakeHandler = h + } + } + if fakeHandler == nil { + w.WriteHeader(http.StatusTeapot) + w.Write([]byte(fmt.Sprintf(`{}`))) + return + } + + bodyBytes, err := ioutil.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + fakeHandler.LastRequestBody = bodyBytes + responseBytes, _ := json.Marshal(fakeHandler.ResponseBody) + w.WriteHeader(fakeHandler.ResponseCode) + w.Write(responseBytes) +} + +func (f *FakeController) SetHandler(route string, handler *FakeHandler) { + f.handlerLock.Lock() + defer f.handlerLock.Unlock() + f.handlers[route] = handler +} + +func (f *FakeController) SetHandlerFunc(route string, handlerFunc FakeHandlerFunc) { + f.handlerLock.Lock() + defer f.handlerLock.Unlock() + f.handlerFuncs[route] = handlerFunc +} + +func StartServer(serverListenAddr string, tlsConfig *tls.Config) *FakeController { + fakeServer := &FakeController{ + handlers: make(map[string]*FakeHandler), + handlerFuncs: make(map[string]FakeHandlerFunc), + } + + someServer := http_server.NewTLSServer(serverListenAddr, fakeServer, tlsConfig) + + members := grouper.Members{{ + Name: "http_server", + Runner: someServer, + }} + group := grouper.NewOrdered(os.Interrupt, members) + monitor := ifrit.Invoke(sigmon.New(group)) + + Eventually(monitor.Ready(), 30*time.Second).Should(BeClosed()) + fakeServer.Process = monitor + return fakeServer +} + +func (f *FakeController) Stop() { + if f == nil { + return + } + f.Process.Signal(os.Interrupt) + Eventually(f.Process.Wait()).Should(Receive()) +} diff --git a/src/code.cloudfoundry.org/tools/tools.go b/src/code.cloudfoundry.org/tools/tools.go index fdb202ff..f3ab60be 100644 --- a/src/code.cloudfoundry.org/tools/tools.go +++ b/src/code.cloudfoundry.org/tools/tools.go @@ -4,9 +4,13 @@ package tools import ( + _ "github.com/onsi/ginkgo/v2/ginkgo" + _ "github.com/containernetworking/cni/plugins/test/noop" _ "github.com/containernetworking/plugins/plugins/ipam/host-local" _ "github.com/containernetworking/plugins/plugins/meta/bandwidth" + + _ "code.cloudfoundry.org/iptables-logger/cmd/iptables-logger" ) // This file imports packages that are used when running go generate, or used diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/http_client.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/http_client.go new file mode 100644 index 00000000..26a98c57 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/http_client.go @@ -0,0 +1,147 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "net/http" + "sync" + + "code.cloudfoundry.org/cf-networking-helpers/json_client" +) + +type HTTPClient struct { + CloseIdleConnectionsStub func() + closeIdleConnectionsMutex sync.RWMutex + closeIdleConnectionsArgsForCall []struct { + } + DoStub func(*http.Request) (*http.Response, error) + doMutex sync.RWMutex + doArgsForCall []struct { + arg1 *http.Request + } + doReturns struct { + result1 *http.Response + result2 error + } + doReturnsOnCall map[int]struct { + result1 *http.Response + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *HTTPClient) CloseIdleConnections() { + fake.closeIdleConnectionsMutex.Lock() + fake.closeIdleConnectionsArgsForCall = append(fake.closeIdleConnectionsArgsForCall, struct { + }{}) + stub := fake.CloseIdleConnectionsStub + fake.recordInvocation("CloseIdleConnections", []interface{}{}) + fake.closeIdleConnectionsMutex.Unlock() + if stub != nil { + fake.CloseIdleConnectionsStub() + } +} + +func (fake *HTTPClient) CloseIdleConnectionsCallCount() int { + fake.closeIdleConnectionsMutex.RLock() + defer fake.closeIdleConnectionsMutex.RUnlock() + return len(fake.closeIdleConnectionsArgsForCall) +} + +func (fake *HTTPClient) CloseIdleConnectionsCalls(stub func()) { + fake.closeIdleConnectionsMutex.Lock() + defer fake.closeIdleConnectionsMutex.Unlock() + fake.CloseIdleConnectionsStub = stub +} + +func (fake *HTTPClient) Do(arg1 *http.Request) (*http.Response, error) { + fake.doMutex.Lock() + ret, specificReturn := fake.doReturnsOnCall[len(fake.doArgsForCall)] + fake.doArgsForCall = append(fake.doArgsForCall, struct { + arg1 *http.Request + }{arg1}) + stub := fake.DoStub + fakeReturns := fake.doReturns + fake.recordInvocation("Do", []interface{}{arg1}) + fake.doMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *HTTPClient) DoCallCount() int { + fake.doMutex.RLock() + defer fake.doMutex.RUnlock() + return len(fake.doArgsForCall) +} + +func (fake *HTTPClient) DoCalls(stub func(*http.Request) (*http.Response, error)) { + fake.doMutex.Lock() + defer fake.doMutex.Unlock() + fake.DoStub = stub +} + +func (fake *HTTPClient) DoArgsForCall(i int) *http.Request { + fake.doMutex.RLock() + defer fake.doMutex.RUnlock() + argsForCall := fake.doArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *HTTPClient) DoReturns(result1 *http.Response, result2 error) { + fake.doMutex.Lock() + defer fake.doMutex.Unlock() + fake.DoStub = nil + fake.doReturns = struct { + result1 *http.Response + result2 error + }{result1, result2} +} + +func (fake *HTTPClient) DoReturnsOnCall(i int, result1 *http.Response, result2 error) { + fake.doMutex.Lock() + defer fake.doMutex.Unlock() + fake.DoStub = nil + if fake.doReturnsOnCall == nil { + fake.doReturnsOnCall = make(map[int]struct { + result1 *http.Response + result2 error + }) + } + fake.doReturnsOnCall[i] = struct { + result1 *http.Response + result2 error + }{result1, result2} +} + +func (fake *HTTPClient) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.closeIdleConnectionsMutex.RLock() + defer fake.closeIdleConnectionsMutex.RUnlock() + fake.doMutex.RLock() + defer fake.doMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *HTTPClient) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ json_client.HttpClient = new(HTTPClient) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/json_client.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/json_client.go new file mode 100644 index 00000000..ed446938 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/json_client.go @@ -0,0 +1,149 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + + "code.cloudfoundry.org/cf-networking-helpers/json_client" +) + +type JSONClient struct { + CloseIdleConnectionsStub func() + closeIdleConnectionsMutex sync.RWMutex + closeIdleConnectionsArgsForCall []struct { + } + DoStub func(string, string, interface{}, interface{}, string) error + doMutex sync.RWMutex + doArgsForCall []struct { + arg1 string + arg2 string + arg3 interface{} + arg4 interface{} + arg5 string + } + doReturns struct { + result1 error + } + doReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *JSONClient) CloseIdleConnections() { + fake.closeIdleConnectionsMutex.Lock() + fake.closeIdleConnectionsArgsForCall = append(fake.closeIdleConnectionsArgsForCall, struct { + }{}) + stub := fake.CloseIdleConnectionsStub + fake.recordInvocation("CloseIdleConnections", []interface{}{}) + fake.closeIdleConnectionsMutex.Unlock() + if stub != nil { + fake.CloseIdleConnectionsStub() + } +} + +func (fake *JSONClient) CloseIdleConnectionsCallCount() int { + fake.closeIdleConnectionsMutex.RLock() + defer fake.closeIdleConnectionsMutex.RUnlock() + return len(fake.closeIdleConnectionsArgsForCall) +} + +func (fake *JSONClient) CloseIdleConnectionsCalls(stub func()) { + fake.closeIdleConnectionsMutex.Lock() + defer fake.closeIdleConnectionsMutex.Unlock() + fake.CloseIdleConnectionsStub = stub +} + +func (fake *JSONClient) Do(arg1 string, arg2 string, arg3 interface{}, arg4 interface{}, arg5 string) error { + fake.doMutex.Lock() + ret, specificReturn := fake.doReturnsOnCall[len(fake.doArgsForCall)] + fake.doArgsForCall = append(fake.doArgsForCall, struct { + arg1 string + arg2 string + arg3 interface{} + arg4 interface{} + arg5 string + }{arg1, arg2, arg3, arg4, arg5}) + stub := fake.DoStub + fakeReturns := fake.doReturns + fake.recordInvocation("Do", []interface{}{arg1, arg2, arg3, arg4, arg5}) + fake.doMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4, arg5) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *JSONClient) DoCallCount() int { + fake.doMutex.RLock() + defer fake.doMutex.RUnlock() + return len(fake.doArgsForCall) +} + +func (fake *JSONClient) DoCalls(stub func(string, string, interface{}, interface{}, string) error) { + fake.doMutex.Lock() + defer fake.doMutex.Unlock() + fake.DoStub = stub +} + +func (fake *JSONClient) DoArgsForCall(i int) (string, string, interface{}, interface{}, string) { + fake.doMutex.RLock() + defer fake.doMutex.RUnlock() + argsForCall := fake.doArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5 +} + +func (fake *JSONClient) DoReturns(result1 error) { + fake.doMutex.Lock() + defer fake.doMutex.Unlock() + fake.DoStub = nil + fake.doReturns = struct { + result1 error + }{result1} +} + +func (fake *JSONClient) DoReturnsOnCall(i int, result1 error) { + fake.doMutex.Lock() + defer fake.doMutex.Unlock() + fake.DoStub = nil + if fake.doReturnsOnCall == nil { + fake.doReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.doReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *JSONClient) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.closeIdleConnectionsMutex.RLock() + defer fake.closeIdleConnectionsMutex.RUnlock() + fake.doMutex.RLock() + defer fake.doMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *JSONClient) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ json_client.JsonClient = new(JSONClient) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/marshaler.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/marshaler.go new file mode 100644 index 00000000..b677f3b1 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/marshaler.go @@ -0,0 +1,116 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + + "code.cloudfoundry.org/cf-networking-helpers/marshal" +) + +type Marshaler struct { + MarshalStub func(interface{}) ([]byte, error) + marshalMutex sync.RWMutex + marshalArgsForCall []struct { + arg1 interface{} + } + marshalReturns struct { + result1 []byte + result2 error + } + marshalReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *Marshaler) Marshal(arg1 interface{}) ([]byte, error) { + fake.marshalMutex.Lock() + ret, specificReturn := fake.marshalReturnsOnCall[len(fake.marshalArgsForCall)] + fake.marshalArgsForCall = append(fake.marshalArgsForCall, struct { + arg1 interface{} + }{arg1}) + stub := fake.MarshalStub + fakeReturns := fake.marshalReturns + fake.recordInvocation("Marshal", []interface{}{arg1}) + fake.marshalMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *Marshaler) MarshalCallCount() int { + fake.marshalMutex.RLock() + defer fake.marshalMutex.RUnlock() + return len(fake.marshalArgsForCall) +} + +func (fake *Marshaler) MarshalCalls(stub func(interface{}) ([]byte, error)) { + fake.marshalMutex.Lock() + defer fake.marshalMutex.Unlock() + fake.MarshalStub = stub +} + +func (fake *Marshaler) MarshalArgsForCall(i int) interface{} { + fake.marshalMutex.RLock() + defer fake.marshalMutex.RUnlock() + argsForCall := fake.marshalArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *Marshaler) MarshalReturns(result1 []byte, result2 error) { + fake.marshalMutex.Lock() + defer fake.marshalMutex.Unlock() + fake.MarshalStub = nil + fake.marshalReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *Marshaler) MarshalReturnsOnCall(i int, result1 []byte, result2 error) { + fake.marshalMutex.Lock() + defer fake.marshalMutex.Unlock() + fake.MarshalStub = nil + if fake.marshalReturnsOnCall == nil { + fake.marshalReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.marshalReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *Marshaler) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.marshalMutex.RLock() + defer fake.marshalMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *Marshaler) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ marshal.Marshaler = new(Marshaler) diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/metrics_sender.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/metrics_sender.go new file mode 100644 index 00000000..9c66d347 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/metrics_sender.go @@ -0,0 +1,114 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + "time" +) + +type MetricsSender struct { + IncrementCounterStub func(string) + incrementCounterMutex sync.RWMutex + incrementCounterArgsForCall []struct { + arg1 string + } + SendDurationStub func(string, time.Duration) + sendDurationMutex sync.RWMutex + sendDurationArgsForCall []struct { + arg1 string + arg2 time.Duration + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *MetricsSender) IncrementCounter(arg1 string) { + fake.incrementCounterMutex.Lock() + fake.incrementCounterArgsForCall = append(fake.incrementCounterArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.IncrementCounterStub + fake.recordInvocation("IncrementCounter", []interface{}{arg1}) + fake.incrementCounterMutex.Unlock() + if stub != nil { + fake.IncrementCounterStub(arg1) + } +} + +func (fake *MetricsSender) IncrementCounterCallCount() int { + fake.incrementCounterMutex.RLock() + defer fake.incrementCounterMutex.RUnlock() + return len(fake.incrementCounterArgsForCall) +} + +func (fake *MetricsSender) IncrementCounterCalls(stub func(string)) { + fake.incrementCounterMutex.Lock() + defer fake.incrementCounterMutex.Unlock() + fake.IncrementCounterStub = stub +} + +func (fake *MetricsSender) IncrementCounterArgsForCall(i int) string { + fake.incrementCounterMutex.RLock() + defer fake.incrementCounterMutex.RUnlock() + argsForCall := fake.incrementCounterArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *MetricsSender) SendDuration(arg1 string, arg2 time.Duration) { + fake.sendDurationMutex.Lock() + fake.sendDurationArgsForCall = append(fake.sendDurationArgsForCall, struct { + arg1 string + arg2 time.Duration + }{arg1, arg2}) + stub := fake.SendDurationStub + fake.recordInvocation("SendDuration", []interface{}{arg1, arg2}) + fake.sendDurationMutex.Unlock() + if stub != nil { + fake.SendDurationStub(arg1, arg2) + } +} + +func (fake *MetricsSender) SendDurationCallCount() int { + fake.sendDurationMutex.RLock() + defer fake.sendDurationMutex.RUnlock() + return len(fake.sendDurationArgsForCall) +} + +func (fake *MetricsSender) SendDurationCalls(stub func(string, time.Duration)) { + fake.sendDurationMutex.Lock() + defer fake.sendDurationMutex.Unlock() + fake.SendDurationStub = stub +} + +func (fake *MetricsSender) SendDurationArgsForCall(i int) (string, time.Duration) { + fake.sendDurationMutex.RLock() + defer fake.sendDurationMutex.RUnlock() + argsForCall := fake.sendDurationArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *MetricsSender) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.incrementCounterMutex.RLock() + defer fake.incrementCounterMutex.RUnlock() + fake.sendDurationMutex.RLock() + defer fake.sendDurationMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *MetricsSender) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/mysql_adapter.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/mysql_adapter.go new file mode 100644 index 00000000..824d472f --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/mysql_adapter.go @@ -0,0 +1,191 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "crypto/tls" + "sync" + + "github.com/go-sql-driver/mysql" +) + +type MySQLAdapter struct { + ParseDSNStub func(string) (*mysql.Config, error) + parseDSNMutex sync.RWMutex + parseDSNArgsForCall []struct { + arg1 string + } + parseDSNReturns struct { + result1 *mysql.Config + result2 error + } + parseDSNReturnsOnCall map[int]struct { + result1 *mysql.Config + result2 error + } + RegisterTLSConfigStub func(string, *tls.Config) error + registerTLSConfigMutex sync.RWMutex + registerTLSConfigArgsForCall []struct { + arg1 string + arg2 *tls.Config + } + registerTLSConfigReturns struct { + result1 error + } + registerTLSConfigReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *MySQLAdapter) ParseDSN(arg1 string) (*mysql.Config, error) { + fake.parseDSNMutex.Lock() + ret, specificReturn := fake.parseDSNReturnsOnCall[len(fake.parseDSNArgsForCall)] + fake.parseDSNArgsForCall = append(fake.parseDSNArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.ParseDSNStub + fakeReturns := fake.parseDSNReturns + fake.recordInvocation("ParseDSN", []interface{}{arg1}) + fake.parseDSNMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *MySQLAdapter) ParseDSNCallCount() int { + fake.parseDSNMutex.RLock() + defer fake.parseDSNMutex.RUnlock() + return len(fake.parseDSNArgsForCall) +} + +func (fake *MySQLAdapter) ParseDSNCalls(stub func(string) (*mysql.Config, error)) { + fake.parseDSNMutex.Lock() + defer fake.parseDSNMutex.Unlock() + fake.ParseDSNStub = stub +} + +func (fake *MySQLAdapter) ParseDSNArgsForCall(i int) string { + fake.parseDSNMutex.RLock() + defer fake.parseDSNMutex.RUnlock() + argsForCall := fake.parseDSNArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *MySQLAdapter) ParseDSNReturns(result1 *mysql.Config, result2 error) { + fake.parseDSNMutex.Lock() + defer fake.parseDSNMutex.Unlock() + fake.ParseDSNStub = nil + fake.parseDSNReturns = struct { + result1 *mysql.Config + result2 error + }{result1, result2} +} + +func (fake *MySQLAdapter) ParseDSNReturnsOnCall(i int, result1 *mysql.Config, result2 error) { + fake.parseDSNMutex.Lock() + defer fake.parseDSNMutex.Unlock() + fake.ParseDSNStub = nil + if fake.parseDSNReturnsOnCall == nil { + fake.parseDSNReturnsOnCall = make(map[int]struct { + result1 *mysql.Config + result2 error + }) + } + fake.parseDSNReturnsOnCall[i] = struct { + result1 *mysql.Config + result2 error + }{result1, result2} +} + +func (fake *MySQLAdapter) RegisterTLSConfig(arg1 string, arg2 *tls.Config) error { + fake.registerTLSConfigMutex.Lock() + ret, specificReturn := fake.registerTLSConfigReturnsOnCall[len(fake.registerTLSConfigArgsForCall)] + fake.registerTLSConfigArgsForCall = append(fake.registerTLSConfigArgsForCall, struct { + arg1 string + arg2 *tls.Config + }{arg1, arg2}) + stub := fake.RegisterTLSConfigStub + fakeReturns := fake.registerTLSConfigReturns + fake.recordInvocation("RegisterTLSConfig", []interface{}{arg1, arg2}) + fake.registerTLSConfigMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *MySQLAdapter) RegisterTLSConfigCallCount() int { + fake.registerTLSConfigMutex.RLock() + defer fake.registerTLSConfigMutex.RUnlock() + return len(fake.registerTLSConfigArgsForCall) +} + +func (fake *MySQLAdapter) RegisterTLSConfigCalls(stub func(string, *tls.Config) error) { + fake.registerTLSConfigMutex.Lock() + defer fake.registerTLSConfigMutex.Unlock() + fake.RegisterTLSConfigStub = stub +} + +func (fake *MySQLAdapter) RegisterTLSConfigArgsForCall(i int) (string, *tls.Config) { + fake.registerTLSConfigMutex.RLock() + defer fake.registerTLSConfigMutex.RUnlock() + argsForCall := fake.registerTLSConfigArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *MySQLAdapter) RegisterTLSConfigReturns(result1 error) { + fake.registerTLSConfigMutex.Lock() + defer fake.registerTLSConfigMutex.Unlock() + fake.RegisterTLSConfigStub = nil + fake.registerTLSConfigReturns = struct { + result1 error + }{result1} +} + +func (fake *MySQLAdapter) RegisterTLSConfigReturnsOnCall(i int, result1 error) { + fake.registerTLSConfigMutex.Lock() + defer fake.registerTLSConfigMutex.Unlock() + fake.RegisterTLSConfigStub = nil + if fake.registerTLSConfigReturnsOnCall == nil { + fake.registerTLSConfigReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.registerTLSConfigReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *MySQLAdapter) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.parseDSNMutex.RLock() + defer fake.parseDSNMutex.RUnlock() + fake.registerTLSConfigMutex.RLock() + defer fake.registerTLSConfigMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *MySQLAdapter) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/sleeper.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/sleeper.go new file mode 100644 index 00000000..b731bfe2 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/sleeper.go @@ -0,0 +1,73 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + "time" +) + +type Sleeper struct { + SleepStub func(time.Duration) + sleepMutex sync.RWMutex + sleepArgsForCall []struct { + arg1 time.Duration + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *Sleeper) Sleep(arg1 time.Duration) { + fake.sleepMutex.Lock() + fake.sleepArgsForCall = append(fake.sleepArgsForCall, struct { + arg1 time.Duration + }{arg1}) + stub := fake.SleepStub + fake.recordInvocation("Sleep", []interface{}{arg1}) + fake.sleepMutex.Unlock() + if stub != nil { + fake.SleepStub(arg1) + } +} + +func (fake *Sleeper) SleepCallCount() int { + fake.sleepMutex.RLock() + defer fake.sleepMutex.RUnlock() + return len(fake.sleepArgsForCall) +} + +func (fake *Sleeper) SleepCalls(stub func(time.Duration)) { + fake.sleepMutex.Lock() + defer fake.sleepMutex.Unlock() + fake.SleepStub = stub +} + +func (fake *Sleeper) SleepArgsForCall(i int) time.Duration { + fake.sleepMutex.RLock() + defer fake.sleepMutex.RUnlock() + argsForCall := fake.sleepArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *Sleeper) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.sleepMutex.RLock() + defer fake.sleepMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *Sleeper) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/unmarshaler.go b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/unmarshaler.go new file mode 100644 index 00000000..cd54458c --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/code.cloudfoundry.org/cf-networking-helpers/fakes/unmarshaler.go @@ -0,0 +1,118 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + + "code.cloudfoundry.org/cf-networking-helpers/marshal" +) + +type Unmarshaler struct { + UnmarshalStub func([]byte, interface{}) error + unmarshalMutex sync.RWMutex + unmarshalArgsForCall []struct { + arg1 []byte + arg2 interface{} + } + unmarshalReturns struct { + result1 error + } + unmarshalReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *Unmarshaler) Unmarshal(arg1 []byte, arg2 interface{}) error { + var arg1Copy []byte + if arg1 != nil { + arg1Copy = make([]byte, len(arg1)) + copy(arg1Copy, arg1) + } + fake.unmarshalMutex.Lock() + ret, specificReturn := fake.unmarshalReturnsOnCall[len(fake.unmarshalArgsForCall)] + fake.unmarshalArgsForCall = append(fake.unmarshalArgsForCall, struct { + arg1 []byte + arg2 interface{} + }{arg1Copy, arg2}) + stub := fake.UnmarshalStub + fakeReturns := fake.unmarshalReturns + fake.recordInvocation("Unmarshal", []interface{}{arg1Copy, arg2}) + fake.unmarshalMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *Unmarshaler) UnmarshalCallCount() int { + fake.unmarshalMutex.RLock() + defer fake.unmarshalMutex.RUnlock() + return len(fake.unmarshalArgsForCall) +} + +func (fake *Unmarshaler) UnmarshalCalls(stub func([]byte, interface{}) error) { + fake.unmarshalMutex.Lock() + defer fake.unmarshalMutex.Unlock() + fake.UnmarshalStub = stub +} + +func (fake *Unmarshaler) UnmarshalArgsForCall(i int) ([]byte, interface{}) { + fake.unmarshalMutex.RLock() + defer fake.unmarshalMutex.RUnlock() + argsForCall := fake.unmarshalArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *Unmarshaler) UnmarshalReturns(result1 error) { + fake.unmarshalMutex.Lock() + defer fake.unmarshalMutex.Unlock() + fake.UnmarshalStub = nil + fake.unmarshalReturns = struct { + result1 error + }{result1} +} + +func (fake *Unmarshaler) UnmarshalReturnsOnCall(i int, result1 error) { + fake.unmarshalMutex.Lock() + defer fake.unmarshalMutex.Unlock() + fake.UnmarshalStub = nil + if fake.unmarshalReturnsOnCall == nil { + fake.unmarshalReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.unmarshalReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *Unmarshaler) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.unmarshalMutex.RLock() + defer fake.unmarshalMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *Unmarshaler) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ marshal.Unmarshaler = new(Unmarshaler) diff --git a/src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/bad_reader.go b/src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/bad_reader.go new file mode 100644 index 00000000..56a09fd2 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/bad_reader.go @@ -0,0 +1,33 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import "errors" + +// BadReader is an io.Reader which always errors +type BadReader struct { + Error error +} + +func (r *BadReader) Read(_ []byte) (int, error) { + if r.Error != nil { + return 0, r.Error + } + return 0, errors.New("banana") +} + +func (r *BadReader) Close() error { + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/cmd.go b/src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/cmd.go new file mode 100644 index 00000000..cd8600d6 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/cmd.go @@ -0,0 +1,112 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "io" + "os" + + "github.com/containernetworking/cni/pkg/skel" + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/version" +) + +func envCleanup() { + os.Unsetenv("CNI_COMMAND") + os.Unsetenv("CNI_PATH") + os.Unsetenv("CNI_NETNS") + os.Unsetenv("CNI_IFNAME") + os.Unsetenv("CNI_CONTAINERID") +} + +func CmdAdd(cniNetns, cniContainerID, cniIfname string, conf []byte, f func() error) (types.Result, []byte, error) { + os.Setenv("CNI_COMMAND", "ADD") + os.Setenv("CNI_PATH", os.Getenv("PATH")) + os.Setenv("CNI_NETNS", cniNetns) + os.Setenv("CNI_IFNAME", cniIfname) + os.Setenv("CNI_CONTAINERID", cniContainerID) + defer envCleanup() + + // Redirect stdout to capture plugin result + oldStdout := os.Stdout + r, w, err := os.Pipe() + if err != nil { + return nil, nil, err + } + + os.Stdout = w + err = f() + w.Close() + + var out []byte + if err == nil { + out, err = io.ReadAll(r) + } + os.Stdout = oldStdout + + // Return errors after restoring stdout so Ginkgo will correctly + // emit verbose error information on stdout + if err != nil { + return nil, nil, err + } + + // Plugin must return result in same version as specified in netconf + versionDecoder := &version.ConfigDecoder{} + confVersion, err := versionDecoder.Decode(conf) + if err != nil { + return nil, nil, err + } + + result, err := version.NewResult(confVersion, out) + if err != nil { + return nil, nil, err + } + + return result, out, nil +} + +func CmdAddWithArgs(args *skel.CmdArgs, f func() error) (types.Result, []byte, error) { + return CmdAdd(args.Netns, args.ContainerID, args.IfName, args.StdinData, f) +} + +func CmdCheck(cniNetns, cniContainerID, cniIfname string, f func() error) error { + os.Setenv("CNI_COMMAND", "CHECK") + os.Setenv("CNI_PATH", os.Getenv("PATH")) + os.Setenv("CNI_NETNS", cniNetns) + os.Setenv("CNI_IFNAME", cniIfname) + os.Setenv("CNI_CONTAINERID", cniContainerID) + defer envCleanup() + + return f() +} + +func CmdCheckWithArgs(args *skel.CmdArgs, f func() error) error { + return CmdCheck(args.Netns, args.ContainerID, args.IfName, f) +} + +func CmdDel(cniNetns, cniContainerID, cniIfname string, f func() error) error { + os.Setenv("CNI_COMMAND", "DEL") + os.Setenv("CNI_PATH", os.Getenv("PATH")) + os.Setenv("CNI_NETNS", cniNetns) + os.Setenv("CNI_IFNAME", cniIfname) + os.Setenv("CNI_CONTAINERID", cniContainerID) + defer envCleanup() + + return f() +} + +func CmdDelWithArgs(args *skel.CmdArgs, f func() error) error { + return CmdDel(args.Netns, args.ContainerID, args.IfName, f) +} diff --git a/src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/dns.go b/src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/dns.go new file mode 100644 index 00000000..bd0de0a8 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/dns.go @@ -0,0 +1,59 @@ +// Copyright 2019 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "fmt" + "os" + "strings" + + "github.com/containernetworking/cni/pkg/types" +) + +// TmpResolvConf will create a temporary file and write the provided DNS settings to +// it in the resolv.conf format. It returns the path of the created temporary file or +// an error if any occurs while creating/writing the file. It is the caller's +// responsibility to remove the file. +func TmpResolvConf(dnsConf types.DNS) (string, error) { + f, err := os.CreateTemp("", "cni_test_resolv.conf") + if err != nil { + return "", fmt.Errorf("failed to get temp file for CNI test resolv.conf: %v", err) + } + defer f.Close() + + path := f.Name() + defer func() { + if err != nil { + os.RemoveAll(path) + } + }() + + // see "man 5 resolv.conf" for the format of resolv.conf + var resolvConfLines []string + for _, nameserver := range dnsConf.Nameservers { + resolvConfLines = append(resolvConfLines, fmt.Sprintf("nameserver %s", nameserver)) + } + resolvConfLines = append(resolvConfLines, fmt.Sprintf("domain %s", dnsConf.Domain)) + resolvConfLines = append(resolvConfLines, fmt.Sprintf("search %s", strings.Join(dnsConf.Search, " "))) + resolvConfLines = append(resolvConfLines, fmt.Sprintf("options %s", strings.Join(dnsConf.Options, " "))) + + resolvConf := strings.Join(resolvConfLines, "\n") + _, err = f.Write([]byte(resolvConf)) + if err != nil { + return "", fmt.Errorf("failed to write temp resolv.conf for CNI test: %v", err) + } + + return path, err +} diff --git a/src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/netns_linux.go b/src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/netns_linux.go new file mode 100644 index 00000000..b467eb3c --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/netns_linux.go @@ -0,0 +1,176 @@ +// Copyright 2018 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "crypto/rand" + "fmt" + "os" + "path" + "runtime" + "strings" + "sync" + "syscall" + + "golang.org/x/sys/unix" + + "github.com/containernetworking/plugins/pkg/ns" +) + +func getNsRunDir() string { + xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR") + + /// If XDG_RUNTIME_DIR is set, check if the current user owns /var/run. If + // the owner is different, we are most likely running in a user namespace. + // In that case use $XDG_RUNTIME_DIR/netns as runtime dir. + if xdgRuntimeDir != "" { + if s, err := os.Stat("/var/run"); err == nil { + st, ok := s.Sys().(*syscall.Stat_t) + if ok && int(st.Uid) != os.Geteuid() { + return path.Join(xdgRuntimeDir, "netns") + } + } + } + + return "/var/run/netns" +} + +// Creates a new persistent (bind-mounted) network namespace and returns an object +// representing that namespace, without switching to it. +func NewNS() (ns.NetNS, error) { + nsRunDir := getNsRunDir() + + b := make([]byte, 16) + _, err := rand.Read(b) + if err != nil { + return nil, fmt.Errorf("failed to generate random netns name: %v", err) + } + + // Create the directory for mounting network namespaces + // This needs to be a shared mountpoint in case it is mounted in to + // other namespaces (containers) + err = os.MkdirAll(nsRunDir, 0o755) + if err != nil { + return nil, err + } + + // Remount the namespace directory shared. This will fail if it is not + // already a mountpoint, so bind-mount it on to itself to "upgrade" it + // to a mountpoint. + err = unix.Mount("", nsRunDir, "none", unix.MS_SHARED|unix.MS_REC, "") + if err != nil { + if err != unix.EINVAL { + return nil, fmt.Errorf("mount --make-rshared %s failed: %q", nsRunDir, err) + } + + // Recursively remount /var/run/netns on itself. The recursive flag is + // so that any existing netns bindmounts are carried over. + err = unix.Mount(nsRunDir, nsRunDir, "none", unix.MS_BIND|unix.MS_REC, "") + if err != nil { + return nil, fmt.Errorf("mount --rbind %s %s failed: %q", nsRunDir, nsRunDir, err) + } + + // Now we can make it shared + err = unix.Mount("", nsRunDir, "none", unix.MS_SHARED|unix.MS_REC, "") + if err != nil { + return nil, fmt.Errorf("mount --make-rshared %s failed: %q", nsRunDir, err) + } + + } + + nsName := fmt.Sprintf("cnitest-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) + + // create an empty file at the mount point + nsPath := path.Join(nsRunDir, nsName) + mountPointFd, err := os.Create(nsPath) + if err != nil { + return nil, err + } + mountPointFd.Close() + + // Ensure the mount point is cleaned up on errors; if the namespace + // was successfully mounted this will have no effect because the file + // is in-use + defer os.RemoveAll(nsPath) + + var wg sync.WaitGroup + wg.Add(1) + + // do namespace work in a dedicated goroutine, so that we can safely + // Lock/Unlock OSThread without upsetting the lock/unlock state of + // the caller of this function + go (func() { + defer wg.Done() + runtime.LockOSThread() + // Don't unlock. By not unlocking, golang will kill the OS thread when the + // goroutine is done (for go1.10+) + + var origNS ns.NetNS + origNS, err = ns.GetNS(getCurrentThreadNetNSPath()) + if err != nil { + return + } + defer origNS.Close() + + // create a new netns on the current thread + err = unix.Unshare(unix.CLONE_NEWNET) + if err != nil { + return + } + + // Put this thread back to the orig ns, since it might get reused (pre go1.10) + defer origNS.Set() + + // bind mount the netns from the current thread (from /proc) onto the + // mount point. This causes the namespace to persist, even when there + // are no threads in the ns. + err = unix.Mount(getCurrentThreadNetNSPath(), nsPath, "none", unix.MS_BIND, "") + if err != nil { + err = fmt.Errorf("failed to bind mount ns at %s: %v", nsPath, err) + } + })() + wg.Wait() + + if err != nil { + return nil, fmt.Errorf("failed to create namespace: %v", err) + } + + return ns.GetNS(nsPath) +} + +// UnmountNS unmounts the NS held by the netns object +func UnmountNS(ns ns.NetNS) error { + nsPath := ns.Path() + // Only unmount if it's been bind-mounted (don't touch namespaces in /proc...) + if strings.HasPrefix(nsPath, getNsRunDir()) { + if err := unix.Unmount(nsPath, 0); err != nil { + return fmt.Errorf("failed to unmount NS: at %s: %v", nsPath, err) + } + + if err := os.Remove(nsPath); err != nil { + return fmt.Errorf("failed to remove ns path %s: %v", nsPath, err) + } + } + + return nil +} + +// getCurrentThreadNetNSPath copied from pkg/ns +func getCurrentThreadNetNSPath() string { + // /proc/self/ns/net returns the namespace of the main thread, not + // of whatever thread this goroutine is running on. Make sure we + // use the thread's net namespace since the thread is switching around + return fmt.Sprintf("/proc/%d/task/%d/ns/net", os.Getpid(), unix.Gettid()) +} diff --git a/src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/ping.go b/src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/ping.go new file mode 100644 index 00000000..8c47c3d7 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/ping.go @@ -0,0 +1,61 @@ +// Copyright 2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "bytes" + "fmt" + "net" + "os/exec" + "strconv" + "syscall" +) + +// Ping shells out to the `ping` command. Returns nil if successful. +func Ping(saddr, daddr string, timeoutSec int) error { + ip := net.ParseIP(saddr) + if ip == nil { + return fmt.Errorf("failed to parse IP %q", saddr) + } + + bin := "ping6" + if ip.To4() != nil { + bin = "ping" + } + + args := []string{ + "-c", "1", + "-W", strconv.Itoa(timeoutSec), + "-I", saddr, + daddr, + } + + cmd := exec.Command(bin, args...) + var stderr bytes.Buffer + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + switch e := err.(type) { + case *exec.ExitError: + return fmt.Errorf("%v exit status %d: %s", + args, e.Sys().(syscall.WaitStatus).ExitStatus(), + stderr.String()) + default: + return err + } + } + + return nil +} diff --git a/src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/testing.go b/src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/testing.go new file mode 100644 index 00000000..9444a8b2 --- /dev/null +++ b/src/code.cloudfoundry.org/vendor/github.com/containernetworking/plugins/pkg/testutils/testing.go @@ -0,0 +1,54 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "github.com/containernetworking/cni/pkg/version" +) + +// AllSpecVersions contains all CNI spec version numbers +var AllSpecVersions = [...]string{"0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0", "1.0.0"} + +// SpecVersionHasIPVersion returns true if the given CNI specification version +// includes the "version" field in the IP address elements +func SpecVersionHasIPVersion(ver string) bool { + for _, i := range []string{"0.3.0", "0.3.1", "0.4.0"} { + if ver == i { + return true + } + } + return false +} + +// SpecVersionHasCHECK returns true if the given CNI specification version +// supports the CHECK command +func SpecVersionHasCHECK(ver string) bool { + ok, _ := version.GreaterThanOrEqualTo(ver, "0.4.0") + return ok +} + +// SpecVersionHasChaining returns true if the given CNI specification version +// supports plugin chaining +func SpecVersionHasChaining(ver string) bool { + ok, _ := version.GreaterThanOrEqualTo(ver, "0.3.0") + return ok +} + +// SpecVersionHasMultipleIPs returns true if the given CNI specification version +// supports more than one IP address of each family +func SpecVersionHasMultipleIPs(ver string) bool { + ok, _ := version.GreaterThanOrEqualTo(ver, "0.3.0") + return ok +} diff --git a/src/code.cloudfoundry.org/vendor/modules.txt b/src/code.cloudfoundry.org/vendor/modules.txt index f4434a14..e5e5633f 100644 --- a/src/code.cloudfoundry.org/vendor/modules.txt +++ b/src/code.cloudfoundry.org/vendor/modules.txt @@ -7,6 +7,7 @@ code.cloudfoundry.org/bbs/models ## explicit; go 1.20 code.cloudfoundry.org/cf-networking-helpers/db code.cloudfoundry.org/cf-networking-helpers/db/monitor +code.cloudfoundry.org/cf-networking-helpers/fakes code.cloudfoundry.org/cf-networking-helpers/httperror code.cloudfoundry.org/cf-networking-helpers/json_client code.cloudfoundry.org/cf-networking-helpers/marshal @@ -66,31 +67,6 @@ code.cloudfoundry.org/routing-info/internalroutes # code.cloudfoundry.org/runtimeschema v0.0.0-20230323223330-5366865eed76 => code.cloudfoundry.org/runtimeschema v0.0.0-20180622181441-7dcd19348be6 ## explicit code.cloudfoundry.org/runtimeschema/metric -# code.cloudfoundry.org/silk v0.0.0-20230728161150-8e48b8d1e681 -## explicit; go 1.20 -code.cloudfoundry.org/silk/client/config -code.cloudfoundry.org/silk/cmd/silk-cni -code.cloudfoundry.org/silk/cmd/silk-controller -code.cloudfoundry.org/silk/cmd/silk-daemon -code.cloudfoundry.org/silk/cmd/silk-teardown -code.cloudfoundry.org/silk/cni/adapter -code.cloudfoundry.org/silk/cni/config -code.cloudfoundry.org/silk/cni/lib -code.cloudfoundry.org/silk/cni/netinfo -code.cloudfoundry.org/silk/controller -code.cloudfoundry.org/silk/controller/config -code.cloudfoundry.org/silk/controller/database -code.cloudfoundry.org/silk/controller/handlers -code.cloudfoundry.org/silk/controller/leaser -code.cloudfoundry.org/silk/controller/server_metrics -code.cloudfoundry.org/silk/daemon -code.cloudfoundry.org/silk/daemon/planner -code.cloudfoundry.org/silk/daemon/poller -code.cloudfoundry.org/silk/daemon/vtep -code.cloudfoundry.org/silk/lib/adapter -code.cloudfoundry.org/silk/lib/datastore -code.cloudfoundry.org/silk/lib/hwaddr -code.cloudfoundry.org/silk/lib/serial # code.cloudfoundry.org/tlsconfig v0.0.0-20230612153104-23c0622de227 ## explicit; go 1.19 code.cloudfoundry.org/tlsconfig @@ -140,6 +116,7 @@ github.com/containernetworking/cni/plugins/test/noop/debug ## explicit; go 1.20 github.com/containernetworking/plugins/pkg/ip github.com/containernetworking/plugins/pkg/ns +github.com/containernetworking/plugins/pkg/testutils github.com/containernetworking/plugins/pkg/utils github.com/containernetworking/plugins/pkg/utils/buildversion github.com/containernetworking/plugins/pkg/utils/sysctl